Doc: api database events (#887)

* docs: add full sequelize model event types

* feat: update docs

* fix: types

* fix: update docs

* docs: refactor db event apis

* docs: add all events to db api

Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
Junyi 2022-10-14 17:22:32 +08:00 committed by GitHub
parent eaea8a7100
commit 4247c1b255
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 546 additions and 77 deletions

View File

@ -510,22 +510,6 @@ asset(post1.authorId === author1.id); // true
监听数据库事件。
Database 除了对 sequelize 原生的事件封装以外,还提供以下可监听事件类型:
| 事件名称 | 是否异步 | 描述 |
| --- | --- | --- |
| `'beforeDefineCollection'` | 否 | 定义 collection 之前触发 |
| `'afterDefineCollection'` | 否 | 定义 collection 之后触发 |
| `'beforeRemoveCollection'` | 否 | 移除 collection 之前触发 |
| `'afterRemoveCollection'` | 否 | 移除 collection 之后触发 |
| `<sequelize_model_global_event>` | - | 所有 sequelize 的全局事件均可通过此方式监听,详见示例部分,是否异步根据具体事件 |
| `<model_name>.<sequelize_model_event>` | 是 | 所有 sequelize model 的事件均可通过此方式监听,详见示例部分 |
| `<model_name>.afterCreateWithAssociations` | 是 | NocoBase 扩展的当连同关联数据一并创建记录成功后触发的事件(使用 Repository 的 create 方法会触发) |
| `<model_name>.afterUpdateWithAssociations` | 是 | NocoBase 扩展的当连同关联数据一并更新记录成功后触发的事件(使用 Repository 的 update 方法会触发) |
| `<model_name>.afterSaveWithAssociations` | 是 | NocoBase 扩展的当连同关联数据一并创建或更新记录成功后触发的事件(使用 Repository 的 create/update 方法都会触发) |
其中 `<model_name>.afterXXXWithAssociations` 事件只有在使用 Repository 的实例方法时才会被触发,所以建议大部分时候都使用 Repository 来进行数据操作。
**签名**
* `on(event: string, listener: (...args: any[]) => void | Promise<void>): void`
@ -537,39 +521,9 @@ Database 除了对 sequelize 原生的事件封装以外,还提供以下可监
| event | string | - | 事件名称 |
| listener | Function | - | 事件监听器 |
**示例**
事件名称默认支持 Sequelize 的 Model 事件。针对全局事件,通过 `<sequelize_model_global_event>` 的名称方式监听,针对单 Model 事件,通过 `<model_name>.<sequelize_model_event>` 的名称方式监听。
监听 books 表事件,当创建新记录成功后触发:
```ts
db.on('books.afterCreate', async (model, options) => {
const { transaction } = options;
const result = await model.constructor.findByPk(model.id, {
transaction
});
console.log(result);
});
```
监听 books 表,当连同关联数据一并创建成功后触发的事件:
```ts
db.on('books.afterCreateWithAssociations', async (model, options) => {
const { transaction } = options;
const result = await model.constructor.findByPk(model.id, {
transaction
});
console.log(result);
});
```
监听任意表的删除事件:
```ts
db.on('afterDestroy', async (model, options) => {
console.log(model);
});
```
所有内置的事件类型的参数说明和详细示例参考 [内置事件](#内置事件) 部分内容。
### `off()`
@ -753,3 +707,424 @@ export default extend({
以上两个文件如在调用 `import()` 时导入,通过 `extend()` 再次扩展以后books 表将拥有 `title``price` 两个字段。
此方法在扩展已有插件已定义的表结构时非常有用。
## 内置事件
数据库会在相应的生命周期触发以下对应的事件,通过 `on()` 方法订阅后进行特定的处理可满足一些业务需要。
### `'beforeSync'` / `'afterSync'`
当新的表结构配置(字段、索引等)被同步到数据库前后触发,通常在执行 `collection.sync()`(内部调用)时会触发,一般用于一些特殊的字段扩展的逻辑处理。
**签名**
```ts
on(eventName: `${string}.beforeSync` | 'beforeSync' | `${string}.afterSync` | 'afterSync', listener: SyncListener): this
```
**类型**
```ts
import type { SyncOptions, HookReturn } from 'sequelize/types';
type SyncListener = (options?: SyncOptions) => HookReturn;
```
**示例**
```ts
const users = db.collection({
name: 'users',
fields: [
{ type: 'string', name: 'username' }
]
});
db.on('beforeSync', async (options) => {
// do something
});
db.on('users.afterSync', async (options) => {
// do something
});
await users.sync();
```
### `'beforeValidate'` / `'afterValidate'`
创建或更新数据前会有基于 collection 定义的规则对数据的验证过程,在验证前后会触发对应事件。当调用 `repository.create()``repository.update()` 时会触发。
**签名**
```ts
on(eventName: `${string}.beforeValidate` | 'beforeValidate' | `${string}.afterValidate` | 'afterValidate', listener: ValidateListener): this
```
**类型**
```ts
import type { ValidationOptions } from 'sequelize/types/lib/instance-validator';
import type { HookReturn } from 'sequelize/types';
import type { Model } from '@nocobase/database';
type ValidateListener = (model: Model, options?: ValidationOptions) => HookReturn;
```
**示例**
```ts
db.collection({
name: 'tests',
fields: [
{
type: 'string',
name: 'email',
validate: {
isEmail: true,
},
}
],
});
// all models
db.on('beforeValidate', async (model, options) => {
// do something
});
// tests model
db.on('tests.beforeValidate', async (model, options) => {
// do something
});
// all models
db.on('afterValidate', async (model, options) => {
// do something
});
// tests model
db.on('tests.afterValidate', async (model, options) => {
// do something
});
const repository = db.getRepository('tests');
await repository.create({
values: {
email: 'abc', // checks for email format
},
});
// or
await repository.update({
filterByTk: 1,
values: {
email: 'abc', // checks for email format
},
});
```
### `'beforeCreate'` / `'afterCreate'`
创建一条数据前后会触发对应事件,当调用 `repository.create()` 时会触发。
**签名**
```ts
on(eventName: `${string}.beforeCreate` | 'beforeCreate' | `${string}.afterCreate` | 'afterCreate', listener: CreateListener): this
```
**类型**
```ts
import type { CreateOptions, HookReturn } from 'sequelize/types';
import type { Model } from '@nocobase/database';
export type CreateListener = (model: Model, options?: CreateOptions) => HookReturn;
```
**示例**
```ts
db.on('beforeCreate', async (model, options) => {
// do something
});
db.on('books.afterCreate', async (model, options) => {
const { transaction } = options;
const result = await model.constructor.findByPk(model.id, {
transaction
});
console.log(result);
});
```
### `'beforeUpdate'` / `'afterUpdate'`
更新一条数据前后会触发对应事件,当调用 `repository.update()` 时会触发。
**签名**
```ts
on(eventName: `${string}.beforeUpdate` | 'beforeUpdate' | `${string}.afterUpdate` | 'afterUpdate', listener: UpdateListener): this
```
**类型**
```ts
import type { UpdateOptions, HookReturn } from 'sequelize/types';
import type { Model } from '@nocobase/database';
export type UpdateListener = (model: Model, options?: UpdateOptions) => HookReturn;
```
**示例**
```ts
db.on('beforeUpdate', async (model, options) => {
// do something
});
db.on('books.afterUpdate', async (model, options) => {
// do something
});
```
### `'beforeSave'` / `'afterSave'`
创建或更新一条数据前后会触发对应事件,当调用 `repository.create()``repository.update()` 时会触发。
**签名**
```ts
on(eventName: `${string}.beforeSave` | 'beforeSave' | `${string}.afterSave` | 'afterSave', listener: SaveListener): this
```
**类型**
```ts
import type { SaveOptions, HookReturn } from 'sequelize/types';
import type { Model } from '@nocobase/database';
export type SaveListener = (model: Model, options?: SaveOptions) => HookReturn;
```
**示例**
```ts
db.on('beforeSave', async (model, options) => {
// do something
});
db.on('books.afterSave', async (model, options) => {
// do something
});
```
### `'beforeDestroy'` / `'afterDestroy'`
删除一条数据前后会触发对应事件,当调用 `repository.destroy()` 时会触发。
**签名**
```ts
on(eventName: `${string}.beforeDestroy` | 'beforeDestroy' | `${string}.afterDestroy` | 'afterDestroy', listener: DestroyListener): this
```
**类型**
```ts
import type { DestroyOptions, HookReturn } from 'sequelize/types';
import type { Model } from '@nocobase/database';
export type DestroyListener = (model: Model, options?: DestroyOptions) => HookReturn;
```
**示例**
```ts
db.on('beforeDestroy', async (model, options) => {
// do something
});
db.on('books.afterDestroy', async (model, options) => {
// do something
});
```
### `'afterCreateWithAssociations'`
创建一条携带层级关系数据的数据之后会触发对应事件,当调用 `repository.create()` 时会触发。
**签名**
```ts
on(eventName: `${string}.afterCreateWithAssociations` | 'afterCreateWithAssociations', listener: CreateWithAssociationsListener): this
```
**类型**
```ts
import type { CreateOptions, HookReturn } from 'sequelize/types';
import type { Model } from '@nocobase/database';
export type CreateWithAssociationsListener = (model: Model, options?: CreateOptions) => HookReturn;
```
**示例**
```ts
db.on('afterCreateWithAssociations', async (model, options) => {
// do something
});
db.on('books.afterCreateWithAssociations', async (model, options) => {
// do something
});
```
### `'afterUpdateWithAssociations'`
更新一条携带层级关系数据的数据之后会触发对应事件,当调用 `repository.update()` 时会触发。
**签名**
```ts
on(eventName: `${string}.afterUpdateWithAssociations` | 'afterUpdateWithAssociations', listener: CreateWithAssociationsListener): this
```
**类型**
```ts
import type { UpdateOptions, HookReturn } from 'sequelize/types';
import type { Model } from '@nocobase/database';
export type UpdateWithAssociationsListener = (model: Model, options?: UpdateOptions) => HookReturn;
```
**示例**
```ts
db.on('afterUpdateWithAssociations', async (model, options) => {
// do something
});
db.on('books.afterUpdateWithAssociations', async (model, options) => {
// do something
});
```
### `'afterSaveWithAssociations'`
创建或更新一条携带层级关系数据的数据之后会触发对应事件,当调用 `repository.create()``repository.update()` 时会触发。
**签名**
```ts
on(eventName: `${string}.afterSaveWithAssociations` | 'afterSaveWithAssociations', listener: SaveWithAssociationsListener): this
```
**类型**
```ts
import type { SaveOptions, HookReturn } from 'sequelize/types';
import type { Model } from '@nocobase/database';
export type SaveWithAssociationsListener = (model: Model, options?: SaveOptions) => HookReturn;
```
**示例**
```ts
db.on('afterSaveWithAssociations', async (model, options) => {
// do something
});
db.on('books.afterSaveWithAssociations', async (model, options) => {
// do something
});
```
### `'beforeDefineCollection'`
当定义一个数据表之前触发,如调用 `db.collection()` 时。
注:该事件是同步事件。
**签名**
```ts
on(eventName: 'beforeDefineCollection', listener: BeforeDefineCollectionListener): this
```
**类型**
```ts
import type { CollectionOptions } from '@nocobase/database';
export type BeforeDefineCollectionListener = (options: CollectionOptions) => void;
```
**示例**
```ts
db.on('beforeDefineCollection', (options) => {
// do something
});
```
### `'afterDefineCollection'`
当定义一个数据表之后触发,如调用 `db.collection()` 时。
注:该事件是同步事件。
**签名**
```ts
on(eventName: 'afterDefineCollection', listener: AfterDefineCollectionListener): this
```
**类型**
```ts
import type { Collection } from '@nocobase/database';
export type AfterDefineCollectionListener = (options: Collection) => void;
```
**示例**
```ts
db.on('afterDefineCollection', (collection) => {
// do something
});
```
### `'beforeRemoveCollection'` / `'afterRemoveCollection'`
当从内存中移除一个数据表前后触发,如调用 `db.removeCollection()` 时。
注:该事件是同步事件。
**签名**
```ts
on(eventName: 'beforeRemoveCollection' | 'afterRemoveCollection', listener: RemoveCollectionListener): this
```
**类型**
```ts
import type { Collection } from '@nocobase/database';
export type RemoveCollectionListener = (options: Collection) => void;
```
**示例**
```ts
db.on('beforeRemoveCollection', (collection) => {
// do something
});
db.on('afterRemoveCollection', (collection) => {
// do something
});
```

View File

@ -61,7 +61,7 @@
"temp-dir": "2.0.0",
"through2": "3.0.1",
"ts-loader": "^8.0.7",
"typescript": "^4.0.5",
"typescript": "4.5.5",
"update-notifier": "3.0.0",
"vinyl-fs": "3.0.3",
"yargs-parser": "13.1.2"

View File

@ -65,7 +65,7 @@ export class Collection<
return this.context.database;
}
constructor(options: CollectionOptions, context?: CollectionContext) {
constructor(options: CollectionOptions, context: CollectionContext) {
super();
this.checkOptions(options);

View File

@ -27,6 +27,33 @@ import { ModelHook } from './model-hook';
import extendOperators from './operators';
import { RelationRepository } from './relation-repository/relation-repository';
import { Repository } from './repository';
import {
AfterDefineCollectionListener,
BeforeDefineCollectionListener,
CreateListener,
CreateWithAssociationsListener,
DatabaseAfterDefineCollectionEventType,
DatabaseAfterRemoveCollectionEventType,
DatabaseBeforeDefineCollectionEventType,
DatabaseBeforeRemoveCollectionEventType,
DestroyListener,
EventType,
ModelCreateEventTypes,
ModelCreateWithAssociationsEventTypes,
ModelDestroyEventTypes,
ModelSaveEventTypes,
ModelSaveWithAssociationsEventTypes,
ModelUpdateEventTypes,
ModelUpdateWithAssociationsEventTypes,
ModelValidateEventTypes,
RemoveCollectionListener,
SaveListener,
SaveWithAssociationsListener,
SyncListener,
UpdateListener,
UpdateWithAssociationsListener,
ValidateListener
} from './types';
export interface MergeOptions extends merge.Options {}
@ -448,7 +475,20 @@ export class Database extends EventEmitter implements AsyncEmitter {
return this.sequelize.close();
}
on(event: string | symbol, listener): this {
on(event: EventType, listener: any): this;
on(event: ModelValidateEventTypes, listener: SyncListener): this;
on(event: ModelValidateEventTypes, listener: ValidateListener): this;
on(event: ModelCreateEventTypes, listener: CreateListener): this;
on(event: ModelUpdateEventTypes, listener: UpdateListener): this;
on(event: ModelSaveEventTypes, listener: SaveListener): this;
on(event: ModelDestroyEventTypes, listener: DestroyListener): this;
on(event: ModelCreateWithAssociationsEventTypes, listener: CreateWithAssociationsListener): this;
on(event: ModelUpdateWithAssociationsEventTypes, listener: UpdateWithAssociationsListener): this;
on(event: ModelSaveWithAssociationsEventTypes, listener: SaveWithAssociationsListener): this;
on(event: DatabaseBeforeDefineCollectionEventType, listener: BeforeDefineCollectionListener): this;
on(event: DatabaseAfterDefineCollectionEventType, listener: AfterDefineCollectionListener): this;
on(event: DatabaseBeforeRemoveCollectionEventType | DatabaseAfterRemoveCollectionEventType, listener: RemoveCollectionListener): this;
on(event: EventType, listener: any): this {
// NOTE: to match if event is a sequelize or model type
const type = this.modelHook.match(event);

View File

@ -9,14 +9,12 @@ export class ContextField extends Field {
return DataTypes[type.toUpperCase()] || DataTypes.STRING;
}
init() {
listener = async (model: Model, options) => {
const { name, dataIndex } = this.options;
this.listener = async (model: Model, options) => {
const { context } = options;
model.set(name, lodash.get(context, dataIndex));
model.changed(name, true);
};
}
const { context } = options;
model.set(name, lodash.get(context, dataIndex));
model.changed(name, true);
};
bind() {
super.bind();

View File

@ -9,6 +9,7 @@ import {
} from 'sequelize';
import { Collection } from '../collection';
import { Database } from '../database';
import { ModelEventTypes } from '../types';
import { checkIdentifier } from '../utils';
export interface FieldContext {
@ -68,7 +69,7 @@ export abstract class Field {
// code
}
on(eventName: string, listener: (...args: any[]) => void) {
on(eventName: ModelEventTypes, listener: (...args: any[]) => void) {
this.database.on(`${this.collection.name}.${eventName}`, listener);
return this;
}

View File

@ -274,7 +274,10 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
transaction,
};
return [await this.find(options), await this.count(options)];
const count = await this.count(options);
const results = count ? await this.find(options) : [];
return [results, count];
}
/**

View File

@ -0,0 +1,64 @@
import type { Model } from './model';
import type { ValidationOptions } from 'sequelize/types/lib/instance-validator';
import type { HookReturn } from 'sequelize/types/lib/hooks';
import type { CreateOptions, DestroyOptions, SaveOptions, SyncOptions, UpdateOptions } from 'sequelize/types';
import { Collection, CollectionOptions } from './collection';
export type CollectionNameType = string;
export type ModelSyncEventType = 'beforeSync' | 'afterSync';
export type ModelValidateEventType = 'beforeValidate' | 'afterValidate';
export type ModelCreateEventType = 'beforeCreate' | 'afterCreate';
export type ModelUpdateEventType = 'beforeUpdate' | 'afterUpdate';
export type ModelSaveEventType = 'beforeSave' | 'afterSave';
export type ModelDestroyEventType = 'beforeDestroy' | 'afterDestroy';
export type ModelCreateWithAssociationsEventType = 'afterCreateWithAssociations';
export type ModelUpdateWithAssociationsEventType = 'afterUpdateWithAssociations';
export type ModelSaveWithAssociationsEventType = 'afterSaveWithAssociations';
export type ModelValidateEventTypes = ModelValidateEventType | `${CollectionNameType}.${ModelValidateEventType}`;
export type ModelCreateEventTypes = ModelCreateEventType | `${CollectionNameType}.${ModelCreateEventType}`;
export type ModelUpdateEventTypes = ModelUpdateEventType | `${CollectionNameType}.${ModelUpdateEventType}`;
export type ModelSaveEventTypes = ModelSaveEventType | `${CollectionNameType}.${ModelSaveEventType}`;
export type ModelDestroyEventTypes = ModelDestroyEventType | `${CollectionNameType}.${ModelDestroyEventType}`;
export type ModelCreateWithAssociationsEventTypes = ModelCreateWithAssociationsEventType | `${CollectionNameType}.${ModelCreateWithAssociationsEventType}`;
export type ModelUpdateWithAssociationsEventTypes = ModelUpdateWithAssociationsEventType | `${CollectionNameType}.${ModelUpdateWithAssociationsEventType}`;
export type ModelSaveWithAssociationsEventTypes = ModelSaveWithAssociationsEventType | `${CollectionNameType}.${ModelSaveWithAssociationsEventType}`;
export type ModelEventTypes = ModelSyncEventType
| ModelValidateEventTypes
| ModelCreateEventTypes
| ModelUpdateEventTypes
| ModelSaveEventTypes
| ModelDestroyEventTypes
| ModelCreateWithAssociationsEventTypes
| ModelUpdateWithAssociationsEventTypes
| ModelSaveWithAssociationsEventTypes;
export type DatabaseBeforeDefineCollectionEventType = 'beforeDefineCollection';
export type DatabaseAfterDefineCollectionEventType = 'afterDefineCollection';
export type DatabaseBeforeRemoveCollectionEventType = 'beforeRemoveCollection';
export type DatabaseAfterRemoveCollectionEventType = 'afterRemoveCollection';
export type DatabaseEventTypes = DatabaseBeforeDefineCollectionEventType
| DatabaseAfterDefineCollectionEventType
| DatabaseBeforeRemoveCollectionEventType
| DatabaseAfterRemoveCollectionEventType;
export type EventType = ModelEventTypes | DatabaseEventTypes | string;
export type { HookReturn };
export type SyncListener = (options?: SyncOptions) => HookReturn;
export type ValidateListener = (model: Model, options?: ValidationOptions) => HookReturn;
export type CreateListener = (model: Model, options?: CreateOptions) => HookReturn;
export type UpdateListener = (model: Model, options?: UpdateOptions) => HookReturn;
export type SaveListener = (model: Model, options?: SaveOptions) => HookReturn;
export type DestroyListener = (model: Model, options?: DestroyOptions) => HookReturn;
export type CreateWithAssociationsListener = (model: Model, options?: CreateOptions) => HookReturn;
export type UpdateWithAssociationsListener = (model: Model, options?: UpdateOptions) => HookReturn;
export type SaveWithAssociationsListener = (model: Model, options?: SaveOptions) => HookReturn;
export type BeforeDefineCollectionListener = (options: CollectionOptions) => void;
export type AfterDefineCollectionListener = (collection: Collection) => void;
export type RemoveCollectionListener = (collection: Collection) => void;

View File

@ -55,19 +55,12 @@ export class CollectionManagerPlugin extends Plugin {
this.app.db.on('fields.beforeCreate', beforeCreateForChildrenCollection(this.app.db));
this.app.db.on('fields.beforeCreate', async (model, options) => {
const type = model.get('type');
await this.app.db.emitAsync(`fields.${type}.beforeInitOptions`, model, {
...options,
database: this.app.db,
});
});
for (const key in beforeInitOptions) {
if (Object.prototype.hasOwnProperty.call(beforeInitOptions, key)) {
const fn = beforeInitOptions[key];
this.app.db.on(`fields.${key}.beforeInitOptions`, fn);
const fn = beforeInitOptions[type];
if (fn) {
await fn(model, { database: this.app.db });
}
}
});
this.app.db.on('fields.afterCreate', afterCreateForReverseField(this.app.db));
this.app.db.on('collections.afterCreateWithAssociations', async (model, { context, transaction }) => {

View File

@ -22052,11 +22052,6 @@ typescript@^3.7.2:
resolved "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz#70f3910ac7a51ed6bef79da7800690b19bf778b8"
integrity sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==
typescript@^4.0.5:
version "4.6.4"
resolved "https://registry.npmjs.org/typescript/-/typescript-4.6.4.tgz#caa78bbc3a59e6a5c510d35703f6a09877ce45e9"
integrity sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==
uglify-js@^2.6.2, uglify-js@^2.8.22:
version "2.8.29"
resolved "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd"