Feat/plugin UI schema v0.6 (#143)
* v0.6 * plugin-ui-schema: insert && getJsonSchema * plugin-ui-schema: insert schema with sort * plugin-ui-schema: node with x-index * insert adjacent method * chore: insert * typo * insert with x-uid * fix: getSchema by subtree * add ui-schema actions * fix: mysql compatibility * remove ui-schema when remove node tree * ui schema patch * ui_schemas.create * test cases * test cases * fix(database): reset changed before update * feat: insert ui schema node after created * feat: patch ui schema node after updated * fix: sqlite error * uid * cleanup * test cases * feat: ui_schema items type support * fix: insert items node * fix: get inner type * change items struct * add insert return value * add insert return value Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
parent
380b5e8c7a
commit
f67658129f
@ -29,6 +29,8 @@ describe('magic-attribute-model', () => {
|
||||
|
||||
test.set('x-component-props', { arr2: [1, 2, 3] });
|
||||
|
||||
await test.save();
|
||||
|
||||
expect(test.toJSON()).toMatchObject({
|
||||
title: 'aa',
|
||||
'x-component-props': {
|
||||
@ -39,6 +41,8 @@ describe('magic-attribute-model', () => {
|
||||
},
|
||||
'x-decorator-props': { key1: 'val1' },
|
||||
});
|
||||
|
||||
await db.close();
|
||||
});
|
||||
|
||||
it('case 2', async () => {
|
||||
@ -57,7 +61,7 @@ describe('magic-attribute-model', () => {
|
||||
|
||||
await db.sync();
|
||||
|
||||
const test = await Test.model.create({
|
||||
let test = await Test.model.create({
|
||||
title: 'aa',
|
||||
'x-component-props': { key1: 'val1', arr1: [1, 2, 3], arr2: [4, 5] },
|
||||
});
|
||||
@ -69,15 +73,27 @@ describe('magic-attribute-model', () => {
|
||||
|
||||
test.set('x-component-props', { arr2: [1, 2, 3] });
|
||||
|
||||
await test.save();
|
||||
|
||||
test = await Test.model.findByPk(test.get('id') as string);
|
||||
|
||||
await test.update({
|
||||
'x-component-props': { arr2: [1, 2, 3, 4] }
|
||||
});
|
||||
|
||||
test = await Test.model.findByPk(test.get('id') as string);
|
||||
|
||||
expect(test.toJSON()).toMatchObject({
|
||||
title: 'aa',
|
||||
'x-component-props': {
|
||||
key1: 'val1',
|
||||
key2: 'val2',
|
||||
arr1: [3, 4],
|
||||
arr2: [1, 2, 3],
|
||||
arr2: [1, 2, 3, 4],
|
||||
},
|
||||
'x-decorator-props': { key1: 'val1' },
|
||||
});
|
||||
|
||||
await db.close();
|
||||
});
|
||||
});
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { DataTypes } from 'sequelize';
|
||||
import { uid } from '../utils';
|
||||
import { BaseColumnFieldOptions, Field } from './field';
|
||||
import { uid } from '@nocobase/utils';
|
||||
|
||||
export class UidField extends Field {
|
||||
get dataType() {
|
||||
|
@ -1,7 +1,6 @@
|
||||
export * from './database';
|
||||
export * from './collection';
|
||||
export * from './repository';
|
||||
export * from './utils';
|
||||
export { Database as default } from './database';
|
||||
export * from './relation-repository/belongs-to-many-repository';
|
||||
export * from './relation-repository/belongs-to-repository';
|
||||
|
@ -5,7 +5,7 @@ import Database from './database';
|
||||
|
||||
export class MagicAttributeModel extends Model {
|
||||
get magicAttribute() {
|
||||
const db: Database = (this.constructor as any).database;
|
||||
const db: Database = (<any>this.constructor).database;
|
||||
const collection = db.getCollection(this.constructor.name);
|
||||
return collection.options.magicAttribute || 'options';
|
||||
}
|
||||
@ -50,4 +50,10 @@ export class MagicAttributeModel extends Model {
|
||||
...data[this.magicAttribute],
|
||||
};
|
||||
}
|
||||
|
||||
async update(values?: any, options?: any) {
|
||||
// @ts-ignore
|
||||
this._changed = new Set();
|
||||
return super.update(values, options);
|
||||
}
|
||||
}
|
||||
|
@ -287,7 +287,7 @@ export class Repository<TModelAttributes extends {} = any, TCreationAttributes e
|
||||
async create<M extends Model>(options: CreateOptions): Promise<M> {
|
||||
const transaction = await this.getTransaction(options);
|
||||
|
||||
const guard = UpdateGuard.fromOptions(this.model, options);
|
||||
const guard = UpdateGuard.fromOptions(this.model, { ...options, action: 'create' });
|
||||
const values = guard.sanitize(options.values || {});
|
||||
|
||||
const instance = await this.model.create<any>(values, {
|
||||
|
@ -11,18 +11,28 @@ type UpdateValues = {
|
||||
[key: string]: UpdateValueItem | Array<UpdateValueItem>;
|
||||
};
|
||||
|
||||
type UpdateAction = 'create' | 'update';
|
||||
export class UpdateGuard {
|
||||
model: ModelCtor<any>;
|
||||
action: UpdateAction;
|
||||
private associationKeysToBeUpdate: AssociationKeysToBeUpdate;
|
||||
private blackList: BlackList;
|
||||
private whiteList: WhiteList;
|
||||
|
||||
setAction(action: UpdateAction) {
|
||||
this.action = action;
|
||||
}
|
||||
|
||||
setModel(model: ModelCtor<any>) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
setAssociationKeysToBeUpdate(associationKeysToBeUpdate: AssociationKeysToBeUpdate) {
|
||||
this.associationKeysToBeUpdate = associationKeysToBeUpdate;
|
||||
if (this.action == 'create') {
|
||||
this.associationKeysToBeUpdate = Object.keys(this.model.associations);
|
||||
} else {
|
||||
this.associationKeysToBeUpdate = associationKeysToBeUpdate;
|
||||
}
|
||||
}
|
||||
|
||||
setWhiteList(whiteList: WhiteList) {
|
||||
@ -148,8 +158,8 @@ export class UpdateGuard {
|
||||
guard.setModel(model);
|
||||
guard.setWhiteList(options.whitelist);
|
||||
guard.setBlackList(options.blacklist);
|
||||
guard.setAction(lodash.get(options, 'action', 'update'));
|
||||
guard.setAssociationKeysToBeUpdate(options.updateAssociationValues);
|
||||
|
||||
return guard;
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +0,0 @@
|
||||
let IDX = 36,
|
||||
HEX = '';
|
||||
while (IDX--) HEX += IDX.toString(36);
|
||||
|
||||
export function uid(len?: number) {
|
||||
let str = '',
|
||||
num = len || 11;
|
||||
while (num--) str += HEX[(Math.random() * 36) | 0];
|
||||
return str;
|
||||
}
|
7
packages/plugin-ui-schema/.npmignore
Normal file
7
packages/plugin-ui-schema/.npmignore
Normal file
@ -0,0 +1,7 @@
|
||||
node_modules
|
||||
*.log
|
||||
docs
|
||||
__tests__
|
||||
tsconfig.json
|
||||
src
|
||||
.fatherrc.ts
|
17
packages/plugin-ui-schema/package.json
Normal file
17
packages/plugin-ui-schema/package.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "@nocobase/plugin-ui-schema",
|
||||
"version": "0.6.0-alpha.0",
|
||||
"main": "lib/index.js",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "rimraf -rf lib esm dist && npm run build:cjs && npm run build:esm",
|
||||
"build:cjs": "tsc --project tsconfig.build.json",
|
||||
"build:esm": "tsc --project tsconfig.build.json --module es2015 --outDir esm"
|
||||
},
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@formily/json-schema": "^2.0.6",
|
||||
"@nocobase/test": "^0.6.0-alpha.0"
|
||||
},
|
||||
"gitHead": "e7df1f93c4e23b9a666d99ee7372c02bdaec97c4"
|
||||
}
|
216
packages/plugin-ui-schema/src/__tests__/action.test.ts
Normal file
216
packages/plugin-ui-schema/src/__tests__/action.test.ts
Normal file
@ -0,0 +1,216 @@
|
||||
import { MockServer, mockServer } from '@nocobase/test';
|
||||
import { Database } from '@nocobase/database';
|
||||
import PluginUiSchema from '../server';
|
||||
|
||||
describe('action test', () => {
|
||||
let app: MockServer;
|
||||
let db: Database;
|
||||
|
||||
beforeEach(async () => {
|
||||
app = mockServer({
|
||||
registerActions: true,
|
||||
});
|
||||
|
||||
db = app.db;
|
||||
|
||||
const queryInterface = db.sequelize.getQueryInterface();
|
||||
await queryInterface.dropAllTables();
|
||||
|
||||
app.plugin(PluginUiSchema);
|
||||
|
||||
await app.load();
|
||||
await db.sync({
|
||||
force: false,
|
||||
alter: {
|
||||
drop: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
test('insert action', async () => {
|
||||
const response = await app
|
||||
.agent()
|
||||
.resource('ui_schemas')
|
||||
.insert({
|
||||
values: {
|
||||
'x-uid': 'n1',
|
||||
name: 'a',
|
||||
type: 'object',
|
||||
properties: {
|
||||
b: {
|
||||
'x-uid': 'n2',
|
||||
type: 'object',
|
||||
properties: {
|
||||
c: { 'x-uid': 'n3' },
|
||||
},
|
||||
},
|
||||
d: { 'x-uid': 'n4' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).toEqual(200);
|
||||
});
|
||||
|
||||
test('getJsonSchema', async () => {
|
||||
await app
|
||||
.agent()
|
||||
.resource('ui_schemas')
|
||||
.insert({
|
||||
values: {
|
||||
'x-uid': 'n1',
|
||||
name: 'a',
|
||||
type: 'object',
|
||||
properties: {
|
||||
b: {
|
||||
'x-uid': 'n2',
|
||||
type: 'object',
|
||||
properties: {
|
||||
c: { 'x-uid': 'n3' },
|
||||
},
|
||||
},
|
||||
d: { 'x-uid': 'n4' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const response = await app.agent().resource('ui_schemas').getJsonSchema({
|
||||
resourceIndex: 'n1',
|
||||
});
|
||||
|
||||
const { data } = response.body;
|
||||
expect(data.properties.b.properties.c['x-uid']).toEqual('n3');
|
||||
});
|
||||
|
||||
test('remove', async () => {
|
||||
await app
|
||||
.agent()
|
||||
.resource('ui_schemas')
|
||||
.insert({
|
||||
values: {
|
||||
'x-uid': 'n1',
|
||||
name: 'a',
|
||||
type: 'object',
|
||||
properties: {
|
||||
b: {
|
||||
'x-uid': 'n2',
|
||||
type: 'object',
|
||||
properties: {
|
||||
c: { 'x-uid': 'n3' },
|
||||
},
|
||||
},
|
||||
d: { 'x-uid': 'n4' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let response = await app.agent().resource('ui_schemas').remove({
|
||||
resourceIndex: 'n2',
|
||||
});
|
||||
|
||||
expect(response.statusCode).toEqual(200);
|
||||
|
||||
response = await app.agent().resource('ui_schemas').getJsonSchema({
|
||||
resourceIndex: 'n1',
|
||||
});
|
||||
|
||||
const { data } = response.body;
|
||||
expect(data.properties.b).not.toBeDefined();
|
||||
});
|
||||
|
||||
test('patch', async () => {
|
||||
await app
|
||||
.agent()
|
||||
.resource('ui_schemas')
|
||||
.insert({
|
||||
values: {
|
||||
'x-uid': 'n1',
|
||||
name: 'a',
|
||||
type: 'object',
|
||||
properties: {
|
||||
b: {
|
||||
'x-uid': 'n2',
|
||||
type: 'object',
|
||||
properties: {
|
||||
c: { 'x-uid': 'n3' },
|
||||
},
|
||||
},
|
||||
d: { 'x-uid': 'n4' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let response = await app
|
||||
.agent()
|
||||
.resource('ui_schemas')
|
||||
.patch({
|
||||
values: {
|
||||
'x-uid': 'n1',
|
||||
properties: {
|
||||
b: {
|
||||
properties: {
|
||||
c: {
|
||||
title: 'c-title',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).toEqual(200);
|
||||
response = await app.agent().resource('ui_schemas').getJsonSchema({
|
||||
resourceIndex: 'n1',
|
||||
});
|
||||
|
||||
const { data } = response.body;
|
||||
expect(data.properties.b['properties']['c']['title']).toEqual('c-title');
|
||||
});
|
||||
|
||||
test('insert adjacent', async () => {
|
||||
await app
|
||||
.agent()
|
||||
.resource('ui_schemas')
|
||||
.insert({
|
||||
values: {
|
||||
'x-uid': 'n1',
|
||||
name: 'a',
|
||||
type: 'object',
|
||||
properties: {
|
||||
b: {
|
||||
'x-uid': 'n2',
|
||||
type: 'object',
|
||||
properties: {
|
||||
c: { 'x-uid': 'n3' },
|
||||
},
|
||||
},
|
||||
d: { 'x-uid': 'n4' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let response = await app
|
||||
.agent()
|
||||
.resource('ui_schemas')
|
||||
.insertAdjacent({
|
||||
resourceIndex: 'n2',
|
||||
position: 'beforeBegin',
|
||||
values: {
|
||||
'x-uid': 'n5',
|
||||
name: 'e',
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.statusCode).toEqual(200);
|
||||
response = await app.agent().resource('ui_schemas').getJsonSchema({
|
||||
resourceIndex: 'n1',
|
||||
});
|
||||
|
||||
const { data } = response.body;
|
||||
expect(data.properties.e['x-uid']).toEqual('n5');
|
||||
});
|
||||
});
|
144
packages/plugin-ui-schema/src/__tests__/ui-schema-model.test.ts
Normal file
144
packages/plugin-ui-schema/src/__tests__/ui-schema-model.test.ts
Normal file
@ -0,0 +1,144 @@
|
||||
import { mockServer, MockServer } from '@nocobase/test';
|
||||
import { Collection, Database } from '@nocobase/database';
|
||||
import PluginUiSchema, { UiSchemaRepository } from '@nocobase/plugin-ui-schema';
|
||||
|
||||
describe('ui schema model', () => {
|
||||
let app: MockServer;
|
||||
let db: Database;
|
||||
|
||||
let RelatedCollection: Collection;
|
||||
|
||||
afterEach(async () => {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
app = mockServer({
|
||||
registerActions: true,
|
||||
});
|
||||
|
||||
db = app.db;
|
||||
|
||||
const queryInterface = db.sequelize.getQueryInterface();
|
||||
await queryInterface.dropAllTables();
|
||||
|
||||
app.plugin(PluginUiSchema);
|
||||
|
||||
await app.load();
|
||||
|
||||
RelatedCollection = db.collection({
|
||||
name: 'hasUiSchemaCollection',
|
||||
fields: [
|
||||
{
|
||||
type: 'belongsTo',
|
||||
name: 'uiSchema',
|
||||
target: 'ui_schemas',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await db.sync({ force: true, alter: { drop: false } });
|
||||
});
|
||||
|
||||
it('should create schema tree after ui_schema created', async () => {
|
||||
const uiSchemaRepository = db.getCollection('ui_schemas').repository as UiSchemaRepository;
|
||||
|
||||
await RelatedCollection.repository.create({
|
||||
values: {
|
||||
uiSchema: {
|
||||
'x-uid': 'root-uid',
|
||||
title: 'root-node',
|
||||
name: 'root-node',
|
||||
properties: {
|
||||
child1: {
|
||||
title: 'child1',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const child1 = await uiSchemaRepository.findOne({
|
||||
filter: {
|
||||
name: 'child1',
|
||||
},
|
||||
});
|
||||
|
||||
const tree = await uiSchemaRepository.getJsonSchema('root-uid');
|
||||
expect(tree).toMatchObject({
|
||||
title: 'root-node',
|
||||
properties: {
|
||||
child1: {
|
||||
title: 'child1',
|
||||
'x-uid': child1.get('uid'),
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
name: 'root-node',
|
||||
'x-uid': 'root-uid',
|
||||
'x-async': false,
|
||||
'x-index': null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should update schema tree after ui_schema updated', async () => {
|
||||
const uiSchemaRepository = db.getCollection('ui_schemas').repository as UiSchemaRepository;
|
||||
|
||||
const relatedInstance = await RelatedCollection.repository.create({
|
||||
values: {
|
||||
uiSchema: {
|
||||
'x-uid': 'root-uid',
|
||||
title: 'root-node',
|
||||
name: 'root-node',
|
||||
properties: {
|
||||
child1: {
|
||||
title: 'child1',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const child1 = await uiSchemaRepository.findOne({
|
||||
filter: {
|
||||
name: 'child1',
|
||||
},
|
||||
});
|
||||
|
||||
await RelatedCollection.repository.update({
|
||||
updateAssociationValues: ['uiSchema'],
|
||||
filterByTk: relatedInstance.get('id') as string,
|
||||
values: {
|
||||
uiSchema: {
|
||||
'x-uid': 'root-uid',
|
||||
title: 'new-root-title',
|
||||
name: 'new-root-name',
|
||||
properties: {
|
||||
child1: {
|
||||
title: 'new-child1-title',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const tree = await uiSchemaRepository.getJsonSchema('root-uid');
|
||||
|
||||
expect(tree).toMatchObject({
|
||||
title: 'new-root-title',
|
||||
properties: {
|
||||
child1: {
|
||||
title: 'new-child1-title',
|
||||
'x-uid': child1.get('uid'),
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
},
|
||||
name: 'new-root-name',
|
||||
'x-uid': 'root-uid',
|
||||
'x-async': false,
|
||||
'x-index': null,
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,732 @@
|
||||
import { mockServer, MockServer } from '@nocobase/test';
|
||||
import { Collection, Database } from '@nocobase/database';
|
||||
import PluginUiSchema from '../server';
|
||||
import UiSchemaRepository from '../repository';
|
||||
import { SchemaNode } from '../dao/ui_schema_node_dao';
|
||||
|
||||
describe('ui_schema repository', () => {
|
||||
let app: MockServer;
|
||||
let db: Database;
|
||||
let repository: UiSchemaRepository;
|
||||
|
||||
let treePathCollection: Collection;
|
||||
|
||||
afterEach(async () => {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
app = mockServer({
|
||||
registerActions: true,
|
||||
|
||||
database: {},
|
||||
});
|
||||
|
||||
db = app.db;
|
||||
|
||||
const queryInterface = db.sequelize.getQueryInterface();
|
||||
await queryInterface.dropAllTables();
|
||||
|
||||
app.plugin(PluginUiSchema);
|
||||
|
||||
await app.load();
|
||||
await db.sync({
|
||||
force: false,
|
||||
alter: {
|
||||
drop: false,
|
||||
},
|
||||
});
|
||||
repository = db.getCollection('ui_schemas').repository as UiSchemaRepository;
|
||||
treePathCollection = db.getCollection('ui_schema_tree_path');
|
||||
});
|
||||
|
||||
it('should be registered', async () => {
|
||||
expect(db.getCollection('ui_schemas').repository).toBeInstanceOf(UiSchemaRepository);
|
||||
});
|
||||
|
||||
it('should insert single ui schema node', async () => {
|
||||
const singleNode: SchemaNode = {
|
||||
name: 'test',
|
||||
'x-uid': 'test',
|
||||
schema: {},
|
||||
};
|
||||
|
||||
const transaction = await db.sequelize.transaction();
|
||||
await repository.insertSingleNode(singleNode, transaction);
|
||||
|
||||
await transaction.commit();
|
||||
// it should save in ui schema tables
|
||||
const testNode = await repository.findOne({
|
||||
filter: {
|
||||
uid: 'test',
|
||||
},
|
||||
});
|
||||
|
||||
expect(testNode).not.toBeNull();
|
||||
|
||||
// it should save tree path
|
||||
const results = await treePathCollection.repository.find();
|
||||
expect(results.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('should insert child node', async () => {
|
||||
const singleNode: SchemaNode = {
|
||||
name: 'test',
|
||||
'x-uid': 'test',
|
||||
schema: {},
|
||||
};
|
||||
|
||||
const transaction = await db.sequelize.transaction();
|
||||
|
||||
await repository.insertSingleNode(singleNode, transaction);
|
||||
|
||||
const child1: SchemaNode = {
|
||||
name: 'child1',
|
||||
'x-uid': 'child1',
|
||||
schema: {},
|
||||
childOptions: {
|
||||
parentUid: 'test',
|
||||
type: 'test',
|
||||
},
|
||||
};
|
||||
|
||||
await repository.insertSingleNode(child1, transaction);
|
||||
|
||||
const child11: SchemaNode = {
|
||||
name: 'child11',
|
||||
'x-uid': 'child11',
|
||||
schema: {},
|
||||
childOptions: {
|
||||
parentUid: 'child1',
|
||||
type: 'test',
|
||||
},
|
||||
};
|
||||
await repository.insertSingleNode(child11, transaction);
|
||||
|
||||
await transaction.commit();
|
||||
expect(
|
||||
(
|
||||
await treePathCollection.repository.findOne({
|
||||
filter: {
|
||||
ancestor: 'test',
|
||||
descendant: 'test',
|
||||
},
|
||||
})
|
||||
).get('depth'),
|
||||
).toEqual(0);
|
||||
|
||||
expect(
|
||||
(
|
||||
await treePathCollection.repository.findOne({
|
||||
filter: {
|
||||
ancestor: 'test',
|
||||
descendant: 'child1',
|
||||
},
|
||||
})
|
||||
).get('depth'),
|
||||
).toEqual(1);
|
||||
|
||||
expect(
|
||||
(
|
||||
await treePathCollection.repository.findOne({
|
||||
filter: {
|
||||
ancestor: 'test',
|
||||
descendant: 'child11',
|
||||
},
|
||||
})
|
||||
).get('depth'),
|
||||
).toEqual(2);
|
||||
|
||||
expect(
|
||||
(
|
||||
await treePathCollection.repository.findOne({
|
||||
filter: {
|
||||
ancestor: 'child1',
|
||||
descendant: 'child11',
|
||||
},
|
||||
})
|
||||
).get('depth'),
|
||||
).toEqual(1);
|
||||
});
|
||||
|
||||
it('should insert child node with difference type', async () => {
|
||||
const schema = {
|
||||
name: 'root-name',
|
||||
'x-uid': 'root',
|
||||
properties: {
|
||||
p1: {
|
||||
'x-uid': 'p1',
|
||||
},
|
||||
p2: {
|
||||
'x-uid': 'p2',
|
||||
},
|
||||
},
|
||||
items: [
|
||||
{
|
||||
name: 'i1',
|
||||
'x-uid': 'i1',
|
||||
},
|
||||
{
|
||||
name: 'i2',
|
||||
'x-uid': 'i2',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const tree = await repository.insert(schema);
|
||||
expect(tree).toMatchObject({
|
||||
items: [
|
||||
{
|
||||
name: 'i1',
|
||||
'x-uid': 'i1',
|
||||
'x-async': false,
|
||||
'x-index': 1,
|
||||
},
|
||||
{
|
||||
name: 'i2',
|
||||
'x-uid': 'i2',
|
||||
'x-async': false,
|
||||
'x-index': 2,
|
||||
},
|
||||
],
|
||||
properties: {
|
||||
p1: { 'x-uid': 'p1', 'x-async': false, 'x-index': 1 },
|
||||
p2: { 'x-uid': 'p2', 'x-async': false, 'x-index': 2 },
|
||||
},
|
||||
name: 'root-name',
|
||||
'x-uid': 'root',
|
||||
'x-async': false,
|
||||
'x-index': null,
|
||||
});
|
||||
});
|
||||
|
||||
describe('schema', () => {
|
||||
let schema;
|
||||
beforeEach(() => {
|
||||
schema = {
|
||||
type: 'object',
|
||||
title: 'title',
|
||||
name: 'root',
|
||||
properties: {
|
||||
a1: {
|
||||
type: 'string',
|
||||
title: 'A1',
|
||||
'x-component': 'Input',
|
||||
},
|
||||
b1: {
|
||||
'x-async': true, // 添加了一个异步节点
|
||||
type: 'string',
|
||||
title: 'B1',
|
||||
properties: {
|
||||
c1: {
|
||||
type: 'string',
|
||||
title: 'C1',
|
||||
},
|
||||
d1: {
|
||||
'x-async': true,
|
||||
type: 'string',
|
||||
title: 'D1',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it('should turn schema to single nodes', async () => {
|
||||
const nodes = UiSchemaRepository.schemaToSingleNodes(schema);
|
||||
expect(nodes.length).toEqual(5);
|
||||
});
|
||||
|
||||
it('should save schema', async () => {
|
||||
await repository.insert(schema);
|
||||
|
||||
expect(await repository.count()).toEqual(5);
|
||||
});
|
||||
|
||||
it('should save schema with sort', async () => {
|
||||
await repository.insert(schema);
|
||||
|
||||
const root = await repository.findOne({
|
||||
filter: {
|
||||
name: 'root',
|
||||
},
|
||||
});
|
||||
const a1 = await repository.findOne({
|
||||
filter: {
|
||||
name: 'a1',
|
||||
},
|
||||
});
|
||||
|
||||
const b1 = await repository.findOne({
|
||||
filter: {
|
||||
name: 'b1',
|
||||
},
|
||||
});
|
||||
|
||||
const c1 = await repository.findOne({
|
||||
filter: {
|
||||
name: 'c1',
|
||||
},
|
||||
});
|
||||
|
||||
const d1 = await repository.findOne({
|
||||
filter: {
|
||||
name: 'd1',
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
(
|
||||
await treePathCollection.repository.findOne({
|
||||
filter: {
|
||||
ancestor: root.get('uid'),
|
||||
descendant: a1.get('uid'),
|
||||
depth: 1,
|
||||
},
|
||||
})
|
||||
).get('sort'),
|
||||
).toEqual(1);
|
||||
|
||||
expect(
|
||||
(
|
||||
await treePathCollection.repository.findOne({
|
||||
filter: {
|
||||
ancestor: root.get('uid'),
|
||||
descendant: b1.get('uid'),
|
||||
depth: 1,
|
||||
},
|
||||
})
|
||||
).get('sort'),
|
||||
).toEqual(2);
|
||||
|
||||
expect(
|
||||
(
|
||||
await treePathCollection.repository.findOne({
|
||||
filter: {
|
||||
ancestor: b1.get('uid'),
|
||||
descendant: c1.get('uid'),
|
||||
depth: 1,
|
||||
},
|
||||
})
|
||||
).get('sort'),
|
||||
).toEqual(1);
|
||||
|
||||
expect(
|
||||
(
|
||||
await treePathCollection.repository.findOne({
|
||||
filter: {
|
||||
ancestor: b1.get('uid'),
|
||||
descendant: d1.get('uid'),
|
||||
depth: 1,
|
||||
},
|
||||
})
|
||||
).get('sort'),
|
||||
).toEqual(2);
|
||||
});
|
||||
|
||||
it('should getJsonSchema', async () => {
|
||||
await repository.insert(schema);
|
||||
const rootNode = await repository.findOne({
|
||||
filter: {
|
||||
name: 'root',
|
||||
},
|
||||
});
|
||||
|
||||
const results = await repository.getJsonSchema(rootNode.get('uid') as string);
|
||||
|
||||
expect(results).toMatchObject({
|
||||
type: 'object',
|
||||
title: 'title',
|
||||
properties: {
|
||||
a1: {
|
||||
type: 'string',
|
||||
title: 'A1',
|
||||
'x-component': 'Input',
|
||||
},
|
||||
},
|
||||
name: 'root',
|
||||
});
|
||||
});
|
||||
|
||||
it('should getJsonSchema by subTree', async () => {
|
||||
await repository.insert({
|
||||
'x-uid': 'n1',
|
||||
name: 'a',
|
||||
type: 'object',
|
||||
properties: {
|
||||
b: {
|
||||
'x-uid': 'n2',
|
||||
type: 'object',
|
||||
properties: {
|
||||
c: { 'x-uid': 'n3' },
|
||||
},
|
||||
},
|
||||
d: { 'x-uid': 'n4' },
|
||||
},
|
||||
});
|
||||
|
||||
const schema = await repository.getJsonSchema('n2');
|
||||
expect(schema).toBeDefined();
|
||||
});
|
||||
|
||||
it('should getProperties', async () => {
|
||||
await repository.insert(schema);
|
||||
const rootNode = await repository.findOne({
|
||||
filter: {
|
||||
name: 'root',
|
||||
},
|
||||
});
|
||||
|
||||
const results = await repository.getProperties(rootNode.get('uid') as string);
|
||||
|
||||
expect(results).toMatchObject({
|
||||
type: 'object',
|
||||
properties: {
|
||||
a1: {},
|
||||
b1: {
|
||||
'x-async': true,
|
||||
properties: {
|
||||
c1: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('insert with items', () => {
|
||||
test('insert with items', async () => {
|
||||
const schema = {
|
||||
name: 'root-name',
|
||||
'x-uid': 'root',
|
||||
properties: {
|
||||
p1: {
|
||||
'x-uid': 'p1',
|
||||
},
|
||||
p2: {
|
||||
'x-uid': 'p2',
|
||||
},
|
||||
},
|
||||
items: [
|
||||
{
|
||||
name: 'i1',
|
||||
'x-uid': 'i1',
|
||||
},
|
||||
{
|
||||
name: 'i2',
|
||||
'x-uid': 'i2',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await repository.insert(schema);
|
||||
await repository.insertBeforeBegin('i1', {
|
||||
'x-uid': 'i2',
|
||||
});
|
||||
|
||||
const results = await repository.getJsonSchema('root');
|
||||
expect(results['items'][0]['name']).toEqual('i2');
|
||||
expect(results['items'][1]['name']).toEqual('i1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('insert', () => {
|
||||
let rootNode;
|
||||
let rootUid: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
const root = {
|
||||
type: 'object',
|
||||
title: 'title',
|
||||
name: 'root',
|
||||
properties: {
|
||||
a1: {
|
||||
type: 'string',
|
||||
title: 'A1',
|
||||
'x-component': 'Input',
|
||||
},
|
||||
b1: {
|
||||
type: 'string',
|
||||
title: 'B1',
|
||||
properties: {
|
||||
c1: {
|
||||
type: 'string',
|
||||
title: 'C1',
|
||||
},
|
||||
d1: {
|
||||
type: 'string',
|
||||
title: 'D1',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await repository.insert(root);
|
||||
|
||||
rootNode = await repository.findOne({
|
||||
filter: {
|
||||
name: 'root',
|
||||
},
|
||||
});
|
||||
|
||||
rootUid = rootNode.get('uid') as string;
|
||||
});
|
||||
|
||||
it('should insertAfterBegin', async () => {
|
||||
const newNode = {
|
||||
name: 'newNode',
|
||||
};
|
||||
|
||||
await repository.insertAfterBegin(rootUid, newNode);
|
||||
const schema = await repository.getJsonSchema(rootUid);
|
||||
expect(schema.properties['newNode']['x-index']).toEqual(1);
|
||||
expect(schema.properties.a1['x-index']).toEqual(2);
|
||||
expect(schema.properties.b1['x-index']).toEqual(3);
|
||||
});
|
||||
|
||||
it('should insertBeforeEnd', async () => {
|
||||
const newNode = {
|
||||
name: 'newNode',
|
||||
};
|
||||
|
||||
await repository.insertBeforeEnd(rootUid, newNode);
|
||||
const schema = await repository.getJsonSchema(rootUid);
|
||||
expect(schema.properties['newNode']['x-index']).toEqual(3);
|
||||
expect(schema.properties.a1['x-index']).toEqual(1);
|
||||
expect(schema.properties.b1['x-index']).toEqual(2);
|
||||
});
|
||||
|
||||
it('should insertBeforeBegin', async () => {
|
||||
const newNode = {
|
||||
name: 'newNode',
|
||||
};
|
||||
|
||||
const b1Node = await repository.findOne({
|
||||
filter: {
|
||||
name: 'b1',
|
||||
},
|
||||
});
|
||||
|
||||
await repository.insertBeforeBegin(b1Node.get('uid') as string, newNode);
|
||||
|
||||
const schema = await repository.getJsonSchema(rootUid);
|
||||
expect(schema.properties['newNode']['x-index']).toEqual(2);
|
||||
expect(schema.properties.a1['x-index']).toEqual(1);
|
||||
expect(schema.properties.b1['x-index']).toEqual(3);
|
||||
});
|
||||
|
||||
it('should insertAfterEnd a1', async () => {
|
||||
const newNode = {
|
||||
name: 'newNode',
|
||||
};
|
||||
|
||||
const a1Node = await repository.findOne({
|
||||
filter: {
|
||||
name: 'a1',
|
||||
},
|
||||
});
|
||||
|
||||
await repository.insertAfterEnd(a1Node.get('uid') as string, newNode);
|
||||
|
||||
const schema = await repository.getJsonSchema(rootUid);
|
||||
expect(schema.properties['newNode']['x-index']).toEqual(2);
|
||||
expect(schema.properties.a1['x-index']).toEqual(1);
|
||||
expect(schema.properties.b1['x-index']).toEqual(3);
|
||||
});
|
||||
|
||||
it('should insertAfterEnd b1', async () => {
|
||||
const newNode = {
|
||||
name: 'newNode',
|
||||
};
|
||||
|
||||
const b1Node = await repository.findOne({
|
||||
filter: {
|
||||
name: 'b1',
|
||||
},
|
||||
});
|
||||
|
||||
await repository.insertAfterEnd(b1Node.get('uid') as string, newNode);
|
||||
|
||||
const schema = await repository.getJsonSchema(rootUid);
|
||||
expect(schema.properties['newNode']['x-index']).toEqual(3);
|
||||
expect(schema.properties.a1['x-index']).toEqual(1);
|
||||
expect(schema.properties.b1['x-index']).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('insert with x-uid', () => {
|
||||
it('should insertAfterBegin by tree', async () => {
|
||||
await repository.insert({
|
||||
'x-uid': 'n1',
|
||||
name: 'a',
|
||||
type: 'object',
|
||||
properties: {
|
||||
b: {
|
||||
'x-uid': 'n2',
|
||||
type: 'object',
|
||||
properties: {
|
||||
c: { 'x-uid': 'n3' },
|
||||
},
|
||||
},
|
||||
d: {
|
||||
'x-uid': 'n4',
|
||||
properties: {
|
||||
e: {
|
||||
'x-uid': 'n5',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await repository.insertAfterBegin('n1', {
|
||||
name: 'f',
|
||||
'x-uid': 'n6',
|
||||
properties: {
|
||||
g: {
|
||||
'x-uid': 'n7',
|
||||
properties: {
|
||||
d: { 'x-uid': 'n4' },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const schema = await repository.getJsonSchema('n1');
|
||||
expect(schema.properties.f.properties.g.properties.d.properties.e['x-uid']).toEqual('n5');
|
||||
});
|
||||
|
||||
it('should insertAfterBegin by node', async () => {
|
||||
await repository.insert({
|
||||
'x-uid': 'n1',
|
||||
name: 'a',
|
||||
type: 'object',
|
||||
properties: {
|
||||
b: {
|
||||
'x-uid': 'n2',
|
||||
type: 'object',
|
||||
properties: {
|
||||
c: { 'x-uid': 'n3' },
|
||||
},
|
||||
},
|
||||
d: { 'x-uid': 'n4' },
|
||||
},
|
||||
});
|
||||
|
||||
await repository.insertAfterBegin('n2', 'n4');
|
||||
const schema = await repository.getJsonSchema('n1');
|
||||
expect(schema.properties.b.properties.d['x-uid']).toEqual('n4');
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
it('should remove node', async () => {
|
||||
await repository.insert({
|
||||
'x-uid': 'n1',
|
||||
name: 'a',
|
||||
type: 'object',
|
||||
properties: {
|
||||
b: {
|
||||
'x-uid': 'n2',
|
||||
type: 'object',
|
||||
properties: {
|
||||
c: { 'x-uid': 'n3' },
|
||||
},
|
||||
},
|
||||
d: { 'x-uid': 'n4' },
|
||||
},
|
||||
});
|
||||
|
||||
await repository.remove('n4');
|
||||
const schema = await repository.getJsonSchema('n1');
|
||||
expect(schema.properties['d']).not.toBeDefined();
|
||||
});
|
||||
|
||||
it('should remove tree', async () => {
|
||||
await repository.insert({
|
||||
'x-uid': 'n1',
|
||||
name: 'a',
|
||||
type: 'object',
|
||||
properties: {
|
||||
b: {
|
||||
'x-uid': 'n2',
|
||||
type: 'object',
|
||||
properties: {
|
||||
c: { 'x-uid': 'n3' },
|
||||
},
|
||||
},
|
||||
d: { 'x-uid': 'n4' },
|
||||
},
|
||||
});
|
||||
|
||||
await repository.remove('n2');
|
||||
const schema = await repository.getJsonSchema('n1');
|
||||
expect(schema.properties['b']).not.toBeDefined();
|
||||
expect(schema.properties['d']).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('patch', function () {
|
||||
let rootNode;
|
||||
let rootUid: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
const root = {
|
||||
type: 'object',
|
||||
title: 'title',
|
||||
name: 'root',
|
||||
properties: {
|
||||
a1: {
|
||||
type: 'string',
|
||||
title: 'A1',
|
||||
'x-component': 'Input',
|
||||
},
|
||||
b1: {
|
||||
type: 'string',
|
||||
title: 'B1',
|
||||
properties: {
|
||||
c1: {
|
||||
type: 'string',
|
||||
title: 'C1',
|
||||
},
|
||||
d1: {
|
||||
type: 'string',
|
||||
title: 'D1',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await repository.insert(root);
|
||||
|
||||
rootNode = await repository.findOne({
|
||||
filter: {
|
||||
name: 'root',
|
||||
},
|
||||
});
|
||||
|
||||
rootUid = rootNode.get('uid') as string;
|
||||
});
|
||||
|
||||
it('should patch root ui schema', async () => {
|
||||
await repository.patch({
|
||||
'x-uid': rootUid,
|
||||
title: 'test-title',
|
||||
properties: {
|
||||
a1: {
|
||||
type: 'string',
|
||||
title: 'new a1 title',
|
||||
'x-component': 'Input',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const newTree = await repository.getJsonSchema(rootUid);
|
||||
expect(newTree.title).toEqual('test-title');
|
||||
expect(newTree.properties.a1.title).toEqual('new a1 title');
|
||||
});
|
||||
});
|
||||
});
|
336
packages/plugin-ui-schema/src/__tests__/ui-schema.test.ts
Normal file
336
packages/plugin-ui-schema/src/__tests__/ui-schema.test.ts
Normal file
@ -0,0 +1,336 @@
|
||||
import { ISchema } from '@formily/json-schema';
|
||||
import { mockServer, MockServer } from '@nocobase/test';
|
||||
import { Collection, Database } from '@nocobase/database';
|
||||
import PluginUiSchema, { UiSchemaRepository } from '@nocobase/plugin-ui-schema';
|
||||
|
||||
describe('ui-schema', () => {
|
||||
let app: MockServer;
|
||||
let db: Database;
|
||||
|
||||
let uiSchemaRepository: UiSchemaRepository;
|
||||
|
||||
afterEach(async () => {
|
||||
await app.destroy();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
app = mockServer({
|
||||
registerActions: true,
|
||||
});
|
||||
|
||||
db = app.db;
|
||||
|
||||
await db.sequelize.getQueryInterface().dropAllTables();
|
||||
|
||||
app.plugin(PluginUiSchema);
|
||||
|
||||
await app.load();
|
||||
uiSchemaRepository = db.getCollection('ui_schemas').repository as UiSchemaRepository;
|
||||
});
|
||||
|
||||
type SchemaProperties = Record<string, ISchema>;
|
||||
// properties、patternProperties、definitions 都是这种类型的
|
||||
describe('SchemaProperties', () => {
|
||||
it('properties is plain object', () => {
|
||||
const items = [
|
||||
{
|
||||
properties: {}, // 有效
|
||||
},
|
||||
{
|
||||
properties: null, // 跳过
|
||||
},
|
||||
{
|
||||
properties: 'aaa', // 无效,跳过并警告
|
||||
},
|
||||
{
|
||||
properties: [], // 无效,跳过并警告
|
||||
},
|
||||
{
|
||||
properties: 1, // 无效,跳过并警告
|
||||
},
|
||||
{
|
||||
properties: true, // 无效,跳过并警告
|
||||
},
|
||||
];
|
||||
});
|
||||
it('properties node is plain object', () => {
|
||||
const schema = {
|
||||
properties: {
|
||||
a: {}, // 有效 {name: 'a'}
|
||||
b: null, // 跳过,不警告
|
||||
c: 'aaa', // 无效,跳过并警告
|
||||
d: true, // 无效,跳过并警告
|
||||
e: 1, // 无效,跳过并警告
|
||||
f: [], // 无效,跳过并警告
|
||||
},
|
||||
};
|
||||
});
|
||||
it('nested', () => {
|
||||
const schema = {
|
||||
properties: {
|
||||
a: {
|
||||
properties: {
|
||||
b: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
type SchemaItems = ISchema | ISchema[];
|
||||
|
||||
describe('items', () => {
|
||||
it('should insert items node', async () => {
|
||||
const schema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
string_array: {
|
||||
type: 'array',
|
||||
'x-component': 'ArrayCards',
|
||||
maxItems: 3,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component-props': {
|
||||
title: 'String array',
|
||||
},
|
||||
items: {
|
||||
type: 'void',
|
||||
properties: {
|
||||
index: {
|
||||
type: 'void',
|
||||
'x-component': 'ArrayCards.Index',
|
||||
},
|
||||
input: {
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
title: 'Input',
|
||||
required: true,
|
||||
'x-component': 'Input',
|
||||
},
|
||||
remove: {
|
||||
type: 'void',
|
||||
'x-component': 'ArrayCards.Remove',
|
||||
},
|
||||
moveUp: {
|
||||
type: 'void',
|
||||
'x-component': 'ArrayCards.MoveUp',
|
||||
},
|
||||
moveDown: {
|
||||
type: 'void',
|
||||
'x-component': 'ArrayCards.MoveDown',
|
||||
},
|
||||
},
|
||||
},
|
||||
properties: {
|
||||
addition: {
|
||||
type: 'void',
|
||||
title: 'Add entry',
|
||||
'x-component': 'ArrayCards.Addition',
|
||||
},
|
||||
},
|
||||
},
|
||||
array: {
|
||||
type: 'array',
|
||||
'x-component': 'ArrayCards',
|
||||
maxItems: 3,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component-props': {
|
||||
title: 'Object array',
|
||||
},
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
index: {
|
||||
type: 'void',
|
||||
'x-component': 'ArrayCards.Index',
|
||||
},
|
||||
input: {
|
||||
type: 'string',
|
||||
'x-decorator': 'FormItem',
|
||||
title: 'Input',
|
||||
required: true,
|
||||
'x-component': 'Input',
|
||||
},
|
||||
remove: {
|
||||
type: 'void',
|
||||
'x-component': 'ArrayCards.Remove',
|
||||
},
|
||||
moveUp: {
|
||||
type: 'void',
|
||||
'x-component': 'ArrayCards.MoveUp',
|
||||
},
|
||||
moveDown: {
|
||||
type: 'void',
|
||||
'x-component': 'ArrayCards.MoveDown',
|
||||
},
|
||||
},
|
||||
},
|
||||
properties: {
|
||||
addition: {
|
||||
type: 'void',
|
||||
title: 'Add entry',
|
||||
'x-component': 'ArrayCards.Addition',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = await uiSchemaRepository.insert(schema);
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
});
|
||||
|
||||
it('items is array or plain object', () => {
|
||||
const examples = [
|
||||
{
|
||||
items: {}, // 有效,等同于 [{}]
|
||||
},
|
||||
{
|
||||
items: [{}], // 有效
|
||||
},
|
||||
{
|
||||
items: [], // 有效,但无节点
|
||||
},
|
||||
{
|
||||
items: 'str', // 无效,跳过并警告
|
||||
},
|
||||
];
|
||||
});
|
||||
it('items node is plain object', () => {
|
||||
const schema = {
|
||||
items: [
|
||||
{},
|
||||
[], // 无效,跳过并警告
|
||||
null, // 无效,跳过并警告
|
||||
'aa', // 无效,跳过并警告
|
||||
],
|
||||
};
|
||||
});
|
||||
});
|
||||
describe('additionalProperties & additionalItems', () => {
|
||||
it('additionalProperties and additionalItems are plain object', () => {
|
||||
const schema = {
|
||||
additionalProperties: {},
|
||||
additionalItems: {
|
||||
properties: {},
|
||||
},
|
||||
};
|
||||
});
|
||||
it('null', () => {
|
||||
const schema = {
|
||||
additionalProperties: null,
|
||||
additionalItems: null,
|
||||
};
|
||||
});
|
||||
it('null', () => {
|
||||
const schema = {
|
||||
additionalProperties: 1, // 跳过并警告
|
||||
additionalItems: 'str', // 跳过并警告
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
it('all props', () => {
|
||||
const schema = {
|
||||
definitions: {
|
||||
address: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
street_address: {
|
||||
type: 'string',
|
||||
},
|
||||
city: {
|
||||
type: 'string',
|
||||
},
|
||||
state: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
required: ['street_address', 'city', 'state'],
|
||||
},
|
||||
},
|
||||
type: 'object',
|
||||
title: 'title',
|
||||
description: 'description',
|
||||
patternProperties: {
|
||||
'^[a-zA-Z0-9]*$': {
|
||||
properties: {
|
||||
model: { type: 'string' },
|
||||
made: { type: 'string' },
|
||||
year: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
additionalProperties: {
|
||||
type: 'string',
|
||||
},
|
||||
properties: {
|
||||
string: {
|
||||
type: 'string',
|
||||
default: 'default',
|
||||
required: true,
|
||||
'x-component': 'Input',
|
||||
'x-component-props': {
|
||||
placeholder: 'placeholder',
|
||||
},
|
||||
'x-decorator': 'FormItem',
|
||||
'x-decorator-props': {
|
||||
labelCol: 3,
|
||||
},
|
||||
'x-disabled': true,
|
||||
'x-display': 'visible',
|
||||
'x-editable': false,
|
||||
'x-hidden': false,
|
||||
'x-pattern': 'readPretty',
|
||||
'x-read-only': true,
|
||||
'x-validator': ['phone'],
|
||||
'x-reactions': [
|
||||
{
|
||||
target: 'xxx',
|
||||
when: '{{aa > bb}}',
|
||||
},
|
||||
],
|
||||
},
|
||||
boolean: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
number: {
|
||||
type: 'number',
|
||||
default: 100,
|
||||
},
|
||||
date: {
|
||||
type: 'date',
|
||||
default: '2020-12-23',
|
||||
},
|
||||
datetime: {
|
||||
type: 'datetime',
|
||||
default: '2020-12-23 23:00:00',
|
||||
},
|
||||
array: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
additionalItems: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
array2: {
|
||||
type: 'array',
|
||||
items: [
|
||||
{
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
},
|
||||
],
|
||||
},
|
||||
void: {
|
||||
type: 'void',
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
});
|
76
packages/plugin-ui-schema/src/actions/ui-schema-action.ts
Normal file
76
packages/plugin-ui-schema/src/actions/ui-schema-action.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import { Context } from '@nocobase/actions';
|
||||
import UiSchemaRepository from '../repository';
|
||||
|
||||
const getRepositoryFromCtx = (ctx: Context) => {
|
||||
return ctx.db.getCollection('ui_schemas').repository as UiSchemaRepository;
|
||||
};
|
||||
|
||||
export const uiSchemaActions = {
|
||||
async getJsonSchema(ctx: Context, next) {
|
||||
const { resourceIndex } = ctx.action.params;
|
||||
const repository = getRepositoryFromCtx(ctx);
|
||||
ctx.body = await repository.getJsonSchema(resourceIndex);
|
||||
await next();
|
||||
},
|
||||
|
||||
async getProperties(ctx: Context, next) {
|
||||
const { resourceIndex } = ctx.action.params;
|
||||
const repository = getRepositoryFromCtx(ctx);
|
||||
ctx.body = await repository.getProperties(resourceIndex);
|
||||
await next();
|
||||
},
|
||||
|
||||
async insert(ctx: Context, next) {
|
||||
const { values } = ctx.action.params;
|
||||
const repository = getRepositoryFromCtx(ctx);
|
||||
|
||||
ctx.body = await repository.insert(values);
|
||||
await next();
|
||||
},
|
||||
|
||||
async remove(ctx: Context, next) {
|
||||
const { resourceIndex } = ctx.action.params;
|
||||
const repository = getRepositoryFromCtx(ctx);
|
||||
await repository.remove(resourceIndex);
|
||||
|
||||
ctx.body = {
|
||||
result: 'ok',
|
||||
};
|
||||
|
||||
await next();
|
||||
},
|
||||
|
||||
async patch(ctx: Context, next) {
|
||||
const { values } = ctx.action.params;
|
||||
const repository = getRepositoryFromCtx(ctx);
|
||||
await repository.patch(values);
|
||||
|
||||
ctx.body = {
|
||||
result: 'ok',
|
||||
};
|
||||
|
||||
await next();
|
||||
},
|
||||
|
||||
async insertAdjacent(ctx: Context, next) {
|
||||
const { resourceIndex, position, values } = ctx.action.params;
|
||||
const repository = getRepositoryFromCtx(ctx);
|
||||
|
||||
ctx.body = await repository.insertAdjacent(position, resourceIndex, values);
|
||||
|
||||
await next();
|
||||
},
|
||||
insertBeforeBegin: insertPositionActionBuilder('beforeBegin'),
|
||||
insertAfterBegin: insertPositionActionBuilder('afterBegin'),
|
||||
insertBeforeEnd: insertPositionActionBuilder('beforeEnd'),
|
||||
insertAfterEnd: insertPositionActionBuilder('afterEnd'),
|
||||
};
|
||||
|
||||
function insertPositionActionBuilder(position: 'beforeBegin' | 'afterBegin' | 'beforeEnd' | 'afterEnd') {
|
||||
return async function (ctx: Context, next) {
|
||||
const { resourceIndex, values } = ctx.action.params;
|
||||
const repository = getRepositoryFromCtx(ctx);
|
||||
ctx.body = await repository.insertAdjacent(position, resourceIndex, values);
|
||||
await next();
|
||||
};
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
export default {
|
||||
name: 'ui_schema_tree_path',
|
||||
autoGenId: false,
|
||||
timestamps: false,
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'ancestor',
|
||||
primaryKey: true,
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'descendant',
|
||||
primaryKey: true,
|
||||
},
|
||||
{
|
||||
type: 'integer',
|
||||
name: 'depth',
|
||||
},
|
||||
{
|
||||
type: 'boolean',
|
||||
name: 'async',
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'type',
|
||||
comment: 'type of node',
|
||||
},
|
||||
{
|
||||
type: 'integer',
|
||||
name: 'sort',
|
||||
comment: 'sort of node in adjacency',
|
||||
},
|
||||
],
|
||||
};
|
34
packages/plugin-ui-schema/src/collections/ui_schemas.ts
Normal file
34
packages/plugin-ui-schema/src/collections/ui_schemas.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { CollectionOptions } from '@nocobase/database';
|
||||
|
||||
export default {
|
||||
name: 'ui_schemas',
|
||||
title: '字段配置',
|
||||
autoGenId: false,
|
||||
timestamps: false,
|
||||
repository: 'UiSchemaRepository',
|
||||
model: 'MagicAttributeModel',
|
||||
magicAttribute: 'schema',
|
||||
fields: [
|
||||
{
|
||||
type: 'uid',
|
||||
name: 'x-uid',
|
||||
field: 'uid',
|
||||
primaryKey: true,
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'uid',
|
||||
field: 'uid',
|
||||
primaryKey: true,
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'name',
|
||||
},
|
||||
{
|
||||
type: 'json',
|
||||
name: 'schema',
|
||||
defaultValue: {},
|
||||
},
|
||||
],
|
||||
} as CollectionOptions;
|
5
packages/plugin-ui-schema/src/dao/ui_schema_dao.ts
Normal file
5
packages/plugin-ui-schema/src/dao/ui_schema_dao.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export class UiSchemaDAO {
|
||||
nodeKeys = ['properties', 'patternProperties', 'additionalProperties'];
|
||||
|
||||
constructor(schema: any) {}
|
||||
}
|
24
packages/plugin-ui-schema/src/dao/ui_schema_node_dao.ts
Normal file
24
packages/plugin-ui-schema/src/dao/ui_schema_node_dao.ts
Normal file
@ -0,0 +1,24 @@
|
||||
export interface TargetPosition {
|
||||
type: 'before' | 'after';
|
||||
target: string;
|
||||
}
|
||||
export interface ChildOptions {
|
||||
parentUid: string;
|
||||
type: string;
|
||||
position?: 'first' | 'last' | TargetPosition;
|
||||
}
|
||||
|
||||
export interface SchemaNode {
|
||||
name: string;
|
||||
'x-uid': string;
|
||||
schema: object;
|
||||
'x-async'?: boolean;
|
||||
childOptions?: ChildOptions;
|
||||
}
|
||||
|
||||
export class UiSchemaNodeDAO {
|
||||
schemaNode: SchemaNode;
|
||||
constructor(schemaNode: SchemaNode) {
|
||||
this.schemaNode = schemaNode;
|
||||
}
|
||||
}
|
0
packages/plugin-ui-schema/src/helper.ts
Normal file
0
packages/plugin-ui-schema/src/helper.ts
Normal file
4
packages/plugin-ui-schema/src/index.ts
Normal file
4
packages/plugin-ui-schema/src/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import PluginUiSchema from './server';
|
||||
import UiSchemaRepository from './repository';
|
||||
export default PluginUiSchema;
|
||||
export { UiSchemaRepository };
|
5
packages/plugin-ui-schema/src/model.ts
Normal file
5
packages/plugin-ui-schema/src/model.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { Model } from '@nocobase/database';
|
||||
|
||||
export class UiSchemaModel extends Model {
|
||||
|
||||
}
|
636
packages/plugin-ui-schema/src/repository.ts
Normal file
636
packages/plugin-ui-schema/src/repository.ts
Normal file
@ -0,0 +1,636 @@
|
||||
import { Repository } from '@nocobase/database';
|
||||
import lodash from 'lodash';
|
||||
import { ChildOptions, SchemaNode, TargetPosition, UiSchemaNodeDAO } from './dao/ui_schema_node_dao';
|
||||
import { uid } from '@nocobase/utils';
|
||||
import { Transaction } from 'sequelize';
|
||||
|
||||
interface GetJsonSchemaOptions {
|
||||
includeAsyncNode?: boolean;
|
||||
transaction?: Transaction;
|
||||
}
|
||||
|
||||
const nodeKeys = ['properties', 'definitions', 'patternProperties', 'additionalProperties', 'items'];
|
||||
|
||||
export default class UiSchemaRepository extends Repository {
|
||||
static schemaToSingleNodes(schema: any, carry: SchemaNode[] = [], childOptions: ChildOptions = null): SchemaNode[] {
|
||||
const node = lodash.cloneDeep(
|
||||
lodash.isString(schema)
|
||||
? {
|
||||
'x-uid': schema,
|
||||
}
|
||||
: schema,
|
||||
);
|
||||
|
||||
if (!lodash.get(node, 'name')) {
|
||||
node.name = uid();
|
||||
}
|
||||
|
||||
if (!lodash.get(node, 'x-uid')) {
|
||||
node['x-uid'] = uid();
|
||||
}
|
||||
|
||||
if (childOptions) {
|
||||
node.childOptions = childOptions;
|
||||
}
|
||||
|
||||
carry.push(node);
|
||||
|
||||
for (const nodeKey of nodeKeys) {
|
||||
const nodeProperty = lodash.get(node, nodeKey);
|
||||
|
||||
// array items
|
||||
if (nodeKey === 'items' && nodeProperty) {
|
||||
const handleItems = lodash.isArray(nodeProperty) ? nodeProperty : [nodeProperty];
|
||||
for (const item of handleItems) {
|
||||
carry = this.schemaToSingleNodes(item, carry, {
|
||||
parentUid: node['x-uid'],
|
||||
type: nodeKey,
|
||||
});
|
||||
}
|
||||
} else if (lodash.isPlainObject(nodeProperty)) {
|
||||
const subNodeNames = lodash.keys(lodash.get(node, nodeKey));
|
||||
|
||||
delete node[nodeKey];
|
||||
|
||||
for (const subNodeName of subNodeNames) {
|
||||
const subSchema = {
|
||||
name: subNodeName,
|
||||
...lodash.get(nodeProperty, subNodeName),
|
||||
};
|
||||
|
||||
carry = this.schemaToSingleNodes(subSchema, carry, {
|
||||
parentUid: node['x-uid'],
|
||||
type: nodeKey,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return carry;
|
||||
}
|
||||
|
||||
async getProperties(uid: string) {
|
||||
const db = this.database;
|
||||
const treeCollection = db.getCollection('ui_schema_tree_path');
|
||||
|
||||
const rawSql = `
|
||||
SELECT SchemaTable.uid as uid, SchemaTable.name as name, SchemaTable.schema as "schema",
|
||||
TreePath.depth as depth,
|
||||
NodeInfo.type as type, NodeInfo.async as async, ParentPath.ancestor as parent
|
||||
FROM ${treeCollection.model.tableName} as TreePath
|
||||
LEFT JOIN ${this.model.tableName} as SchemaTable ON SchemaTable.uid = TreePath.descendant
|
||||
LEFT JOIN ${treeCollection.model.tableName} as NodeInfo ON NodeInfo.descendant = SchemaTable.uid and NodeInfo.descendant = NodeInfo.ancestor and NodeInfo.depth = 0
|
||||
LEFT JOIN ${treeCollection.model.tableName} as ParentPath ON (ParentPath.descendant = SchemaTable.uid AND ParentPath.depth = 1)
|
||||
WHERE TreePath.ancestor = :ancestor AND (NodeInfo.async = false or TreePath.depth = 1)`;
|
||||
|
||||
const nodes = await db.sequelize.query(rawSql, {
|
||||
replacements: {
|
||||
ancestor: uid,
|
||||
},
|
||||
});
|
||||
|
||||
const schema = this.nodesToSchema(nodes[0], uid);
|
||||
return lodash.pick(schema, ['type', 'properties']);
|
||||
}
|
||||
|
||||
async getJsonSchema(uid: string, options?: GetJsonSchemaOptions) {
|
||||
const db = this.database;
|
||||
const treeCollection = db.getCollection('ui_schema_tree_path');
|
||||
|
||||
const treeTable = treeCollection.model.tableName;
|
||||
|
||||
const rawSql = `
|
||||
SELECT SchemaTable.uid as uid, SchemaTable.name as name, SchemaTable.schema as "schema" ,
|
||||
TreePath.depth as depth,
|
||||
NodeInfo.type as type, NodeInfo.async as async, ParentPath.ancestor as parent, ParentPath.sort as sort
|
||||
FROM ${treeTable} as TreePath
|
||||
LEFT JOIN ${this.model.tableName} as SchemaTable ON SchemaTable.uid = TreePath.descendant
|
||||
LEFT JOIN ${treeTable} as NodeInfo ON NodeInfo.descendant = SchemaTable.uid and NodeInfo.descendant = NodeInfo.ancestor and NodeInfo.depth = 0
|
||||
LEFT JOIN ${treeTable} as ParentPath ON (ParentPath.descendant = SchemaTable.uid AND ParentPath.depth = 1)
|
||||
WHERE TreePath.ancestor = :ancestor ${options?.includeAsyncNode ? '' : 'AND (NodeInfo.async != true )'}
|
||||
`;
|
||||
|
||||
const nodes = await db.sequelize.query(rawSql, {
|
||||
replacements: {
|
||||
ancestor: uid,
|
||||
},
|
||||
transaction: options?.transaction,
|
||||
});
|
||||
|
||||
const schema = this.nodesToSchema(nodes[0], uid);
|
||||
return schema;
|
||||
}
|
||||
|
||||
nodesToSchema(nodes, rootUid) {
|
||||
const nodeAttributeSanitize = (node) => {
|
||||
const schema = {
|
||||
...(lodash.isPlainObject(node.schema) ? node.schema : JSON.parse(node.schema)),
|
||||
...lodash.pick(node, [...nodeKeys, 'name']),
|
||||
['x-uid']: node.uid,
|
||||
['x-async']: !!node.async,
|
||||
['x-index']: node.sort,
|
||||
};
|
||||
|
||||
return schema;
|
||||
};
|
||||
|
||||
const buildTree = (rootNode) => {
|
||||
const children = nodes.filter((node) => node.parent == rootNode.uid);
|
||||
|
||||
if (children.length > 0) {
|
||||
const childrenGroupByType = lodash.groupBy(children, 'type');
|
||||
|
||||
for (const childType of Object.keys(childrenGroupByType)) {
|
||||
const properties = childrenGroupByType[childType]
|
||||
.map((child) => buildTree(child))
|
||||
.sort((a, b) => a['x-index'] - b['x-index']) as any;
|
||||
|
||||
rootNode[childType] =
|
||||
childType == 'items'
|
||||
? properties.length == 1
|
||||
? properties[0]
|
||||
: properties
|
||||
: properties.reduce((carry, item) => {
|
||||
carry[item.name] = item;
|
||||
delete item['name'];
|
||||
return carry;
|
||||
}, {});
|
||||
}
|
||||
}
|
||||
|
||||
return nodeAttributeSanitize(rootNode);
|
||||
};
|
||||
|
||||
return buildTree(nodes.find((node) => node.uid == rootUid));
|
||||
}
|
||||
|
||||
treeCollection() {
|
||||
return this.database.getCollection('ui_schema_tree_path');
|
||||
}
|
||||
|
||||
async patch(newSchema: any, options?) {
|
||||
let handleTransaction = true;
|
||||
let transaction;
|
||||
if (options?.transaction) {
|
||||
handleTransaction = false;
|
||||
transaction = options.transaction;
|
||||
} else {
|
||||
transaction = await this.database.sequelize.transaction();
|
||||
}
|
||||
|
||||
const rootUid = newSchema['x-uid'];
|
||||
const oldTree = await this.getJsonSchema(rootUid);
|
||||
|
||||
const traverSchemaTree = async (schema, path = []) => {
|
||||
const node = schema;
|
||||
const oldNode = path.length == 0 ? oldTree : lodash.get(oldTree, path);
|
||||
const oldNodeUid = oldNode['x-uid'];
|
||||
|
||||
await this.updateNode(oldNodeUid, node, transaction);
|
||||
|
||||
const properties = node.properties;
|
||||
if (lodash.isPlainObject(properties)) {
|
||||
for (const name of Object.keys(properties)) {
|
||||
await traverSchemaTree(properties[name], [...path, 'properties', name]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
await traverSchemaTree(newSchema);
|
||||
|
||||
handleTransaction && (await transaction.commit());
|
||||
} catch (err) {
|
||||
handleTransaction && (await transaction.rollback());
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async updateNode(uid: string, schema: any, transaction?: Transaction) {
|
||||
const nodeModel = await this.findOne({
|
||||
filter: {
|
||||
uid,
|
||||
},
|
||||
});
|
||||
|
||||
await nodeModel.update(
|
||||
{
|
||||
schema: {
|
||||
...(nodeModel.get('schema') as any),
|
||||
...lodash.omit(schema, ['x-async', 'name', 'x-uid', 'properties']),
|
||||
},
|
||||
},
|
||||
{
|
||||
hooks: false,
|
||||
transaction,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async remove(uid: string, options?) {
|
||||
let handleTransaction: boolean = true;
|
||||
let transaction;
|
||||
|
||||
if (options?.transaction) {
|
||||
transaction = options.transaction;
|
||||
handleTransaction = false;
|
||||
} else {
|
||||
transaction = await this.database.sequelize.transaction();
|
||||
}
|
||||
|
||||
const treePathTable = this.treeCollection().model.tableName;
|
||||
|
||||
try {
|
||||
await this.database.sequelize.query(
|
||||
`DELETE FROM ${this.model.tableName} WHERE uid IN (
|
||||
SELECT descendant FROM ${treePathTable} WHERE ancestor = :uid
|
||||
)
|
||||
`,
|
||||
{
|
||||
replacements: {
|
||||
uid,
|
||||
},
|
||||
transaction,
|
||||
},
|
||||
);
|
||||
await this.database.sequelize.query(
|
||||
`
|
||||
DELETE FROM ${treePathTable}
|
||||
WHERE descendant IN (
|
||||
select descendant FROM
|
||||
(SELECT descendant
|
||||
FROM ${treePathTable}
|
||||
WHERE ancestor = :uid)as descendantTable) `,
|
||||
{
|
||||
replacements: {
|
||||
uid,
|
||||
},
|
||||
transaction,
|
||||
},
|
||||
);
|
||||
|
||||
if (handleTransaction) {
|
||||
await transaction.commit();
|
||||
}
|
||||
} catch (err) {
|
||||
if (handleTransaction) {
|
||||
await transaction.rollback();
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async insertBeside(targetUid: string, schema: any, side: 'before' | 'after') {
|
||||
const targetParent = await this.treeCollection().repository.findOne({
|
||||
filter: {
|
||||
descendant: targetUid,
|
||||
depth: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const db = this.database;
|
||||
const treeTable = this.treeCollection().model.tableName;
|
||||
const typeQuery = await db.sequelize.query(`SELECT type from ${treeTable} WHERE ancestor = :uid AND depth = 0;`, {
|
||||
type: 'SELECT',
|
||||
replacements: {
|
||||
uid: targetUid,
|
||||
},
|
||||
});
|
||||
|
||||
const nodes = UiSchemaRepository.schemaToSingleNodes(schema);
|
||||
|
||||
const rootNode = nodes[0];
|
||||
|
||||
rootNode.childOptions = {
|
||||
parentUid: targetParent.get('ancestor') as string,
|
||||
type: typeQuery[0]['type'],
|
||||
position: {
|
||||
type: side,
|
||||
target: targetUid,
|
||||
},
|
||||
};
|
||||
|
||||
const insertedNodes = await this.insertNodes(nodes);
|
||||
return await this.getJsonSchema(insertedNodes[0].get('uid'));
|
||||
}
|
||||
|
||||
async insertInner(targetUid: string, schema: any, position: 'first' | 'last') {
|
||||
const nodes = UiSchemaRepository.schemaToSingleNodes(schema);
|
||||
const rootNode = nodes[0];
|
||||
rootNode.childOptions = {
|
||||
parentUid: targetUid,
|
||||
type: lodash.get(schema, 'x-node-type', 'properties'),
|
||||
position,
|
||||
};
|
||||
|
||||
const insertedNodes = await this.insertNodes(nodes);
|
||||
return await this.getJsonSchema(insertedNodes[0].get('uid'));
|
||||
}
|
||||
|
||||
async insertAdjacent(position: 'beforeBegin' | 'afterBegin' | 'beforeEnd' | 'afterEnd', target: string, schema: any) {
|
||||
return await this[`insert${lodash.upperFirst(position)}`](target, schema);
|
||||
}
|
||||
|
||||
async insertAfterBegin(targetUid: string, schema: any) {
|
||||
return await this.insertInner(targetUid, schema, 'first');
|
||||
}
|
||||
|
||||
async insertBeforeEnd(targetUid: string, schema: any) {
|
||||
return await this.insertInner(targetUid, schema, 'last');
|
||||
}
|
||||
|
||||
async insertBeforeBegin(targetUid: string, schema: any) {
|
||||
return await this.insertBeside(targetUid, schema, 'before');
|
||||
}
|
||||
|
||||
async insertAfterEnd(targetUid: string, schema: any) {
|
||||
return await this.insertBeside(targetUid, schema, 'after');
|
||||
}
|
||||
|
||||
async insertNodes(nodes: SchemaNode[], options?) {
|
||||
let handleTransaction: boolean = true;
|
||||
let transaction;
|
||||
|
||||
if (options?.transaction) {
|
||||
transaction = options.transaction;
|
||||
handleTransaction = false;
|
||||
} else {
|
||||
transaction = await this.database.sequelize.transaction();
|
||||
}
|
||||
|
||||
const insertedNodes = [];
|
||||
|
||||
try {
|
||||
for (const node of nodes) {
|
||||
insertedNodes.push(await this.insertSingleNode(node, transaction));
|
||||
}
|
||||
|
||||
if (handleTransaction) {
|
||||
await transaction.commit();
|
||||
}
|
||||
return insertedNodes;
|
||||
} catch (err) {
|
||||
if (handleTransaction) {
|
||||
await transaction.rollback();
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async insert(schema: any, options?) {
|
||||
const nodes = UiSchemaRepository.schemaToSingleNodes(schema);
|
||||
const insertedNodes = await this.insertNodes(nodes, options);
|
||||
return this.getJsonSchema(insertedNodes[0].get('uid'), {
|
||||
transaction: options?.transaction,
|
||||
});
|
||||
}
|
||||
|
||||
async insertSingleNode(schema: SchemaNode, transaction: Transaction) {
|
||||
const db = this.database;
|
||||
const treeCollection = db.getCollection('ui_schema_tree_path');
|
||||
|
||||
const uid = schema['x-uid'];
|
||||
const name = schema['name'];
|
||||
const async = lodash.get(schema, 'x-async', false);
|
||||
const childOptions = schema['childOptions'];
|
||||
|
||||
delete schema['x-uid'];
|
||||
delete schema['x-async'];
|
||||
delete schema['name'];
|
||||
delete schema['childOptions'];
|
||||
|
||||
let savedNode;
|
||||
|
||||
// check node exists or not
|
||||
const existsNode = await this.findOne({
|
||||
filter: {
|
||||
uid,
|
||||
},
|
||||
transaction,
|
||||
});
|
||||
|
||||
const treeTable = treeCollection.model.tableName;
|
||||
|
||||
if (existsNode) {
|
||||
savedNode = existsNode;
|
||||
} else {
|
||||
savedNode = await this.create({
|
||||
values: {
|
||||
name,
|
||||
uid,
|
||||
schema,
|
||||
},
|
||||
transaction,
|
||||
hooks: false,
|
||||
});
|
||||
}
|
||||
|
||||
if (childOptions) {
|
||||
const parentUid = childOptions.parentUid;
|
||||
|
||||
const isTreeQuery = await db.sequelize.query(
|
||||
`SELECT COUNT(*) as childrenCount from ${treeTable} WHERE ancestor = :ancestor AND descendant != ancestor`,
|
||||
{
|
||||
type: 'SELECT',
|
||||
replacements: {
|
||||
ancestor: uid,
|
||||
},
|
||||
transaction,
|
||||
},
|
||||
);
|
||||
|
||||
const isTree = isTreeQuery[0]['childrenCount'];
|
||||
|
||||
// if node is a tree root move tree to new path
|
||||
if (isTree) {
|
||||
await db.sequelize.query(
|
||||
`DELETE FROM ${treeTable}
|
||||
WHERE descendant IN (SELECT descendant FROM (SELECT descendant FROM ${treeTable} WHERE ancestor = :uid) as descendantTable )
|
||||
AND ancestor IN (SELECT ancestor FROM (SELECT ancestor FROM ${treeTable} WHERE descendant = :uid AND ancestor != descendant) as ancestorTable)
|
||||
`,
|
||||
{
|
||||
type: 'DELETE',
|
||||
replacements: {
|
||||
uid,
|
||||
},
|
||||
transaction,
|
||||
},
|
||||
);
|
||||
|
||||
await db.sequelize.query(
|
||||
`INSERT INTO ${treeTable} (ancestor, descendant, depth)
|
||||
SELECT supertree.ancestor, subtree.descendant, supertree.depth + subtree.depth + 1
|
||||
FROM ${treeTable} AS supertree
|
||||
CROSS JOIN ${treeTable} AS subtree
|
||||
WHERE supertree.descendant = :parentUid
|
||||
AND subtree.ancestor = :uid;`,
|
||||
{
|
||||
type: 'INSERT',
|
||||
replacements: {
|
||||
uid,
|
||||
parentUid,
|
||||
},
|
||||
transaction,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (!isTree) {
|
||||
if (existsNode) {
|
||||
// remove old path
|
||||
await db.sequelize.query(`DELETE FROM ${treeTable} WHERE descendant = :uid AND ancestor != descendant`, {
|
||||
type: 'DELETE',
|
||||
replacements: {
|
||||
uid,
|
||||
},
|
||||
transaction,
|
||||
});
|
||||
}
|
||||
|
||||
// insert tree path
|
||||
await db.sequelize.query(
|
||||
`INSERT INTO ${treeTable} (ancestor, descendant, depth)
|
||||
SELECT t.ancestor, :modelKey, depth + 1 FROM ${treeTable} AS t WHERE t.descendant = :modelParentKey `,
|
||||
{
|
||||
type: 'INSERT',
|
||||
transaction,
|
||||
replacements: {
|
||||
modelKey: savedNode.get('uid'),
|
||||
modelParentKey: parentUid,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (!existsNode) {
|
||||
// insert type && async
|
||||
await db.sequelize.query(
|
||||
`INSERT INTO ${treeTable}(ancestor, descendant, depth, type, async) VALUES (:modelKey, :modelKey, 0, :type, :async )`,
|
||||
{
|
||||
type: 'INSERT',
|
||||
replacements: {
|
||||
modelKey: savedNode.get('uid'),
|
||||
type: childOptions.type,
|
||||
async,
|
||||
},
|
||||
transaction,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const nodePosition = childOptions.position || 'last';
|
||||
|
||||
let sort;
|
||||
|
||||
// insert at first
|
||||
if (nodePosition === 'first') {
|
||||
sort = 1;
|
||||
// move all child last index
|
||||
await db.sequelize.query(
|
||||
`UPDATE ${treeTable} as TreeTable
|
||||
SET sort = TreeTable.sort + 1
|
||||
FROM ${treeTable} as NodeInfo
|
||||
WHERE NodeInfo.descendant = TreeTable.descendant and NodeInfo.depth = 0
|
||||
AND TreeTable.depth = 1 AND TreeTable.ancestor = :ancestor and NodeInfo.type = :type`,
|
||||
{
|
||||
replacements: {
|
||||
ancestor: childOptions.parentUid,
|
||||
type: childOptions.type,
|
||||
},
|
||||
transaction,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (nodePosition === 'last') {
|
||||
const maxSort = await db.sequelize.query(
|
||||
`SELECT ${
|
||||
this.database.sequelize.getDialect() === 'postgres' ? 'coalesce' : 'ifnull'
|
||||
}(MAX(TreeTable.sort), 0) as maxsort FROM ${treeTable} as TreeTable
|
||||
LEFT JOIN ${treeTable} as NodeInfo
|
||||
ON NodeInfo.descendant = TreeTable.descendant and NodeInfo.depth = 0
|
||||
WHERE TreeTable.depth = 1 AND TreeTable.ancestor = :ancestor and NodeInfo.type = :type`,
|
||||
{
|
||||
type: 'SELECT',
|
||||
replacements: {
|
||||
ancestor: childOptions.parentUid,
|
||||
type: childOptions.type,
|
||||
},
|
||||
transaction,
|
||||
},
|
||||
);
|
||||
|
||||
sort = parseInt(maxSort[0]['maxsort']) + 1;
|
||||
}
|
||||
|
||||
if (lodash.isPlainObject(nodePosition)) {
|
||||
const targetPosition = nodePosition as TargetPosition;
|
||||
const target = targetPosition.target;
|
||||
|
||||
const targetSort = await db.sequelize.query(
|
||||
`SELECT TreeTable.sort as sort FROM ${treeTable} as TreeTable
|
||||
LEFT JOIN ${treeTable} as NodeInfo
|
||||
ON NodeInfo.descendant = TreeTable.descendant and NodeInfo.depth = 0 WHERE TreeTable.depth = 1 AND TreeTable.ancestor = :ancestor AND TreeTable.descendant = :descendant and NodeInfo.type = :type`,
|
||||
{
|
||||
type: 'SELECT',
|
||||
replacements: {
|
||||
ancestor: childOptions.parentUid,
|
||||
descendant: target,
|
||||
type: childOptions.type,
|
||||
},
|
||||
transaction,
|
||||
},
|
||||
);
|
||||
|
||||
sort = targetSort[0].sort;
|
||||
|
||||
if (targetPosition.type == 'after') {
|
||||
sort += 1;
|
||||
}
|
||||
|
||||
await db.sequelize.query(
|
||||
`UPDATE ${treeTable} as TreeTable
|
||||
SET sort = TreeTable.sort + 1
|
||||
FROM ${treeTable} as NodeInfo
|
||||
WHERE NodeInfo.descendant = TreeTable.descendant and NodeInfo.depth = 0
|
||||
AND TreeTable.depth = 1 AND TreeTable.ancestor = :ancestor and TreeTable.sort >= :sort and NodeInfo.type = :type`,
|
||||
{
|
||||
replacements: {
|
||||
ancestor: childOptions.parentUid,
|
||||
sort,
|
||||
type: childOptions.type,
|
||||
},
|
||||
transaction,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// update order
|
||||
const updateSql = `UPDATE ${treeTable} SET sort = :sort WHERE depth = 1 AND ancestor = :ancestor AND descendant = :descendant`;
|
||||
await db.sequelize.query(updateSql, {
|
||||
type: 'UPDATE',
|
||||
replacements: {
|
||||
ancestor: childOptions.parentUid,
|
||||
sort,
|
||||
descendant: uid,
|
||||
},
|
||||
transaction,
|
||||
});
|
||||
} else {
|
||||
// insert root node path
|
||||
await db.sequelize.query(
|
||||
`INSERT INTO ${treeTable}(ancestor, descendant, depth, async) VALUES (:modelKey, :modelKey, 0, :async )`,
|
||||
{
|
||||
type: 'INSERT',
|
||||
replacements: {
|
||||
modelKey: savedNode.get('uid'),
|
||||
async,
|
||||
},
|
||||
transaction,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return savedNode;
|
||||
}
|
||||
}
|
66
packages/plugin-ui-schema/src/server.ts
Normal file
66
packages/plugin-ui-schema/src/server.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import { MagicAttributeModel } from '@nocobase/database';
|
||||
import { Plugin } from '@nocobase/server';
|
||||
import path from 'path';
|
||||
import { uiSchemaActions } from './actions/ui-schema-action';
|
||||
import UiSchemaRepository from './repository';
|
||||
|
||||
export default class PluginUiSchema extends Plugin {
|
||||
registerRepository() {
|
||||
this.app.db.registerRepositories({
|
||||
UiSchemaRepository,
|
||||
});
|
||||
}
|
||||
|
||||
async load() {
|
||||
const db = this.app.db;
|
||||
|
||||
this.app.db.registerModels({ MagicAttributeModel });
|
||||
|
||||
this.registerRepository();
|
||||
|
||||
await db.import({
|
||||
directory: path.resolve(__dirname, 'collections'),
|
||||
});
|
||||
|
||||
db.on('ui_schemas.beforeCreate', (model) => {
|
||||
model.set('uid', model.get('x-uid'));
|
||||
});
|
||||
|
||||
db.on('ui_schemas.afterCreate', async (model, options) => {
|
||||
const { transaction } = options;
|
||||
const uiSchemaRepository = db.getCollection('ui_schemas').repository as UiSchemaRepository;
|
||||
|
||||
await uiSchemaRepository.insert(model.toJSON(), {
|
||||
transaction,
|
||||
});
|
||||
});
|
||||
|
||||
db.on('ui_schemas.afterUpdate', async (model, options) => {
|
||||
const { transaction } = options;
|
||||
const uiSchemaRepository = db.getCollection('ui_schemas').repository as UiSchemaRepository;
|
||||
|
||||
await uiSchemaRepository.patch(model.toJSON(), {
|
||||
transaction,
|
||||
});
|
||||
});
|
||||
|
||||
await db.getCollection('ui_schemas').sync({
|
||||
force: false,
|
||||
alter: {
|
||||
drop: false,
|
||||
},
|
||||
});
|
||||
|
||||
await db.getCollection('ui_schema_tree_path').sync({
|
||||
force: false,
|
||||
alter: {
|
||||
drop: false,
|
||||
},
|
||||
});
|
||||
|
||||
this.app.resourcer.define({
|
||||
name: 'ui_schemas',
|
||||
actions: uiSchemaActions,
|
||||
});
|
||||
}
|
||||
}
|
9
packages/plugin-ui-schema/tsconfig.build.json
Normal file
9
packages/plugin-ui-schema/tsconfig.build.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.build.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./lib",
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
|
||||
"exclude": ["./src/__tests__/*", "./esm/*", "./lib/*"]
|
||||
}
|
5
packages/plugin-ui-schema/tsconfig.json
Normal file
5
packages/plugin-ui-schema/tsconfig.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
|
||||
"exclude": ["./esm/*", "./lib/*"]
|
||||
}
|
@ -17,6 +17,7 @@ export function getConfig(config = {}, options?: any): DatabaseOptions {
|
||||
{
|
||||
username: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
storage: process.env.DB_STORAGE,
|
||||
database: process.env.DB_DATABASE,
|
||||
host: process.env.DB_HOST,
|
||||
port: process.env.DB_PORT,
|
||||
|
Loading…
Reference in New Issue
Block a user