feat: insertNewSchema (#245)

* fix: transaction error

* stage

* feat: insertNewSchema

* feat: insertNewSchema Return Value

* test

* fix: insertAdjacent with root node

* feat: insertAdjacent with wrap

* fix: test

* feat: wrap with new schema

* feat: action with wrap params

* feat: improve client

* feat: improve client

* fix: test

Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
ChengLei Shao 2022-03-20 20:04:07 +08:00 committed by GitHub
parent 4422324980
commit ed8c60eb85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 7276 additions and 104 deletions

View File

@ -41,7 +41,7 @@ const RequestSchemaComponent: React.FC<RemoteSchemaComponentProps> = (props) =>
} }
return ( return (
<FormProvider form={form}> <FormProvider form={form}>
<SchemaComponent memoized scope={scope} schema={schemaTransform(data?.data || {})} />; <SchemaComponent memoized scope={scope} schema={schemaTransform(data?.data || {})} />
</FormProvider> </FormProvider>
); );
}; };

View File

@ -0,0 +1,87 @@
import { Schema } from '@formily/react';
import { splitWrapSchema } from '../useDesignable';
describe('splitWrapSchema', () => {
test('case 1', () => {
const wrap = new Schema({
'x-uid': 'aaa',
properties: {
aa: {
'x-uid': 'aa',
properties: {
a: {
'x-uid': '123',
properties: {
bbb: {
'x-uid': 'bbb',
},
},
},
},
},
},
});
const [schema1, schema2] = splitWrapSchema(wrap, { ['x-uid']: 'aaa' });
expect(schema1).toBeNull();
expect(schema2).toMatchObject({
'x-uid': 'aaa',
properties: {
aa: {
'x-uid': 'aa',
properties: {
a: {
'x-uid': '123',
properties: {
bbb: {
'x-uid': 'bbb',
},
},
},
},
},
},
});
});
test('case 2', () => {
const wrap = new Schema({
'x-uid': 'aaa',
properties: {
aa: {
'x-uid': 'aa',
properties: {
a: {
'x-uid': '123',
properties: {
bbb: {
'x-uid': 'bbb',
},
},
},
},
},
},
});
const [schema1, schema2] = splitWrapSchema(wrap, { ['x-uid']: '123' });
expect(schema1).toMatchObject({
'x-uid': 'aaa',
properties: {
aa: {
'x-uid': 'aa',
properties: {},
name: 'aa',
},
},
});
expect(schema2).toMatchObject({
'x-uid': '123',
properties: {
bbb: {
'x-uid': 'bbb',
name: 'bbb',
},
},
name: 'a',
});
});
});

View File

