feat: improve code

This commit is contained in:
chenos 2021-09-23 00:16:04 +08:00
parent a51c5058fb
commit 27f6bde775
52 changed files with 1324 additions and 857 deletions

View File

@ -10,7 +10,7 @@
"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",
"start-server": "ts-node-dev -r dotenv/config ./packages/api/src/index.ts",
"start-docs": "dumi dev",
"build-docs": "dumi build",
"build2": "lerna run build",
@ -26,7 +26,7 @@
},
"devDependencies": {
"@types/file-saver": "^2.0.3",
"@types/jest": "^24.0.18",
"@types/jest": "^27.0.1",
"@types/koa": "^2.13.1",
"@types/koa-mount": "^4.0.1",
"@types/lodash": "^4.14.169",

View File

@ -1,15 +1,6 @@
#!/usr/bin/env node
const keys = process.argv;
const key = keys.pop();
const dotenv = require('dotenv');
dotenv.config();
if (key === 'start') {
require('../lib/index');
} else if (key === 'db-init') {
require('../lib/migrations/init');
}
require('../lib/index');

View File

@ -1,58 +0,0 @@
import Server from '@nocobase/server';
// @ts-ignore
const sync = global.sync || {
force: false,
alter: {
drop: false,
},
};
console.log('process.env.NOCOBASE_ENV', process.env.NOCOBASE_ENV);
const api = new Server({
database: {
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
host: process.env.DB_HOST,
port: process.env.DB_PORT as any,
dialect: process.env.DB_DIALECT as any,
dialectOptions: {
charset: 'utf8mb4',
collate: 'utf8mb4_unicode_ci',
},
pool: {
max: 5,
min: 0,
acquire: 60000,
idle: 10000,
},
logging: process.env.DB_LOG_SQL === 'on' ? console.log : false,
define: {},
sync,
},
resourcer: {
prefix: '/api',
},
});
const plugins = [
'@nocobase/plugin-collections',
'@nocobase/plugin-ui-router',
'@nocobase/plugin-ui-schema',
'@nocobase/plugin-users',
'@nocobase/plugin-action-logs',
'@nocobase/plugin-file-manager',
'@nocobase/plugin-permissions',
'@nocobase/plugin-export',
'@nocobase/plugin-system-settings',
// // '@nocobase/plugin-automations',
'@nocobase/plugin-china-region',
];
for (const plugin of plugins) {
api.registerPlugin(plugin, [require(`${plugin}/${__filename.endsWith('.ts') ? 'src' : 'lib'}/server`).default]);
}
export default api;

View File

@ -1,30 +1,65 @@
import api from './app';
import { middlewares } from '@nocobase/server';
import Server from '@nocobase/server';
const api = new Server({
database: {
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
host: process.env.DB_HOST,
port: process.env.DB_PORT as any,
dialect: process.env.DB_DIALECT as any,
dialectOptions: {
charset: 'utf8mb4',
collate: 'utf8mb4_unicode_ci',
},
pool: {
max: 5,
min: 0,
acquire: 60000,
idle: 10000,
},
logging: process.env.DB_LOG_SQL === 'on' ? console.log : false,
define: {},
sync: {
force: false,
alter: {
drop: false,
},
},
},
resourcer: {
prefix: '/api',
},
});
const plugins = [
'@nocobase/plugin-collections',
'@nocobase/plugin-ui-router',
'@nocobase/plugin-ui-schema',
'@nocobase/plugin-users',
'@nocobase/plugin-action-logs',
'@nocobase/plugin-file-manager',
'@nocobase/plugin-permissions',
'@nocobase/plugin-export',
'@nocobase/plugin-system-settings',
'@nocobase/plugin-china-region',
];
for (const plugin of plugins) {
api.plugin(require(`${plugin}/${__filename.endsWith('.ts') ? 'src' : 'lib'}/server`).default);
}
(async () => {
api.resourcer.use(middlewares.actionParams());
api.on('plugins.afterLoad', async () => {
console.log('plugins.afterLoad')
if (process.env.NOCOBASE_ENV === 'demo') {
api.resourcer.use(middlewares.demoBlacklistedActions({
emails: [process.env.ADMIN_EMAIL],
}));
}
api.use(middlewares.appDistServe({
root: process.env.APP_DIST,
useStaticServer: !(process.env.APP_USE_STATIC_SERVER === 'false' || !process.env.APP_USE_STATIC_SERVER),
}));
});
const start = Date.now();
if (process.argv.length < 3) {
process.argv.push('start', '--port', process.env.API_PORT);
}
await api.start(process.argv);
console.log(process.argv);
await api.parse(process.argv);
console.log(api.db.getTables().map(t => t.getName()));
console.log(`Start-up time: ${(Date.now() - start) / 1000}s`);
console.log(`http://localhost:${process.env.API_PORT}/`);
// console.log(`http://localhost:${process.env.API_PORT}/`);
})();

View File

@ -1,15 +0,0 @@
// @ts-ignore
const keys = process.argv;
// @ts-ignore
global.sync = {
force: false,
alter: {
drop: false,
},
};
// @ts-ignore
const filename: string = keys.pop();
require(`./migrations/${filename}`);

View File

@ -1,125 +0,0 @@
// @ts-ignore
global.sync = {
force: true,
alter: {
drop: true,
},
};
import Database from '@nocobase/database';
import api from '../app';
import * as uiSchema from './ui-schema';
(async () => {
await api.loadPlugins();
const database: Database = api.db;
await database.sync({
// tables: ['collections', 'fields', 'actions', 'views', 'tabs'],
});
const config =
require('@nocobase/plugin-users/src/collections/users').default;
const Collection = database.getModel('collections');
const collection = await Collection.create(config);
await collection.updateAssociations({
generalFields: config.fields.filter((field) => field.state !== 0),
systemFields: config.fields.filter((field) => field.state === 0),
});
await collection.migrate();
const Route = database.getModel('routes');
const data = [
{
type: 'redirect',
from: '/',
to: '/admin',
exact: true,
},
{
type: 'route',
path: '/admin/:name(.+)?',
component: 'AdminLayout',
title: `后台`,
uiSchema: uiSchema.menu,
},
{
type: 'route',
component: 'AuthLayout',
children: [
{
type: 'route',
path: '/login',
component: 'RouteSchemaRenderer',
title: `登录`,
uiSchema: uiSchema.login,
},
{
type: 'route',
path: '/register',
component: 'RouteSchemaRenderer',
title: `注册`,
uiSchema: uiSchema.register,
},
],
},
];
for (const item of data) {
const route = await Route.create(item);
await route.updateAssociations(item);
}
const Storage = database.getModel('storages');
await Storage.create({
title: '本地存储',
name: `local`,
type: 'local',
baseUrl: process.env.LOCAL_STORAGE_BASE_URL,
default: process.env.STORAGE_TYPE === 'local',
});
await Storage.create({
name: `ali-oss`,
type: 'ali-oss',
baseUrl: process.env.ALI_OSS_STORAGE_BASE_URL,
options: {
region: process.env.ALI_OSS_REGION,
accessKeyId: process.env.ALI_OSS_ACCESS_KEY_ID,
accessKeySecret: process.env.ALI_OSS_ACCESS_KEY_SECRET,
bucket: process.env.ALI_OSS_BUCKET,
},
default: process.env.STORAGE_TYPE === 'ali-oss',
});
// 导入地域数据
const ChinaRegion = database.getModel('china_regions');
ChinaRegion && (await ChinaRegion.importData());
const SystemSetting = database.getModel('system_settings');
if (SystemSetting) {
const setting = await SystemSetting.create({
title: 'NocoBase',
showLogoOnly: true,
});
await setting.updateAssociations({
logo: {
title: 'nocobase-logo',
filename: '682e5ad037dd02a0fe4800a3e91c283b.png',
extname: '.png',
mimetype: 'image/png',
url: 'https://nocobase.oss-cn-beijing.aliyuncs.com/682e5ad037dd02a0fe4800a3e91c283b.png',
storage_id: 2,
},
});
}
const User = database.getModel('users');
const user = await User.create({
nickname: '超级管理员',
email: process.env.ADMIN_EMAIL,
password: process.env.ADMIN_PASSWORD,
});
await database.close();
})();

View File

@ -1,11 +0,0 @@
import Database from '@nocobase/database';
import api from '../app';
(async () => {
await api.loadPlugins();
const database: Database = api.db;
await database.sync({
// tables: ['collections', 'fields', 'actions', 'views', 'tabs'],
});
await database.close();
})();

View File

@ -1,3 +0,0 @@
export * from './login';
export * from './menu';
export * from './register';

View File

@ -1,64 +0,0 @@
export const login = {
key: 'dtf9j0b8p9u',
type: 'object',
title: '登录',
properties: {
email: {
type: 'string',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: '电子邮箱',
style: {
// width: 240,
},
},
},
password: {
type: 'string',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Password',
'x-component-props': {
placeholder: '密码',
style: {
// width: 240,
},
},
},
actions: {
type: 'void',
'x-component': 'Div',
properties: {
submit: {
type: 'void',
'x-component': 'Action',
'x-component-props': {
block: true,
type: 'primary',
useAction: '{{ useLogin }}',
style: {
width: '100%',
},
},
title: '登录',
},
},
},
registerlink: {
type: 'void',
'x-component': 'Div',
properties: {
link: {
type: 'void',
'x-component': 'Action.Link',
'x-component-props': {
to: '/register',
},
title: '注册账号',
},
},
},
},
};

