diff --git a/packages/actions/src/actions/common.ts b/packages/actions/src/actions/common.ts index aa940a252..34862ac84 100644 --- a/packages/actions/src/actions/common.ts +++ b/packages/actions/src/actions/common.ts @@ -117,6 +117,7 @@ export async function create(ctx: Context, next: Next) { } = ctx.action.params; const values = filterByFields(data, fields); const transaction = await ctx.db.sequelize.transaction(); + const options = { transaction, context: ctx }; let model: Model; if (associated && resourceField) { const AssociatedModel = ctx.db.getModel(associatedName); @@ -125,15 +126,15 @@ export async function create(ctx: Context, next: Next) { } const { create } = resourceField.getAccessors(); // @ts-ignore - model = await associated[create](values, { transaction, context: ctx }); - await model.updateAssociations(values, { transaction, context: ctx }); + model = await associated[create](values, options); + await model.updateAssociations(values, options); ctx.body = model; } else { const ResourceModel = ctx.db.getModel(resourceName); // @ts-ignore - model = await ResourceModel.create(values, { transaction, context: ctx }); + model = await ResourceModel.create(values, options); // @ts-ignore - await model.updateAssociations(values, { transaction, context: ctx }); + await model.updateAssociations(values, options); ctx.body = model; } await transaction.commit(); @@ -219,6 +220,7 @@ export async function update(ctx: Context, next: Next) { } = ctx.action.params; const values = filterByFields(data, fields); const transaction = await ctx.db.sequelize.transaction(); + const options = { transaction, context: ctx }; if (associated && resourceField) { const AssociatedModel = ctx.db.getModel(associatedName); if (!(associated instanceof AssociatedModel)) { @@ -227,21 +229,20 @@ export async function update(ctx: Context, next: Next) { } const { get: getAccessor } = resourceField.getAccessors(); if (resourceField instanceof HASONE || resourceField instanceof BELONGSTO) { - let model: Model = await associated[getAccessor]({ transaction, context: ctx }); + let model: Model = await associated[getAccessor](options); if (model) { // @ts-ignore - await model.update(values, { transaction, context: ctx }); - await model.updateAssociations(values, { transaction, context: ctx }); + await model.update(values, options); + await model.updateAssociations(values, options); ctx.body = model; } } else if (resourceField instanceof HASMANY || resourceField instanceof BELONGSTOMANY) { const TargetModel = ctx.db.getModel(resourceField.getTarget()); const [model]: Model[] = await associated[getAccessor]({ + ...options, where: { [resourceKeyAttribute || resourceField.options.targetKey || TargetModel.primaryKeyAttribute]: resourceKey, - }, - transaction, - context: ctx, + } }); if (resourceField instanceof BELONGSTOMANY) { @@ -257,32 +258,30 @@ export async function update(ctx: Context, next: Next) { }, transaction }); - await through.updateAssociations(throughValues, { transaction, context: ctx }); - await through.update(throughValues, { transaction, context: ctx }); + await through.updateAssociations(throughValues, options); + await through.update(throughValues, options); delete values[throughName]; } } if (!_.isEmpty(values)) { // @ts-ignore - await model.update(values, { transaction, context: ctx }); - await model.updateAssociations(values, { transaction, context: ctx }); + await model.update(values, options); + await model.updateAssociations(values, options); } ctx.body = model; } } else { const Model = ctx.db.getModel(resourceName); const model = await Model.findOne({ + ...options, where: { [resourceKeyAttribute || Model.primaryKeyAttribute]: resourceKey, - }, - // @ts-ignore hooks 里添加 context - context: ctx, - transaction + } }); // @ts-ignore - await model.update(values, { transaction, context: ctx }); + await model.update(values, options); // @ts-ignore - await model.updateAssociations(values, { transaction, context: ctx }); + await model.updateAssociations(values, options); ctx.body = model; } await transaction.commit(); diff --git a/packages/database/src/fields/field-types.ts b/packages/database/src/fields/field-types.ts index 4b68631db..b890d91df 100644 --- a/packages/database/src/fields/field-types.ts +++ b/packages/database/src/fields/field-types.ts @@ -430,7 +430,7 @@ export abstract class Relation extends Field { return { as: name, ...restOptions, - } + }; } public getAssociationArguments() { @@ -438,8 +438,8 @@ export abstract class Relation extends Field { target: this.getTarget(), type: this.getAssociationType(), options: this.getAssociationOptions(), - } - }; + }; + } } class HasOneOrMany extends Relation { @@ -495,7 +495,7 @@ export class HASONE extends HasOneOrMany { } public getAssociationOptions(): HasOneOptions { - const { name, ...restOptions }= this.options; + const { name, ...restOptions } = this.options; return { as: name, ...restOptions, @@ -514,17 +514,16 @@ export class HASMANY extends HasOneOrMany { target = name; } - super({target, ...options}, context); - + super({ target, ...options }, context); this.targetTableInit(); } public getAssociationOptions(): HasManyOptions { - const { name, ...restOptions }= this.options; + const { name, ...restOptions } = this.options; return { as: name, ...restOptions, - } + }; } } @@ -539,7 +538,7 @@ export class BELONGSTO extends Relation { target = Utils.pluralize(name); } - super({target, ...options}, context); + super({ target, ...options }, context); this.targetTableInit(); this.updateOptionsAfterTargetModelBeDefined(); diff --git a/packages/plugin-collections/src/interfaces/types.ts b/packages/plugin-collections/src/interfaces/types.ts index 993c331b7..b32c6c5c6 100644 --- a/packages/plugin-collections/src/interfaces/types.ts +++ b/packages/plugin-collections/src/interfaces/types.ts @@ -344,6 +344,7 @@ export const createdBy = { options: { interface: 'createdBy', type: 'belongsTo', + name: 'createdBy', filterable: true, component: { type: 'drawerSelect', @@ -374,6 +375,7 @@ export const updatedBy = { disabled: true, options: { interface: 'updatedBy', + name: 'updatedBy', type: 'belongsTo', filterable: true, component: { diff --git a/packages/plugin-users/package.json b/packages/plugin-users/package.json index f9b0039d0..a5eae0671 100644 --- a/packages/plugin-users/package.json +++ b/packages/plugin-users/package.json @@ -11,6 +11,7 @@ }, "devDependencies": { "@nocobase/actions": "^0.3.0-alpha.0", + "@nocobase/server": "^0.3.0-alpha.0", "crypto-random-string": "^3.3.0", "umi": "^3.2.23" }, diff --git a/packages/plugin-users/src/__tests__/fields.test.ts b/packages/plugin-users/src/__tests__/fields.test.ts new file mode 100644 index 000000000..513509acf --- /dev/null +++ b/packages/plugin-users/src/__tests__/fields.test.ts @@ -0,0 +1,277 @@ +import { getApp, getAgent } from '.'; + +describe('user fields', () => { + let app; + let agent; + let db; + + beforeEach(async () => { + app = await getApp(); + agent = getAgent(app); + db = app.database; + await db.sync({ force: true }); + }); + + afterEach(async () => { + await db.close(); + }); + + describe('model definition', () => { + it('add model without createdBy/updatedBy field', async () => { + const Collection = db.getModel('collections'); + await Collection.create({ name: 'posts' }); + const Post = db.getModel('posts'); + + const post = await Post.create(); + expect(post.created_by_id).toBeUndefined(); + expect(post.updated_by_id).toBeUndefined(); + }); + + it('add model with named createdBy/updatedBy field', async () => { + const Collection = db.getModel('collections'); + await Collection.create({ name: 'posts', createdBy: 'author', updatedBy: 'editor' }); + const Post = db.getModel('posts'); + + const post = await Post.create(); + expect(post.author_id).toBeDefined(); + expect(post.editor_id).toBeDefined(); + }); + + it('add model with named createdBy/updatedBy field and target', async () => { + const Collection = db.getModel('collections'); + await Collection.create({ + name: 'posts', + createdBy: { name: 'author', target: 'users' }, + updatedBy: { name: 'editor', target: 'users' } + }); + const Post = db.getModel('posts'); + + const post = await Post.create(); + expect(post.author_id).toBeDefined(); + expect(post.editor_id).toBeDefined(); + }); + + it('add model with boolean createdBy/updatedBy field', async () => { + const Collection = db.getModel('collections'); + await Collection.create({ name: 'posts', createdBy: true, updatedBy: true }); + const Post = db.getModel('posts'); + + const post = await Post.create(); + expect(post.created_by_id).toBeDefined(); + expect(post.updated_by_id).toBeDefined(); + }); + + // TODO(bug): 重复添加字段不能与 fields 表同步,应做到同步 + it.only('add model and then add createdBy/updatedBy field', async () => { + const Collection = db.getModel('collections'); + const collection = await Collection.import({ + name: 'posts', + createdBy: true, + updatedBy: true, + fields: [ + { + interface: 'createdBy', + title: '创建人1', + type: 'createdBy', + name: 'createdBy1', + target: 'users', + foreignKey: 'created_by_id', + }, + { + interface: 'updatedBy', + title: '更新人1', + type: 'updatedBy', + name: 'updatedBy1', + target: 'users', + foreignKey: 'updated_by_id', + }, + { + interface: 'createdBy', + title: '创建人2', + type: 'createdBy', + name: 'createdBy2', + target: 'users', + foreignKey: 'created_by_id', + }, + { + interface: 'updatedBy', + title: '更新人2', + type: 'updatedBy', + name: 'updatedBy2', + target: 'users', + foreignKey: 'updated_by_id', + }, + ] + }); + const table = db.getTable('posts'); + // console.log(table.getFields()); + + const User = db.getModel('users'); + const Post = db.getModel('posts'); + + // 用户1 操作 + const user1 = await User.create(); + const postWithUser = await Post.create({}, { context: { state: { currentUser: user1 } } }); + + const post = await Post.findOne(Post.parseApiJson({ + filter: { + id: postWithUser.id, + }, + fields: ['createdBy1', 'updatedBy1', 'createdBy2', 'updatedBy2'], + })); + + expect(post.createdBy1.id).toBe(user1.id); + expect(post.updatedBy1.id).toBe(user1.id); + expect(post.createdBy2.id).toBe(user1.id); + expect(post.updatedBy2.id).toBe(user1.id); + + // 换个用户 + const user2 = await User.create(); + await postWithUser.update({title: 'title1'}, { context: { state: { currentUser: user2 } } }); + + const post2 = await Post.findOne(Post.parseApiJson({ + filter: { + id: postWithUser.id, + }, + fields: ['createdBy1', 'updatedBy1', 'createdBy2', 'updatedBy2'], + })); + + expect(post2.createdBy1.id).toBe(user1.id); + expect(post2.createdBy2.id).toBe(user1.id); + expect(post2.updatedBy1.id).toBe(user2.id); + expect(post2.updatedBy2.id).toBe(user2.id); + + + // const Collection = db.getModel('collections'); + // const collection = await Collection.create({ + // name: 'posts' + // }); + // const createdByField = await collection.createField({ type: 'createdBy', name: 'author', target: 'users' }); + // const updatedByField = await collection.createField({ type: 'updatedBy', name: 'editor', target: 'users' }); + + // const postTable = db.getTable('posts'); + // const Post = db.getModel('posts'); + + // // create data should contain added fields + // const post = await Post.create(); + // expect(post[postTable.getField(createdByField.get('name')).options.foreignKey]).toBeDefined(); + // expect(post[postTable.getField(updatedByField.get('name')).options.foreignKey]).toBeDefined(); + + // // add same type field twice should get same field + // const createdByField2 = await collection.createField({ type: 'createdBy', target: 'users' }); + // expect(createdByField2.get('name')).toBe(createdByField.get('name')); + + // // add same type field twice with a new name should get same field name as before + // const updatedByField2 = await collection.createField({ type: 'updatedBy', name: 'proofreader', target: 'users' }); + // expect(updatedByField2.get('name')).toBe(updatedByField.get('name')); + + // // delete field data should not really remove the column in table + // await createdByField2.destroy(); + // expect(postTable.getField('author')).toBeDefined(); + }); + }); + + describe('createdBy field', () => { + it('create data with createdBy/updatedBy field', async () => { + const Collection = db.getModel('collections'); + await Collection.create({ name: 'posts', createdBy: true, updatedBy: true }); + const User = db.getModel('users'); + const currentUser = await User.create(); + const user2 = await User.create(); + const Post = db.getModel('posts'); + + const postWithoutUser = await Post.create(); + expect(postWithoutUser.created_by_id).toBe(null); + expect(postWithoutUser.updated_by_id).toBe(null); + + const postWithUser = await Post.create({}, { context: { state: { currentUser } } }); + expect(postWithUser.created_by_id).toBe(currentUser.id); + expect(postWithUser.updated_by_id).toBe(currentUser.id); + + // 更新数据 createdBy 数据不变 + await postWithUser.update({title: 'title1'}, { context: { state: { currentUser: user2 } } }); + expect(postWithUser.created_by_id).toBe(currentUser.id); + expect(postWithUser.updated_by_id).toBe(user2.id); + }); + + it('create data with value of createdBy/updatedBy field', async () => { + const Collection = db.getModel('collections'); + await Collection.create({ name: 'posts', createdBy: true, updatedBy: true }); + const User = db.getModel('users'); + const user1 = await User.create(); + const user2 = await User.create(); + const Post = db.getModel('posts'); + + const post = await Post.create({ + created_by_id: user1.id, + updated_by_id: user1.id, + }, { context: { state: { currentUser: user2 } } }); + expect(post.created_by_id).toBe(user1.id); + expect(post.updated_by_id).toBe(user1.id); + }); + }); + + describe('updatedBy field', () => { + it('update data ', async () => { + const Collection = db.getModel('collections'); + await Collection.create({ + name: 'posts', + updatedBy: true, + fields: [ + { + type: 'string', + name: 'title' + } + ] + }); + const User = db.getModel('users'); + const currentUser = await User.create(); + const Post = db.getModel('posts'); + + const post = await Post.create(); + expect(post.updated_by_id).toBe(null); + + await post.update({ title: 'title' }, { context: { state: { currentUser } } }) + expect(post.updated_by_id).toBe(currentUser.id); + }); + + it('update data by different user', async () => { + const Collection = db.getModel('collections'); + await Collection.create({ + name: 'posts', + updatedBy: true, + fields: [ + { + type: 'string', + name: 'title' + } + ] + }); + const User = db.getModel('users'); + const user1 = await User.create(); + const user2 = await User.create(); + const Post = db.getModel('posts'); + let context = { state: { currentUser: user2 } }; + + const post = await Post.create({ + updated_by_id: user1.id, + }, { context }); + expect(post.updated_by_id).toBe(user1.id); + + await post.update({ title: 'title' }, { context }); + expect(post.updated_by_id).toBe(user2.id); + + await post.update({ title: 'title', updated_by_id: user1.id }, { context }); + expect(post.updated_by_id).toBe(user1.id); + + // 不同用户更新数据 + context = { state: { currentUser: user1 } }; + await post.update({ title: 'title234' }, { context }); + expect(post.updated_by_id).toBe(user1.id); + + // 重新查询 + const post2 = await Post.findByPk(post.id); + expect(post2.updated_by_id).toBe(user1.id); + }); + }); +}); diff --git a/packages/plugin-users/src/__tests__/index.ts b/packages/plugin-users/src/__tests__/index.ts new file mode 100644 index 000000000..c662d299d --- /dev/null +++ b/packages/plugin-users/src/__tests__/index.ts @@ -0,0 +1,142 @@ +import path from 'path'; +import qs from 'qs'; +import supertest from 'supertest'; +import bodyParser from 'koa-bodyparser'; +import { Dialect } from 'sequelize'; +import Database from '@nocobase/database'; +import { actions, middlewares } from '@nocobase/actions'; +import { Application, middleware } from '@nocobase/server'; +import plugin from '../server'; + +function getTestKey() { + const { id } = require.main; + const key = id + .replace(`${process.env.PWD}/packages`, '') + .replace(/src\/__tests__/g, '') + .replace('.test.ts', '') + .replace(/[^\w]/g, '_') + .replace(/_+/g, '_'); + return key +} + +const config = { + username: process.env.DB_USER, + password: process.env.DB_PASSWORD, + database: process.env.DB_DATABASE, + host: process.env.DB_HOST, + port: Number.parseInt(process.env.DB_PORT, 10), + dialect: process.env.DB_DIALECT as Dialect, + define: { + hooks: { + beforeCreate(model, options) { + + }, + }, + }, + logging: process.env.DB_LOG_SQL === 'on', + sync: { + force: true, + alter: { + drop: true, + }, + }, +}; + +export async function getApp() { + const app = new Application({ + database: { + ...config, + hooks: { + beforeDefine(columns, model) { + model.tableName = `${getTestKey()}_${model.tableName || model.name.plural}`; + } + }, + }, + resourcer: { + prefix: '/api', + }, + }); + app.resourcer.use(middlewares.associated); + app.resourcer.registerActionHandlers({...actions.associate, ...actions.common}); + await app.plugins([ + [path.resolve(__dirname, '../../../plugin-collections')], + [plugin] + ]); + await app.database.sync({ + force: true, + }); + app.use(async (ctx, next) => { + ctx.db = app.database; + await next(); + }); + app.use(bodyParser()); + app.use(middleware({ + prefix: '/api', + resourcer: app.resourcer, + database: app.database, + })); + return app; +} + +interface ActionParams { + resourceKey?: string | number; + // resourceName?: string; + // associatedName?: string; + associatedKey?: string | number; + fields?: any; + filter?: any; + values?: any; + [key: string]: any; +} + +interface Handler { + get: (params?: ActionParams) => Promise; + list: (params?: ActionParams) => Promise; + create: (params?: ActionParams) => Promise; + update: (params?: ActionParams) => Promise; + destroy: (params?: ActionParams) => Promise; + [name: string]: (params?: ActionParams) => Promise; +} + +export interface Agent { + resource: (name: string) => Handler; +} + +export function getAgent(app: Application): Agent { + const agent = supertest.agent(app.callback()); + return { + resource(name: string): any { + return new Proxy({}, { + get(target, method, receiver) { + return (params: ActionParams = {}) => { + const { associatedKey, resourceKey, values = {}, ...restParams } = params; + let url = `/api/${name}`; + if (associatedKey) { + url = `/api/${name.split('.').join(`/${associatedKey}/`)}`; + } + url += `:${method as string}`; + if (resourceKey) { + url += `/${resourceKey}`; + } + if (['list', 'get'].indexOf(method as string) !== -1) { + return agent.get(`${url}?${qs.stringify(restParams)}`); + } else { + return agent.post(`${url}?${qs.stringify(restParams)}`).send(values); + } + } + } + }); + } + }; +} + +export function getDatabase() { + return new Database({ + ...config, + hooks: { + beforeDefine(columns, model) { + model.tableName = `${getTestKey()}_${model.tableName || model.name.plural}`; + } + } + }); +}; diff --git a/packages/plugin-users/src/fields/CreatedBy.ts b/packages/plugin-users/src/fields/CreatedBy.ts new file mode 100644 index 000000000..1f3493ca4 --- /dev/null +++ b/packages/plugin-users/src/fields/CreatedBy.ts @@ -0,0 +1,27 @@ +import { BelongsToOptions, BELONGSTO, FieldContext } from '@nocobase/database'; +import { setUserValue } from './utils'; + +export interface CreatedByOptions extends Omit { + type: 'createdBy' | 'createdby' +} + +export default class CreatedBy extends BELONGSTO { + + static beforeBulkCreateHook(this: CreatedBy, models, { context }) { + models.forEach(model => { + setUserValue.call(this, model, { context }); + }); + } + + constructor({ type, ...options }: CreatedByOptions, context: FieldContext) { + super({ ...options, type: 'belongsTo' } as BelongsToOptions, context); + const Model = context.sourceTable.getModel(); + // TODO(feature): 可考虑策略模式,以在需要时对外提供接口 + Model.addHook('beforeCreate', setUserValue.bind(this)); + Model.addHook('beforeBulkCreate', CreatedBy.beforeBulkCreateHook.bind(this)); + } + + public getDataType(): Function { + return BELONGSTO; + } +} diff --git a/packages/plugin-users/src/fields/UpdatedBy.ts b/packages/plugin-users/src/fields/UpdatedBy.ts new file mode 100644 index 000000000..5e2ccef25 --- /dev/null +++ b/packages/plugin-users/src/fields/UpdatedBy.ts @@ -0,0 +1,36 @@ +import { BelongsToOptions, BELONGSTO, FieldContext } from '@nocobase/database'; +import { setUserValue } from './utils'; + +export interface UpdatedByOptions extends Omit { + type: 'updatedBy' | 'updatedby' +} + +export default class UpdatedBy extends BELONGSTO { + + static beforeBulkCreateHook(this: UpdatedBy, models, { context }) { + models.forEach(model => { + setUserValue.call(this, model, { context }); + }); + } + + static beforeBulkUpdateHook(this: UpdatedBy, models, { context }) { + models.forEach(model => { + setUserValue.call(this, model, { context }); + }); + } + + constructor({ type, ...options }: UpdatedByOptions, context: FieldContext) { + super({ ...options, type: 'belongsTo' } as BelongsToOptions, context); + const Model = context.sourceTable.getModel(); + // TODO(feature): 可考虑策略模式,以在需要时对外提供接口 + Model.addHook('beforeCreate', setUserValue.bind(this)); + Model.addHook('beforeBulkCreate', UpdatedBy.beforeBulkCreateHook.bind(this)); + + Model.addHook('beforeUpdate', setUserValue.bind(this)); + Model.addHook('beforeBulkUpdate', UpdatedBy.beforeBulkUpdateHook.bind(this)); + } + + public getDataType(): Function { + return BELONGSTO; + } +} diff --git a/packages/plugin-users/src/fields/index.ts b/packages/plugin-users/src/fields/index.ts new file mode 100644 index 000000000..dddf8ea33 --- /dev/null +++ b/packages/plugin-users/src/fields/index.ts @@ -0,0 +1,2 @@ +export { default as CreatedBy } from './CreatedBy'; +export { default as UpdatedBy } from './UpdatedBy'; diff --git a/packages/plugin-users/src/fields/utils.ts b/packages/plugin-users/src/fields/utils.ts new file mode 100644 index 000000000..8d04c7cbc --- /dev/null +++ b/packages/plugin-users/src/fields/utils.ts @@ -0,0 +1,23 @@ +import { CreatedBy, UpdatedBy } from "."; + +export function setUserValue(this: CreatedBy | UpdatedBy, model, { context }) { + const { foreignKey } = this.options; + // 已有外键数据(只在创建时生效) + if (model.getDataValue(foreignKey)) { + if (model.isNewRecord) { + return; + } + const changed = model.changed(); + if (Array.isArray(changed) && changed.find(key => key === foreignKey)) { + return; + } + } + if (!context) { + return; + } + const { currentUser } = context.state; + if (!currentUser) { + return; + } + model.set(foreignKey, currentUser.get(this.options.targetKey)); +} diff --git a/packages/plugin-users/src/hooks/collection-after-create.ts b/packages/plugin-users/src/hooks/collection-after-create.ts new file mode 100644 index 000000000..b78bf20b6 --- /dev/null +++ b/packages/plugin-users/src/hooks/collection-after-create.ts @@ -0,0 +1,42 @@ +function makeOptions(type: string, options: any) { + if (!options) { + return; + } + let name = type; + let target = 'users'; + switch (typeof options) { + case 'string': + name = options; + break; + // 今后支持多账号体系时可以扩展配置 + case 'object': + name = options.name || name; + target = options.target || target; + break; + } + return { + type, + name, + target + }; +} + +export default async function(model, options) { + const { database } = model; + const tableName = model.get('name') as string; + const table = database.getTable(tableName); + const { createdBy, updatedBy } = table.getOptions(); + const fieldsToMake = { createdBy, updatedBy }; + const addedFields = Object.keys(fieldsToMake) + .filter(type => Boolean(fieldsToMake[type])) + .map(type => table.addField(makeOptions(type, fieldsToMake[type]))); + + if (addedFields.length) { + await table.sync({ + force: false, + alter: { + drop: false, + } + }); + } +} diff --git a/packages/plugin-users/src/hooks/fields-before-create.ts b/packages/plugin-users/src/hooks/fields-before-create.ts new file mode 100644 index 000000000..857fa7204 --- /dev/null +++ b/packages/plugin-users/src/hooks/fields-before-create.ts @@ -0,0 +1,18 @@ +import { Model, getDataTypeKey, getField } from '@nocobase/database'; + +export default async function(model: Model, options) { +// const { database } = model; +// const { type, target, collection_name } = model.get(); +// const table = database.getTable(collection_name); +// const Type = getField(getDataTypeKey(type)); +// let Exist; +// for (const Field of table.getFields().values()) { +// if (Field instanceof Type && Field.options.target === target) { +// Exist = Field; +// break; +// } +// } +// if (Exist) { +// model.set('name', Exist.options.name); +// } +} diff --git a/packages/plugin-users/src/hooks/index.ts b/packages/plugin-users/src/hooks/index.ts new file mode 100644 index 000000000..273f30d70 --- /dev/null +++ b/packages/plugin-users/src/hooks/index.ts @@ -0,0 +1,12 @@ +import Database from '@nocobase/database'; +import collectionsAfterCreate from './collection-after-create'; +import fieldsBeforeCreate from './fields-before-create'; + +export default function () { + const database: Database = this.database; + // TODO(feature): 应该通过新的插件机制暴露接口,而不是直接访问其他插件的底层代码 + database.getModel('collections').addHook('afterCreate', collectionsAfterCreate); + + // 由于创建字段不是同时完成的,可能要在不同的 hook 里处理才行 + database.getModel('fields').addHook('beforeCreate', fieldsBeforeCreate); +} diff --git a/packages/plugin-users/src/server.ts b/packages/plugin-users/src/server.ts index 90d497c05..0aa891c2c 100644 --- a/packages/plugin-users/src/server.ts +++ b/packages/plugin-users/src/server.ts @@ -1,19 +1,29 @@ import path from 'path'; -import Database from '@nocobase/database'; + +import Database, { registerFields } from '@nocobase/database'; import Resourcer from '@nocobase/resourcer'; + +import * as fields from './fields'; +import hooks from './hooks'; import login from './actions/login'; import register from './actions/register'; import logout from './actions/logout'; import check from './actions/check'; + + export default async function (options = {}) { const database: Database = this.database; const resourcer: Resourcer = this.resourcer; + registerFields(fields); + database.import({ directory: path.resolve(__dirname, 'collections'), }); + hooks.call(this); + resourcer.registerActionHandlers({ 'users:login': login, 'users:register': register, diff --git a/packages/server/src/application.ts b/packages/server/src/application.ts index 610a286f3..3c8356627 100644 --- a/packages/server/src/application.ts +++ b/packages/server/src/application.ts @@ -20,25 +20,23 @@ export class Application extends Koa { } async plugins(plugins: any[]) { - for (const pluginOption of plugins) { - let plugin: Function; - let options = {}; - if (Array.isArray(pluginOption)) { - plugin = pluginOption.shift(); - options = pluginOption.shift()||{}; - if (typeof plugin === 'function') { - plugin = plugin.bind(this); - } else if (typeof plugin === 'string') { - const libDir = __filename.endsWith('.ts') ? 'src' : 'lib'; - plugin = require(`${plugin}/${libDir}/server`).default; - plugin = plugin.bind(this); - } - } else if (typeof pluginOption === 'function') { - plugin = pluginOption.bind(this); + for (const pluginOptions of plugins) { + if (Array.isArray(pluginOptions)) { + const [entry, options = {}] = pluginOptions; + await this.plugin(entry, options); + } else { + await this.plugin(pluginOptions); } - await plugin(options); } } + + async plugin(entry: string | Function, options: any = {}) { + const main = typeof entry === 'function' + ? entry + : require(`${entry}/${__filename.endsWith('.ts') ? 'src' : 'lib'}/server`).default; + + await main.call(this, options); + } } export default Application;