# DB Schema extension js

The json file of DB Schema cannot be programmed, and the programmable extended js will greatly enhance the control ability of the schema.

In the past, action was used in clientDB to deal with the shortcomings of schema.json. However, the action cloud function has a security defect, which cannot prevent the client from invoking the specified action.

From HBuilderX 3.6.11+, uniCloud provides a programmable schema, and each ${table name}.schema.json can be configured with a ${table name}.schema.ext.js.

  • Under the HBuilderX project, ${table name}.schema.ext.js can be created under the directory uniCloud/database/.
  • In the database table management interface of the uniCloud web console, there is also an online management of ${table name}.schema.ext.js next to schema.json.

Schema extension js can achieve many things in planning, and currently only the database trigger function is online.

It is recommended that developers use JQL database triggers instead of action cloud functions.

# Database trigger

A JQL database trigger is used to trigger corresponding operations while executing a JQL database command (addition, deletion, modification, etc.).

Only use JQL to operate the database, and both the client and the cloud can execute JQL. However, using the traditional MongoDB writing method does not support database triggers.

Triggers can be used to implement many functions conveniently, such as:

  1. Automatically modify the update time to the current time when updating data
  2. After reading the details of the article, the reading volume will increase by 1
  3. After publishing an article, the number of articles in the article author list will be automatically increased by 1

Since the database trigger is executed in the cloud, many codes that should not be written in the front end when clientDB operates the database can be implemented in the database trigger.

If the schema of the database is defined, including json and ext.js, then each business module can call the database at will, and the data consistency logic and security guarantee will be managed in a unified manner, without worrying about the destruction of bad business codes or Which call will miss the update time field.

# Trigger configuration

Create ${table name}.schema.ext.js under the uniCloud/database directory of the project, the content is as follows.

module.exports = {
  trigger: {
	// Register the pre-read event of the data table
    beforeRead: async function (
	// Determine what kind of JQL command to listen to
	{
      collection,
      operation,
      where,
      field
    } = {}) {
		// When the above jql directive is triggered, the code here will be executed. Here is the normal uniCloud code, which can call various APIs of uniCloud.
		console.log("这个触发器被触发了")
    },
    afterRead: async function ({
      collection,
      operation,
      where,
      field
    } = {}) {

    }
  }
}

The above configuration will trigger beforeRead and afterRead when using jql to read the content of this table. In addition to these two timings, there are trigger timings such as beforeCount, afterCount, beforeCreate, afterCreate, beforeUpdate, afterUpdate, beforeDelete, afterDelete, which will be described in detail in subsequent chapters

The mechanism of introducing public modules in ext.js:

  • Common modules included in clientDB can be used in triggers when accessing through clientDB. For how to import public modules into clientDB, please refer to: jql depends on public modules
  • When using jql access through cloud functions/cloud objects, triggers can use public modules that cloud functions/cloud objects depend on.

# Trigger input parameter

All triggers are executed after data verification and permission verification, beforeXxx will be executed before the actual execution of the database command, and afterXxx will be executed after the actual execution of the database command.

The input parameters of the trigger are as follows, and the trigger parameters at different times are slightly different

参数名 类型 默认值 是否必备 说明
collection string - 当前表名
operation string - 当前操作类型:createupdatedeletereadcount
where object - 当前请求使用的查询条件(见下方说明)
field array<string> - read必备 当前请求访问的字段列表(见下方说明)
addDataList array<object> - create必备 新增操作传入的数据列表(见下方说明)
updateData object - update必备 更新操作传入的数据(见下方说明)
clientInfo object - 客户端信息,包括设备信息、用户token等,详见:clientInfo
userInfo object - 用户信息
result object - afterXxx内必备 本次请求结果
isEqualToJql function - 用于判断当前执行的jql语句和执行语句是否相等
triggerContext object - 用于在before和after内共享数据,新增于3.6.16
subCollection array - 请使用secondaryCollection替代此参数,此参数仍可访问只是会给出警告
secondaryCollection array - 获取联表查询的副表列表,新增于3.7.1
rawWhere object|string - 未经转化的原始查询条件,新增于3.7.0
rawGeoNear object - 未经转化的原始geoNear参数,新增于3.7.0
skip number - 跳过记录条数,新增于3.7.0
limit number - 返回的结果集(文档数量)的限制,新增于3.7.0
sample object - sample(随机选取)方法的参数,新增于3.7.0
docId string - doc方法的参数,数据库记录的_id,新增于3.7.0
isGetTempLookup boolean - 联表触发时必备 联表查询时用于标识,本次查询是否使用了getTemp,新增于3.7.1
primaryCollection string - 副表read必备 联表查询时主表的表名,新增于3.7.13

