Skip to main content

Bookshelf Connector package

The leemons-connector-bookshelf package is a SQL abstraction layer, which will be in charge of managing connections and queries to the database.

To perform this abstraction, we use the bookshelf.js ORM and the knex.js query builder.

Initialization

When the connector is initialized, it connects leemons with each of the specified database connections.

  • What database engines are supported?

    Currently we have tested compatibility with MySql, PostgreSQL and SQLite3.

  • But how does it work?

    Well, the first thing to do is to initialize a knex object with the database connection data, once everything is ready, we proceed in creating the schema from the models provided. During the creation of the schema, the relationships between tables are created and the bookshelf models are instantiated to manage the queries.

Models Generation

To generate the database, the first thing to do is to check the current database schema, which we do in the following order:

  1. The user-defined models are obtained.
  2. A transaction is started to keep an open thread and to enable a return to the previous state in case of failure.
  3. For each model defined, a check is made to see if a corresponding table exists in the database.
  4. A check is performed to confirm if the model has been altered since the previous execution, which is stored in the core_store.
  5. The tables that need it are created or modified (based on points 2 and 3).
  6. The bookshelf model is generated.
  7. The foreign keys are created.

Definition of templates

The models are used to define the structure of your tables, for this it is necessary to define them as indicated in the leemons documentation.

Next, it is necessary to define the different columns that your table is going to have, this is done in the attributes property being able to choose one of the defined data types or set a specific type, next to these, we can specify some options per data. Instead of data, you can define a relationship with another table, this is achieved by the relationship declaration.

For example: In the users table we have the fields: first name, last name and course.

{
"modelName": "users",
"attributes": {
"name": {
"type": "string"
},
"surnames": {
"type": "string"
},
"course": {
"type": "integer"
}
}
}

Data types

We have selected most of the data types provided by Knex.js, in an attempt to get the most efficient data types to be used, these are the following:

string

string

Specifies the use of strings of the specified length (default 255 characters).

{
"attributes": {
"name": {
"type": "string",
"length": 255
}
}
}
text/richtext

text/richtext

Specifies the use of strings with a variable determined by SQL, this type is more efficient for long texts (default 65535 characters).

{
"attributes": {
"name": {
"type": "text",
"textType": "text" // possible values: "mediumtext" y "longtext"
}
}
}
enum/enu/enumeration

enum/enu/enumeration

Defines the possible values that a field can accept:

{
"attributes": {
// role can be only student, teacher or admin.
"role": {
"type": "enum",
"enum": ["student", "teacher", "admin"]
}
}
}
json/jsonb

json/jsonb

It allows to use the database engine's own JSON data type, thus obtaining some extra functionalities from the engine.

{
"attributes": {
// `metadata` can be a json that allows more efficient
// queries to certain properties of the metadata.
"metadata": {
"type": "json"
}
}
}
int/integer

int/integer

Specifies that the data is a number.

{
"attributes": {
"course": {
"type": "int"
}
}
}
bigint/biginteger

bigint/biginteger

You can also use larger numbers (they have a much higher limit).

{
"attributes": {
// A number that is so large that in JavaScript
// must be treated as a string.
"bigInt": {
"type": "bigInt"
}
}
}
float

float

Allows the declaration of a float with a precision and a scale.

{
"attributes": {
"evaluation": {
"type": "float",
"precision": 8,
"scale": 2
}
}
}
decimal

decimal

Allows the declaration of a decimal number with a precision and a scale.

{
"attributes": {
"evaluation": {
"type": "decimal",
"precision": 8,
"scale": 2
}
}
}
binary

binary

Sets a binary field with a specified length.

{
"attributes": {
"file": {
"type": "binary",
"length": 255
}
}
}
boolean

boolean

Sets a boolean field (true or false)

{
"attributes": {
"attendance": {
"type": "boolean"
}
}
}
uuid

uuid

Allows to declare a UUID (Universally Unique Identifier).

{
"attributes": {
"user_id": {
"type": "uuid"
}
}
}

Options

In addition to specifying the data type, we can specify some more concrete options within the attribute with the options property, there are the following options:

unique

unique

