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] });
|
test.set('x-component-props', { arr2: [1, 2, 3] });
|
||||||
|
|
||||||
|
await test.save();
|
||||||
|
|
||||||
expect(test.toJSON()).toMatchObject({
|
expect(test.toJSON()).toMatchObject({
|
||||||
title: 'aa',
|
title: 'aa',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
@ -39,6 +41,8 @@ describe('magic-attribute-model', () => {
|
|||||||
},
|
},
|
||||||
'x-decorator-props': { key1: 'val1' },
|
'x-decorator-props': { key1: 'val1' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await db.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('case 2', async () => {
|
it('case 2', async () => {
|
||||||
@ -57,7 +61,7 @@ describe('magic-attribute-model', () => {
|
|||||||
|
|
||||||
await db.sync();
|
await db.sync();
|
||||||
|
|
||||||
const test = await Test.model.create({
|
let test = await Test.model.create({
|
||||||
title: 'aa',
|
title: 'aa',
|
||||||
'x-component-props': { key1: 'val1', arr1: [1, 2, 3], arr2: [4, 5] },
|
'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] });
|
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({
|
expect(test.toJSON()).toMatchObject({
|
||||||
title: 'aa',
|
title: 'aa',
|
||||||
'x-component-props': {
|
'x-component-props': {
|
||||||
key1: 'val1',
|
key1: 'val1',
|
||||||
key2: 'val2',
|
key2: 'val2',
|
||||||
arr1: [3, 4],
|
arr1: [3, 4],
|
||||||
arr2: [1, 2, 3],
|
arr2: [1, 2, 3, 4],
|
||||||
},
|
},
|
||||||
'x-decorator-props': { key1: 'val1' },
|
'x-decorator-props': { key1: 'val1' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await db.close();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { DataTypes } from 'sequelize';
|
import { DataTypes } from 'sequelize';
|
||||||
import { uid } from '../utils';
|
|
||||||
import { BaseColumnFieldOptions, Field } from './field';
|
import { BaseColumnFieldOptions, Field } from './field';
|
||||||
|
import { uid } from '@nocobase/utils';
|
||||||
|
|
||||||
export class UidField extends Field {
|
export class UidField extends Field {
|
||||||
get dataType() {
|
get dataType() {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
export * from './database';
|
export * from './database';
|
||||||
export * from './collection';
|
export * from './collection';
|
||||||
export * from './repository';
|
export * from './repository';
|
||||||
export * from './utils';
|
|
||||||
export { Database as default } from './database';
|
export { Database as default } from './database';
|
||||||
export * from './relation-repository/belongs-to-many-repository';
|
export * from './relation-repository/belongs-to-many-repository';
|
||||||
export * from './relation-repository/belongs-to-repository';
|
export * from './relation-repository/belongs-to-repository';
|
||||||
|
@ -5,7 +5,7 @@ import Database from './database';
|
|||||||
|
|
||||||
export class MagicAttributeModel extends Model {
|
export class MagicAttributeModel extends Model {
|
||||||
get magicAttribute() {
|
get magicAttribute() {
|
||||||
const db: Database = (this.constructor as any).database;
|
const db: Database = (<any>this.constructor).database;
|
||||||
const collection = db.getCollection(this.constructor.name);
|
const collection = db.getCollection(this.constructor.name);
|
||||||
return collection.options.magicAttribute || 'options';
|
return collection.options.magicAttribute || 'options';
|
||||||
}
|
}
|
||||||
@ -50,4 +50,10 @@ export class MagicAttributeModel extends Model {
|
|||||||
...data[this.magicAttribute],
|
...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> {
|
async create<M extends Model>(options: CreateOptions): Promise<M> {
|
||||||
const transaction = await this.getTransaction(options);
|
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 values = guard.sanitize(options.values || {});
|
||||||
|
|
||||||
const instance = await this.model.create<any>(values, {
|
const instance = await this.model.create<any>(values, {
|
||||||
|
@ -11,19 +11,29 @@ type UpdateValues = {
|
|||||||
[key: string]: UpdateValueItem | Array<UpdateValueItem>;
|
[key: string]: UpdateValueItem | Array<UpdateValueItem>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type UpdateAction = 'create' | 'update';
|
||||||
export class UpdateGuard {
|
export class UpdateGuard {
|
||||||
model: ModelCtor<any>;
|
model: ModelCtor<any>;
|
||||||
|
action: UpdateAction;
|
||||||
private associationKeysToBeUpdate: AssociationKeysToBeUpdate;
|
private associationKeysToBeUpdate: AssociationKeysToBeUpdate;
|
||||||
private blackList: BlackList;
|
private blackList: BlackList;
|
||||||
private whiteList: WhiteList;
|
private whiteList: WhiteList;
|
||||||
|
|
||||||
|
setAction(action: UpdateAction) {
|
||||||
|
this.action = action;
|
||||||
|
}
|
||||||
|
|
||||||
setModel(model: ModelCtor<any>) {
|
setModel(model: ModelCtor<any>) {
|
||||||
this.model = model;
|
this.model = model;
|
||||||
}
|
}
|
||||||
|
|
||||||
setAssociationKeysToBeUpdate(associationKeysToBeUpdate: AssociationKeysToBeUpdate) {
|
setAssociationKeysToBeUpdate(associationKeysToBeUpdate: AssociationKeysToBeUpdate) {
|
||||||
|
if (this.action == 'create') {
|
||||||
|
this.associationKeysToBeUpdate = Object.keys(this.model.associations);
|
||||||
|
} else {
|
||||||
this.associationKeysToBeUpdate = associationKeysToBeUpdate;
|
this.associationKeysToBeUpdate = associationKeysToBeUpdate;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setWhiteList(whiteList: WhiteList) {
|
setWhiteList(whiteList: WhiteList) {
|
||||||
this.whiteList = whiteList;
|
this.whiteList = whiteList;
|
||||||
@ -148,8 +158,8 @@ export class UpdateGuard {
|
|||||||
guard.setModel(model);
|
guard.setModel(model);
|
||||||
guard.setWhiteList(options.whitelist);
|
guard.setWhiteList(options.whitelist);
|
||||||
guard.setBlackList(options.blacklist);
|
guard.setBlackList(options.blacklist);
|
||||||
|
guard.setAction(lodash.get(options, 'action', 'update'));
|
||||||
guard.setAssociationKeysToBeUpdate(options.updateAssociationValues);
|
guard.setAssociationKeysToBeUpdate(options.updateAssociationValues);
|
||||||
|
|
||||||
return guard;
|
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,
|
username: process.env.DB_USER,
|
||||||
password: process.env.DB_PASSWORD,
|
password: process.env.DB_PASSWORD,
|
||||||
|
storage: process.env.DB_STORAGE,
|
||||||
database: process.env.DB_DATABASE,
|
database: process.env.DB_DATABASE,
|
||||||
host: process.env.DB_HOST,
|
host: process.env.DB_HOST,
|
||||||
port: process.env.DB_PORT,
|
port: process.env.DB_PORT,
|
||||||
|
Loading…
Reference in New Issue
Block a user