# secondaryCollection

仅read操作联表有此参数,新增于 3.7.1

Join table to query a list of sub-tables

// article.schema.ext.js
module.exports = {
  trigger: {
    beforeRead: async function({
      secondaryCollection
    } = {}) {
      if(secondaryCollection && secondaryCollection.length > 1) {
        throw new Error('仅允许关联一个副表')
      }
    }
  }
}

# where

read, count, delete, update operations may have this parameter

The where parameter received by the trigger is the converted query condition, which can be directly passed as a parameter to the where method of db.collection() and dbJql.collection(). When the jql statement uses the doc method, it will also be converted into where, such as: {_id: 'xxx'}

# docId

read, delete, update operations may have this parameter, new in 3.7.0

The document_id passed in the doc method received by the trigger

example

// article.schema.ext.js
module.exports = {
  trigger: {
    beforeDelete: async function({
      docId
    } = {}) {
      if(!docId) {
        throw new Error('仅能指定文档id删除')
      }
    }
  }
}

# rawWhere

read, delete, update operations may have this parameter, new in 3.7.0

The original parameters of the where or match method, without jql conversion. If it is a string or a database method is used, it can only be passed to the database instance returned by databaseForJQL, and cannot be passed to the database instance returned by the database method.

example

// article.schema.ext.js
module.exports = {
  trigger: {
    beforeDelete: async function({
      rawWhere
    } = {}) {
      if(rawWhere && typeof rawWhere !== string) {
        throw new Error('仅能使用字符串作为查询条件')
      }
    }
  }
}

# rawGeoNear

Only the read operation has this parameter, which was added in 3.7.0

The original parameters of the geoNear method have not been converted by jql. If the query is a string or a database method is used, it can only be passed to the database instance returned by databaseForJQL, and cannot be passed to the database instance returned by the database method.

# skip

Only the read operation has this parameter, which was added in 3.7.0

Number of documents skipped

example

// article.schema.ext.js
module.exports = {
  trigger: {
    beforeRead: async function({
      skip
    } = {}) {
      if(skip && skip > 10000) {
        throw new Error('无法访问10000条以后的数据')
      }
    }
  }
}

# limit

Only the read operation has this parameter, which was added in 3.7.0

The limit on the returned result set (number of documents)

example

// article.schema.ext.js
module.exports = {
  trigger: {
    beforeRead: async function({
      limit
    } = {}) {
      if(limit && limit > 100) {
        throw new Error('每次最多访问100条数据')
      }
    }
  }
}

# sample

Only the read operation has this parameter, which was added in 3.7.0

Parameters for the Random Screening Method

example

// article.schema.ext.js
module.exports = {
  trigger: {
    beforeRead: async function({
      sample
    } = {}) {
      if(sample && sample.size > 100) {
        throw new Error('每次最多随机筛选100条数据')
      }
    }
  }
}

# field

Only the read operation has this parameter

field is an array of all accessed fields, and nested fields will be flattened. When no field is passed, all fields will be returned

# addDataList

Only the create operation has this parameter

The list of the parameters of the database command add method and the defaultValue and forceDefaultValue in the schema combined. Note that for a unified data structure, when only one object is passed in the add method, this parameter is also an array with only one item.

If the data of addDataList is intercepted and modified before inserting data into the database, then the newly modified data will be inserted into the database.

# updateData

Only the update operation has this parameter

Parameter for the update method of the database directive.

If the updateData data is intercepted and modified before the data is modified in the database, the newly modified data will be updated into the database.

# userInfo

Added in HBuilderX 3.6.14

User information contains the following fields

Field Name Type Description
uid string|null user id, null when user information cannot be obtained
role array role list, default is an empty array
permission array permission list, default is an empty array

# result

Added in HBuilderX 3.6.14

As a result of this database operation, different operations return different structures. Modifications to the result object will be applied to the final returned result

Inquire

{
	data: [] // 获取到的数据列表
}

Query with count

{
	data: [], // 获取到的数据列表
  count: 0 // 符合条件的数据条数
}

count

{
  total: 0 // 符合条件的数据条数
}

** Add a single item **

{
	id: '' // 新增数据的id
}

Add more items

{
	ids: [], // 新增数据的id列表
	inserted: 3 // 新增成功的条数
}

update data

{
	updated: 1 // 更新的条数,数据更新前后无变化则更新条数为0
}

# isEqualToJql

Added in HBuilderX 3.6.14

A method for judging whether the jql statement currently executed by the trigger is equivalent to the statement passed in by the method.

In addition to using decomposed objects such as field and where, developers can also use isEqualToJql to determine what the currently executing JQL statement is.

