Feat/plugin collection manager (#147)
* refactor: collection manager plugin * feat(database): magic attribute model * MagicAttributeModel * load collections & fields options * collections filterTargetKey
This commit is contained in:
parent
43f33044ea
commit
380b5e8c7a
@ -188,9 +188,6 @@ export class Collection<
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
* @param name
|
||||
* @param options
|
||||
*/
|
||||
updateOptions(options: CollectionOptions, mergeOptions?: any) {
|
||||
let newOptions = lodash.cloneDeep(options);
|
||||
@ -206,6 +203,8 @@ export class Collection<
|
||||
}
|
||||
|
||||
this.context.database.emit('afterUpdateCollection', this);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
setUpHooks(bindHooks) {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Collection } from '../collection';
|
||||
import { Database } from '../database';
|
||||
import _ from 'lodash';
|
||||
import { DataType, ModelAttributeColumnOptions, ModelIndexesOptions } from 'sequelize';
|
||||
import { DataType, ModelAttributeColumnOptions, ModelIndexesOptions, SyncOptions } from 'sequelize';
|
||||
|
||||
export interface FieldContext {
|
||||
database: Database;
|
||||
@ -45,6 +45,17 @@ export abstract class Field {
|
||||
this.init();
|
||||
}
|
||||
|
||||
// TODO
|
||||
async sync(syncOptions: SyncOptions) {
|
||||
await this.collection.sync({
|
||||
...syncOptions,
|
||||
force: false,
|
||||
alter: {
|
||||
drop: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
init() {
|
||||
// code
|
||||
}
|
||||
|
7
packages/plugin-collection-manager/.npmignore
Normal file
7
packages/plugin-collection-manager/.npmignore
Normal file
@ -0,0 +1,7 @@
|
||||
node_modules
|
||||
*.log
|
||||
docs
|
||||
__tests__
|
||||
tsconfig.json
|
||||
src
|
||||
.fatherrc.ts
|
11
packages/plugin-collection-manager/package.json
Normal file
11
packages/plugin-collection-manager/package.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-plugin-manager",
|
||||
"version": "0.6.0-alpha.0",
|
||||
"main": "lib/index.js",
|
||||
"license": "MIT",
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@nocobase/test": "^0.6.0-alpha.0"
|
||||
},
|
||||
"gitHead": "e7df1f93c4e23b9a666d99ee7372c02bdaec97c4"
|
||||
}
|
@ -0,0 +1,162 @@
|
||||
import Database, { Collection as DBCollection } from '@nocobase/database';
|
||||
import Application from '@nocobase/server';
|
||||
import { createApp } from '.';
|
||||
|
||||
describe('collections repository', () => {
|
||||
let db: Database;
|
||||
let app: Application;
|
||||
let Collection: DBCollection;
|
||||
let Field: DBCollection;
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await createApp();
|
||||
await app.db.sync();
|
||||
db = app.db;
|
||||
Collection = db.getCollection('collections');
|
||||
Field = db.getCollection('fields');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
it('case 1', async () => {
|
||||
// 什么都没提供,随机 name 和 key
|
||||
const data = await Collection.repository.create({
|
||||
values: {},
|
||||
});
|
||||
expect(data.get('key')).toBeDefined();
|
||||
expect(data.get('name')).toBeDefined();
|
||||
});
|
||||
|
||||
it('case 2', async () => {
|
||||
// 提供了 name
|
||||
const data = await Collection.repository.create({
|
||||
values: {
|
||||
name: 'tests',
|
||||
},
|
||||
});
|
||||
expect(data.toJSON()).toMatchObject({
|
||||
name: 'tests',
|
||||
});
|
||||
});
|
||||
|
||||
it('case 3', async () => {
|
||||
// 动态参数,存 options 字段里
|
||||
const data = await Collection.repository.create({
|
||||
values: {
|
||||
name: 'tests',
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
timestamps: true,
|
||||
},
|
||||
});
|
||||
expect(data.toJSON()).toMatchObject({
|
||||
name: 'tests',
|
||||
createdBy: true,
|
||||
updatedBy: true,
|
||||
timestamps: true,
|
||||
});
|
||||
const [updated] = await Collection.repository.update({
|
||||
filterByTk: data.get('key') as any,
|
||||
values: {
|
||||
createdBy: false,
|
||||
updatedBy: false,
|
||||
timestamps: false,
|
||||
},
|
||||
});
|
||||
expect(updated.toJSON()).toMatchObject({
|
||||
name: 'tests',
|
||||
createdBy: false,
|
||||
updatedBy: false,
|
||||
timestamps: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('case 4', async () => {
|
||||
await Collection.repository.create({
|
||||
values: {
|
||||
name: 'tests',
|
||||
fields: [
|
||||
{
|
||||
type: 'uid',
|
||||
name: 'name',
|
||||
prefix: 'f_',
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
unique: true,
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'title',
|
||||
unique: true,
|
||||
},
|
||||
{
|
||||
type: 'belongsToMany',
|
||||
target: 'tests',
|
||||
},
|
||||
{
|
||||
type: 'belongsTo',
|
||||
target: 'foos',
|
||||
},
|
||||
{
|
||||
type: 'hasMany',
|
||||
target: 'foos',
|
||||
},
|
||||
{
|
||||
type: 'hasOne',
|
||||
target: 'foos',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const data = await Collection.repository.findOne({
|
||||
filter: {
|
||||
name: 'tests',
|
||||
},
|
||||
appends: ['fields'],
|
||||
});
|
||||
|
||||
const json = data.toJSON();
|
||||
|
||||
expect(json.fields.length).toBe(7);
|
||||
|
||||
expect(json).toMatchObject({
|
||||
name: 'tests',
|
||||
fields: [
|
||||
{
|
||||
type: 'uid',
|
||||
name: 'name',
|
||||
prefix: 'f_',
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
unique: true,
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'title',
|
||||
unique: true,
|
||||
},
|
||||
{
|
||||
type: 'belongsToMany',
|
||||
target: 'tests',
|
||||
},
|
||||
{
|
||||
type: 'belongsTo',
|
||||
target: 'foos',
|
||||
},
|
||||
{
|
||||
type: 'hasMany',
|
||||
target: 'foos',
|
||||
},
|
||||
{
|
||||
type: 'hasOne',
|
||||
target: 'foos',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,86 @@
|
||||
import Database, { Collection as DBCollection, StringFieldOptions } from '@nocobase/database';
|
||||
import Application from '@nocobase/server';
|
||||
import { createApp } from '.';
|
||||
|
||||
describe('collections repository', () => {
|
||||
let db: Database;
|
||||
let app: Application;
|
||||
let Collection: DBCollection;
|
||||
let Field: DBCollection;
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await createApp();
|
||||
await app.db.sync();
|
||||
db = app.db;
|
||||
Collection = db.getCollection('collections');
|
||||
Field = db.getCollection('fields');
|
||||
await Collection.repository.create({
|
||||
values: {
|
||||
name: 'tests',
|
||||
},
|
||||
});
|
||||
await Collection.repository.create({
|
||||
values: {
|
||||
name: 'foos',
|
||||
},
|
||||
});
|
||||
await Collection.repository.create({
|
||||
values: {
|
||||
name: 'bars',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
it('should generate the name and key randomly', async () => {
|
||||
const field = await Field.repository.create({
|
||||
values: {
|
||||
type: 'string',
|
||||
collectionName: 'tests',
|
||||
},
|
||||
});
|
||||
expect(field.toJSON()).toMatchObject({
|
||||
type: 'string',
|
||||
collectionName: 'tests',
|
||||
});
|
||||
expect(field.get('name')).toBeDefined();
|
||||
expect(field.get('key')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should not generate the name randomly', async () => {
|
||||
const field = await Field.repository.create({
|
||||
values: {
|
||||
type: 'string',
|
||||
name: 'name',
|
||||
collectionName: 'tests',
|
||||
},
|
||||
});
|
||||
expect(field.toJSON()).toMatchObject({
|
||||
type: 'string',
|
||||
name: 'name',
|
||||
collectionName: 'tests',
|
||||
});
|
||||
});
|
||||
|
||||
it('dynamic parameters', async () => {
|
||||
const field = await Field.repository.create({
|
||||
values: {
|
||||
type: 'string',
|
||||
name: 'name',
|
||||
collectionName: 'tests',
|
||||
unique: true,
|
||||
defaultValue: 'abc',
|
||||
} as StringFieldOptions,
|
||||
});
|
||||
expect(field.toJSON()).toMatchObject({
|
||||
type: 'string',
|
||||
name: 'name',
|
||||
collectionName: 'tests',
|
||||
unique: true,
|
||||
defaultValue: 'abc',
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,79 @@
|
||||
import Database, { Collection as DBCollection, StringFieldOptions } from '@nocobase/database';
|
||||
import Application from '@nocobase/server';
|
||||
import { createApp } from '..';
|
||||
|
||||
describe('children options', () => {
|
||||
let db: Database;
|
||||
let app: Application;
|
||||
let Collection: DBCollection;
|
||||
let Field: DBCollection;
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await createApp();
|
||||
await app.db.sync();
|
||||
db = app.db;
|
||||
Collection = db.getCollection('collections');
|
||||
Field = db.getCollection('fields');
|
||||
await Collection.repository.create({
|
||||
values: {
|
||||
name: 'tests',
|
||||
},
|
||||
});
|
||||
await Collection.repository.create({
|
||||
values: {
|
||||
name: 'foos',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
it('when there are no children, the target collection is not created', async () => {
|
||||
const field = await Field.repository.create({
|
||||
values: {
|
||||
type: 'hasMany',
|
||||
collectionName: 'tests',
|
||||
},
|
||||
});
|
||||
const json = field.toJSON();
|
||||
expect(json).toMatchObject({
|
||||
type: 'hasMany',
|
||||
collectionName: 'tests',
|
||||
sourceKey: 'id',
|
||||
targetKey: 'id',
|
||||
});
|
||||
expect(json.name).toBeDefined();
|
||||
expect(json.target).toBeDefined();
|
||||
expect(json.foreignKey).toBeDefined();
|
||||
// 无 children 时,target collection 不创建
|
||||
const target = await Collection.model.findOne({
|
||||
where: {
|
||||
name: json.target,
|
||||
},
|
||||
});
|
||||
expect(target).toBeNull();
|
||||
});
|
||||
|
||||
it('the collectionName of the child field is the target of the parent field', async () => {
|
||||
const field = await Field.repository.create({
|
||||
values: {
|
||||
type: 'hasMany',
|
||||
collectionName: 'tests',
|
||||
children: [{ type: 'string' }, { type: 'string' }],
|
||||
},
|
||||
});
|
||||
const json = field.toJSON();
|
||||
const target = await Collection.model.findOne({
|
||||
where: {
|
||||
name: json.target,
|
||||
},
|
||||
});
|
||||
expect(target).toBeDefined();
|
||||
// 子字段的 collectionName 是父字段的 target
|
||||
for (const child of json.children) {
|
||||
expect(child.collectionName).toBe(json.target);
|
||||
}
|
||||
});
|
||||
});
|
@ -0,0 +1,75 @@
|
||||
import Database, { Collection as DBCollection, StringFieldOptions } from '@nocobase/database';
|
||||
import Application from '@nocobase/server';
|
||||
import { createApp } from '..';
|
||||
|
||||
describe('hasMany field options', () => {
|
||||
let db: Database;
|
||||
let app: Application;
|
||||
let Collection: DBCollection;
|
||||
let Field: DBCollection;
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await createApp();
|
||||
await app.db.sync();
|
||||
db = app.db;
|
||||
Collection = db.getCollection('collections');
|
||||
Field = db.getCollection('fields');
|
||||
await Collection.repository.create({
|
||||
values: {
|
||||
name: 'tests',
|
||||
},
|
||||
});
|
||||
await Collection.repository.create({
|
||||
values: {
|
||||
name: 'foos',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
it('should generate the foreignKey randomly', async () => {
|
||||
const field = await Field.repository.create({
|
||||
values: {
|
||||
type: 'hasMany',
|
||||
collectionName: 'tests',
|
||||
target: 'foos',
|
||||
},
|
||||
});
|
||||
const json = field.toJSON();
|
||||
expect(json).toMatchObject({
|
||||
type: 'hasMany',
|
||||
collectionName: 'tests',
|
||||
target: 'foos',
|
||||
sourceKey: 'id',
|
||||
targetKey: 'id',
|
||||
});
|
||||
expect(json.name).toBeDefined();
|
||||
expect(json.foreignKey).toBeDefined();
|
||||
});
|
||||
|
||||
it('the parameters are not generated randomly', async () => {
|
||||
const field = await Field.repository.create({
|
||||
values: {
|
||||
name: 'foos',
|
||||
type: 'hasMany',
|
||||
collectionName: 'tests',
|
||||
target: 'foos',
|
||||
sourceKey: 'abc',
|
||||
foreignKey: 'def',
|
||||
targetKey: 'ghi',
|
||||
},
|
||||
});
|
||||
expect(field.toJSON()).toMatchObject({
|
||||
name: 'foos',
|
||||
type: 'hasMany',
|
||||
collectionName: 'tests',
|
||||
target: 'foos',
|
||||
sourceKey: 'abc',
|
||||
foreignKey: 'def',
|
||||
targetKey: 'ghi',
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,73 @@
|
||||
import Database, { Collection as DBCollection, StringFieldOptions } from '@nocobase/database';
|
||||
import Application from '@nocobase/server';
|
||||
import { createApp } from '..';
|
||||
|
||||
describe('hasOne field options', () => {
|
||||
let db: Database;
|
||||
let app: Application;
|
||||
let Collection: DBCollection;
|
||||
let Field: DBCollection;
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await createApp();
|
||||
await app.db.sync();
|
||||
db = app.db;
|
||||
Collection = db.getCollection('collections');
|
||||
Field = db.getCollection('fields');
|
||||
await Collection.repository.create({
|
||||
values: {
|
||||
name: 'tests',
|
||||
},
|
||||
});
|
||||
await Collection.repository.create({
|
||||
values: {
|
||||
name: 'foos',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
it('should generate the foreignKey randomly', async () => {
|
||||
const field = await Field.repository.create({
|
||||
values: {
|
||||
type: 'hasOne',
|
||||
collectionName: 'tests',
|
||||
target: 'foos',
|
||||
},
|
||||
});
|
||||
const json = field.toJSON();
|
||||
// hasOne 的 sourceKey 默认为 id,foreignKey 随机生成
|
||||
expect(json).toMatchObject({
|
||||
type: 'hasOne',
|
||||
collectionName: 'tests',
|
||||
target: 'foos',
|
||||
sourceKey: 'id',
|
||||
});
|
||||
expect(json.name).toBeDefined();
|
||||
expect(json.foreignKey).toBeDefined();
|
||||
});
|
||||
|
||||
it('the parameters are not generated randomly', async () => {
|
||||
const field = await Field.repository.create({
|
||||
values: {
|
||||
name: 'foo',
|
||||
type: 'hasOne',
|
||||
collectionName: 'tests',
|
||||
target: 'foos',
|
||||
sourceKey: 'abc',
|
||||
foreignKey: 'def',
|
||||
},
|
||||
});
|
||||
expect(field.toJSON()).toMatchObject({
|
||||
name: 'foo',
|
||||
type: 'hasOne',
|
||||
collectionName: 'tests',
|
||||
target: 'foos',
|
||||
sourceKey: 'abc',
|
||||
foreignKey: 'def',
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,59 @@
|
||||
import Database, { Collection as DBCollection, StringFieldOptions } from '@nocobase/database';
|
||||
import Application from '@nocobase/server';
|
||||
import { createApp } from '..';
|
||||
|
||||
describe('reverseField options', () => {
|
||||
let db: Database;
|
||||
let app: Application;
|
||||
let Collection: DBCollection;
|
||||
let Field: DBCollection;
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await createApp();
|
||||
await app.db.sync();
|
||||
db = app.db;
|
||||
Collection = db.getCollection('collections');
|
||||
Field = db.getCollection('fields');
|
||||
await Collection.repository.create({
|
||||
values: {
|
||||
name: 'tests',
|
||||
},
|
||||
});
|
||||
await Collection.repository.create({
|
||||
values: {
|
||||
name: 'targets',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
it('reverseField', async () => {
|
||||
const field = await Field.repository.create({
|
||||
values: {
|
||||
type: 'hasMany',
|
||||
collectionName: 'tests',
|
||||
target: 'targets',
|
||||
reverseField: {},
|
||||
},
|
||||
});
|
||||
const json = JSON.parse(JSON.stringify(field.toJSON()));
|
||||
expect(json).toMatchObject({
|
||||
type: 'hasMany',
|
||||
collectionName: 'tests',
|
||||
target: 'targets',
|
||||
targetKey: 'id',
|
||||
sourceKey: 'id',
|
||||
reverseField: {
|
||||
type: 'belongsTo',
|
||||
collectionName: 'targets',
|
||||
target: 'tests',
|
||||
targetKey: 'id',
|
||||
sourceKey: 'id',
|
||||
},
|
||||
});
|
||||
expect(json.foreignKey).toBe(json.reverseField.foreignKey);
|
||||
});
|
||||
});
|
11
packages/plugin-collection-manager/src/__tests__/index.ts
Normal file
11
packages/plugin-collection-manager/src/__tests__/index.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { mockServer } from '@nocobase/test';
|
||||
import CollectionManagerPlugin from '..';
|
||||
|
||||
export async function createApp() {
|
||||
const app = mockServer();
|
||||
const queryInterface = app.db.sequelize.getQueryInterface();
|
||||
await queryInterface.dropAllTables();
|
||||
app.plugin(CollectionManagerPlugin);
|
||||
await app.load();
|
||||
return app;
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
import { CollectionOptions } from '@nocobase/database';
|
||||
|
||||
export default {
|
||||
name: 'collections',
|
||||
title: '数据表配置',
|
||||
sortable: 'sort',
|
||||
autoGenId: false,
|
||||
model: 'CollectionModel',
|
||||
timestamps: false,
|
||||
filterTargetKey: 'name',
|
||||
fields: [
|
||||
{
|
||||
type: 'uid',
|
||||
name: 'key',
|
||||
primaryKey: true,
|
||||
},
|
||||
{
|
||||
type: 'uid',
|
||||
name: 'name',
|
||||
unique: true,
|
||||
prefix: 't_',
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'title',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
type: 'json',
|
||||
name: 'options',
|
||||
defaultValue: {},
|
||||
},
|
||||
{
|
||||
type: 'hasMany',
|
||||
name: 'fields',
|
||||
target: 'fields',
|
||||
sourceKey: 'name',
|
||||
targetKey: 'name',
|
||||
foreignKey: 'collectionName',
|
||||
},
|
||||
],
|
||||
} as CollectionOptions;
|
65
packages/plugin-collection-manager/src/collections/fields.ts
Normal file
65
packages/plugin-collection-manager/src/collections/fields.ts
Normal file
@ -0,0 +1,65 @@
|
||||
import { CollectionOptions } from '@nocobase/database';
|
||||
|
||||
export default {
|
||||
name: 'fields',
|
||||
autoGenId: false,
|
||||
model: 'FieldModel',
|
||||
timestamps: false,
|
||||
sortable: {
|
||||
type: 'sort',
|
||||
name: 'sort',
|
||||
scope: ['parentKey'],
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
type: 'uid',
|
||||
name: 'key',
|
||||
primaryKey: true,
|
||||
},
|
||||
{
|
||||
type: 'uid',
|
||||
name: 'name',
|
||||
prefix: 'f_',
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'type',
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'interface',
|
||||
allowNull: true,
|
||||
},
|
||||
{
|
||||
type: 'belongsTo',
|
||||
name: 'collection',
|
||||
target: 'collections',
|
||||
foreignKey: 'collectionName',
|
||||
targetKey: 'name',
|
||||
},
|
||||
{
|
||||
type: 'hasMany',
|
||||
name: 'children',
|
||||
target: 'fields',
|
||||
sourceKey: 'key',
|
||||
foreignKey: 'parentKey',
|
||||
},
|
||||
{
|
||||
type: 'hasOne',
|
||||
name: 'reverseField',
|
||||
target: 'fields',
|
||||
sourceKey: 'key',
|
||||
foreignKey: 'reverseKey',
|
||||
},
|
||||
{
|
||||
type: 'belongsTo',
|
||||
name: 'uiSchema',
|
||||
target: 'ui_schemas',
|
||||
},
|
||||
{
|
||||
type: 'json',
|
||||
name: 'options',
|
||||
defaultValue: {},
|
||||
},
|
||||
],
|
||||
} as CollectionOptions;
|
@ -0,0 +1,14 @@
|
||||
import Database from '@nocobase/database';
|
||||
|
||||
export function afterCreateForReverseField(db: Database) {
|
||||
const Field = db.getCollection('fields');
|
||||
|
||||
return async (model, { transaction }) => {
|
||||
const reverseKey = model.get('reverseKey');
|
||||
if (!reverseKey) {
|
||||
return;
|
||||
}
|
||||
const reverse = await Field.model.findByPk(reverseKey, { transaction });
|
||||
await reverse.update({ reverseKey: model.get('key') }, { hooks: false, transaction });
|
||||
};
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import Database from '@nocobase/database';
|
||||
|
||||
export function beforeCreateForChildrenCollection(db: Database) {
|
||||
const Collection = db.getCollection('collections');
|
||||
const Field = db.getCollection('fields');
|
||||
|
||||
return async (model, { transaction }) => {
|
||||
const parentKey = model.get('parentKey');
|
||||
if (!parentKey) {
|
||||
return;
|
||||
}
|
||||
const parent = await Field.model.findByPk(parentKey, { transaction });
|
||||
const parentTarget = parent.get('target');
|
||||
model.set('collectionName', parentTarget);
|
||||
const collection = await Collection.model.findOne({
|
||||
transaction,
|
||||
where: {
|
||||
name: parentTarget,
|
||||
},
|
||||
});
|
||||
if (!collection) {
|
||||
await Collection.model.create({ name: parentTarget }, { transaction });
|
||||
}
|
||||
};
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
import Database from '@nocobase/database';
|
||||
|
||||
export function beforeCreateForReverseField(db: Database) {
|
||||
const Field = db.getCollection('fields');
|
||||
|
||||
return async (model, { transaction }) => {
|
||||
const reverseKey = model.get('reverseKey');
|
||||
if (!reverseKey) {
|
||||
return;
|
||||
}
|
||||
const reverse = await Field.model.findByPk(reverseKey, { transaction });
|
||||
model.set('collectionName', reverse.get('target'));
|
||||
model.set('target', reverse.get('collectionName'));
|
||||
const reverseType = reverse.get('type') as any;
|
||||
if (['hasMany', 'hasOne'].includes(reverseType)) {
|
||||
model.set('type', 'belongsTo');
|
||||
model.set('targetKey', reverse.get('sourceKey'));
|
||||
model.set('foreignKey', reverse.get('foreignKey'));
|
||||
model.set('sourceKey', reverse.get('targetKey'));
|
||||
}
|
||||
if (['belongsTo'].includes(reverseType)) {
|
||||
if (!model.get('type')) {
|
||||
model.set('type', 'hasMany');
|
||||
}
|
||||
model.set('sourceKey', reverse.get('targetKey'));
|
||||
model.set('foreignKey', reverse.get('foreignKey'));
|
||||
model.set('targetKey', reverse.get('sourceKey'));
|
||||
}
|
||||
if (['belongsToMany'].includes(reverseType)) {
|
||||
model.set('type', 'belongsToMany');
|
||||
model.set('through', reverse.get('through'));
|
||||
model.set('sourceKey', reverse.get('targetKey'));
|
||||
model.set('foreignKey', reverse.get('otherKey'));
|
||||
model.set('targetKey', reverse.get('sourceKey'));
|
||||
model.set('otherKey', reverse.get('foreignKey'));
|
||||
}
|
||||
};
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
import { uid } from '@nocobase/utils';
|
||||
import { Model } from 'sequelize';
|
||||
|
||||
export default {
|
||||
belongsTo(model: Model) {
|
||||
const defaults = {
|
||||
targetKey: 'id',
|
||||
foreignKey: `f_${uid()}`,
|
||||
};
|
||||
for (const key in defaults) {
|
||||
if (model.get(key)) {
|
||||
continue;
|
||||
}
|
||||
model.set(key, defaults[key]);
|
||||
}
|
||||
},
|
||||
belongsToMany(model: Model) {
|
||||
const defaults = {
|
||||
targetKey: 'id',
|
||||
sourceKey: 'id',
|
||||
through: `t_${uid()}`,
|
||||
foreignKey: `f_${uid()}`,
|
||||
otherKey: `f_${uid()}`,
|
||||
};
|
||||
for (const key in defaults) {
|
||||
if (model.get(key)) {
|
||||
continue;
|
||||
}
|
||||
model.set(key, defaults[key]);
|
||||
}
|
||||
},
|
||||
hasMany(model: Model) {
|
||||
const defaults = {
|
||||
targetKey: 'id',
|
||||
sourceKey: 'id',
|
||||
foreignKey: `f_${uid()}`,
|
||||
target: `t_${uid()}`,
|
||||
};
|
||||
for (const key in defaults) {
|
||||
if (model.get(key)) {
|
||||
continue;
|
||||
}
|
||||
model.set(key, defaults[key]);
|
||||
}
|
||||
},
|
||||
hasOne(model: Model) {
|
||||
const defaults = {
|
||||
sourceKey: 'id',
|
||||
foreignKey: `f_${uid()}`,
|
||||
};
|
||||
for (const key in defaults) {
|
||||
if (model.get(key)) {
|
||||
continue;
|
||||
}
|
||||
model.set(key, defaults[key]);
|
||||
}
|
||||
},
|
||||
};
|
45
packages/plugin-collection-manager/src/index.ts
Normal file
45
packages/plugin-collection-manager/src/index.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import path from 'path';
|
||||
import { Plugin } from '@nocobase/server';
|
||||
import { CollectionModel } from './models/collection';
|
||||
import { FieldModel } from './models/field';
|
||||
import { uid } from '@nocobase/utils';
|
||||
import beforeInitOptions from './hooks/beforeInitOptions';
|
||||
import { beforeCreateForChildrenCollection } from './hooks/beforeCreateForChildrenCollection';
|
||||
import { beforeCreateForReverseField } from './hooks/beforeCreateForReverseField';
|
||||
import { afterCreateForReverseField } from './hooks/afterCreateForReverseField';
|
||||
|
||||
export default class CollectionManagerPlugin extends Plugin {
|
||||
async load() {
|
||||
this.app.db.registerModels({
|
||||
CollectionModel,
|
||||
FieldModel,
|
||||
});
|
||||
await this.app.db.import({
|
||||
directory: path.resolve(__dirname, './collections'),
|
||||
});
|
||||
// 要在 beforeInitOptions 之前处理
|
||||
this.app.db.on('fields.beforeCreate', beforeCreateForReverseField(this.app.db));
|
||||
this.app.db.on('fields.beforeCreate', beforeCreateForChildrenCollection(this.app.db));
|
||||
this.app.db.on('fields.beforeCreate', async (model, options) => {
|
||||
const type = model.get('type');
|
||||
await this.app.db.emitAsync(`fields.${type}.beforeInitOptions`, model, options);
|
||||
});
|
||||
for (const key in beforeInitOptions) {
|
||||
if (Object.prototype.hasOwnProperty.call(beforeInitOptions, key)) {
|
||||
const fn = beforeInitOptions[key];
|
||||
this.app.db.on(`fields.${key}.beforeInitOptions`, fn);
|
||||
}
|
||||
}
|
||||
this.app.db.on('fields.afterCreate', afterCreateForReverseField(this.app.db));
|
||||
this.app.db.on('collections.afterCreate', async (model, options) => {
|
||||
if (options.context) {
|
||||
await model.migrate();
|
||||
}
|
||||
});
|
||||
this.app.db.on('fields.afterCreate', async (model, options) => {
|
||||
if (options.context) {
|
||||
await model.migrate();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
54
packages/plugin-collection-manager/src/models/collection.ts
Normal file
54
packages/plugin-collection-manager/src/models/collection.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { SyncOptions } from 'sequelize';
|
||||
import Database, { Collection, MagicAttributeModel } from '@nocobase/database';
|
||||
import { FieldModel } from './field';
|
||||
|
||||
interface LoadOptions {
|
||||
// TODO
|
||||
skipField?: boolean;
|
||||
skipExist?: boolean;
|
||||
}
|
||||
|
||||
export class CollectionModel extends MagicAttributeModel {
|
||||
|
||||
get db(): Database {
|
||||
return (<any>this.constructor).database;
|
||||
}
|
||||
|
||||
async load(loadOptions?: LoadOptions) {
|
||||
const { skipExist, skipField } = loadOptions;
|
||||
const name = this.get('name');
|
||||
let collection: Collection;
|
||||
if (this.db.hasCollection(name)) {
|
||||
collection = this.db.getCollection(name);
|
||||
if (skipExist) {
|
||||
return collection;
|
||||
}
|
||||
collection.updateOptions(this.get());
|
||||
} else {
|
||||
collection = this.db.collection(this.get());
|
||||
}
|
||||
if (!skipField) {
|
||||
await this.loadFields();
|
||||
}
|
||||
return collection;
|
||||
}
|
||||
|
||||
async loadFields() {
|
||||
// @ts-ignore
|
||||
const instances: FieldModel[] = await this.getFields();
|
||||
for (const instance of instances) {
|
||||
await instance.load();
|
||||
}
|
||||
}
|
||||
|
||||
async migrate(options?: SyncOptions) {
|
||||
const collection = await this.load();
|
||||
await collection.sync({
|
||||
force: false,
|
||||
alter: {
|
||||
drop: false,
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
}
|
32
packages/plugin-collection-manager/src/models/field.ts
Normal file
32
packages/plugin-collection-manager/src/models/field.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { SyncOptions } from 'sequelize';
|
||||
import Database, { MagicAttributeModel } from '@nocobase/database';
|
||||
|
||||
interface LoadOptions {
|
||||
// TODO
|
||||
skipExist?: boolean;
|
||||
}
|
||||
|
||||
export class FieldModel extends MagicAttributeModel {
|
||||
get db(): Database {
|
||||
return (<any>this.constructor).database;
|
||||
}
|
||||
|
||||
async load(loadOptions?: LoadOptions) {
|
||||
const { skipExist } = loadOptions;
|
||||
const collectionName = this.get('collectionName');
|
||||
if (!this.db.hasCollection(collectionName)) {
|
||||
throw new Error(`${collectionName} collection does not exist.`);
|
||||
}
|
||||
const collection = this.db.getCollection(collectionName);
|
||||
const name = this.get('name');
|
||||
if (skipExist && collection.hasField(name)) {
|
||||
return collection.getField(name);
|
||||
}
|
||||
return collection.setField(name, this.get());
|
||||
}
|
||||
|
||||
async migrate(options?: SyncOptions) {
|
||||
const field = await this.load();
|
||||
await field.sync(options);
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
import { Repository } from '@nocobase/database';
|
||||
import { CollectionModel } from '../models/collection';
|
||||
|
||||
interface LoadOptions {
|
||||
filter?: any;
|
||||
skipExist?: boolean;
|
||||
}
|
||||
|
||||
export class CollectionRepository extends Repository {
|
||||
async load(options?: LoadOptions) {
|
||||
const { filter, skipExist } = options;
|
||||
const instances = (await this.find({ filter })) as CollectionModel[];
|
||||
for (const instance of instances) {
|
||||
await instance.load({ skipExist });
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user