View File

@ -1,15 +0,0 @@
export const menu = {
key: 'qqzzjakwkwl',
name: 'qqzzjakwkwl',
type: 'void',
'x-component': 'Menu',
'x-designable-bar': 'Menu.DesignableBar',
'x-component-props': {
mode: 'mix',
theme: 'dark',
defaultSelectedKeys: '{{ selectedKeys }}',
sideMenuRef: '{{ sideMenuRef }}',
onSelect: '{{ onSelect }}',
onRemove: '{{ onMenuItemRemove }}',
},
};

View File

@ -1,100 +0,0 @@
export const register = {
key: '46qlxqam3xk',
type: 'object',
title: '注册',
properties: {
email: {
type: 'string',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: '电子邮箱',
style: {
// width: 240,
},
},
},
password: {
type: 'string',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Password',
'x-component-props': {
placeholder: '密码',
checkStrength: true,
style: {
// width: 240,
},
},
'x-reactions': [
{
dependencies: ['.confirm_password'],
fulfill: {
state: {
errors:
'{{$deps[0] && $self.value && $self.value !== $deps[0] ? "确认密码不匹配" : ""}}',
},
},
},
],
},
confirm_password: {
type: 'string',
required: true,
'x-decorator': 'FormItem',
'x-component': 'Password',
'x-component-props': {
placeholder: '确认密码',
checkStrength: true,
style: {
// width: 240,
},
},
'x-reactions': [
{
dependencies: ['.password'],
fulfill: {
state: {
errors:
'{{$deps[0] && $self.value && $self.value !== $deps[0] ? "确认密码不匹配" : ""}}',
},
},
},
],
},
actions: {
type: 'void',
'x-component': 'Div',
properties: {
submit: {
type: 'void',
title: '注册',
'x-component': 'Action',
'x-component-props': {
block: true,
type: 'primary',
useAction: '{{ useRegister }}',
style: {
width: '100%',
},
},
},
},
},
registerlink: {
type: 'void',
'x-component': 'Div',
properties: {
link: {
type: 'void',
'x-component': 'Action.Link',
'x-component-props': {
to: '/login',
},
title: '使用已有账号登录',
},
},
},
},
}

View File