If you simply use string comparison, developers will encounter reasons such as single and double quotes, newlines, etc., which cause the comparison to fail. So the isEqualToJql method is provided.

usage

isEqualToJql(string JQLString)

return value

This method returns a bool value, true means that the currently executed jql statement is equal to the incoming statement, otherwise it is not equal

example

// article.schema.ext.js
module.exports = {
  trigger: {
    beforeCount: async function({
      isEqualToJql
    } = {}) {
      if(isEqualToJql('db.collection("article").count()')) {
        console.log('成功匹配了JQL命令:对article表进行count计数且未带条件')
      } else {
        throw new Error('禁止执行带条件的count')
      }
    }
  }
}

# triggerContext

Added in HBuilderX 3.6.16

This parameter is an empty object, which is only used to mount data in before and get it in after

example

// article.schema.ext.js
module.exports = {
  trigger: {
    beforeUpdate: async function({
      triggerContext
    } = {}) {
      triggerContext.shareVar = 1
    },
    afterUpdate: async function(){
      if (triggerContext.shareVar === 1) {
        console.log('获取到的triggerContext.shareVar为1')
      }
    }
  }
}

# Trigger timing

触发时机 说明
beforeRead 读取前触发
afterRead 读取后触发
beforeCount 计数前触发
afterCount 计数后触发
beforeCreate 新增前触发
afterCreate 新增后触发
beforeUpdate 更新前触发
afterUpdate 更新后触发
beforeDelete 删除前触发
afterDelete 删除后触发
beforeReadAsSecondaryCollection 集合作为副表被读取前触发,仅使用了getTemp的联表查询才会触发
afterReadAsSecondaryCollection 集合作为副表被读取后触发,仅使用了getTemp的联表查询才会触发

Notice

  • count有两种触发情况一种是在数据库指令使用了count方法,另一种是在get方法内传getCount参数。HBuilderX 3.6.16版本之前,get方法内传getCount参数不会触发count触发器,HBuilderX 3.6.16及后续版本已修复此问题。

# Example

The following article table is taken as an example.

In order not to increase the complexity of the example, all permissions are set to true, do not imitate in actual projects.

// article.schema.ext.js
{
  "bsonType": "object",
  "required": ["title", "content"],
  "permission": {
    "read": true,
    "create": true,
    "update": true,
    "delete": true
  },
  "properties": {
    "_id": {
      "bsonType": "string"
    },
    "title": {
      "bsonType": "string"
    },
    "summary": {
      "bsonType": "string"
    },
    "content": {
      "bsonType": "string"
    },
    "author": {
      "bsonType": "string"
    },
    "view_count": {
      "bsonType": "int"
    },
    "create_date": {
      "bsonType": "timestamp"
    },
    "update_date": {
      "bsonType": "timestamp"
    }
  }
}

# Modify the article update time

// article.schema.ext.js
module.exports = {
  trigger: {
    beforeUpdate: async function({
      collection,
      operation,
      docId,
      updateData,
      clientInfo
    } = {}) {
      if(typeof docId === 'string' && (updateData.title || updateData.content)) { //如果字段较多,也可以不列举字段,删掉后半个判断
        if(updateData.content) {
          // updateData.summary = 'xxxx' // generate summary based on content
        }
        updateData.update_date = Date.now() // 更新数据的update_date字段赋值为当前服务器时间
      }
    }
  }
}

# Trigger after reading to increase the reading amount by 1

// article.schema.ext.js
module.exports = {
  trigger: {
    afterRead: async function({
      collection,
      operation,
      docId,
      field,
      clientInfo
    } = {}) {
      const db = uniCloud.database()
      // clientInfo.uniIdToken可以解出客户端用户信息,再进行判断是否应该加1。为了让示例简单清晰,此处省略相关逻辑
      if(typeof docId === 'string' && field.includes('content')) {
        // 读取了content字段后view_count加1
        await db.collection('article').doc(docId).update({
          view_count: db.command.inc(1)
        })
      }
    }
  }
}

# backup before deletion

// article.schema.ext.js
module.exports = {
  trigger: {
    beforeDelete: async function({
      collection,
      operation,
      docId,
      clientInfo
    } = {}) {
      const db = uniCloud.database()
      if(typeof docId !== 'string') { // 此处也可以加入管理员可以批量删除的逻辑
        throw new Error('禁止批量删除')
      }
      const res = await db.collection('article').doc(docId).get()
      const record = res.data[0]
      if(record) {
        await db.collection('article-archived').add(record)
      }
    }
  }
}

# Automatically add a summary when adding a new article

