Database ======== The underlying data store of CARE is a relational database. The `PostgresSQL `_ database is managed and accessed via `Sequelize `_. The backend services and sockets of CARE access the database via predefined methods provided in with the MetaModel class in ``backend/db`` directory, which in turn utilize Sequelize as an layer of abstraction to query and update the database. The database schema is modified via `Sequelize's migration system `_. ``make init`` adds any new migrations to the database. Modifying the database schema therefore means adding a new migration and potentially a new Sequelize model. All relevant files can be found in the ``backend/db`` folder. Models ------ Models are defined in the ``models`` folder. They are defined using the `Sequelize `_ ORM. The following entity relationship model (ERM) shows the relations between the different models: .. image:: ./ERM.drawio.svg :width: 100% :align: center Additionally, the following attributes are added to each table: - ``deleted``: Boolean indicating whether the entry is deleted or not. - ``createdAt``: The time when the entry was created. - ``updatedAt``: The time when the entry was last updated. - ``deletedAt``: The time when the entry was deleted. Migrations ---------- Working with migrations is done using the `Sequelize CLI `_. To install the CLI, run ``npm install --save-dev sequelize-cli`` inside of the db folder. After that, you can run ``npx sequelize --help`` to see all available commands. To create a new migration, run ``npx sequelize migration:generate --name ``. This will create a new file in the ``migrations`` folder, containing a ``up`` and a ``down`` function. Adding a New Model ~~~~~~~~~~~~~~~~~~ 1. Use the CLI to create a model and a migration file. .. code-block:: bash npx sequelize migration:generate --name -nav 2. In the newly generated migration file under migrations change the table name. It should be a singular lower-case term. 3. Now you need to add a new model file for this table. Create a new file in ``backend/db/models`` that exports a function for generating the model as an instance of a MetaModel from a given Sequelize object. This looks as follows for a table called ``simpletable`` (the name introduced in 2.): .. code-block:: javascript 'use strict'; const MetaModel = require("../MetaModel.js"); module.exports = (sequelize, DataTypes) => { class SimpleTable extends MetaModel { static associate(models) { } } SimpleTable.init({ updatedAt: DataTypes.DATE, deleted: DataTypes.BOOLEAN, deletedAt: DataTypes.DATE, createdAt: DataTypes.DATE }, { sequelize: sequelize, modelName: 'simpletable', tableName: 'simpletable' }); return SimpleTable; }; 4. Add the foreign keys and constraints to the migration file as desired. For examples check out the other migrations. For a very simple table ``simpletable`` this would look like this: .. code-block:: javascript module.exports = { async up(queryInterface, Sequelize) { await queryInterface.createTable('simpletable', { id: { allowNull: false, autoIncrement: true, primaryKey: true, type: Sequelize.INTEGER }, deleted: { type: Sequelize.BOOLEAN, defaultValue: false }, createdAt: { allowNull: false, type: Sequelize.DATE }, updatedAt: { allowNull: false, type: Sequelize.DATE }, deletedAt: { allowNull: true, defaultValue: null, type: Sequelize.DATE } }); }, async down(queryInterface, Sequelize) { await queryInterface.dropTable('simpletable'); } 5. If necessary update the model file accordingly (if fields have been changed). Make sure adding additional columns like deleted, createdAt, updatedAt and deletedAt is done in the migration file. Accessing a Model ~~~~~~~~~~~~~~~~~ The above steps are necessary to create a new migration and associated model. To encapsulate the database interactions, the ``MetaModel`` class already contains several convenience functions for accessing the respective table. For instance, getting a single row entry by a provided ID (``getById(id)``). Accessing the DB via these default functions is as simple as accessing the table model and executing the static method. .. code-block:: javascript // usually, you just access the models loaded in the web server; for completeness we provide the imports here: const {DataTypes} = require("sequelize") const db = require("./db/index.js") const SimpleTable = require("./db/models/simpletable.js")(db.sequelize, DataTypes); SimpleTable.getById("x"); .. note:: Generally you should *not* import the db object yourself and load a model as in the provided example. Instead, the web server object already holds the respective models in a class attribute. Please check out the :doc:`./socket` for the details. In case you need more specific functions, you may simply add static access methods to the new model class. For instance: .. code-block:: javascript 'use strict'; const MetaModel = require("../MetaModel.js"); module.exports = (sequelize, DataTypes) => { class SimpleTable extends MetaModel { static associate(models) { } // new access function specific to this model static async getDefaultRow() { //do error management -- ommitted for brevity return await this.findOne({ where: {'id': "x"}, raw: true}); } } //... initialization etc. }; Populating a Table ~~~~~~~~~~~~~~~~~~ To populate a table with basic data rows, you can use seeds to test and debug and then convert it into a regular migration. 1. Create a seeder .. code-block:: bash npx sequelize-cli seed:generate --name 2. Adapt the seeder in the respective file under directory "seeders". 3. Move it to the migrations folder. 4. Double check that you have set the down method accordingly -- you don't want to drop an entire table if you only add a few rows. Delete just these rows.