@ -67,6 +67,29 @@ const matchSchema = (source: ISchema, target: ISchema) => {
return true; return true;
}; };
export const splitWrapSchema = (wrapped: Schema, schema: ISchema) => {
if (wrapped['x-uid'] && wrapped['x-uid'] === schema['x-uid']) {
return [null, wrapped.toJSON()];
}
const wrappedJson: ISchema = wrapped.toJSON();
let schema1 = { ...wrappedJson, properties: {} };
let schema2 = null;
const findSchema = (properties, parent) => {
Object.keys(properties || {}).forEach((key) => {
const current = properties[key];
if (current['x-uid'] === schema['x-uid']) {
schema2 = properties[key];
return;
} else {
parent.properties[key] = { ...current, properties: {} };
findSchema(current?.properties, parent.properties[key]);
}
});
};
findSchema(wrappedJson.properties, schema1);
return [schema1, schema2];
};
export class Designable { export class Designable {
current: Schema; current: Schema;
options: CreateDesignableProps; options: CreateDesignableProps;
@ -82,12 +105,15 @@ export class Designable {
if (!api) { if (!api) {
return; return;
} }
this.on('insertAdjacent', async ({ onSuccess, current, position, schema, removed }) => { this.on('insertAdjacent', async ({ onSuccess, current, position, schema, wrap, removed }) => {
refresh(); refresh();
await api.request({ await api.request({
url: `/uiSchemas:insertAdjacent/${current['x-uid']}?position=${position}`, url: `/uiSchemas:insertAdjacent/${current['x-uid']}?position=${position}`,
method: 'post', method: 'post',
data: schema.toJSON(), data: {
schema,
wrap,
},
}); });
if (removed?.['x-uid']) { if (removed?.['x-uid']) {
await api.request({ await api.request({
@ -313,9 +339,11 @@ export class Designable {
s['x-index'] = newOrder; s['x-index'] = newOrder;
s.parent = this.current.parent; s.parent = this.current.parent;
this.current.parent.setProperties(properties); this.current.parent.setProperties(properties);
const [schema1, schema2] = splitWrapSchema(s, schema);
this.emit('insertAdjacent', { this.emit('insertAdjacent', {
position: 'beforeBegin', position: 'beforeBegin',
schema: s, schema: schema2,
wrap: schema1,
...opts, ...opts,
}); });
} }
@ -359,9 +387,11 @@ export class Designable {
s['x-index'] = 0; s['x-index'] = 0;
s.parent = this.current; s.parent = this.current;
this.current.setProperties(properties); this.current.setProperties(properties);
const [schema1, schema2] = splitWrapSchema(s, schema);
this.emit('insertAdjacent', { this.emit('insertAdjacent', {
position: 'afterBegin', position: 'afterBegin',
schema: s, schema: schema2,
wrap: schema1,
...opts, ...opts,
}); });
} }
@ -395,9 +425,11 @@ export class Designable {
const wrapped = wrap(schema); const wrapped = wrap(schema);
const s = this.current.addProperty(wrapped.name || uid(), wrapped); const s = this.current.addProperty(wrapped.name || uid(), wrapped);
s.parent = this.current; s.parent = this.current;
const [schema1, schema2] = splitWrapSchema(s, schema);
this.emit('insertAdjacent', { this.emit('insertAdjacent', {
position: 'beforeEnd', position: 'beforeEnd',
schema: s, schema: schema2,
wrap: schema1,
...opts, ...opts,
}); });
} }
@ -451,9 +483,11 @@ export class Designable {
s.parent = this.current.parent; s.parent = this.current.parent;
s['x-index'] = newOrder; s['x-index'] = newOrder;
this.current.parent.setProperties(properties); this.current.parent.setProperties(properties);
const [schema1, schema2] = splitWrapSchema(s, schema);
this.emit('insertAdjacent', { this.emit('insertAdjacent', {
position: 'afterEnd', position: 'afterEnd',
schema: s, schema: schema2,
wrap: schema1,
...opts, ...opts,
}); });
} }

View File

@ -229,4 +229,33 @@ describe('action test', () => {
const { data } = response.body; const { data } = response.body;
expect(data.properties.e['x-uid']).toEqual('n5'); expect(data.properties.e['x-uid']).toEqual('n5');
}); });
test('insert adjacent with bit schema', async () => {
const schema = require('./fixtures/data').default;
await app
.agent()
.resource('uiSchemas')
.insert({
values: {
'x-uid': 'root',
properties: {
A: {
'x-uid': 'A',
},
B: {
'x-uid': 'B',
},
},
},
});
let response = await app.agent().resource('uiSchemas').insertAdjacent({
resourceIndex: 'A',
position: 'afterEnd',
values: schema,
});
expect(response.statusCode).toEqual(200);
});
}); });

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,35 @@
export default {
name: 'root-name',
'x-uid': 'root',
properties: {
p1: {
'x-uid': 'p1',
'x-component-props': {
title: "alias's",
},
},
p2: {
'x-uid': 'p2',
properties: {
p21: {
'x-uid': 'p21',
properties: {
p211: {
'x-uid': 'p211',
},
},
},
},
},
},
items: [
{
name: 'i1',
'x-uid': 'i1',
},
{
name: 'i2',
'x-uid': 'i2',
},
],
};

View File

@ -338,20 +338,24 @@ describe('server hooks', () => {
await uiSchemaRepository.insert(schema); await uiSchemaRepository.insert(schema);
await uiSchemaRepository.insertAfterEnd('E', { await uiSchemaRepository.insertAdjacent(
'x-uid': 'F', 'afterEnd',
name: 'F', 'E',
properties: { {
G: { 'x-uid': 'D',
'x-uid': 'G', },
{
wrap: {
'x-uid': 'F',
name: 'F',
properties: { properties: {
D: { G: {
'x-uid': 'D', 'x-uid': 'G',
}, },
}, },
}, },
}, },
}); );
const A = await uiSchemaRepository.getJsonSchema('A'); const A = await uiSchemaRepository.getJsonSchema('A');
expect(A).toEqual({ expect(A).toEqual({

View File

@ -316,24 +316,23 @@ describe('server hooks', () => {
await uiSchemaRepository.insert(schema); await uiSchemaRepository.insert(schema);
await uiSchemaRepository.insertAfterEnd( await uiSchemaRepository.insertAdjacent(
'afterEnd',
'E', 'E',
{ {
'x-uid': 'F', 'x-uid': 'D',
name: 'F',
properties: {
G: {
'x-uid': 'G',
properties: {
D: {
'x-uid': 'D',
},
},
},
},
}, },
{ {
removeParentsIfNoChildren: true, removeParentsIfNoChildren: true,
wrap: {
'x-uid': 'F',
name: 'F',
properties: {
G: {
'x-uid': 'G',
},
},
},
}, },
); );

View File

@ -418,7 +418,7 @@ describe('ui_schema repository', () => {
}; };
await repository.insert(schema); await repository.insert(schema);
await repository.insertBeforeBegin('i1', { await repository.insertAdjacent('beforeBegin', 'i1', {
'x-uid': 'i2', 'x-uid': 'i2',
}); });
@ -476,7 +476,7 @@ describe('ui_schema repository', () => {
name: 'newNode', name: 'newNode',
}; };
await repository.insertAfterBegin(rootUid, newNode); await repository.insertAdjacent('afterBegin', rootUid, newNode);
const schema = await repository.getJsonSchema(rootUid); const schema = await repository.getJsonSchema(rootUid);
expect(schema['properties']['newNode']['x-index']).toEqual(1); expect(schema['properties']['newNode']['x-index']).toEqual(1);
expect(schema['properties'].a1['x-index']).toEqual(2); expect(schema['properties'].a1['x-index']).toEqual(2);
@ -488,7 +488,7 @@ describe('ui_schema repository', () => {
name: 'newNode', name: 'newNode',
}; };
await repository.insertBeforeEnd(rootUid, newNode); await repository.insertAdjacent('beforeEnd', rootUid, newNode);
const schema = await repository.getJsonSchema(rootUid); const schema = await repository.getJsonSchema(rootUid);
expect(schema['properties']['newNode']['x-index']).toEqual(3); expect(schema['properties']['newNode']['x-index']).toEqual(3);
expect(schema['properties'].a1['x-index']).toEqual(1); expect(schema['properties'].a1['x-index']).toEqual(1);
@ -506,7 +506,7 @@ describe('ui_schema repository', () => {
}, },
}); });
await repository.insertBeforeBegin(b1Node.get('x-uid') as string, newNode); await repository.insertAdjacent('beforeBegin', b1Node.get('x-uid') as string, newNode);
const schema = await repository.getJsonSchema(rootUid); const schema = await repository.getJsonSchema(rootUid);
expect(schema['properties']['newNode']['x-index']).toEqual(2); expect(schema['properties']['newNode']['x-index']).toEqual(2);
@ -525,7 +525,7 @@ describe('ui_schema repository', () => {
}, },
}); });
await repository.insertAfterEnd(a1Node.get('x-uid') as string, newNode); await repository.insertAdjacent('afterEnd', a1Node.get('x-uid') as string, newNode);
const schema = await repository.getJsonSchema(rootUid); const schema = await repository.getJsonSchema(rootUid);
expect(schema['properties']['newNode']['x-index']).toEqual(2); expect(schema['properties']['newNode']['x-index']).toEqual(2);
@ -544,7 +544,7 @@ describe('ui_schema repository', () => {
}, },
}); });
await repository.insertAfterEnd(b1Node.get('x-uid') as string, newNode); await repository.insertAdjacent('afterEnd', b1Node.get('x-uid') as string, newNode);
const schema = await repository.getJsonSchema(rootUid); const schema = await repository.getJsonSchema(rootUid);
expect(schema['properties']['newNode']['x-index']).toEqual(3); expect(schema['properties']['newNode']['x-index']).toEqual(3);
@ -578,21 +578,28 @@ describe('ui_schema repository', () => {
}, },
}); });
await repository.insertAfterBegin('n1', { await repository.insertAdjacent(
name: 'f', 'afterBegin',
'x-uid': 'n6', 'n1',
properties: { {
g: { 'x-uid': 'n4',
'x-uid': 'n7', },
{
wrap: {
name: 'f',
'x-uid': 'n6',
properties: { properties: {
d: { 'x-uid': 'n4' }, g: {
'x-uid': 'n7',
},
}, },
}, },
}, },
}); );
const schema = await repository.getJsonSchema('n1'); const schema = await repository.getJsonSchema('n1');
expect(schema.properties.f.properties.g.properties.d.properties.e['x-uid']).toEqual('n5'); expect(schema.properties.f.properties.g.properties.d.properties.e['x-uid']).toEqual('n5');
expect(schema.properties.f.properties.g.properties.d['x-uid']).toEqual('n4');
}); });
it('should insertAfterBegin by node', async () => { it('should insertAfterBegin by node', async () => {
@ -612,7 +619,7 @@ describe('ui_schema repository', () => {
}, },
}); });
await repository.insertAfterBegin('n2', 'n4'); await repository.insertAdjacent('afterBegin', 'n2', 'n4');
const schema = await repository.getJsonSchema('n1'); const schema = await repository.getJsonSchema('n1');
expect(schema['properties'].b.properties.d['x-uid']).toEqual('n4'); expect(schema['properties'].b.properties.d['x-uid']).toEqual('n4');
}); });
@ -878,24 +885,23 @@ describe('ui_schema repository', () => {
await repository.insert(schema); await repository.insert(schema);
await repository.insertAfterBegin( await repository.insertAdjacent(
'afterBegin',
'E', 'E',
{ {
'x-uid': 'F', 'x-uid': 'D',
name: 'F',
properties: {
G: {
'x-uid': 'G',
properties: {
D: {
'x-uid': 'D',
},
},
},
},
}, },
{ {
removeParentsIfNoChildren: true, removeParentsIfNoChildren: true,
wrap: {
'x-uid': 'F',
name: 'F',
properties: {
G: {
'x-uid': 'G',
},
},
},
}, },
); );
@ -962,24 +968,23 @@ describe('ui_schema repository', () => {
await repository.insert(schema); await repository.insert(schema);
await repository.insertAfterEnd( await repository.insertAdjacent(
'afterEnd',
'E', 'E',
{ {
'x-uid': 'F', 'x-uid': 'D',
name: 'F',
properties: {
G: {
'x-uid': 'G',
properties: {
D: {
'x-uid': 'D',
},
},
},
},
}, },
{ {
removeParentsIfNoChildren: true, removeParentsIfNoChildren: true,
wrap: {
'x-uid': 'F',
name: 'F',
properties: {
G: {
'x-uid': 'G',
},
},
},
}, },
); );
@ -1108,4 +1113,490 @@ describe('ui_schema repository', () => {
}, },
}); });
}); });
it('should insert big schema', async () => {
const schema = require('./fixtures/data').default;
console.time('test');
await repository.insertNewSchema(schema);
console.timeEnd('test');
const rootUid = schema['x-uid'];
const savedSchema = await repository.getJsonSchema(rootUid);
expect(savedSchema).toBeDefined();
});
it('should insert new with insertAfterEnd', async () => {
const root = {
'x-uid': 'root',
name: 'root',
properties: {
c1: {
'x-uid': 'c1',
},
},
};
const newNode = {
'x-uid': 'new',
name: 'new',
properties: {
nc1: {
'x-uid': 'nc1',
},
},
};
await repository.insertNewSchema(root);
await repository.insertNewSchema(newNode);
await repository.insertAdjacent('afterEnd', 'c1', {
'x-uid': 'new',
});
const json = await repository.getJsonSchema('root');
expect(json).toEqual({
properties: {
c1: {
'x-uid': 'c1',
'x-async': false,
'x-index': 1,
},
new: {
properties: {
nc1: {
'x-uid': 'nc1',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'new',
'x-async': false,
'x-index': 2,
},
},
name: 'root',
'x-uid': 'root',
'x-async': false,
});
});
it('should insert big schema using insertAfterEnd', async () => {
const tree = {
'x-uid': 'root',
properties: {
A: {
'x-uid': 'A',
},
B: {
'x-uid': 'B',
},
},
};
await repository.insert(tree);
const schema = require('./fixtures/data').default;
await repository.insertAdjacent('afterEnd', 'A', schema);
const rootUid = schema['x-uid'];
const savedSchema = await repository.getJsonSchema(rootUid);
expect(savedSchema).toBeDefined();
});
describe('schemaToSingleNodes', () => {
it('should with parent Paths', async () => {
const schema = {
name: 'root-name',
'x-uid': 'root',
properties: {
p1: {
'x-uid': 'p1',
},
p2: {
'x-uid': 'p2',
properties: {
p21: {
'x-uid': 'p21',
properties: {
p211: {
'x-uid': 'p211',
},
},
},
},
},
},
items: [
{
name: 'i1',
'x-uid': 'i1',
},
{
name: 'i2',
'x-uid': 'i2',
},
],
};
const nodes = UiSchemaRepository.schemaToSingleNodes(schema);
const p211Node = nodes.find((node) => node['x-uid'] === 'p211');
expect(p211Node['childOptions'].parentPath).toEqual(['p21', 'p2', 'root']);
});
});
describe('insertAdjacent', () => {
it('should works with wrap and new schema', async () => {
const schema = {
name: 'root-name',
'x-uid': 'root',
properties: {
p1: {
'x-uid': 'p1',
properties: {
p11: {
'x-uid': 'p11',
},
},
},
p2: {
'x-uid': 'p2',
properties: {
p21: {
'x-uid': 'p21',
},
},
},
},
};
await repository.insert(schema);
await repository.insertAdjacent(
'afterEnd',
'p2',
{
name: 'p311',
'x-uid': 'p311',
},
{
wrap: {
'x-uid': 'p3',
name: 'p3',
properties: {
p31: {
'x-uid': 'p31',
},
},
},
},
);
const root = await repository.getJsonSchema('root');
expect(root).toEqual({
properties: {
p1: {
properties: {
p11: {
'x-uid': 'p11',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'p1',
'x-async': false,
'x-index': 1,
},
p2: {
properties: {
p21: {
'x-uid': 'p21',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'p2',
'x-async': false,
'x-index': 2,
},
p3: {
properties: {
p31: {
properties: {
p311: {
'x-uid': 'p311',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'p31',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'p3',
'x-async': false,
'x-index': 3,
},
},
name: 'root-name',
'x-uid': 'root',
'x-async': false,
});
});
it('should works with wrap', async () => {
const schema = {
name: 'root-name',
'x-uid': 'root',
properties: {
p1: {
'x-uid': 'p1',
properties: {
p11: {
'x-uid': 'p11',
},
},
},
p2: {
'x-uid': 'p2',
properties: {
p21: {
'x-uid': 'p21',
},
},
},
},
};
await repository.insert(schema);
await repository.insertAdjacent(
'afterEnd',
'p1',
{
'x-uid': 'p21',
},
{
removeParentsIfNoChildren: true,
breakRemoveOn: {
'x-uid': 'root',
},
wrap: {
'x-uid': 'p3',
name: 'p3',
properties: {
p31: {
'x-uid': 'p31',
},
},
},
},
);
const root = await repository.getJsonSchema('root');
expect(root).toEqual({
properties: {
p1: {
properties: {
p11: {
'x-uid': 'p11',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'p1',
'x-async': false,
'x-index': 1,
},
p3: {
properties: {
p31: {
properties: {
p21: {
'x-uid': 'p21',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'p31',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'p3',
'x-async': false,
'x-index': 2,
},
},
name: 'root-name',
'x-uid': 'root',
'x-async': false,
});
});
it('should insert newSchema using insertNewSchema', async () => {
const schema = {
name: 'root-name',
'x-uid': 'root',
properties: {
p1: {
'x-uid': 'p1',
properties: {
p11: {
'x-uid': 'p11',
},
},
},
p2: {
'x-uid': 'p2',
properties: {
p21: {
'x-uid': 'p21',
},
},
},
},
};
await repository.insert(schema);
await repository.insertAdjacent('afterEnd', 'p2', {
'x-uid': 'p3',
name: 'p3',
properties: {
p31: {
'x-uid': 'p31',
},
},
});
const root = await repository.getJsonSchema('root');
expect(root).toEqual({
properties: {
p1: {
properties: {
p11: {
'x-uid': 'p11',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'p1',
'x-async': false,
'x-index': 1,
},
p2: {
properties: {
p21: {
'x-uid': 'p21',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'p2',
'x-async': false,
'x-index': 2,
},
p3: {
properties: {
p31: {
'x-uid': 'p31',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'p3',
'x-async': false,
'x-index': 3,
},
},
name: 'root-name',
'x-uid': 'root',
'x-async': false,
});
});
it('should insert oldSchema using first x-uid', async () => {
const schema = {
name: 'root-name',
'x-uid': 'root',
properties: {
p1: {
'x-uid': 'p1',
properties: {
p11: {
'x-uid': 'p11',
},
},
},
p2: {
'x-uid': 'p2',
properties: {
p21: {
'x-uid': 'p21',
},
},
},
},
};
await repository.insert(schema);
const insertSchema = {
'x-uid': 'p3',
name: 'p3',
properties: {
p31: {
'x-uid': 'p31',
},
},
};
await repository.insert(insertSchema);
await repository.insertAdjacent('afterEnd', 'p2', insertSchema);
const root = await repository.getJsonSchema('root');
expect(root).toEqual({
properties: {
p1: {
properties: {
p11: {
'x-uid': 'p11',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'p1',
'x-async': false,
'x-index': 1,
},
p2: {
properties: {
p21: {
'x-uid': 'p21',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'p2',
'x-async': false,
'x-index': 2,
},
p3: {
properties: {
p31: {
'x-uid': 'p31',
'x-async': false,
'x-index': 1,
},
},
'x-uid': 'p3',
'x-async': false,
'x-index': 3,
},
},
name: 'root-name',
'x-uid': 'root',
'x-async': false,
});
});
});
}); });

View File

@ -1,6 +1,6 @@
import { Context } from '@nocobase/actions'; import { Context } from '@nocobase/actions';
import UiSchemaRepository from '../repository';
import lodash from 'lodash'; import lodash from 'lodash';
import UiSchemaRepository from '../repository';
const getRepositoryFromCtx = (ctx: Context) => { const getRepositoryFromCtx = (ctx: Context) => {
return ctx.db.getCollection('uiSchemas').repository as UiSchemaRepository; return ctx.db.getCollection('uiSchemas').repository as UiSchemaRepository;
@ -21,10 +21,19 @@ const callRepositoryMethod = (method, paramsKey: 'resourceIndex' | 'values') =>
}; };
}; };
function parseInsertAdjacentValues(values) {
if (lodash.has(values, 'schema')) {
return values;
}
return { schema: values, wrap: null };
}
export const uiSchemaActions = { export const uiSchemaActions = {
getJsonSchema: callRepositoryMethod('getJsonSchema', 'resourceIndex'), getJsonSchema: callRepositoryMethod('getJsonSchema', 'resourceIndex'),
getProperties: callRepositoryMethod('getProperties', 'resourceIndex'), getProperties: callRepositoryMethod('getProperties', 'resourceIndex'),
insert: callRepositoryMethod('insert', 'values'), insert: callRepositoryMethod('insert', 'values'),
insertNewSchema: callRepositoryMethod('insertNewSchema', 'values'),
remove: callRepositoryMethod('remove', 'resourceIndex'), remove: callRepositoryMethod('remove', 'resourceIndex'),
patch: callRepositoryMethod('patch', 'values'), patch: callRepositoryMethod('patch', 'values'),
clearAncestor: callRepositoryMethod('clearAncestor', 'resourceIndex'), clearAncestor: callRepositoryMethod('clearAncestor', 'resourceIndex'),
@ -33,9 +42,12 @@ export const uiSchemaActions = {
const { resourceIndex, position, values, removeParentsIfNoChildren, breakRemoveOn } = ctx.action.params; const { resourceIndex, position, values, removeParentsIfNoChildren, breakRemoveOn } = ctx.action.params;
const repository = getRepositoryFromCtx(ctx); const repository = getRepositoryFromCtx(ctx);
ctx.body = await repository.insertAdjacent(position, resourceIndex, values, { const { schema, wrap } = parseInsertAdjacentValues(values);
ctx.body = await repository.insertAdjacent(position, resourceIndex, schema, {
removeParentsIfNoChildren, removeParentsIfNoChildren,
breakRemoveOn, breakRemoveOn,
wrap,
}); });
await next(); await next();
@ -51,9 +63,12 @@ function insertPositionActionBuilder(position: 'beforeBegin' | 'afterBegin' | 'b
return async function (ctx: Context, next) { return async function (ctx: Context, next) {
const { resourceIndex, values, removeParentsIfNoChildren, breakRemoveOn } = ctx.action.params; const { resourceIndex, values, removeParentsIfNoChildren, breakRemoveOn } = ctx.action.params;
const repository = getRepositoryFromCtx(ctx); const repository = getRepositoryFromCtx(ctx);
ctx.body = await repository.insertAdjacent(position, resourceIndex, values, { const { schema, wrap } = parseInsertAdjacentValues(values);
ctx.body = await repository.insertAdjacent(position, resourceIndex, schema, {
removeParentsIfNoChildren, removeParentsIfNoChildren,
breakRemoveOn, breakRemoveOn,
wrap,
}); });
await next(); await next();
}; };

View File

@ -4,8 +4,10 @@ export interface TargetPosition {
} }
export interface ChildOptions { export interface ChildOptions {
parentUid: string; parentUid: string;
parentPath?: string[];
type: string; type: string;
position?: 'first' | 'last' | TargetPosition; position?: 'first' | 'last' | TargetPosition;
sort?: number;
} }
export interface SchemaNode { export interface SchemaNode {

View File

@ -18,7 +18,9 @@ export interface removeParentOptions extends TransactionAble {
breakRemoveOn?: BreakRemoveOnType; breakRemoveOn?: BreakRemoveOnType;
} }
interface InsertAdjacentOptions extends removeParentOptions {} interface InsertAdjacentOptions extends removeParentOptions {
wrap?: any;
}
const nodeKeys = ['properties', 'definitions', 'patternProperties', 'additionalProperties', 'items']; const nodeKeys = ['properties', 'definitions', 'patternProperties', 'additionalProperties', 'items'];
@ -111,31 +113,30 @@ export class UiSchemaRepository extends Repository {
for (const nodeKey of nodeKeys) { for (const nodeKey of nodeKeys) {
const nodeProperty = lodash.get(node, nodeKey); const nodeProperty = lodash.get(node, nodeKey);
const childNodeChildOptions = {
parentUid: node['x-uid'],
parentPath: [node['x-uid'], ...lodash.get(childOptions, 'parentPath', [])],
type: nodeKey,
};
// array items // array items
if (nodeKey === 'items' && nodeProperty) { if (nodeKey === 'items' && nodeProperty) {
const handleItems = lodash.isArray(nodeProperty) ? nodeProperty : [nodeProperty]; const handleItems = lodash.isArray(nodeProperty) ? nodeProperty : [nodeProperty];
for (const item of handleItems) { for (const [i, item] of handleItems.entries()) {
carry = this.schemaToSingleNodes(item, carry, { carry = this.schemaToSingleNodes(item, carry, { ...childNodeChildOptions, sort: i + 1 });
parentUid: node['x-uid'],
type: nodeKey,
});
} }
} else if (lodash.isPlainObject(nodeProperty)) { } else if (lodash.isPlainObject(nodeProperty)) {
const subNodeNames = lodash.keys(lodash.get(node, nodeKey)); const subNodeNames = lodash.keys(lodash.get(node, nodeKey));
delete node[nodeKey]; delete node[nodeKey];
for (const subNodeName of subNodeNames) { for (const [i, subNodeName] of subNodeNames.entries()) {
const subSchema = { const subSchema = {
name: subNodeName, name: subNodeName,
...lodash.get(nodeProperty, subNodeName), ...lodash.get(nodeProperty, subNodeName),
}; };
carry = this.schemaToSingleNodes(subSchema, carry, { carry = this.schemaToSingleNodes(subSchema, carry, { ...childNodeChildOptions, sort: i + 1 });
parentUid: node['x-uid'],
type: nodeKey,
});
} }
} }
} }
@ -538,11 +539,32 @@ export class UiSchemaRepository extends Repository {
}; };
const insertedNodes = await this.insertNodes(nodes, options); const insertedNodes = await this.insertNodes(nodes, options);
return await this.getJsonSchema(insertedNodes[0].get('x-uid'), { return await this.getJsonSchema(insertedNodes[0].get('x-uid'), {
transaction, transaction,
}); });
} }
private async schemaExists(schema: any, options?: TransactionAble): Promise<boolean> {
if (lodash.isObject(schema) && !schema['x-uid']) {
return false;
}
const { transaction } = options;
const result = await this.database.sequelize.query(
this.sqlAdapter(`select "x-uid" from ${this.uiSchemasTableName} where "x-uid" = :uid`),
{
type: 'SELECT',
replacements: {
uid: lodash.isString(schema) ? schema : schema['x-uid'],
},
transaction,
},
);
return result.length > 0;
}
@transaction() @transaction()
async insertAdjacent( async insertAdjacent(
position: 'beforeBegin' | 'afterBegin' | 'beforeEnd' | 'afterEnd', position: 'beforeBegin' | 'afterBegin' | 'beforeEnd' | 'afterEnd',
@ -550,26 +572,57 @@ export class UiSchemaRepository extends Repository {
schema: any, schema: any,
options?: InsertAdjacentOptions, options?: InsertAdjacentOptions,
) { ) {
const { transaction } = options;
if (options.wrap) {
// insert wrap schema using insertNewSchema
const wrapSchemaNodes = await this.insertNewSchema(options.wrap, {
transaction,
returnNode: true,
});
const lastWrapNode = wrapSchemaNodes[wrapSchemaNodes.length - 1];
// insert schema into wrap schema
await this.insertAdjacent('afterBegin', lastWrapNode['x-uid'], schema, lodash.omit(options, 'wrap'));
schema = wrapSchemaNodes[0]['x-uid'];
options.removeParentsIfNoChildren = false;
} else {
const schemaExists = await this.schemaExists(schema, { transaction });
if (schemaExists) {
schema = lodash.isString(schema) ? schema : schema['x-uid'];
} else {
const insertedSchema = await this.insertNewSchema(schema, {
transaction,
returnNode: true,
});
schema = insertedSchema[0]['x-uid'];
}
}
return await this[`insert${lodash.upperFirst(position)}`](target, schema, options); return await this[`insert${lodash.upperFirst(position)}`](target, schema, options);
} }
@transaction() @transaction()
async insertAfterBegin(targetUid: string, schema: any, options?: InsertAdjacentOptions) { protected async insertAfterBegin(targetUid: string, schema: any, options?: InsertAdjacentOptions) {
return await this.insertInner(targetUid, schema, 'first', options); return await this.insertInner(targetUid, schema, 'first', options);
} }
@transaction() @transaction()
async insertBeforeEnd(targetUid: string, schema: any, options?: InsertAdjacentOptions) { protected async insertBeforeEnd(targetUid: string, schema: any, options?: InsertAdjacentOptions) {
return await this.insertInner(targetUid, schema, 'last', options); return await this.insertInner(targetUid, schema, 'last', options);
} }
@transaction() @transaction()
async insertBeforeBegin(targetUid: string, schema: any, options?: InsertAdjacentOptions) { protected async insertBeforeBegin(targetUid: string, schema: any, options?: InsertAdjacentOptions) {
return await this.insertBeside(targetUid, schema, 'before', options); return await this.insertBeside(targetUid, schema, 'before', options);
} }
@transaction() @transaction()
async insertAfterEnd(targetUid: string, schema: any, options?: InsertAdjacentOptions) { protected async insertAfterEnd(targetUid: string, schema: any, options?: InsertAdjacentOptions) {
return await this.insertBeside(targetUid, schema, 'after', options); return await this.insertBeside(targetUid, schema, 'after', options);
} }
@ -600,6 +653,70 @@ export class UiSchemaRepository extends Repository {
}); });
} }
@transaction()
async insertNewSchema(
schema: any,
options?: TransactionAble & {
returnNode?: boolean;
},
) {
const { transaction } = options;
const nodes = UiSchemaRepository.schemaToSingleNodes(schema);
// insert schema fist
await this.database.sequelize.query(
this.sqlAdapter(
`INSERT INTO ${this.uiSchemasTableName} ("x-uid", "name", "schema") VALUES ${nodes
.map((n) => '(?)')
.join(',')};`,
),
{
replacements: lodash.cloneDeep(nodes).map((node) => {
const { uid, name } = this.prepareSingleNodeForInsert(node);
return [uid, name, JSON.stringify(node)];
}),
type: 'insert',
transaction,
},
);
const treePathData: Array<any> = lodash.cloneDeep(nodes).reduce((carry, item) => {
const { uid, childOptions, async } = this.prepareSingleNodeForInsert(item);
return [
...carry,
// self reference
[uid, uid, 0, childOptions?.type || null, async, null],
// parent references
...lodash.get(childOptions, 'parentPath', []).map((parentUid, index) => {
return [parentUid, uid, index + 1, null, null, childOptions.sort];
}),
];
}, []);
// insert tree path
await this.database.sequelize.query(
this.sqlAdapter(
`INSERT INTO ${
this.uiSchemaTreePathTableName
} (ancestor, descendant, depth, type, async, sort) VALUES ${treePathData.map((item) => '(?)').join(',')}`,
),
{
replacements: treePathData,
type: 'insert',
transaction,
},
);
if (options?.returnNode) {
return nodes;
}
return this.getJsonSchema(nodes[0]['x-uid'], {
transaction,
});
}
private async insertSchemaRecord(name, uid, schema, transaction) { private async insertSchemaRecord(name, uid, schema, transaction) {
const serverHooks = schema['x-server-hooks'] || []; const serverHooks = schema['x-server-hooks'] || [];
@ -619,11 +736,7 @@ export class UiSchemaRepository extends Repository {
return node; return node;
} }
async insertSingleNode(schema: SchemaNode, options: TransactionAble & removeParentOptions) { private prepareSingleNodeForInsert(schema: SchemaNode) {
const { transaction } = options;
const db = this.database;
const uid = schema['x-uid']; const uid = schema['x-uid'];
const name = schema['name']; const name = schema['name'];
const async = lodash.get(schema, 'x-async', false); const async = lodash.get(schema, 'x-async', false);
@ -634,6 +747,15 @@ export class UiSchemaRepository extends Repository {
delete schema['name']; delete schema['name'];
delete schema['childOptions']; delete schema['childOptions'];
return { uid, name, async, childOptions };
}
async insertSingleNode(schema: SchemaNode, options: TransactionAble & removeParentOptions) {
const { transaction } = options;
const db = this.database;
const { uid, name, async, childOptions } = this.prepareSingleNodeForInsert(schema);
let savedNode; let savedNode;
// check node exists or not // check node exists or not
@ -656,18 +778,9 @@ export class UiSchemaRepository extends Repository {
const oldParentUid = await this.findParentUid(uid, transaction); const oldParentUid = await this.findParentUid(uid, transaction);
const parentUid = childOptions.parentUid; const parentUid = childOptions.parentUid;
const isTreeQuery = await db.sequelize.query( const childrenCount = await this.childrenCount(uid, transaction);
`SELECT COUNT(*) as childrenCount from ${treeTable} WHERE ancestor = :ancestor AND descendant != ancestor`,
{
type: 'SELECT',
replacements: {
ancestor: uid,
},
transaction,
},
);
const isTree = isTreeQuery[0]['childrenCount']; const isTree = childrenCount > 0;
// if node is a tree root move tree to new path // if node is a tree root move tree to new path
if (isTree) { if (isTree) {
@ -692,6 +805,19 @@ export class UiSchemaRepository extends Repository {
); );
} }
// update type
await db.sequelize.query(
`UPDATE ${treeTable} SET type = :type WHERE depth = 0 AND ancestor = :uid AND descendant = :uid`,
{
type: 'update',
transaction,
replacements: {
type: childOptions.type,
uid,
},
},
);
if (!isTree) { if (!isTree) {
if (existsNode) { if (existsNode) {
// remove old path // remove old path