nocobase/docs/en-US/development/server/collections-fields.md
2022-11-06 09:52:27 +08:00

13 KiB

Collections and Fields

Basic Concepts

Data modeling is the lowest level foundation of an application. In NocoBase applications we model data through data tables (Collection) and fields (Field), and the modeling is also mapped to database tables for persistence.

Collection

Collection is a collection of all similar data, which corresponds to the concept of database tables in NocoBase. Such as orders, products, users, comments, etc. can form a collection definition. Different collections are distinguished by name and contain fields defined by fields, such as

db.collection({
  name: 'posts',
  fields: [
    { name: 'title', type: 'string' }
    { name: 'content', type: 'text' },
    // ...
  ]
});

The collection is only in memory after the definition, you need to call the [``db.sync()`'' (/api/database#sync) method to synchronize it to the database.

Field

Corresponding to the concept of database table "fields", each data table (Collection) can have a number of Fields, for example.

db.collection({
  name: 'users',
  fields: [
    { type: 'string', name: 'name' }
    { type: 'integer', name: 'age' }
    // Other fields
  ],
});

The field name (name) and field type (type) are required, and different fields are distinguished by the field name (name). All field types and their configurations are described in the [List of built-in field types](/api/database/field#List of built-in field types) section of the API reference.

Example

For developers, we usually build functional collections that are different from normal collections and solidify these collections as part of the plugin and combine them with other data processing processes to form a complete functionality.

Let's take a simple online store plugin as an example to show how to model and manage the collections of the plugin. Assuming you have already learned about Develop your first plugin, we continue to build on the previous plugin code, except that the name of the plugin is changed from hello to shop-modeling.

Define and create collections in the plugin

For a store, you first need to create a collection of products, named products. Instead of calling db.collection() directly, in the plugin we will use a more convenient method to import multiple files of defined data tables at once. So let's start by creating a file for the product collection definition named collections/products.ts and fill it with the following content.

export default {
  name: 'products',
  fields: [
    {
      type: 'string',
      name: 'title'
    },
    {
      type: 'integer',
      name: 'price'
    },
    {
      type: 'boolean',
      name: 'enabled'
    },
    {
      type: 'integer',
      name: 'inventory'
    }
  ]
};

As you can see, the collections structure definition can be used directly in standard JSON format, where name and fields are required representing the collection's name and the field definitions in the collection. Field definitions similar to Sequelize create system fields such as primary key (id), data creation time (createdAt) and data update time (updatedAt) by default, which can be overridden by a configuration with the same name if there is a special need.

The data table defined in this file we can introduce and complete the definition in the load() cycle of the main plugin class using db.import(). This is shown below.

import path from 'path';
import { Plugin } from '@nocobase/server';

export default class ShopPlugin extends Plugin {
  async load() {
    await this.db.import({
      directory: path.resolve(__dirname, 'collections'),
    });

    this.app.acl.allow('products', '*');
    this.app.acl.allow('categories', '*');
    this.app.acl.allow('orders', '*');
  }
}

In the meantime, for testing purposes, we will temporarily allow all access permissions for the data in these collections, and later we will detail how to manage data permissions in Permissions Management.

This way, when the plugin is loaded by the main application, the products collection we defined is also loaded into the memory of the database management instance. At the same time, the NocoBase constraint-based resource mapping of the collections automatically generates the corresponding CRUD HTTP API after the application's service is started.

When the following URLs are requested from the client, the corresponding responses are obtained.

  • GET /api/products:list: Get a list of all product data
  • GET /api/products:get?filterByTk=<id>: Get the product data for the specified ID
  • POST /api/products: Create a new product data
  • PUT /api/products:update?filterByTk=<id>: Update a product data
  • DELETE /api/products:destroy?filterByTk=<id>: Delete a product data

Defining associated collections and fields

In the above example, we only defined a product collection, but in reality a product also needs to be associated to a category, a brand, a supplier, etc. For example, we can define a categories collection to store the categories, and then add a category field to the product collection to associate it with the category collection.

Add a new file collections/categories.ts and fill in the content.

export default {
  name: 'categories',
  fields: [
    {
      type: 'string',
      name: 'title'
    },
    {
      type: 'hasMany',
      name: 'products',
    }
  ]
};

We have defined two fields for the categories collection, one for the title and another one-to-many field for all the products associated under that category, which will be described later. Since we have already used the db.import() method in the plugin's main class to import all the data table definitions under the collections directory, the new categories collection added here will also be automatically imported into the database management instance.

Modify the file collections/products.ts`` to add a categoryfield to thefields`.

{
  name: 'products',
  fields: [
    // ...
    {
      type: 'belongsTo',
      name: 'category',
      target: 'categories',
    }
  ]
}

As you can see, the category field we added to the products collection is a belongsTo type field, and its target property points to the categories collection, thus defining a many-to-one relationship between the products collection and the categories collection. Combined with the hasMany field defined in the categories collection, we can achieve a relationship where one product can be associated to multiple categories and multiple products under one category. Usually belongsTo and hasMany can appear in pairs, defined in two separate collections.

Once the relationship between the two collections is defined, we can also request the associated data directly through the HTTP API

  • GET /api/products:list?appends=category: Get all products data, including the associated categories data
  • GET /api/products:get?filterByTk=<id>&appends=category: Get the product data for the specified ID, including the associated category data.
  • GET /api/categories/<categoryId>/products:list: Get all the products under the specified category
  • POST /api/categories/<categoryId>/products: Create a new product under the specified category

Similar to the general ORM framework, NocoBase has four built-in relational field types, for more information you can refer to the section about API field types.

Extend an existing collection

In the above example, we already have a product collection and a category collection, in order to provide the sales process we also need an order collection. We can add a new orders.ts file to the collections directory and define an orders collection as follows

export default {
  name: 'orders',
  fields: [
    {
      type: 'uuid',
      name: 'id',
      primaryKey: true
    },
    {
      type: 'belongsTo',
      name: 'product'
    },
    {
      type: 'integer',
      name: 'quantity'
    },
    {
      type: 'integer',
      name: 'totalPrice'
    },
    {
      type: 'integer',
      name: 'status'
    },
    {
      type: 'string',
      name: 'address'
    },
    {
      type: 'belongsTo',
      name: 'user'
    }
  ]
}

For the sake of simplicity, the association between the order collection and the product collection we simply define as a many-to-one relationship, while in the actual business may be used in a complex modeling approach such as many-to-many or snapshot. As you can see, an order in addition to corresponding to a commodity, we also added a relationship definition corresponding to the users, which is a collection managed by the NocoBase built-in plugins (refer to code for users plugin for details plugins/users)). If we want to extend the definition of the "multiple orders owned by a user" relationship for the existing users collection, we can continue to add a new collection file collections/users.ts inside the current shop-modeling plugin, which is different from exporting the JSON collection directly. Unlike the direct export of a JSON, the @nocobase/database package's extend() method is used here to extend the definition of an existing collection: ``ts