@ -48,6 +48,32 @@ describe('has many field', () => {
]);
});
it.only('custom sourceKey', async () => {
const collection = db.collection({
name: 'posts',
schema: [
{ type: 'string', name: 'key', unique: true },
{
type: 'hasMany',
name: 'comments',
sourceKey: 'key',
// foreignKey: 'postKey',
},
],
});
const comments = db.collection({
name: 'comments',
schema: [],
});
const association = collection.model.associations.comments;
expect(association).toBeDefined();
expect(association.foreignKey).toBe('postKey');
// @ts-ignore
expect(association.sourceKey).toBe('key');
expect(comments.model.rawAttributes['postKey']).toBeDefined();
await db.sync();
});
it('custom sourceKey and foreignKey', async () => {
const collection = db.collection({
name: 'posts',

View File

@ -0,0 +1,67 @@
import { Database } from '../../database';
import { mockDatabase } from '../';
describe('has many field', () => {
let db: Database;
beforeEach(() => {
db = mockDatabase();
});
afterEach(async () => {
await db.close();
});
it('association undefined', async () => {
const User = db.collection({
name: 'users',
schema: [{ type: 'hasOne', name: 'profile' }],
});
await db.sync();
expect(User.model.associations.profile).toBeUndefined();
});
it('association defined', async () => {
const User = db.collection({
name: 'users',
schema: [{ type: 'hasOne', name: 'profile' }],
});
expect(User.model.associations.phone).toBeUndefined();
const Profile = db.collection({
name: 'profiles',
schema: [{ type: 'string', name: 'content' }],
});
const association = User.model.associations.profile;
expect(association).toBeDefined();
expect(association.foreignKey).toBe('userId');
// @ts-ignore
expect(association.sourceKey).toBe('id');
expect(Profile.model.rawAttributes['userId']).toBeDefined();
await db.sync();
// const post = await model.create<any>();
// await post.createComment({
// content: 'content111',
// });
// const postComments = await post.getComments();
// expect(postComments.map((comment) => comment.content)).toEqual([
// 'content111',
// ]);
});
it('schema delete', async () => {
const User = db.collection({
name: 'users',
schema: [{ type: 'hasOne', name: 'profile' }],
});
const Profile = db.collection({
name: 'profiles',
schema: [{ type: 'belongsTo', name: 'user' }],
});
await db.sync();
User.schema.delete('profile');
expect(User.model.associations.profile).toBeUndefined();
expect(Profile.model.rawAttributes.userId).toBeDefined();
Profile.schema.delete('user');
expect(Profile.model.rawAttributes.userId).toBeUndefined();
});
});

View File

@ -0,0 +1,70 @@
import { Database } from '../../database';
import { mockDatabase } from '../';
import { SortField } from '../../schema-fields';
describe('string field', () => {
let db: Database;
beforeEach(() => {
db = mockDatabase();
db.registerSchemaTypes({
sort: SortField
});
});
afterEach(async () => {
await db.close();
});
it('sort', async () => {
const Test = db.collection({
name: 'tests',
schema: [
{ type: 'sort', name: 'sort' },
],
});
await db.sync();
const test1 = await Test.model.create<any>();
expect(test1.sort).toBe(1);
const test2 = await Test.model.create<any>();
expect(test2.sort).toBe(2);
const test3 = await Test.model.create<any>();
expect(test3.sort).toBe(3);
});
it('skip if sort value not empty', async () => {
const Test = db.collection({
name: 'tests',
schema: [
{ type: 'sort', name: 'sort' },
],
});
await db.sync();
const test1 = await Test.model.create<any>({ sort: 3 });
expect(test1.sort).toBe(3);
const test2 = await Test.model.create<any>();
expect(test2.sort).toBe(4);
const test3 = await Test.model.create<any>();
expect(test3.sort).toBe(5);
});
it('scopeKey', async () => {
const Test = db.collection({
name: 'tests',
schema: [
{ type: 'sort', name: 'sort', scopeKey: 'status' },
{ type: 'string', name: 'status' },
],
});
await db.sync();
const t1 = await Test.model.create({ status: 'publish' });
const t2 = await Test.model.create({ status: 'publish' });
const t3 = await Test.model.create({ status: 'draft' });
const t4 = await Test.model.create({ status: 'draft' });
expect(t1.get('sort')).toBe(1);
expect(t2.get('sort')).toBe(2);
expect(t3.get('sort')).toBe(1);
expect(t4.get('sort')).toBe(2);
});
});

View File

@ -0,0 +1,101 @@
import { Database } from '../database';
import { updateAssociation, updateAssociations } from '../update-associations';
import { mockDatabase } from './';
describe('update associations', () => {
let db: Database;
beforeEach(() => {
db = mockDatabase();
});
afterEach(async () => {
await db.close();
});
describe('hasMany', () => {
it.only('model', async () => {
const User = db.collection({
name: 'users',
schema: [
{ type: 'string', name: 'name' },
{ type: 'hasMany', name: 'posts' },
],
});
const Post = db.collection({
name: 'posts',
schema: [
{ type: 'string', name: 'title' },
],
});
await db.sync();
const user = await User.model.create();
const post1 = await Post.model.create();
const post2 = await Post.model.create<any>();
const post3 = await Post.model.create<any>();
const post4 = await Post.model.create<any>();
await updateAssociations(user, {
posts: {
title: 'post0',
},
});
await updateAssociations(user, {
posts: post1,
});
await updateAssociations(user, {
posts: post2.id,
});
await updateAssociations(user, {
posts: [post3.id],
});
await updateAssociations(user, {
posts: {
id: post4.id,
title: 'post4',
},
});
});
});
it('nested', async () => {
const User = db.collection({
name: 'users',
schema: [
{ type: 'string', name: 'name' },
{ type: 'hasMany', name: 'posts' },
],
});
const Post = db.collection({
name: 'posts',
schema: [
{ type: 'string', name: 'title' },
{ type: 'belongsTo', name: 'user' },
{ type: 'hasMany', name: 'comments' },
],
});
const Comment = db.collection({
name: 'comments',
schema: [
{ type: 'string', name: 'content' },
{ type: 'belongsTo', name: 'post' },
],
});
await db.sync();
const user = await User.model.create();
await updateAssociations(user, {
posts: [
{
title: 'post1',
// user: {
// name: 'user1',
// },
comments: [
{
content: 'content1',
},
],
},
],
});
});
});

View File

@ -29,12 +29,15 @@ export class Collection {
const { name, tableName } = options;
this.model = class extends Model<any, any> {};
const attributes = {};
// TODO: 不能重复 model.init如果有涉及 InitOptions 参数修改,需要另外处理。
this.model.init(attributes, {
..._.omit(options, ['name', 'schema']),
sequelize: context.database.sequelize,
modelName: name,
tableName: tableName || name,
});
// schema 只针对字段,对应 Sequelize 的 Attributes
// 其他 InitOptions 参数放在 Collection 里,通过其他方法同步给 model
this.schema = new Schema(options.schema, {
...context,
collection: this,

View File

@ -4,6 +4,7 @@ import { Collection, CollectionOptions } from './collection';
import {
RelationField,
StringField,
HasOneField,
HasManyField,
BelongsToField,
BelongsToManyField,
@ -21,6 +22,8 @@ export type DatabaseOptions = Options | Sequelize;
export class Database extends EventEmitter {
sequelize: Sequelize;
schemaTypes = new Map();
models = new Map();
repositories = new Map();
collections: Map<string, Collection>;
pendingFields = new Map<string, RelationField[]>();
@ -42,6 +45,7 @@ export class Database extends EventEmitter {
string: StringField,
json: JsonField,
jsonb: JsonbField,
hasOne: HasOneField,
hasMany: HasManyField,
belongsTo: BelongsToField,
belongsToMany: BelongsToManyField,
@ -72,9 +76,7 @@ export class Database extends EventEmitter {
removePendingField(field: RelationField) {
const items = this.pendingFields.get(field.target) || [];
const index = items.findIndex(
(item) => item && item.name === field.name,
);
const index = items.indexOf(field);
if (index !== -1) {
delete items[index];
this.pendingFields.set(field.target, items);
@ -87,6 +89,18 @@ export class Database extends EventEmitter {
}
}
registerModels(models: any) {
for (const [type, schemaType] of Object.entries(models)) {
this.models.set(type, schemaType);
}
}
registerRepositories(repositories: any) {
for (const [type, schemaType] of Object.entries(repositories)) {
this.repositories.set(type, schemaType);
}
}
buildSchemaField(options, context) {
const { type } = options;
const Field = this.schemaTypes.get(type);

View File

@ -0,0 +1,23 @@
import { ModelCtor, Model } from 'sequelize';
export interface IRepository {
}
export class Repository implements IRepository {
model: ModelCtor<Model>;
constructor(model: ModelCtor<Model>) {
this.model = model;
}
findAll() {}
findOne() {}
create() {}
update() {}
destroy() {}
}

View File

@ -3,10 +3,6 @@ import { Sequelize, ModelCtor, Model, DataTypes, Utils } from 'sequelize';
import { RelationField } from './relation-field';
export class BelongsToManyField extends RelationField {
get target() {
const { target, name } = this.options;
return target || name;
}
get through() {
return (
@ -48,22 +44,10 @@ export class BelongsToManyField extends RelationField {
}
unbind() {
// const { database, collection } = this.context;
// // 如果关系字段还没建立就删除了,也同步删除待建立关联的关系字段
// database.removePendingField(this);
// // 如果外键没有显式的创建,关系表也无反向关联字段,删除关系时,外键也删除掉
// const tcoll = database.collections.get(this.target);
// const foreignKey = this.options.foreignKey;
// const field1 = collection.schema.get(foreignKey);
// const field2 = tcoll.schema.find((field) => {
// return field.type === 'hasMany' && field.foreignKey === foreignKey;
// });
// if (!field1 && !field2) {
// collection.model.removeAttribute(foreignKey);
// }
// // 删掉 model 的关联字段
// delete collection.model.associations[this.name];
// // @ts-ignore
// collection.model.refreshAttributes();
const { database, collection } = this.context;
// 如果关系字段还没建立就删除了,也同步删除待建立关联的关系字段
database.removePendingField(this);
// 删掉 model 的关联字段
delete collection.model.associations[this.name];
}
}

View File

@ -0,0 +1,8 @@
import { DataTypes } from 'sequelize';
import { SchemaField } from './schema-field';
export class DateField extends SchemaField {
get dataType() {
return DataTypes.DATE;
}
}

View File

@ -0,0 +1,8 @@
import { DataTypes } from 'sequelize';
import { SchemaField } from './schema-field';
export class FloatField extends SchemaField {
get dataType() {
return DataTypes.FLOAT;
}
}

View File

@ -72,6 +72,7 @@ export interface HasManyFieldOptions extends HasManyOptions {
}
export class HasManyField extends RelationField {
bind() {
const { database, collection } = this.context;
const Target = this.TargetModel;

View File

@ -0,0 +1,140 @@
import { omit } from 'lodash';
import {
Sequelize,
ModelCtor,
Model,
DataType,
AssociationScope,
ForeignKeyOptions,
HasOneOptions,
Utils,
} from 'sequelize';
import { RelationField } from './relation-field';
export interface HasOneFieldOptions extends HasOneOptions {
/**
* The name of the field to use as the key for the association in the source table. Defaults to the primary
* key of the source table
*/
sourceKey?: string;
/**
* A string or a data type to represent the identifier in the table
*/
keyType?: DataType;
scope?: AssociationScope;
/**
* The alias of this model, in singular form. See also the `name` option passed to `sequelize.define`. If
* you create multiple associations between the same tables, you should provide an alias to be able to
* distinguish between them. If you provide an alias when creating the assocition, you should provide the
* same alias when eager loading and when getting associated models. Defaults to the singularized name of
* target
*/
as?: string | { singular: string; plural: string };
/**
* The name of the foreign key in the target table or an object representing the type definition for the
* foreign column (see `Sequelize.define` for syntax). When using an object, you can add a `name` property
* to set the name of the column. Defaults to the name of source + primary key of source
*/
foreignKey?: string | ForeignKeyOptions;
/**
* What happens when delete occurs.
*
* Cascade if this is a n:m, and set null if it is a 1:m
*
* @default 'SET NULL' or 'CASCADE'
*/
onDelete?: string;
/**
* What happens when update occurs
*
* @default 'CASCADE'
*/
onUpdate?: string;
/**
* Should on update and on delete constraints be enabled on the foreign key.
*/
constraints?: boolean;
foreignKeyConstraint?: boolean;
// scope?: AssociationScope;
/**
* If `false` the applicable hooks will not be called.
* The default value depends on the context.
*/
hooks?: boolean;
}
export class HasOneField extends RelationField {
get target() {
const { target, name } = this.options;
return target || Utils.pluralize(name);
}
get foreignKey() {
if (this.options.foreignKey) {
return this.options.foreignKey;
}
const { model } = this.context.collection;
return Utils.camelize(
[
model.options.name.singular,
model.primaryKeyAttribute
].join('_')
);
}
bind() {
const { database, collection } = this.context;
const Target = this.TargetModel;
if (!Target) {
database.addPendingField(this);
return false;
}
const association = collection.model.hasOne(Target, {
as: this.name,
foreignKey: this.foreignKey,
...omit(this.options, ['name', 'type', 'target']),
});
// 建立关系之后从 pending 列表中删除
database.removePendingField(this);
if (!this.options.foreignKey) {
this.options.foreignKey = association.foreignKey;
}
if (!this.options.sourceKey) {
// @ts-ignore
this.options.sourceKey = association.sourceKey;
}
return true;
}
unbind() {
const { database, collection } = this.context;
// 如果关系字段还没建立就删除了,也同步删除待建立关联的关系字段
database.removePendingField(this);
// 如果关系表内没有显式的创建外键字段,删除关系时,外键也删除掉
const tcoll = database.collections.get(this.target);
const foreignKey = this.options.foreignKey;
const field = tcoll.schema.find((field) => {
if (field.name === foreignKey) {
return true;
}
return field.type === 'belongsTo' && field.foreignKey === foreignKey;
});
if (!field) {
tcoll.model.removeAttribute(foreignKey);
}
// 删掉 model 的关联字段
delete collection.model.associations[this.name];
// @ts-ignore
collection.model.refreshAttributes();
}
}

View File

@ -3,5 +3,7 @@ export * from './string-field';
export * from './relation-field'
export * from './belongs-to-field'
export * from './belongs-to-many-field';
export * from './has-one-field';
export * from './has-many-field';
export * from './json-field';
export * from './sort-field';

View File

@ -0,0 +1,8 @@
import { DataTypes } from 'sequelize';
import { SchemaField } from './schema-field';
export class IntegerField extends SchemaField {
get dataType() {
return DataTypes.INTEGER;
}
}

View File

@ -5,13 +5,6 @@ export class JsonField extends SchemaField {
get dataType() {
return DataTypes.JSON;
}
toSequelize() {
return {
...this.options,
type: this.dataType,
};
}
}
export class JsonbField extends SchemaField {
@ -22,11 +15,4 @@ export class JsonbField extends SchemaField {
}
return DataTypes.JSON;
}
toSequelize() {
return {
...this.options,
type: this.dataType,
};
}
}

View File

@ -0,0 +1,32 @@
import { DataTypes } from 'sequelize';
import { SchemaField } from './schema-field';
export class IntegerField extends SchemaField {
get dataType() {
return DataTypes.INTEGER;
}
}
export class FloatField extends SchemaField {
get dataType() {
return DataTypes.FLOAT;
}
}
export class DoubleField extends SchemaField {
get dataType() {
return DataTypes.DOUBLE;
}
}
export class RealField extends SchemaField {
get dataType() {
return DataTypes.REAL;
}
}
export class DecimalField extends SchemaField {
get dataType() {
return DataTypes.DECIMAL;
}
}

View File

@ -60,6 +60,10 @@ export abstract class SchemaField {
}
toSequelize(): any {
return _.omit(this.options, ['name'])
const opts = _.omit(this.options, ['name']);
if (this.dataType) {
Object.assign(opts, { type: this.dataType });
}
return opts;
}
}

View File

@ -0,0 +1,25 @@
import { isNumber } from 'lodash';
import { DataTypes } from 'sequelize';
import { SchemaField } from './schema-field';
export class SortField extends SchemaField {
get dataType() {
return DataTypes.INTEGER;
}
init() {
const { name, scopeKey } = this.options;
const { model } = this.context.collection;
model.beforeCreate(async (instance, options) => {
if (isNumber(instance.get(name))) {
return;
}
const where = {};
if (scopeKey) {
where[scopeKey] = instance.get(scopeKey);
}
const max = await model.max<number, any>(name, { ...options, where });
instance.set(name, (max || 0) + 1);
});
}
}

View File

@ -5,11 +5,4 @@ export class StringField extends SchemaField {
get dataType() {
return DataTypes.STRING;
}
toSequelize() {
return {
...this.options,
type: this.dataType,
};
}
}

View File

@ -0,0 +1,8 @@
import { DataTypes } from 'sequelize';
import { SchemaField } from './schema-field';
export class TextField extends SchemaField {
get dataType() {
return DataTypes.TEXT;
}
}

View File

@ -0,0 +1,8 @@
import { DataTypes } from 'sequelize';
import { SchemaField } from './schema-field';
export class TimeField extends SchemaField {
get dataType() {
return DataTypes.TIME;
}
}

View File

@ -0,0 +1,8 @@
import { DataTypes } from 'sequelize';
import { SchemaField } from './schema-field';
export class VirtualField extends SchemaField {
get dataType() {
return DataTypes.VIRTUAL;
}
}

View File

@ -41,6 +41,7 @@ export class Schema extends EventEmitter {
schema: this,
model: this.context.collection.model,
});
// console.log('field', field);
this.fields.set(name, field);
this.emit('setted', field);
} else if (Array.isArray(name)) {
@ -59,7 +60,9 @@ export class Schema extends EventEmitter {
delete(name: string) {
const field = this.fields.get(name);
const bool = this.fields.delete(name);
this.emit('deleted', field);
if (bool) {
this.emit('deleted', field);
}
return bool;
}

View File

@ -0,0 +1,192 @@
import {
Sequelize,
ModelCtor,
Model,
DataTypes,
Utils,
Association,
} from 'sequelize';
function isUndefinedOrNull(value: any) {
return typeof value === 'undefined' || value === null;
}
function isStringOrNumber(value: any) {
return typeof value === 'string' || typeof value === 'number';
}
export async function updateAssociations(
model: Model,
values: any,
options: any = {},
) {
const { transaction = await model.sequelize.transaction() } = options;
// @ts-ignore
for (const key of Object.keys(model.constructor.associations)) {
// 如果 key 不存在才跳过
if (!Object.keys(values).includes(key)) {
continue;
}
await updateAssociation(model, key, values[key], {
...options,
transaction,
});
}
if (!options.transaction) {
await transaction.commit();
}
}
export async function updateAssociation(
model: Model,
key: string,
value: any,
options: any = {},
) {
// @ts-ignore
const association = model.constructor.associations[key] as Association;
if (!association) {
return false;
}
switch (association.associationType) {
case 'HasOne':
case 'BelongsTo':
return updateSingleAssociation(model, key, value, options);
case 'HasMany':
case 'BelongsToMany':
return updateMultipleAssociation(model, key, value, options);
}
}
export async function updateSingleAssociation(
model: Model,
key: string,
value: any,
options: any = {},
) {
// @ts-ignore
const association = model.constructor.associations[key] as Association;
if (!association) {
return false;
}
if (!['undefined', 'string', 'number', 'object'].includes(typeof value)) {
return false;
}
const { transaction = await model.sequelize.transaction() } = options;
try {
// @ts-ignore
const setAccessor = association.accessors.set;
if (isUndefinedOrNull(value)) {
return await model[setAccessor](null, { transaction });
}
if (isStringOrNumber(value)) {
return await model[setAccessor](value, { transaction });
}
// @ts-ignore
const createAccessor = association.accessors.create;
let key: string;
let M: ModelCtor<Model>;
if (association.associationType === 'BelongsTo') {
// @ts-ignore
key = association.targetKey;
M = association.target;
} else {
// @ts-ignore
key = association.sourceKey;
M = association.source;
}
if (isStringOrNumber(value)) {
let instance: any = await M.findOne({
where: {
[key]: value[key],
},
transaction,
});
if (!instance) {
instance = await M.create(value, { transaction });
}
await model[setAccessor](value[key]);
await updateAssociations(instance, value, { transaction, ...options });
} else {
const instance = await model[createAccessor](value, { transaction });
await updateAssociations(instance, value, { transaction, ...options });
}
if (!options.transaction) {
await transaction.commit();
}
} catch (error) {
if (!options.transaction) {
await transaction.rollback();
}
throw error;
}
}
export async function updateMultipleAssociation(
model: Model,
key: string,
value: any,
options: any = {},
) {
// @ts-ignore
const association = model.constructor.associations[key] as Association;
if (!association) {
return false;
}
if (!['undefined', 'string', 'number', 'object'].includes(typeof value)) {
return false;
}
const { transaction = await model.sequelize.transaction() } = options;
try {
// @ts-ignore
const setAccessor = association.accessors.set;
// @ts-ignore
const createAccessor = association.accessors.create;
if (isUndefinedOrNull(value)) {
return await model[setAccessor](null, { transaction });
}
if (isStringOrNumber(value)) {
return await model[setAccessor](value, { transaction });
}
if (!Array.isArray(value)) {
value = [value];
}
const list1 = []; // to be setted
const list2 = []; // to be added
for (const item of value) {
if (isUndefinedOrNull(item)) {
continue;
}
if (isStringOrNumber(item)) {
list1.push(item);
} else if (item instanceof Model) {
list1.push(item);
} else if (item.sequelize) {
list1.push(item);
} else if (typeof item === 'object') {
list2.push(item);
}
}
console.log('updateMultipleAssociation', list1, list2);
await model[setAccessor](list1, { transaction });
for (const item of list2) {
const pk = association.target.primaryKeyAttribute;
if (isUndefinedOrNull(item[pk])) {
const instance = await model[createAccessor](item, { transaction });
await updateAssociations(instance, item, { transaction, ...options });
} else {
const instance = await association.target.findByPk(item[pk], { transaction });
// @ts-ignore
const addAccessor = association.accessors.add;
await model[addAccessor](item[pk], { transaction });
await updateAssociations(instance, item, { transaction, ...options });
}
}
if (!options.transaction) {
await transaction.commit();
}
} catch (error) {
await transaction.rollback();
throw error;
}
}

View File

@ -0,0 +1,18 @@
export default {
fiter: {
and: [
{ a: 'a' },
{ b: 'b' },
{ c: 'c' },
{ 'assoc.a': 'abc1' },
{ 'assoc.b': 'abc2' },
{ 'assoc.c': 'abc3' },
{
and: [
{ 'assoc.a': 'abc1' },
{ 'assoc.b': 'abc2' },
],
},
],
},
};

View File

@ -1,13 +1,16 @@
import path from 'path';
import Application from '@nocobase/server';
import { IPlugin } from '@nocobase/server';
import { afterCreate, afterUpdate, afterDestroy } from './hooks';
export default async function (this: Application) {
const { database } = this;
database.import({
directory: path.resolve(__dirname, 'collections'),
});
database.on('afterCreate', afterCreate);
database.on('afterUpdate', afterUpdate);
database.on('afterDestroy', afterDestroy);
}
export default {
name: 'action-logs',
async load() {
const database = this.app.db;
database.import({
directory: path.resolve(__dirname, 'collections'),
});
database.on('afterCreate', afterCreate);
database.on('afterUpdate', afterUpdate);
database.on('afterDestroy', afterDestroy);
}
} as IPlugin;

View File

@ -1,3 +1,4 @@
// @ts-nocheck
import Database, { registerModels } from '@nocobase/database';
import Resourcer from '@nocobase/resourcer';
import path from 'path';

View File

@ -1,19 +1,22 @@
import path from 'path';
import Database, { registerModels } from '@nocobase/database';
import { registerModels } from '@nocobase/database';
import { ChinaRegion } from './models/china-region';
import Application from '@nocobase/server';
import { Plugin } from '@nocobase/server';
registerModels({ ChinaRegion });
export default async function (this: Application, options = {}) {
const { db } = this;
db.import({
directory: path.resolve(__dirname, 'collections'),
});
this.on('db.init', async () => {
const M = db.getModel('china_regions');
await M.importData();
});
}
export default {
name: 'china-region',
async load(this: Plugin) {
const db = this.app.db;
db.import({
directory: path.resolve(__dirname, 'collections'),
});
this.app.on('db.init', async () => {
const M = db.getModel('china_regions');
await M.importData();
});
}
};

View File

@ -1,123 +1,132 @@
import path from 'path';
import { Application } from '@nocobase/server';
import { Plugin } from '@nocobase/server';
import { registerModels, Table, uid } from '@nocobase/database';
import * as models from './models';
import { createOrUpdate, findAll } from './actions';
import { create } from './actions/fields';
export default async function (this: Application, options = {}) {
const database = this.db;
export default {
name: 'collections',
async load(this: Plugin) {
const database = this.app.db;
registerModels(models);
registerModels(models);
database.import({
directory: path.resolve(__dirname, 'collections'),
});
this.on('afterLoadPlugins', async () => {
await database.getModel('collections').load();
});
this.on('db.init', async () => {
const userTable = database.getTable('users');
const config = userTable.getOptions();
const Collection = database.getModel('collections');
const collection = await Collection.create(config);
await collection.updateAssociations({
generalFields: config.fields.filter((field) => field.state !== 0),
systemFields: config.fields.filter((field) => field.state === 0),
database.import({
directory: path.resolve(__dirname, 'collections'),
});
await collection.migrate();
});
const [Collection, Field] = database.getModels(['collections', 'fields']);
this.app.on('beforeStart', async () => {
await database.getModel('collections').load();
});
database.on('fields.beforeCreate', async (model) => {
if (!model.get('name')) {
model.set('name', model.get('key'));
}
if (!model.get('collection_name') && model.get('parentKey')) {
const field = await Field.findByPk(model.get('parentKey'));
if (field) {
const { target } = field.get('options') || {};
if (target) {
model.set('collection_name', target);
this.app.on('db.init', async () => {
const userTable = database.getTable('users');
const config = userTable.getOptions();
const Collection = database.getModel('collections');
const collection = await Collection.create(config);
await collection.updateAssociations({
generalFields: config.fields.filter((field) => field.state !== 0),
systemFields: config.fields.filter((field) => field.state === 0),
});
await collection.migrate();
});
const [Collection, Field] = database.getModels(['collections', 'fields']);
database.on('fields.beforeCreate', async (model) => {
if (!model.get('name')) {
model.set('name', model.get('key'));
}
if (!model.get('collection_name') && model.get('parentKey')) {
const field = await Field.findByPk(model.get('parentKey'));
if (field) {
const { target } = field.get('options') || {};
if (target) {
model.set('collection_name', target);
}
}
}
}
});
});
database.on('fields.beforeUpdate', async (model) => {
console.log('beforeUpdate', model.key);
if (!model.get('collection_name') && model.get('parentKey')) {
const field = await Field.findByPk(model.get('parentKey'));
if (field) {
const { target } = field.get('options') || {};
if (target) {
model.set('collection_name', target);
database.on('fields.beforeUpdate', async (model) => {
console.log('beforeUpdate', model.key);
if (!model.get('collection_name') && model.get('parentKey')) {
const field = await Field.findByPk(model.get('parentKey'));
if (field) {
const { target } = field.get('options') || {};
if (target) {
model.set('collection_name', target);
}
}
}
}
});
});
database.on('fields.afterCreate', async (model) => {
console.log('afterCreate', model.key, model.get('collection_name'));
if (model.get('interface') !== 'subTable') {
return;
}
const { target } = model.get('options') || {};
// const uiSchemaKey = model.get('ui_schema_key');
// console.log({ uiSchemaKey })
try {
let collection = await Collection.findOne({
where: {
name: target,
},
});
if (!collection) {
collection = await Collection.create({
name: target,
// ui_schema_key: uiSchemaKey,
});
database.on('fields.afterCreate', async (model) => {
console.log('afterCreate', model.key, model.get('collection_name'));
if (model.get('interface') !== 'subTable') {
return;
}
// if (model.get('ui_schema_key')) {
// collection.set('ui_schema_key', model.get('ui_schema_key'));
// await collection.save({ hooks: false });
// }
await collection.migrate();
} catch (error) {
throw error;
}
});
database.on('fields.afterUpdate', async (model) => {
console.log('afterUpdate');
if (model.get('interface') !== 'subTable') {
return;
}
const { target } = model.get('options') || {};
try {
let collection = await Collection.findOne({
where: {
name: target,
},
});
if (!collection) {
collection = await Collection.create({
name: target,
const { target } = model.get('options') || {};
// const uiSchemaKey = model.get('ui_schema_key');
// console.log({ uiSchemaKey })
try {
let collection = await Collection.findOne({
where: {
name: target,
},
});
if (!collection) {
collection = await Collection.create({
name: target,
// ui_schema_key: uiSchemaKey,
});
}
// if (model.get('ui_schema_key')) {
// collection.set('ui_schema_key', model.get('ui_schema_key'));
// await collection.save({ hooks: false });
// }
await collection.migrate();
} catch (error) {
throw error;
}
// if (model.get('ui_schema_key')) {
// collection.set('ui_schema_key', model.get('ui_schema_key'));
// await collection.save({ hooks: false });
// }
await collection.migrate();
} catch (error) {
throw error;
}
});
});
this.resourcer.registerActionHandler('collections.fields:create', create);
this.resourcer.registerActionHandler('collections:findAll', findAll);
this.resourcer.registerActionHandler('collections:createOrUpdate', createOrUpdate);
}
database.on('fields.afterUpdate', async (model) => {
console.log('afterUpdate');
if (model.get('interface') !== 'subTable') {
return;
}
const { target } = model.get('options') || {};
try {
let collection = await Collection.findOne({
where: {
name: target,
},
});
if (!collection) {
collection = await Collection.create({
name: target,
});
}
// if (model.get('ui_schema_key')) {
// collection.set('ui_schema_key', model.get('ui_schema_key'));
// await collection.save({ hooks: false });
// }
await collection.migrate();
} catch (error) {
throw error;
}
});
this.app.resourcer.registerActionHandler(
'collections.fields:create',
create,
);
this.app.resourcer.registerActionHandler('collections:findAll', findAll);
this.app.resourcer.registerActionHandler(
'collections:createOrUpdate',
createOrUpdate,
);
},
};

View File

@ -1,28 +1,29 @@
import Resourcer from '@nocobase/resourcer';
import { PluginOptions } from '@nocobase/server';
import _export from './actions/export';
export const ACTION_NAME_EXPORT = 'export';
export default async function (options = {}) {
const resourcer: Resourcer = this.resourcer;
resourcer.registerActionHandler(ACTION_NAME_EXPORT, _export);
// // TODO(temp): 继承 list 权限的临时写法
// resourcer.use(async (ctx, next) => {
// if (ctx.action.params.actionName === ACTION_NAME_EXPORT) {
// ctx.action.mergeParams({
// actionName: 'list'
// });
// console.log('action name in export has been rewritten to:', ctx.action.params.actionName);
// const permissionPlugin = ctx.app.getPluginInstance('@nocobase/plugin-permissions');
// if (permissionPlugin) {
// return permissionPlugin.middleware(ctx, next);
// }
// }
// await next();
// });
}
export default {
name: 'export',
async load() {
const resourcer = this.app.resourcer;
resourcer.registerActionHandler(ACTION_NAME_EXPORT, _export);
// // TODO(temp): 继承 list 权限的临时写法
// resourcer.use(async (ctx, next) => {
// if (ctx.action.params.actionName === ACTION_NAME_EXPORT) {
// ctx.action.mergeParams({
// actionName: 'list'
// });
// console.log('action name in export has been rewritten to:', ctx.action.params.actionName);
// const permissionPlugin = ctx.app.getPluginInstance('@nocobase/plugin-permissions');
// if (permissionPlugin) {
// return permissionPlugin.middleware(ctx, next);
// }
// }
// await next();
// });
}
} as PluginOptions;

View File

@ -1,6 +1,7 @@
import path from 'path';
import Database from '@nocobase/database';
import Resourcer from '@nocobase/resourcer';
import { PluginOptions, Plugin } from '@nocobase/server';
import {
action as uploadAction,
@ -10,40 +11,43 @@ import {
middleware as localMiddleware,
} from './storages/local';
export default async function () {
const database: Database = this.database;
const resourcer: Resourcer = this.resourcer;
database.import({
directory: path.resolve(__dirname, 'collections'),
});
// 暂时中间件只能通过 use 加进来
resourcer.use(uploadMiddleware);
resourcer.registerActionHandler('upload', uploadAction);
localMiddleware(this);
const Storage = database.getModel('storages');
this.on('db.init', async () => {
await Storage.create({
title: '本地存储',
name: `local`,
type: 'local',
baseUrl: process.env.LOCAL_STORAGE_BASE_URL,
default: process.env.STORAGE_TYPE === 'local',
export default {
name: 'file-manager',
async load() {
const database: Database = this.app.db;
const resourcer: Resourcer = this.app.resourcer;
database.import({
directory: path.resolve(__dirname, 'collections'),
});
await Storage.create({
name: `ali-oss`,
type: 'ali-oss',
baseUrl: process.env.ALI_OSS_STORAGE_BASE_URL,
options: {
region: process.env.ALI_OSS_REGION,
accessKeyId: process.env.ALI_OSS_ACCESS_KEY_ID,
accessKeySecret: process.env.ALI_OSS_ACCESS_KEY_SECRET,
bucket: process.env.ALI_OSS_BUCKET,
},
default: process.env.STORAGE_TYPE === 'ali-oss',
// 暂时中间件只能通过 use 加进来
resourcer.use(uploadMiddleware);
resourcer.registerActionHandler('upload', uploadAction);
localMiddleware(this.app);
const Storage = database.getModel('storages');
this.app.on('db.init', async () => {
await Storage.create({
title: '本地存储',
name: `local`,
type: 'local',
baseUrl: process.env.LOCAL_STORAGE_BASE_URL,
default: process.env.STORAGE_TYPE === 'local',
});
await Storage.create({
name: `ali-oss`,
type: 'ali-oss',
baseUrl: process.env.ALI_OSS_STORAGE_BASE_URL,
options: {
region: process.env.ALI_OSS_REGION,
accessKeyId: process.env.ALI_OSS_ACCESS_KEY_ID,
accessKeySecret: process.env.ALI_OSS_ACCESS_KEY_SECRET,
bucket: process.env.ALI_OSS_BUCKET,
},
default: process.env.STORAGE_TYPE === 'ali-oss',
});
});
});
}
},
} as PluginOptions;

View File

@ -1,12 +1,12 @@
import path from 'path';
import Database from '@nocobase/database';
import Resourcer from '@nocobase/resourcer';
import { PluginOptions } from '@nocobase/server';
export default async function () {
const database: Database = this.database;
const resourcer: Resourcer = this.resourcer;
database.import({
directory: path.resolve(__dirname, 'collections'),
});
}
export default {
name: 'permissions',
async load() {
const database = this.app.db;
database.import({
directory: path.resolve(__dirname, 'collections'),
});
}
} as PluginOptions;

View File

@ -1,42 +1,45 @@
import path from 'path';
import { Application } from '@nocobase/server';
import { PluginOptions } from '@nocobase/server';
export default async function (this: Application, options = {}) {
const database = this.database;
const resourcer = this.resourcer;
export default {
name: 'system-settings',
async load() {
const database = this.app.db;
const resourcer = this.app.resourcer;
database.import({
directory: path.resolve(__dirname, 'collections'),
});
database.import({
directory: path.resolve(__dirname, 'collections'),
});
const SystemSetting = database.getModel('system_settings');
resourcer.use(async (ctx, next) => {
const { actionName, resourceName, resourceKey } = ctx.action.params;
if (resourceName === 'system_settings' && actionName === 'get') {
let model = await SystemSetting.findOne();
if (!model) {
model = await SystemSetting.create();
const SystemSetting = database.getModel('system_settings');
resourcer.use(async (ctx, next) => {
const { actionName, resourceName, resourceKey } = ctx.action.params;
if (resourceName === 'system_settings' && actionName === 'get') {
let model = await SystemSetting.findOne();
if (!model) {
model = await SystemSetting.create();
}
ctx.action.mergeParams({
resourceKey: model.id,
});
}
ctx.action.mergeParams({
resourceKey: model.id,
await next();
});
this.app.on('db.init', async () => {
const setting = await SystemSetting.create({
title: 'NocoBase',
});
await setting.updateAssociations({
logo: {
title: 'nocobase-logo',
filename: '682e5ad037dd02a0fe4800a3e91c283b.png',
extname: '.png',
mimetype: 'image/png',
url: 'https://nocobase.oss-cn-beijing.aliyuncs.com/682e5ad037dd02a0fe4800a3e91c283b.png',
},
});
}
await next();
});
this.on('db.init', async () => {
const setting = await SystemSetting.create({
title: 'NocoBase',
});
await setting.updateAssociations({
logo: {
title: 'nocobase-logo',
filename: '682e5ad037dd02a0fe4800a3e91c283b.png',
extname: '.png',
mimetype: 'image/png',
url: 'https://nocobase.oss-cn-beijing.aliyuncs.com/682e5ad037dd02a0fe4800a3e91c283b.png',
},
});
});
}
}
} as PluginOptions;

View File

@ -1,61 +1,64 @@
import path from 'path';
import { Application } from '@nocobase/server';
import { PluginOptions } from '@nocobase/server';
import { registerModels } from '@nocobase/database';
import * as models from './models';
import getAccessible from './actions/getAccessible';
import * as uiSchema from './ui-schema';
export default async function (this: Application, options = {}) {
const database = this.database;
registerModels(models);
export default {
name: 'ui-router',
async load() {
const database = this.app.db;
registerModels(models);
database.import({
directory: path.resolve(__dirname, 'collections'),
});
this.resourcer.registerActionHandler('routes:getAccessible', getAccessible);
const Route = database.getModel('routes');
this.on('db.init', async () => {
const data = [
{
type: 'redirect',
from: '/',
to: '/admin',
exact: true,
},
{
type: 'route',
path: '/admin/:name(.+)?',
component: 'AdminLayout',
title: `后台`,
uiSchema: uiSchema.menu,
},
{
type: 'route',
component: 'AuthLayout',
children: [
{
type: 'route',
path: '/login',
component: 'RouteSchemaRenderer',
title: `登录`,
uiSchema: uiSchema.login,
},
{
type: 'route',
path: '/register',
component: 'RouteSchemaRenderer',
title: `注册`,
uiSchema: uiSchema.register,
},
],
},
];
for (const item of data) {
const route = await Route.create(item);
await route.updateAssociations(item);
}
});
}
database.import({
directory: path.resolve(__dirname, 'collections'),
});
this.app.resourcer.registerActionHandler('routes:getAccessible', getAccessible);
const Route = database.getModel('routes');
this.app.on('db.init', async () => {
const data = [
{
type: 'redirect',
from: '/',
to: '/admin',
exact: true,
},
{
type: 'route',
path: '/admin/:name(.+)?',
component: 'AdminLayout',
title: `后台`,
uiSchema: uiSchema.menu,
},
{
type: 'route',
component: 'AuthLayout',
children: [
{
type: 'route',
path: '/login',
component: 'RouteSchemaRenderer',
title: `登录`,
uiSchema: uiSchema.login,
},
{
type: 'route',
path: '/register',
component: 'RouteSchemaRenderer',
title: `注册`,
uiSchema: uiSchema.register,
},
],
},
];
for (const item of data) {
const route = await Route.create(item);
await route.updateAssociations(item);
}
});
}
} as PluginOptions;

View File

@ -1,18 +1,22 @@
import path from 'path';
import { Application } from '@nocobase/server';
import { PluginOptions } from '@nocobase/server';
import { registerModels } from '@nocobase/database';
import * as models from './models';
import * as actions from './actions';
export default async function (this: Application, options = {}) {
const database = this.database;
registerModels(models);
registerModels(models);
database.import({
directory: path.resolve(__dirname, 'collections'),
});
for (const [name, action] of Object.entries(actions)) {
this.resourcer.registerActionHandler(`ui_schemas:${name}`, action);
export default {
name: 'ui-schema',
async load() {
const database = this.app.db;
database.import({
directory: path.resolve(__dirname, 'collections'),
});
for (const [name, action] of Object.entries(actions)) {
this.app.resourcer.registerActionHandler(`ui_schemas:${name}`, action);
}
}
}
} as PluginOptions;

View File

@ -1,51 +1,53 @@
import path from 'path';
import Database, { registerFields, Table } from '@nocobase/database';
import Resourcer from '@nocobase/resourcer';
import { registerFields, Table } from '@nocobase/database';
import * as fields from './fields';
import * as usersActions from './actions/users';
import * as middlewares from './middlewares';
import Application from '@nocobase/server';
import { PluginOptions } from '@nocobase/server';
export default async function (this: Application, options = {}) {
const database: Database = this.database;
const resourcer: Resourcer = this.resourcer;
export default {
name: 'users',
async load() {
const database = this.app.db;
const resourcer = this.app.resourcer;
registerFields(fields);
registerFields(fields);
this.on('db.init', async () => {
const User = database.getModel('users');
await User.create({
nickname: '超级管理员',
email: process.env.ADMIN_EMAIL,
password: process.env.ADMIN_PASSWORD,
this.app.on('db.init', async () => {
const User = database.getModel('users');
await User.create({
nickname: '超级管理员',
email: process.env.ADMIN_EMAIL,
password: process.env.ADMIN_PASSWORD,
});
});
});
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',
});
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 (updatedBy !== false) {
table.addField({
type: 'updatedBy',
name: typeof updatedBy === 'string' ? updatedBy : 'updatedBy',
target: 'users',
});
}
});
database.import({
directory: path.resolve(__dirname, 'collections'),
});
for (const [key, action] of Object.entries(usersActions)) {
resourcer.registerActionHandler(`users:${key}`, action);
}
if (updatedBy !== false) {
table.addField({
type: 'updatedBy',
name: typeof updatedBy === 'string' ? updatedBy : 'updatedBy',
target: 'users',
});
}
});
database.import({
directory: path.resolve(__dirname, 'collections'),
});
for (const [key, action] of Object.entries(usersActions)) {
resourcer.registerActionHandler(`users:${key}`, action);
}
resourcer.use(middlewares.parseToken(options));
}
resourcer.use(middlewares.parseToken({}));
},
} as PluginOptions;

View File

@ -38,20 +38,25 @@ describe('plugin', () => {
expect(plugin).toBeInstanceOf(Plugin);
expect(plugin.getName()).toBe('abc');
});
it('plugin name', async () => {
const plugin = app.plugin(function abc() {}, {
name: 'plugin-name2'
const plugin = app.plugin({
name: 'plugin-name2',
async load() {},
});
expect(plugin).toBeInstanceOf(Plugin);
expect(plugin.getName()).toBe('plugin-name2');
});
it('plugin name', async () => {
const plugin = app.plugin(function () {}, {
name: 'plugin-name3'
const plugin = app.plugin({
name: 'plugin-name3',
load: function () {},
});
expect(plugin).toBeInstanceOf(Plugin);
expect(plugin.getName()).toBe('plugin-name3');
});
it('plugin name', async () => {
class MyPlugin extends Plugin {}
const plugin = app.plugin(MyPlugin);
@ -59,6 +64,15 @@ describe('plugin', () => {
expect(plugin.getName()).toBe('MyPlugin');
});
it('plugin name', async () => {
class MyPlugin extends Plugin {}
const plugin = app.plugin({
plugin: MyPlugin,
});
expect(plugin).toBeInstanceOf(MyPlugin);
expect(plugin.getName()).toBe('MyPlugin');
});
it('plugin name', async () => {
const plugin = app.plugin(path.resolve(__dirname, './plugins/plugin1'));
expect(plugin).toBeInstanceOf(Plugin);

View File

@ -142,17 +142,16 @@ export class Application<
console.log(args);
const opts = cli.opts();
await this.load();
await this.emitAsync('server.beforeStart');
await this.emitAsync('beforeStart');
this.listen(opts.port || 3000);
console.log(`http://localhost:${opts.port || 3000}/`);
});
}
// @ts-ignore
use<NewStateT = {}, NewContextT = {}>(
middleware: Koa.Middleware<StateT & NewStateT, ContextT & NewContextT>,
options?: MiddlewareOptions,
): Application<StateT & NewStateT, ContextT & NewContextT> {
) {
// @ts-ignore
return super.use(middleware);
}
@ -173,25 +172,34 @@ export class Application<
return this.cli.command(nameAndArgs, opts);
}
plugin(plugin: PluginType, options?: PluginOptions): Plugin {
if (typeof plugin === 'string') {
plugin = require(plugin).default;
plugin(options?: PluginType | PluginOptions): Plugin {
if (typeof options === 'string') {
return this.plugin(require(options).default);
}
let instance: Plugin;
try {
// @ts-ignore
const p = new plugin();
if (p instanceof Plugin) {
if (typeof options === 'function') {
try {
// @ts-ignore
instance = new plugin({}, {
...options,
instance = new options({
name: options.name,
app: this,
});
if (!(instance instanceof Plugin)) {
throw new Error('plugin must be instanceof Plugin');
}
} catch (err) {
// console.log(err);
instance = new Plugin({
name: options.name,
// @ts-ignore
load: options,
app: this,
});
} else {
throw new Error('plugin must be instanceof Plugin');
}
} catch (err) {
instance = new Plugin(plugin, {
} else if (typeof options === 'object') {
const plugin = options.plugin || Plugin;
instance = new plugin({
name: options.plugin ? plugin.name : undefined,
...options,
app: this,
});

View File

@ -1,3 +1,4 @@
export * from './application';
export * as middlewares from './middlewares';
export * from './plugin';
export { Application as default } from './application';

View File

@ -1,47 +1,37 @@
import { uid } from '@nocobase/database';
import { Application } from './application';
export interface PluginOptions {
app?: Application;
name?: string;
activate?: boolean;
displayName?: string;
description?: string;
version?: string;
}
import _ from 'lodash';
export interface IPlugin {
install?: (this: Plugin) => void;
load?: (this: Plugin) => void;
}
export interface PluginOptions {
name?: string;
activate?: boolean;
displayName?: string;
description?: string;
version?: string;
install?: (this: Plugin) => void;
load?: (this: Plugin) => void;
plugin?: typeof Plugin;
[key: string]: any;
}
export type PluginFn = (this: Plugin) => void;
export type PluginType = string | PluginFn | typeof Plugin | IPlugin;
export type PluginType = string | PluginFn | typeof Plugin;
export class Plugin implements IPlugin {
options: PluginOptions = {};
app: Application;
callbacks: IPlugin = {};
constructor(plugin?: PluginType, options?: PluginOptions) {
constructor(options?: PluginOptions & { app?: Application }) {
this.app = options?.app;
this.options = options || {};
if (typeof plugin === 'function') {
if (!this.options?.name && plugin.name) {
this.options.name = plugin.name;
}
this.callbacks.load = plugin as any;
} else if (
typeof plugin === 'object' &&
plugin.constructor === {}.constructor
) {
this.callbacks = plugin;
}
const cName = this.constructor.name;
if (this.options && !this.options?.name && cName && cName !== 'Plugin') {
this.options.name = cName;
}
this.options = options;
this.callbacks = _.pick(options, ['load', 'activate']);
}
getName() {

View File

@ -2381,6 +2381,17 @@
"@types/yargs" "^15.0.0"
chalk "^4.0.0"
"@jest/types@^27.1.1":
version "27.1.1"
resolved "https://registry.npmjs.org/@jest/types/-/types-27.1.1.tgz#77a3fc014f906c65752d12123a0134359707c0ad"
integrity sha512-yqJPDDseb0mXgKqmNqypCsb85C22K1aY5+LUxh7syIM9n/b0AsaltxNy+o6tt29VcfGDpYEve175bm3uOhcehA==
dependencies:
"@types/istanbul-lib-coverage" "^2.0.0"
"@types/istanbul-reports" "^3.0.0"
"@types/node" "*"
"@types/yargs" "^16.0.0"
chalk "^4.0.0"
"@juggle/resize-observer@^3.3.1":
version "3.3.1"
resolved "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.3.1.tgz#b50a781709c81e10701004214340f25475a171a0"
@ -3758,12 +3769,13 @@
dependencies:
"@types/istanbul-lib-report" "*"
"@types/jest@^24.0.18":
version "24.9.1"
resolved "https://registry.npmjs.org/@types/jest/-/jest-24.9.1.tgz#02baf9573c78f1b9974a5f36778b366aa77bd534"
integrity sha512-Fb38HkXSVA4L8fGKEZ6le5bB8r6MRWlOCZbVuWZcmOMSCd2wCYOwN1ibj8daIoV9naq7aaOZjrLCoCMptKU/4Q==
"@types/jest@^27.0.1":
version "27.0.1"
resolved "https://registry.npmjs.org/@types/jest/-/jest-27.0.1.tgz#fafcc997da0135865311bb1215ba16dba6bdf4ca"
integrity sha512-HTLpVXHrY69556ozYkcq47TtQJXpcWAWfkoqz+ZGz2JnmZhzlRjprCIyFnetSy8gpDWwTTGBcRVv1J1I1vBrHw==
dependencies:
jest-diff "^24.3.0"
jest-diff "^27.0.0"
pretty-format "^27.0.0"
"@types/js-cookie@^2.2.6":
version "2.2.7"
@ -4103,6 +4115,13 @@
dependencies:
"@types/yargs-parser" "*"
"@types/yargs@^16.0.0":
version "16.0.4"
resolved "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz#26aad98dd2c2a38e421086ea9ad42b9e51642977"
integrity sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw==
dependencies:
"@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@^4.9.1":
version "4.29.1"
resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.29.1.tgz#808d206e2278e809292b5de752a91105da85860b"
@ -4932,6 +4951,11 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0:
dependencies:
color-convert "^2.0.1"
ansi-styles@^5.0.0:
version "5.2.0"
resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b"
integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==
ansi-wrap@0.1.0, ansi-wrap@^0.1.0:
version "0.1.0"
resolved "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz#a82250ddb0015e9a27ca82e82ea603bbfa45efaf"
@ -7666,6 +7690,11 @@ diff-sequences@^26.6.2:
resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1"
integrity sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==
diff-sequences@^27.0.6:
version "27.0.6"
resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.0.6.tgz#3305cb2e55a033924054695cc66019fd7f8e5723"
integrity sha512-ag6wfpBFyNXZ0p8pcuIDS//D8H062ZQJ3fzYxjpmeKjnz8W4pekL3AI8VohmyZmsWW2PWaHgjsmqR6L13101VQ==
diff@^4.0.1:
version "4.0.2"
resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
@ -11153,7 +11182,7 @@ jest-config@^26.6.3:
micromatch "^4.0.2"
pretty-format "^26.6.2"
jest-diff@^24.3.0, jest-diff@^24.9.0:
jest-diff@^24.9.0:
version "24.9.0"
resolved "https://registry.npmjs.org/jest-diff/-/jest-diff-24.9.0.tgz#931b7d0d5778a1baf7452cb816e325e3724055da"
integrity sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ==
@ -11173,6 +11202,16 @@ jest-diff@^26.6.2:
jest-get-type "^26.3.0"
pretty-format "^26.6.2"
jest-diff@^27.0.0:
version "27.2.0"
resolved "https://registry.npmjs.org/jest-diff/-/jest-diff-27.2.0.tgz#bda761c360f751bab1e7a2fe2fc2b0a35ce8518c"
integrity sha512-QSO9WC6btFYWtRJ3Hac0sRrkspf7B01mGrrQEiCW6TobtViJ9RWL0EmOs/WnBsZDsI/Y2IoSHZA2x6offu0sYw==
dependencies:
chalk "^4.0.0"
diff-sequences "^27.0.6"
jest-get-type "^27.0.6"
pretty-format "^27.2.0"
jest-docblock@^24.3.0:
version "24.9.0"
resolved "https://registry.npmjs.org/jest-docblock/-/jest-docblock-24.9.0.tgz#7970201802ba560e1c4092cc25cbedf5af5a8ce2"
@ -11279,6 +11318,11 @@ jest-get-type@^26.3.0:
resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0"
integrity sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==
jest-get-type@^27.0.6:
version "27.0.6"
resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.0.6.tgz#0eb5c7f755854279ce9b68a9f1a4122f69047cfe"
integrity sha512-XTkK5exIeUbbveehcSR8w0bhH+c0yloW/Wpl+9vZrjzztCPWrxhHwkIFpZzCt71oRBsgxmuUfxEqOYoZI2macg==
jest-haste-map@^24.9.0:
version "24.9.0"
resolved "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-24.9.0.tgz#b38a5d64274934e21fa417ae9a9fbeb77ceaac7d"
@ -15401,6 +15445,16 @@ pretty-format@^26.6.2:
ansi-styles "^4.0.0"
react-is "^17.0.1"
pretty-format@^27.0.0, pretty-format@^27.2.0:
version "27.2.0"
resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-27.2.0.tgz#ee37a94ce2a79765791a8649ae374d468c18ef19"
integrity sha512-KyJdmgBkMscLqo8A7K77omgLx5PWPiXJswtTtFV7XgVZv2+qPk6UivpXXO+5k6ZEbWIbLoKdx1pZ6ldINzbwTA==
dependencies:
"@jest/types" "^27.1.1"
ansi-regex "^5.0.0"
ansi-styles "^5.0.0"
react-is "^17.0.1"
printj@~1.1.0, printj@~1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz#d90deb2975a8b9f600fb3a1c94e3f4c53c78a222"