// article.schema.ext.js
module.exports = {
  trigger: {
    beforeCreate: async function({
      collection,
      operation,
      addDataList,
      clientInfo
    } = {}) {
      for(let i = 0; i < addDataList.length; i++) {
        const addDataItem = addDataList[i]
        if(!addDataItem.content) {
          throw new Error('缺少文章内容')
        }
        addDataItem.summary = addDataItem.content.slice(0, 100)
      }
    }
  }
}

# Use the jql syntax

The jql syntax can be used in the jql trigger to operate the database.

Note: Using jql syntax to operate the database in the trigger will also execute the trigger, which can easily lead to an infinite loop!

For this reason, the uniCloud.databaseForJQL method adds a parameter skipTrigger, which is used to specify that this database operation skips the execution of the trigger.

skipTrigger is a bool value, true skips the execution of the trigger, false continues to execute the trigger. The default is false.

This parameter does not take effect on the client side, but only on the cloud.

Examples are as follows:

uniCloud.databaseForJQL({
  clientInfo,
  skipTrigger: true // true跳过执行触发器,false则继续执行触发器。默认为false
})

We now add a reading record table, the schema is as follows

In order not to increase the complexity of the example, all permissions are set to true, do not imitate in actual projects.

// article.schema.ext.js
{
  "bsonType": "object",
  "required": ["title", "content"],
  "permission": {
    "read": true,
    "create": true,
    "update": true,
    "delete": true
  },
  "properties": {
    "_id": {
      "bsonType": "string"
    },
    "article_id": {
      "bsonType": "string",
      "foreignKey": "article._id"
    },
    "reader_id": {
      "bsonType": "string",
      "foreignKey": "uni-id-users._id",
      "forceDefaultValue": {
        "$env": "uid"
      }
    }
  }
}

Using the jql syntax can automatically verify the identity of the client. Still taking the above article table as an example, insert the reading record in the afterRead trigger. At this point, the reader id will be inserted into the reader_id field

module.exports = {
  trigger: {
    afterRead: async function ({
      docId,
      field,
      clientInfo
    } = {}) {
      if(typeof docId !== 'string' || !field.includes('content')) {
        return
      }
      const dbJQL = uniCloud.databaseForJQL({
        clientInfo,
        skipTrigger: true
      })
      await dbJQL.collection('article-view-log')
        .add({
          article_id: docId,
          reader_id: dbJQL.getCloudEnv('$cloudEnv_uid')
        })
    }
}

# Use extension libraries and common modules inside triggers

The public modules and extension libraries that schema extension depends on can also be used by action and validateFunction.

Built-in dependencies: Currently, the schema extension relies on uni-id or uni-id-common, and uni-id 3.0.7 and above also relies on uni-config-center, these two public modules can be used directly in the trigger. If redis is enabled in the service space, the redis extension can be directly used in the schema extension.

From HBuilderX 3.7.0, you can right-click on the uniCloud/database directory of the project to manage the public modules and extension libraries that the schema extension depends on. Also right-click on this directory and select Upload schema extension Js configuration to synchronize the configuration dependencies to the cloud.

For versions between HBuilderX 3.2.7 and HBuilderX 3.7.0, you can associate public modules with schema extensions by configuring "includeInClientDB":true in the package.json of the public modules to be used,This usage is not recommended for HBuilderX 3.7.0 and later versions

An example package.json of a common module used within JQL is below.

{
  "name": "test-common",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "keywords": [],
  "author": "",
  "license": "ISC",
  "includeInClientDB": true
}

After the association relationship is established through the above steps, the public module can be used normally in the database trigger or action cloud function.

Notice

  • 尽量不要依赖体积过大的公共模块,会延长冷启动时间
  • 仅能依赖公共模块,不能添加npm包依赖

# Precautions

  • Non-getTemp joint table query (not recommended usage) where obtained in the trigger is null, and field is all fields of the current table.
  • Only the trigger of the main table will be triggered when querying the joint table, and the trigger of the sub-table will not be triggered
  • When getTemp joins the table, the where and field in the getTemp where the main table is located will be passed to the trigger, and the where and field of the virtual join table will not be passed to the trigger
  • Content read through jql's redis cache will not trigger a read trigger
  • When using the jql data management function in HBuilderX to execute the jql statement, no trigger will be triggered

# Relationship with action cloud functions

  • Database triggers are safer than action cloud functions and will not be misspecified by the front end.
  • Database triggers support JQL syntax. The action cloud function only supports the traditional MongoDB method.
  • Database triggers can implement many common action cloud function functions without modifying schema and database instructions.
  • If you use database triggers and action cloud functions at the same time, note that the before of the trigger will be executed before the before of all actions are executed, and the after will be executed after the after of all actions are executed. Actions cannot catch errors thrown by triggers.