Indicates whether the column should have a unique value throughout the table.

{
"attributes": {
"title": {
"type": "string",
"options": {
"unique": true
}
}
}
}
defaultTo

defaultTo

Indicates the default value in the case that no value is specified in the insertion.

{
"attributes": {
// If no role is specified, it is filled as 'student'.
"role": {
"type": "string",
"options": {
"defaultTo": "student"
}
}
}
}
unsigned

unsigned

Indicates whether the field value should be signed or unsigned (applicable for numeric values).

{
"attributes": {
// In this case positive numerical values are accepted (>= 0).
"id": {
"type": "int",
"options": {
"unsigned": true
}
}
}
}
notNull/notNullable

notNull/notNullable

Specifies whether the field can (true) or cannot be null (false) (default is can (false)).

{
"attributes": {
// Email is required (cannot be null)
"email": {
"type": "string",
"options": {
"notNull": true
}
},
// While the phone is optional (can be null)
"phone": {
"type": "int",
"options": {
"notNull": false
}
}
}
}

Specific data types

If you prefer to use a non-provided data type, it is possible to use a more specific one by using the specificType property following Knex.js instructions

Relationships between tables

In certain occasions it will be necessary to relate several tables, we have made it as easy as possible without losing the original essence. Instead of defining a type in an attribute of the table, you define a reference:

Let's see how you would define a relationship between the users and roles table.

{
"modelName": "users",
"attributes": {
"roles": {
"references": {
"collection": "roles"
}
}
}
}

Relationships

Relationships between tables are a very important feature in SQL databases, they allow the creation of relationships between data avoiding redundancy as much as possible.

If you have already worked with SQL you will already know the different relationships. Here are the different relationships and how to establish them:

one-to-one

In this type of relationship, one data in a table A is directly related to one data in another table B, moreover, there can only be one relationship for each data in table A and only one for each data in table B. In this type of relation it is said that a data belongs to another one.

For example: a student has a locker and a locker belongs to only one student. As the locker belongs to the student, it will be this table that creates the relationship, in the locker table the relationship is not specified.

{
"modelName": "students",
"attributes": {
"locker": {
// It could be read as: a locker belongs to a single student.
"references": {
"collection": "lockers",
"relation": "one to one"
}
}
}
}

What is going on inside?

On the inside, a field is being created in the students table called "locker" (as specified), this is a foreign key of the "lockers" table, in addition, a unique key is created to preserve uniqueness.

one-to-many

The one-to-many relationship directly relates one data in a table A with several in a table B, there can be as many relationships for each data in A as there are data in B, but each data in B can only relate to one in A.

For example: a student belongs to a single class, but a class belongs to several students. In this case, it would be the class that has the one-to-many relationship, but trying to keep all the relationships in the same table, it would be the student table that creates the relationship.

{
"modelName": "students",
"attributes": {
"classroom": {
// It could be read as: a classroom has several students
"references": {
"collection": "classrooms",
"relation": "one to many"
}
}
}
}

What is going on inside?

On the back end a field is being created in the students table called "class" (as specified), this is a foreign key of the "classes" table.

many-to-many

Finally, you may have situations where several data in table A are related to several data in table B. In this case, a third table is needed to store the relationships.

For example: A user can be enrolled in several subjects, and in turn, a subject can be given by several users, in this case it is irrelevant who declares the relationship, but as always, we recommend creating as many relationships as possible in the same model.

{
"modelName": "students",
"attributes": {
"subject": {
// It could be read as: several students attend several subjects
"references": {
"collection": "subjects",
"relation": "many to many"
}
}
}
}

What is going on inside?

Inside a table is created that contains a foreign key to the other two tables, thus being a join table, this table stores the relationships between the other two tables.

TIP

When deciding who should define the relationship, always follow these guidelines:

  • The second table you mention usually declares the relationship (a locker belongs to a single student).
  • You should always choose to have as many relationships as possible in the same table, so it is advisable to start declaring the relationships in order of: one-to-many, one-to-one and finally many-to-many.
  • The one-to-many are usually declared in the 'many' table and not in the 'one' table (many students in a class, one class many students. Always in the students table).