refactor: users plugin

This commit is contained in:
chenos 2021-09-09 23:58:01 +08:00
parent 336e0b17b8
commit f300fd6ae9
11 changed files with 113 additions and 364 deletions

View File

@ -3,14 +3,13 @@
"version": "0.4.0-alpha.7",
"main": "lib/index.js",
"license": "MIT",
"devDependencies": {
"@nocobase/actions": "^0.4.0-alpha.7",
"@nocobase/server": "^0.4.0-alpha.7"
},
"dependencies": {
"@nocobase/database": "^0.4.0-alpha.7",
"@nocobase/resourcer": "^0.4.0-alpha.7",
"crypto-random-string": "^3.3.0"
},
"devDependencies": {
"@nocobase/test": "^0.4.0-alpha.7"
},
"gitHead": "f0b335ac30f29f25c95d7d137655fa64d8d67f1e"
}

View File

@ -1,111 +1,96 @@
import { getApp, getAgent } from '.';
// @ts-nocheck
import Database from '@nocobase/database';
import { mockServer, MockServer } from '@nocobase/test';
describe('user fields', () => {
let app;
let agent;
let db;
let api: MockServer;
let db: Database;
beforeEach(async () => {
app = await getApp();
agent = getAgent(app);
db = app.database;
api = mockServer();
api.registerPlugin('users', require('../server').default);
await api.loadPlugins();
db = api.database;
});
afterEach(async () => {
await db.sync();
await db.close();
});
describe('model definition', () => {
it('add model without createdBy/updatedBy field', async () => {
const Collection = db.getModel('collections');
await Collection.create({ name: 'posts', internal: true });
it('createdBy=false/updatedBy=false', async () => {
db.table({
name: 'posts',
createdBy: false,
updatedBy: false,
});
await db.sync();
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 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).toBeDefined();
expect(post.updated_by_id).toBeDefined();
});
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({
it('without createdBy/updatedBy', async () => {
db.table({
name: 'posts',
createdBy: { name: 'author', target: 'users' },
updatedBy: { name: 'editor', target: 'users' }
});
await db.sync();
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('add model and then add createdBy/updatedBy field', async () => {
const Collection = db.getModel('collections');
const collection = await Collection.import({
it('updatedBy=author/updatedBy=editor', async () => {
db.table({
name: 'posts',
createdBy: 'author',
updatedBy: 'editor',
});
await db.sync();
const Post = db.getModel('posts');
const post = await Post.create();
expect(post.author_id).toBeDefined();
expect(post.editor_id).toBeDefined();
});
it('createdBy=true/updatedBy=true', async () => {
db.table({
name: 'posts',
createdBy: true,
updatedBy: true,
});
await db.sync();
const Post = db.getModel('posts');
const post = await Post.create();
expect(post.created_by_id).toBeDefined();
expect(post.updated_by_id).toBeDefined();
});
it('add model and then add createdBy/updatedBy field', async () => {
db.table({
name: 'posts',
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',
@ -113,8 +98,8 @@ describe('user fields', () => {
},
]
});
const table = db.getTable('posts');
// console.log(table.getFields());
await db.sync();
const User = db.getModel('users');
const Post = db.getModel('posts');
@ -150,41 +135,13 @@ describe('user fields', () => {
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 });
db.table({ name: 'posts', createdBy: true, updatedBy: true });
await db.sync();
const User = db.getModel('users');
const currentUser = await User.create();
const user2 = await User.create();
@ -193,20 +150,21 @@ describe('user fields', () => {
const postWithoutUser = await Post.create();
expect(postWithoutUser.created_by_id).toBe(null);
expect(postWithoutUser.updated_by_id).toBe(null);
// @ts-ignore
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 数据不变
// @ts-ignore
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 });
db.table({ name: 'posts', createdBy: true, updatedBy: true });
await db.sync();
const User = db.getModel('users');
const user1 = await User.create();
const user2 = await User.create();
@ -215,6 +173,7 @@ describe('user fields', () => {
const post = await Post.create({
created_by_id: user1.id,
updated_by_id: user1.id,
// @ts-ignore
}, { context: { state: { currentUser: user2 } } });
expect(post.created_by_id).toBe(user1.id);
expect(post.updated_by_id).toBe(user1.id);
@ -223,9 +182,9 @@ describe('user fields', () => {
describe('updatedBy field', () => {
it('update data ', async () => {
const Collection = db.getModel('collections');
await Collection.create({
db.table({
name: 'posts',
createdBy: false,
updatedBy: true,
fields: [
{
@ -234,20 +193,20 @@ describe('user fields', () => {
}
]
});
await db.sync();
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);
// @ts-ignore
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({
db.table({
name: 'posts',
updatedBy: true,
fields: [
@ -257,6 +216,7 @@ describe('user fields', () => {
}
]
});
await db.sync();
const User = db.getModel('users');
const user1 = await User.create();
const user2 = await User.create();
@ -285,9 +245,10 @@ describe('user fields', () => {
});
it('bulkUpdate', async () => {
const Collection = db.getModel('collections');
const postCollection = await Collection.create({ name: 'posts', createdBy: false, updatedBy: true });
await postCollection.updateAssociations({
db.table({
name: 'posts',
createdBy: false,
updatedBy: true,
fields: [
{
type: 'string',
@ -295,6 +256,7 @@ describe('user fields', () => {
}
]
});
await db.sync();
const User = db.getModel('users');
const user1 = await User.create();
const user2 = await User.create();

View File

@ -1,138 +0,0 @@
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 '../../../server/src';
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 });
app.registerPlugin('collections', [path.resolve(__dirname, '../../../plugin-collections')]);
app.registerPlugin('users', [plugin]);
await app.loadPlugins();
await app.database.sync();
app.use(async (ctx, next) => {
ctx.db = app.database;
await next();
});
app.use(bodyParser());
app.use(middleware({
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}`;
}
}
});
};

View File

@ -1,8 +1,8 @@
import { actions } from '@nocobase/actions';
import { Context, Next } from '@nocobase/actions';
import { PASSWORD } from '@nocobase/database';
import cryptoRandomString from 'crypto-random-string';
export async function check(ctx: actions.Context, next: actions.Next) {
export async function check(ctx: Context, next: Next) {
if (ctx.state.currentUser) {
const user = ctx.state.currentUser.toJSON();
delete user.password;
@ -13,7 +13,7 @@ export async function check(ctx: actions.Context, next: actions.Next) {
}
}
export async function login(ctx: actions.Context, next: actions.Next) {
export async function login(ctx: Context, next: Next) {
const { uniqueField = 'email', values } = ctx.action.params;
// console.log(values);
if (!values[uniqueField]) {
@ -42,12 +42,12 @@ export async function login(ctx: actions.Context, next: actions.Next) {
await next();
}
export async function logout(ctx: actions.Context, next: actions.Next) {
export async function logout(ctx: Context, next: Next) {
ctx.body = {};
await next();
}
export async function register(ctx: actions.Context, next: actions.Next) {
export async function register(ctx: Context, next: Next) {
const User = ctx.db.getModel('users');
const { values } = ctx.action.params;
try {
@ -66,7 +66,7 @@ export async function register(ctx: actions.Context, next: actions.Next) {
await next();
}
export async function lostpassword(ctx: actions.Context, next: actions.Next) {
export async function lostpassword(ctx: Context, next: Next) {
const { values: { email } } = ctx.action.params;
if (!email) {
ctx.throw(401, '请填写邮箱账号');
@ -86,7 +86,7 @@ export async function lostpassword(ctx: actions.Context, next: actions.Next) {
await next();
}
export async function resetpassword(ctx: actions.Context, next: actions.Next) {
export async function resetpassword(ctx: Context, next: Next) {
const { values: { email, password, reset_token } } = ctx.action.params;
const User = ctx.db.getModel('users');
const user = await User.findOne({
@ -106,7 +106,7 @@ export async function resetpassword(ctx: actions.Context, next: actions.Next) {
await next();
}
export async function getUserByResetToken(ctx: actions.Context, next: actions.Next) {
export async function getUserByResetToken(ctx: Context, next: Next) {
const { token } = ctx.action.params;
const User = ctx.db.getModel('users');
const user = await User.findOne({
@ -121,7 +121,7 @@ export async function getUserByResetToken(ctx: actions.Context, next: actions.Ne
await next();
}
export async function updateProfile(ctx: actions.Context, next: actions.Next) {
export async function updateProfile(ctx: Context, next: Next) {
const { values } = ctx.action.params;
if (!ctx.state.currentUser) {
ctx.throw(401, 'Unauthorized');

View File

@ -15,10 +15,14 @@ export default class CreatedBy extends BELONGSTO {
constructor({ type, ...options }: CreatedByOptions, context: FieldContext) {
super({ ...options, type: 'belongsTo' } as BelongsToOptions, context);
const Model = context.sourceTable.getModel();
// const Model = context.sourceTable.getModel();
// TODO(feature): 可考虑策略模式,以在需要时对外提供接口
Model.addHook('beforeCreate', setUserValue.bind(this));
Model.addHook('beforeBulkCreate', CreatedBy.beforeBulkCreateHook.bind(this));
// Model.addHook('beforeCreate', setUserValue.bind(this));
// Model.addHook('beforeBulkCreate', CreatedBy.beforeBulkCreateHook.bind(this));
const { sourceTable, database } = context;
const name = sourceTable.getName();
database.on(`${name}.beforeCreate`, setUserValue.bind(this));
database.on(`${name}.beforeBulkCreate`, CreatedBy.beforeBulkCreateHook.bind(this));
}
public getDataType(): Function {

View File

@ -27,13 +27,18 @@ export default class UpdatedBy extends BELONGSTO {
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));
// 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));
const { sourceTable, database } = context;
const name = sourceTable.getName();
database.on(`${name}.beforeCreate`, setUserValue.bind(this));
database.on(`${name}.beforeBulkCreate`, UpdatedBy.beforeBulkCreateHook.bind(this));
database.on(`${name}.beforeUpdate`, setUserValue.bind(this));
database.on(`${name}.beforeBulkUpdate`, UpdatedBy.beforeBulkUpdateHook.bind(this));
}
public getDataType(): Function {

View File

@ -1,50 +0,0 @@
export 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 (table) {
const { createdBy, updatedBy } = table.getOptions();
const fieldsToMake = { createdBy, updatedBy };
Object.keys(fieldsToMake)
.filter(type => Boolean(fieldsToMake[type]))
.map(type => table.addField(makeOptions(type, fieldsToMake[type])));
}
// 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,
// }
// });
// }
// }

View File

@ -1,18 +0,0 @@
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);
// }
}

View File

@ -1,12 +0,0 @@
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);
}

View File

@ -1,12 +1,8 @@
import path from 'path';
import Database, { registerFields } from '@nocobase/database';
import Database, { registerFields, Table } from '@nocobase/database';
import Resourcer from '@nocobase/resourcer';
import * as fields from './fields';
// import hooks from './hooks';
import * as usersActions from './actions/users';
import { makeOptions } from './hooks/collection-after-create';
import * as middlewares from './middlewares';
export default async function (options = {}) {
@ -15,21 +11,22 @@ export default async function (options = {}) {
registerFields(fields);
database.addHook('afterTableInit', (table) => {
let { createdBy, updatedBy, internal } = table.getOptions();
// 非内置表,默认创建 createdBy 和 updatedBy
if (!internal) {
if (typeof createdBy === 'undefined') {
createdBy = true;
database.on('afterTableInit', (table: Table) => {
let { createdBy, updatedBy } = table.getOptions();
if (createdBy !== false) {
table.addField({
type: 'createdBy',
name: typeof createdBy === 'string' ? createdBy : 'createdBy',
target: 'users',
});
}
if (typeof updatedBy === 'undefined') {
updatedBy = true;
if (updatedBy !== false) {
table.addField({
type: 'updatedBy',
name: typeof updatedBy === 'string' ? updatedBy : 'updatedBy',
target: 'users',
});
}
}
const fieldsToMake = { createdBy, updatedBy };
Object.keys(fieldsToMake)
.filter(type => Boolean(fieldsToMake[type]))
.map(type => table.addField(makeOptions(type, fieldsToMake[type])));
});
database.import({

View File

@ -17608,7 +17608,7 @@ sequelize-pool@^6.0.0:
resolved "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-6.1.0.tgz#caaa0c1e324d3c2c3a399fed2c7998970925d668"
integrity sha512-4YwEw3ZgK/tY/so+GfnSgXkdwIJJ1I32uZJztIEgZeAO6HMgj64OzySbWLgxj+tXhZCJnzRfkY9gINw8Ft8ZMg==
sequelize@^6.3.3, sequelize@^6.3.4:
sequelize@^6.3.3:
version "6.6.5"
resolved "https://registry.npmjs.org/sequelize/-/sequelize-6.6.5.tgz#6f618e99f3df1fc81f28709e8a3139cec3ef1f0c"
integrity sha512-QyRrJrDRiwuiILqTMHUA1yWOPIL12KlfmgZ3hnzQwbMvp2vJ6fzu9bYJQB+qPMosck4mBUggY4Cjoc6Et8FBIQ==