feat: add createdBy/updatedBy field config for table managed by collections (#43)
* feat: add createdBy/updatedBy field config for table managed by collections * fix: update by different users and field initialization * fix: function argument * add: test cases Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
parent
59db7a34dc
commit
110d5bcc9c
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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: {
|
||||
|
@ -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"
|
||||
},
|
||||
|
277
packages/plugin-users/src/__tests__/fields.test.ts
Normal file
277
packages/plugin-users/src/__tests__/fields.test.ts
Normal file
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
142
packages/plugin-users/src/__tests__/index.ts
Normal file
142
packages/plugin-users/src/__tests__/index.ts
Normal file
@ -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<supertest.Response>;
|
||||
list: (params?: ActionParams) => Promise<supertest.Response>;
|
||||
create: (params?: ActionParams) => Promise<supertest.Response>;
|
||||
update: (params?: ActionParams) => Promise<supertest.Response>;
|
||||
destroy: (params?: ActionParams) => Promise<supertest.Response>;
|
||||
[name: string]: (params?: ActionParams) => Promise<supertest.Response>;
|
||||
}
|
||||
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
27
packages/plugin-users/src/fields/CreatedBy.ts
Normal file
27
packages/plugin-users/src/fields/CreatedBy.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { BelongsToOptions, BELONGSTO, FieldContext } from '@nocobase/database';
|
||||
import { setUserValue } from './utils';
|
||||
|
||||
export interface CreatedByOptions extends Omit<BelongsToOptions, 'type'> {
|
||||
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;
|
||||
}
|
||||
}
|
36
packages/plugin-users/src/fields/UpdatedBy.ts
Normal file
36
packages/plugin-users/src/fields/UpdatedBy.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { BelongsToOptions, BELONGSTO, FieldContext } from '@nocobase/database';
|
||||
import { setUserValue } from './utils';
|
||||
|
||||
export interface UpdatedByOptions extends Omit<BelongsToOptions, 'type'> {
|
||||
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;
|
||||
}
|
||||
}
|
2
packages/plugin-users/src/fields/index.ts
Normal file
2
packages/plugin-users/src/fields/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { default as CreatedBy } from './CreatedBy';
|
||||
export { default as UpdatedBy } from './UpdatedBy';
|
23
packages/plugin-users/src/fields/utils.ts
Normal file
23
packages/plugin-users/src/fields/utils.ts
Normal file
@ -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));
|
||||
}
|
42
packages/plugin-users/src/hooks/collection-after-create.ts
Normal file
42
packages/plugin-users/src/hooks/collection-after-create.ts
Normal file
@ -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,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
18
packages/plugin-users/src/hooks/fields-before-create.ts
Normal file
18
packages/plugin-users/src/hooks/fields-before-create.ts
Normal file
@ -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);
|
||||
// }
|
||||
}
|
12
packages/plugin-users/src/hooks/index.ts
Normal file
12
packages/plugin-users/src/hooks/index.ts
Normal file
@ -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);
|
||||
}
|
@ -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,
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user