import { extend } from '@nocobase/database';

export extend({
  name: 'users',
  fields: [
    {
      type: 'hasMany',
      name: 'orders'
    }
  ]
});

This way, the existing users table also has an orders associated field, and we can retrieve all the order data for a given user via GET /api/users/<userId>/orders:list.

This method is very useful when extending collections already defined by other plugins, so that other existing plugins do not depend on the new plugin in reverse, but only form one-way dependencies, facilitating a certain degree of decoupling at the extension level.

Extend field types

We use uuid type for id field when we define order table, which is a built-in field type. Sometimes we may feel that UUID looks too long and waste space, and the query performance is not good, we want to use a more suitable field type, such as a complex numbering logic with date information, or Snowflake algorithm, we need to extend a custom field type.

Suppose we need to apply the Snowflake ID generation algorithm directly to extend a snowflake field type, we can create a fields/snowflake.ts file.

// Import the algorithm toolkit
import { Snowflake } from 'nodejs-snowflake';
// Import field type base class
import { DataTypes, Field, BaseColumnFieldOptions } from '@nocobase/database';

export interface SnowflakeFieldOptions extends BaseColumnFieldOptions {
  type: 'snowflake';
  epoch: number;
  instanceId: number;
}

export class SnowflakeField extends Field {
  get dataType() {
    return DataTypes.BIGINT;
  }

  constructor(options: SnowflakeFieldOptions, context) {
    super(options, context);

    const {
      epoch: custom_epoch,
      instanceId: instance_id = process.env.INSTANCE_ID ? Number.parseInt(process.env.INSTANCE_ID) : 0,
    } = options;
    this.generator = new Snowflake({ custom_epoch, instance_id });
  }
  
  setValue = (instance) => {
    const { name } = this.options;
    instance.set(name, this.generator.getUniqueID());
  };

  bind() {
    super.bind();
    this.on('beforeCreate', this.setValue);
  }

  unbind() {
    super.unbind();
    this.off('beforeCreate', this.setValue);
  }
}

export default SnowflakeField;

Afterwards, register the new field type into the collection in the main plugin file.

import SnowflakeField from '. /fields/snowflake';

export default class ShopPlugin extends Plugin {
  initialize() {
    // ...
    this.db.registerFieldTypes({
      snowflake: SnowflakeField
    });
    // ...
  }
}

This allows us to use the snowflake field type in the order table:

export default {
  name: 'orders',
  fields: [
    {
      type: 'snowflake'
      name: 'id',
      primaryKey: true
    },
    // ... . other fields
  ]
}

Summary

With the above example, we basically understand how to model data in a plugin, including.

  • Defining collections and common fields
  • Defining association collections and fields relationships
  • Extending fields of an existing collections
  • Extending new field types

We have put the code covered in this chapter into a complete sample package packages/samples/shop-modeling, which can be run directly locally to see the results.