feat: improve code

This commit is contained in:
chenos 2021-09-16 00:38:48 +08:00
parent 09c42abdcc
commit b7eb720eb4
11 changed files with 164 additions and 275 deletions

View File

@ -7,6 +7,7 @@
"scripts": {
"bootstrap": "lerna bootstrap",
"clean": "lerna clean",
"examples": "ts-node-dev -r dotenv/config ./examples",
"start": "cd packages/app && npm start",
"start-client": "cd packages/app && npm run start-client",
"start-server": "nodemon",

View File

@ -6,6 +6,7 @@ import Database, { DatabaseOptions, TableOptions } from '@nocobase/database';
import Resourcer, { ResourceOptions } from '@nocobase/resourcer';
import { dataWrapping, table2resource } from './middlewares';
import { PluginType, Plugin, PluginOptions } from './plugin';
import { registerActions } from '@nocobase/actions';
export interface ResourcerOptions {
prefix?: string;
@ -20,6 +21,7 @@ export interface ApplicationOptions {
}
export class Application extends Koa {
public readonly db: Database;
public readonly resourcer: Resourcer;
@ -31,7 +33,12 @@ export class Application extends Koa {
constructor(options: ApplicationOptions) {
super();
this.db = new Database(options.database);
if (options.database instanceof Database) {
this.db = options.database;
} else {
this.db = new Database(options.database);
}
this.resourcer = new Resourcer({ ...options.resourcer });
this.cli = new Command();
@ -55,28 +62,30 @@ export class Application extends Koa {
});
if (options.dataWrapping !== false) {
this.use(dataWrapping);
this.use(dataWrapping());
}
this.use(table2resource);
this.use(table2resource());
this.use(this.resourcer.restApiMiddleware());
registerActions(this);
this.cli
.command('db sync')
.option('-f, --force')
.action(async (...args) => {
console.log('db sync');
console.log('db sync...');
const cli = args.pop();
const force = cli.opts()?.force;
await this.load();
await this.db.sync(
force
? {
force: true,
alter: {
drop: true,
},
}
force: true,
alter: {
drop: true,
},
}
: {},
);
await this.destroy();
@ -120,6 +129,10 @@ export class Application extends Koa {
return this.resourcer.define(options);
}
actions(handlers: any) {
return this.resourcer.registerActions(handlers);
}
command(nameAndArgs: string, opts?: CommandOptions) {
return this.cli.command(nameAndArgs, opts);
}
@ -216,58 +229,13 @@ export class Application extends Koa {
return true;
}
// registerPlugin(key: string | object, plugin?: any) {
// if (typeof key === 'object') {
// Object.keys(key).forEach((k) => {
// this.registerPlugin(k, key[k]);
// });
// } else {
// const config = {};
// if (Array.isArray(plugin)) {
// const [entry, options = {}] = plugin;
// Object.assign(config, { entry, options });
// } else {
// Object.assign(config, { entry: plugin, options: {} });
// }
// this.plugins.set(key, config);
// }
// }
// async loadPlugins() {
// await this.emitAsync('plugins.beforeLoad');
// const allPlugins = this.plugins.values();
// for (const plugin of allPlugins) {
// plugin.instance = await this.loadPlugin(plugin);
// }
// await this.emitAsync('plugins.afterLoad');
// }
async start(argv = process.argv) {
async parse(argv = process.argv) {
return this.cli.parseAsync(argv);
}
async destroy() {
await this.db.close();
}
// protected async loadPlugin({
// entry,
// options = {},
// }: {
// entry: string | Function;
// options: any;
// }) {
// let main: any;
// if (typeof entry === 'function') {
// main = entry;
// } else if (typeof entry === 'string') {
// const pathname = `${entry}/${
// __filename.endsWith('.ts') ? 'src' : 'lib'
// }/server`;
// main = require(pathname).default;
// }
// return main && (await main.call(this, options));
// }
}
export default Application;

View File

@ -1,31 +0,0 @@
import { Context, Next } from '@nocobase/actions';
export function actionParams(options: any = {}) {
return async (ctx: Context, next: Next) => {
const { actionName, resourceField, resourceName, fields = {} } = ctx.action.params;
const table = ctx.db.getTable(resourceField ? resourceField.options.target : resourceName);
// ctx.state.developerMode = {[Op.not]: null};
ctx.state.developerMode = false;
if (table && table.hasField('developerMode') && ctx.state.developerMode === false) {
ctx.action.mergeParams({ filter: { "developerMode.$isFalsy": true } }, { filter: 'and' });
}
if (table && ['get', 'list'].includes(actionName)) {
const except = [];
const appends = [];
for (const [name, field] of table.getFields()) {
if (field.options.hidden) {
except.push(field.options.name);
}
// if (field.options.appends) {
// appends.push(field.options.name);
// }
}
ctx.action.mergeParams({ fields: {
except,
appends
} }, { fields: 'append' });
// console.log('ctx.action.params.fields', ctx.action.params.fields, except, appends);
}
await next();
}
}

View File

@ -1,25 +0,0 @@
import path from 'path';
import send from 'koa-send';
import serve from 'koa-static';
import { Context, Next } from '@nocobase/actions';
export interface AppDistServeOptions {
root?: string;
useStaticServer?: boolean;
}
export function appDistServe(options: AppDistServeOptions = {}) {
return async (ctx: Context, next: Next) => {
if (!options.root || !options.useStaticServer) {
return next();
}
let root = options.root;
if (!root.startsWith('/')) {
root = path.resolve(process.cwd(), root);
}
await serve(root)(ctx, next);
if (ctx.status == 404) {
return send(ctx, 'index.html', { root });
}
}
}

View File

@ -1,29 +1,31 @@
import { Context, Next } from '@nocobase/actions';
export async function dataWrapping(ctx: Context, next: Next) {
await next();
if (ctx.withoutDataWrapping) {
return;
}
if (!ctx?.action?.params) {
return;
}
if (ctx.body instanceof Buffer) {
return;
}
if (!ctx.body) {
ctx.body = {};
}
const { rows, ...meta } = ctx.body;
if (rows) {
ctx.body = {
data: rows,
meta,
};
} else {
ctx.body = {
data: ctx.body,
};
export function dataWrapping() {
return async function dataWrapping(ctx: Context, next: Next) {
await next();
if (ctx.withoutDataWrapping) {
return;
}
if (!ctx?.action?.params) {
return;
}
if (ctx.body instanceof Buffer) {
return;
}
if (!ctx.body) {
ctx.body = {};
}
const { rows, ...meta } = ctx.body;
if (rows) {
ctx.body = {
data: rows,
meta,
};
} else {
ctx.body = {
data: ctx.body,
};
}
}
}

View File

@ -1,27 +0,0 @@
import { Context, Next } from '@nocobase/actions';
export interface DemoBlackListedActionsOptions {
emails?: string[];
blacklist?: string[];
}
const defaultBlacklist = ['create', 'update', 'destroy', 'sort', 'upload'];
export function demoBlacklistedActions(options: DemoBlackListedActionsOptions = {}) {
const { emails, blacklist = defaultBlacklist } = options;
return async (ctx: Context, next: Next) => {
const currentUser = ctx.state.currentUser;
if (currentUser && emails.includes(currentUser.email)) {
return next();
}
const { actionName } = ctx.action.params;
if (blacklist.includes(actionName)) {
ctx.body = {
data: {},
meta: {},
};
return;
}
await next();
}
}

View File

@ -1,5 +1,2 @@
export * from './action-params';
export * from './app-dist-serve';
export * from './demo-blacklisted-actions';
export * from './table2resource';
export * from './data-wrapping';

View File

@ -6,67 +6,69 @@ import {
} from '@nocobase/resourcer';
import { BELONGSTO, BELONGSTOMANY, HASMANY, HASONE } from '@nocobase/database';
export async function table2resource(
ctx: ResourcerContext,
next: () => Promise<any>,
) {
const resourcer = ctx.resourcer;
const database = ctx.database;
let params = parseRequest(
{
path: ctx.request.path,
method: ctx.request.method,
},
{
prefix: resourcer.options.prefix,
accessors: resourcer.options.accessors,
},
);
if (!params) {
return next();
}
const resourceName = getNameByParams(params);
// 如果资源名称未被定义
if (resourcer.isDefined(resourceName)) {
return next();
}
const [tableName, fieldName] = resourceName.split('.');
// 如果经过加载后是已经定义的表
if (!database.isDefined(tableName)) {
return next();
}
const table = database.getTable(tableName);
const field = table.getField(fieldName) as
| BELONGSTO
| HASMANY
| BELONGSTOMANY
| HASONE;
if (!fieldName || field) {
let resourceType: ResourceType = 'single';
let actions = {};
if (field) {
if (field instanceof HASONE) {
resourceType = 'hasOne';
} else if (field instanceof HASMANY) {
resourceType = 'hasMany';
} else if (field instanceof BELONGSTO) {
resourceType = 'belongsTo';
} else if (field instanceof BELONGSTOMANY) {
resourceType = 'belongsToMany';
}
if (field.options.actions) {
actions = field.options.actions || {};
}
} else {
actions = table.getOptions('actions') || {};
export function table2resource() {
return async function table2resource(
ctx: ResourcerContext,
next: () => Promise<any>,
) {
const resourcer = ctx.resourcer;
const database = ctx.db;
let params = parseRequest(
{
path: ctx.request.path,
method: ctx.request.method,
},
{
prefix: resourcer.options.prefix,
accessors: resourcer.options.accessors,
},
);
if (!params) {
return next();
}
resourcer.define({
type: resourceType,
name: resourceName,
actions,
});
}
return next();
const resourceName = getNameByParams(params);
// 如果资源名称未被定义
if (resourcer.isDefined(resourceName)) {
return next();
}
const [tableName, fieldName] = resourceName.split('.');
// 如果经过加载后是已经定义的表
if (!database.isDefined(tableName)) {
return next();
}
const table = database.getTable(tableName);
const field = table.getField(fieldName) as
| BELONGSTO
| HASMANY
| BELONGSTOMANY
| HASONE;
if (!fieldName || field) {
let resourceType: ResourceType = 'single';
let actions = {};
if (field) {
if (field instanceof HASONE) {
resourceType = 'hasOne';
} else if (field instanceof HASMANY) {
resourceType = 'hasMany';
} else if (field instanceof BELONGSTO) {
resourceType = 'belongsTo';
} else if (field instanceof BELONGSTOMANY) {
resourceType = 'belongsToMany';
}
if (field.options.actions) {
actions = field.options.actions || {};
}
} else {
actions = table.getOptions('actions') || {};
}
resourcer.define({
type: resourceType,
name: resourceName,
actions,
});
}
return next();
};
}
export default table2resource;

View File

@ -28,7 +28,7 @@ describe('mock server', () => {
});
it('resource', async () => {
const response = await api.resource('test').list();
const response = await api.agent().resource('test').list();
expect(response.body).toEqual([1, 2]);
});
});

View File

@ -1,5 +1,5 @@
import qs from 'qs';
import supertest from 'supertest';
import supertest, { SuperAgentTest } from 'supertest';
import Application, { ApplicationOptions } from '@nocobase/server';
import { getConfig } from './mockDatabase';
@ -46,59 +46,61 @@ interface Resource {
}
export class MockServer extends Application {
protected agentInstance: supertest.SuperAgentTest;
agent() {
if (!this.agentInstance) {
this.agentInstance = supertest.agent(this.callback());
}
return this.agentInstance;
}
resource(name: string) {
const agent = this.agent();
const keys = name.split('.');
agent(): SuperAgentTest & { resource: (name: string) => Resource } {
const agent = supertest.agent(this.callback());
const prefix = this.resourcer.options.prefix;
const proxy = new Proxy({}, {
get(target, method: string, receiver) {
return (params: ActionParams = {}) => {
const {
associatedKey,
resourceKey,
values = {},
file,
...restParams
} = params;
let url = prefix;
if (keys.length > 1) {
url = `/${keys[0]}/${associatedKey}/${keys[1]}`
} else {
url = `/${name}`;
}
url += `:${method as string}`;
if (resourceKey) {
url += `/${resourceKey}`;
}
console.log('request url: ' + url);
switch (method) {
case 'upload':
return agent
.post(`${url}?${qs.stringify(restParams)}`)
.attach('file', file)
.field(values);
case 'list':
case 'get':
return agent.get(`${url}?${qs.stringify(restParams)}`);
default:
return agent
.post(`${url}?${qs.stringify(restParams)}`)
.send(values);
if (method === 'resource') {
return (name: string) => {
const keys = name.split('.');
const proxy = new Proxy({}, {
get(target, method: string, receiver) {
return (params: ActionParams = {}) => {
const {
associatedKey,
resourceKey,
values = {},
file,
...restParams
} = params;
let url = prefix;
if (keys.length > 1) {
url = `/${keys[0]}/${associatedKey}/${keys[1]}`
} else {
url = `/${name}`;
}
url += `:${method as string}`;
if (resourceKey) {
url += `/${resourceKey}`;
}
console.log('request url: ' + url);
switch (method) {
case 'upload':
return agent
.post(`${url}?${qs.stringify(restParams)}`)
.attach('file', file)
.field(values);
case 'list':
case 'get':
return agent.get(`${url}?${qs.stringify(restParams)}`);
default:
return agent
.post(`${url}?${qs.stringify(restParams)}`)
.send(values);
}
};
},
});
return proxy;
}
}
return (...args: any[]) => {
return agent[method](...args);
};
},
}
});
return proxy as Resource;
return proxy as any;
}
}

View File

@ -16,7 +16,7 @@
"baseUrl": ".",
"paths": {
"@nocobase/*": [
"./packages/*"
"./packages/*/src"
]
}
}