2022-02-10 13:58:26 +08:00
|
|
|
import { Repository, TransactionAble } from '@nocobase/database';
|
2022-01-19 10:09:30 +08:00
|
|
|
import { uid } from '@nocobase/utils';
|
2022-02-11 18:13:14 +08:00
|
|
|
import lodash from 'lodash';
|
2022-01-19 10:09:30 +08:00
|
|
|
import { Transaction } from 'sequelize';
|
2022-02-11 18:13:14 +08:00
|
|
|
import { ChildOptions, SchemaNode, TargetPosition } from './dao/ui_schema_node_dao';
|
2022-01-19 10:09:30 +08:00
|
|
|
|
|
|
|
interface GetJsonSchemaOptions {
|
|
|
|
includeAsyncNode?: boolean;
|
|
|
|
transaction?: Transaction;
|
|
|
|
}
|
|
|
|
|
2022-02-18 12:29:03 +08:00
|
|
|
type BreakRemoveOnType = {
|
|
|
|
[key: string]: any;
|
|
|
|
};
|
|
|
|
|
|
|
|
export interface removeParentOptions {
|
|
|
|
removeParentsIfNoChildren?: boolean;
|
|
|
|
breakRemoveOn?: BreakRemoveOnType;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface InsertAdjacentOptions extends removeParentOptions {}
|
|
|
|
|
2022-01-19 10:09:30 +08:00
|
|
|
const nodeKeys = ['properties', 'definitions', 'patternProperties', 'additionalProperties', 'items'];
|
|
|
|
|
2022-02-11 18:13:14 +08:00
|
|
|
export class UiSchemaRepository extends Repository {
|
2022-02-20 20:01:07 +08:00
|
|
|
tableNameAdapter(tableName) {
|
2022-02-16 00:22:47 +08:00
|
|
|
if (this.database.sequelize.getDialect() === 'postgres') {
|
2022-02-20 20:01:07 +08:00
|
|
|
return `"${tableName}"`;
|
2022-02-16 00:22:47 +08:00
|
|
|
}
|
2022-02-20 20:01:07 +08:00
|
|
|
return tableName;
|
|
|
|
}
|
|
|
|
|
|
|
|
get uiSchemasTableName() {
|
|
|
|
return this.tableNameAdapter(this.model.tableName);
|
2022-02-16 00:22:47 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
get uiSchemaTreePathTableName() {
|
2022-02-18 12:29:03 +08:00
|
|
|
const model = this.database.getCollection('uiSchemaTreePath').model;
|
2022-02-20 20:01:07 +08:00
|
|
|
return this.tableNameAdapter(model.tableName);
|
2022-02-16 00:22:47 +08:00
|
|
|
}
|
|
|
|
|
2022-02-20 01:23:04 +08:00
|
|
|
sqlAdapter(sql: string) {
|
|
|
|
if (this.database.sequelize.getDialect() === 'mysql') {
|
|
|
|
return lodash.replace(sql, /"/g, '`');
|
|
|
|
}
|
|
|
|
|
|
|
|
return sql;
|
|
|
|
}
|
|
|
|
|
2022-01-19 10:09:30 +08:00
|
|
|
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 rawSql = `
|
2022-02-20 01:23:04 +08:00
|
|
|
SELECT "SchemaTable"."x-uid" as "x-uid", "SchemaTable"."name" as "name", "SchemaTable"."schema" as "schema",
|
2022-01-19 10:09:30 +08:00
|
|
|
TreePath.depth as depth,
|
2022-02-11 12:55:30 +08:00
|
|
|
NodeInfo.type as type, NodeInfo.async as async, ParentPath.ancestor as parent, ParentPath.sort as sort
|
2022-02-16 00:22:47 +08:00
|
|
|
FROM ${this.uiSchemaTreePathTableName} as TreePath
|
2022-02-20 01:23:04 +08:00
|
|
|
LEFT JOIN ${this.uiSchemasTableName} as "SchemaTable" ON "SchemaTable"."x-uid" = TreePath.descendant
|
|
|
|
LEFT JOIN ${this.uiSchemaTreePathTableName} as NodeInfo ON NodeInfo.descendant = "SchemaTable"."x-uid" and NodeInfo.descendant = NodeInfo.ancestor and NodeInfo.depth = 0
|
|
|
|
LEFT JOIN ${this.uiSchemaTreePathTableName} as ParentPath ON (ParentPath.descendant = "SchemaTable"."x-uid" AND ParentPath.depth = 1)
|
2022-01-19 10:09:30 +08:00
|
|
|
WHERE TreePath.ancestor = :ancestor AND (NodeInfo.async = false or TreePath.depth = 1)`;
|
|
|
|
|
2022-02-20 01:23:04 +08:00
|
|
|
const nodes = await db.sequelize.query(this.sqlAdapter(rawSql), {
|
2022-01-19 10:09:30 +08:00
|
|
|
replacements: {
|
|
|
|
ancestor: uid,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
2022-02-13 10:36:25 +08:00
|
|
|
if (nodes[0].length == 0) {
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
2022-01-19 10:09:30 +08:00
|
|
|
const schema = this.nodesToSchema(nodes[0], uid);
|
|
|
|
return lodash.pick(schema, ['type', 'properties']);
|
|
|
|
}
|
|
|
|
|
2022-02-20 20:01:07 +08:00
|
|
|
async getJsonSchema(uid: string, options?: GetJsonSchemaOptions): Promise<any> {
|
2022-01-19 10:09:30 +08:00
|
|
|
const db = this.database;
|
|
|
|
|
2022-02-16 00:22:47 +08:00
|
|
|
const treeTable = this.uiSchemaTreePathTableName;
|
2022-01-19 10:09:30 +08:00
|
|
|
|
|
|
|
const rawSql = `
|
2022-02-20 01:23:04 +08:00
|
|
|
SELECT "SchemaTable"."x-uid" as "x-uid", "SchemaTable"."name" as name, "SchemaTable"."schema" as "schema" ,
|
2022-01-19 10:09:30 +08:00
|
|
|
TreePath.depth as depth,
|
|
|
|
NodeInfo.type as type, NodeInfo.async as async, ParentPath.ancestor as parent, ParentPath.sort as sort
|
|
|
|
FROM ${treeTable} as TreePath
|
2022-02-20 01:23:04 +08:00
|
|
|
LEFT JOIN ${this.uiSchemasTableName} as "SchemaTable" ON "SchemaTable"."x-uid" = TreePath.descendant
|
|
|
|
LEFT JOIN ${treeTable} as NodeInfo ON NodeInfo.descendant = "SchemaTable"."x-uid" and NodeInfo.descendant = NodeInfo.ancestor and NodeInfo.depth = 0
|
|
|
|
LEFT JOIN ${treeTable} as ParentPath ON (ParentPath.descendant = "SchemaTable"."x-uid" AND ParentPath.depth = 1)
|
2022-01-19 10:09:30 +08:00
|
|
|
WHERE TreePath.ancestor = :ancestor ${options?.includeAsyncNode ? '' : 'AND (NodeInfo.async != true )'}
|
|
|
|
`;
|
|
|
|
|
2022-02-20 01:23:04 +08:00
|
|
|
const nodes = await db.sequelize.query(this.sqlAdapter(rawSql), {
|
2022-01-19 10:09:30 +08:00
|
|
|
replacements: {
|
|
|
|
ancestor: uid,
|
|
|
|
},
|
|
|
|
transaction: options?.transaction,
|
|
|
|
});
|
|
|
|
|
2022-02-13 10:36:25 +08:00
|
|
|
if (nodes[0].length == 0) {
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
2022-01-19 10:09:30 +08:00
|
|
|
const schema = this.nodesToSchema(nodes[0], uid);
|
|
|
|
return schema;
|
|
|
|
}
|
|
|
|
|
2022-02-20 20:01:07 +08:00
|
|
|
private ignoreSchemaProperties(schemaProperties) {
|
|
|
|
return lodash.omit(schemaProperties, nodeKeys);
|
|
|
|
}
|
|
|
|
|
2022-01-19 10:09:30 +08:00
|
|
|
nodesToSchema(nodes, rootUid) {
|
|
|
|
const nodeAttributeSanitize = (node) => {
|
|
|
|
const schema = {
|
2022-02-20 20:01:07 +08:00
|
|
|
...this.ignoreSchemaProperties(lodash.isPlainObject(node.schema) ? node.schema : JSON.parse(node.schema)),
|
2022-01-19 10:09:30 +08:00
|
|
|
...lodash.pick(node, [...nodeKeys, 'name']),
|
2022-02-20 01:23:04 +08:00
|
|
|
['x-uid']: node['x-uid'],
|
2022-01-19 10:09:30 +08:00
|
|
|
['x-async']: !!node.async,
|
|
|
|
};
|
|
|
|
|
2022-02-13 09:38:41 +08:00
|
|
|
if (lodash.isNumber(node.sort)) {
|
|
|
|
schema['x-index'] = node.sort;
|
|
|
|
}
|
|
|
|
|
2022-01-19 10:09:30 +08:00
|
|
|
return schema;
|
|
|
|
};
|
|
|
|
|
|
|
|
const buildTree = (rootNode) => {
|
2022-02-20 01:23:04 +08:00
|
|
|
const children = nodes.filter((node) => node.parent == rootNode['x-uid']);
|
2022-01-19 10:09:30 +08:00
|
|
|
|
|
|
|
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);
|
|
|
|
};
|
|
|
|
|
2022-02-20 01:23:04 +08:00
|
|
|
return buildTree(nodes.find((node) => node['x-uid'] == rootUid));
|
2022-01-19 10:09:30 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
treeCollection() {
|
2022-02-18 12:29:03 +08:00
|
|
|
return this.database.getCollection('uiSchemaTreePath');
|
2022-01-19 10:09:30 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
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: {
|
2022-02-20 01:23:04 +08:00
|
|
|
'x-uid': uid,
|
2022-01-19 10:09:30 +08:00
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
await nodeModel.update(
|
|
|
|
{
|
|
|
|
schema: {
|
|
|
|
...(nodeModel.get('schema') as any),
|
|
|
|
...lodash.omit(schema, ['x-async', 'name', 'x-uid', 'properties']),
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
hooks: false,
|
|
|
|
transaction,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-02-18 12:29:03 +08:00
|
|
|
protected async childrenCount(uid, transaction) {
|
2022-02-10 20:28:27 +08:00
|
|
|
const db = this.database;
|
|
|
|
|
2022-02-18 12:29:03 +08:00
|
|
|
const countResult = await db.sequelize.query(
|
|
|
|
`SELECT COUNT(*) as count FROM ${this.uiSchemaTreePathTableName} where ancestor = :ancestor and depth = 1`,
|
|
|
|
{
|
|
|
|
replacements: {
|
|
|
|
ancestor: uid,
|
|
|
|
},
|
|
|
|
type: 'SELECT',
|
|
|
|
transaction,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
return parseInt(countResult[0]['count']);
|
|
|
|
}
|
|
|
|
|
|
|
|
protected async isLeafNode(uid, transaction) {
|
|
|
|
const childrenCount = await this.childrenCount(uid, transaction);
|
|
|
|
return childrenCount === 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
async findParentUid(uid, transaction?) {
|
|
|
|
const parent = await this.database.getRepository('uiSchemaTreePath').findOne({
|
2022-02-10 20:28:27 +08:00
|
|
|
filter: {
|
|
|
|
descendant: uid,
|
|
|
|
depth: 1,
|
|
|
|
},
|
2022-02-15 22:32:02 +08:00
|
|
|
transaction,
|
2022-02-10 20:28:27 +08:00
|
|
|
});
|
|
|
|
|
2022-02-18 12:29:03 +08:00
|
|
|
return parent ? (parent.get('ancestor') as string) : null;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected async findNodeSchemaWithParent(uid, transaction) {
|
|
|
|
const schema = await this.database.getRepository('uiSchemas').findOne({
|
|
|
|
filter: {
|
2022-02-20 01:23:04 +08:00
|
|
|
'x-uid': uid,
|
2022-02-18 12:29:03 +08:00
|
|
|
},
|
|
|
|
transaction,
|
|
|
|
});
|
|
|
|
|
|
|
|
return {
|
|
|
|
parentUid: await this.findParentUid(uid, transaction),
|
|
|
|
schema,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
protected async isSingleChild(uid, transaction) {
|
|
|
|
const db = this.database;
|
|
|
|
|
|
|
|
const parent = await this.findParentUid(uid, transaction);
|
|
|
|
|
2022-02-10 21:06:22 +08:00
|
|
|
if (!parent) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2022-02-18 12:29:03 +08:00
|
|
|
const parentChildrenCount = await this.childrenCount(parent, transaction);
|
2022-02-10 20:28:27 +08:00
|
|
|
|
|
|
|
if (parentChildrenCount == 1) {
|
2022-02-16 00:22:47 +08:00
|
|
|
const schema = await db.getRepository('uiSchemas').findOne({
|
2022-02-10 21:06:22 +08:00
|
|
|
filter: {
|
2022-02-20 01:23:04 +08:00
|
|
|
'x-uid': parent,
|
2022-02-10 21:06:22 +08:00
|
|
|
},
|
2022-02-15 22:32:02 +08:00
|
|
|
transaction,
|
2022-02-10 21:06:22 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
return schema;
|
2022-02-10 20:28:27 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2022-02-18 12:29:03 +08:00
|
|
|
async removeEmptyParents(options: TransactionAble & { uid: string; breakRemoveOn?: BreakRemoveOnType }) {
|
|
|
|
const { transaction, uid, breakRemoveOn } = options;
|
2022-02-10 20:28:27 +08:00
|
|
|
|
|
|
|
const removeParent = async (nodeUid: string) => {
|
2022-02-10 21:06:22 +08:00
|
|
|
const parent = await this.isSingleChild(nodeUid, transaction);
|
|
|
|
|
2022-02-18 12:29:03 +08:00
|
|
|
if (parent && !this.breakOnMatched(parent, breakRemoveOn)) {
|
2022-02-20 01:23:04 +08:00
|
|
|
await removeParent(parent.get('x-uid') as string);
|
2022-02-10 20:28:27 +08:00
|
|
|
} else {
|
|
|
|
await this.remove(nodeUid, { transaction });
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
await removeParent(uid);
|
|
|
|
}
|
|
|
|
|
2022-02-18 12:29:03 +08:00
|
|
|
private breakOnMatched(schemaInstance, breakRemoveOn: BreakRemoveOnType): boolean {
|
|
|
|
if (!breakRemoveOn) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const key of Object.keys(breakRemoveOn)) {
|
|
|
|
const instanceValue = schemaInstance.get(key);
|
|
|
|
const breakRemoveOnValue = breakRemoveOn[key];
|
|
|
|
if (instanceValue !== breakRemoveOnValue) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
async recursivelyRemoveIfNoChildren(options: TransactionAble & { uid: string; breakRemoveOn?: BreakRemoveOnType }) {
|
|
|
|
const { uid, transaction, breakRemoveOn } = options;
|
|
|
|
|
|
|
|
const removeLeafNode = async (nodeUid: string) => {
|
|
|
|
const isLeafNode = await this.isLeafNode(nodeUid, transaction);
|
|
|
|
|
|
|
|
if (isLeafNode) {
|
|
|
|
const { parentUid, schema } = await this.findNodeSchemaWithParent(nodeUid, transaction);
|
|
|
|
|
|
|
|
if (this.breakOnMatched(schema, breakRemoveOn)) {
|
|
|
|
// break at here
|
|
|
|
return;
|
|
|
|
} else {
|
|
|
|
// remove current node
|
|
|
|
await this.remove(nodeUid, {
|
|
|
|
transaction,
|
|
|
|
});
|
|
|
|
// continue remove
|
|
|
|
await removeLeafNode(parentUid);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
await removeLeafNode(uid);
|
|
|
|
}
|
|
|
|
|
|
|
|
async remove(uid: string, options?: TransactionAble & removeParentOptions) {
|
2022-01-19 10:09:30 +08:00
|
|
|
let handleTransaction: boolean = true;
|
|
|
|
let transaction;
|
|
|
|
|
|
|
|
if (options?.transaction) {
|
|
|
|
transaction = options.transaction;
|
|
|
|
handleTransaction = false;
|
|
|
|
} else {
|
|
|
|
transaction = await this.database.sequelize.transaction();
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
2022-02-18 12:29:03 +08:00
|
|
|
if (options?.removeParentsIfNoChildren) {
|
|
|
|
await this.removeEmptyParents({ transaction, uid, breakRemoveOn: options.breakRemoveOn });
|
|
|
|
if (handleTransaction) {
|
|
|
|
await transaction.commit();
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-01-19 10:09:30 +08:00
|
|
|
await this.database.sequelize.query(
|
2022-02-20 20:01:07 +08:00
|
|
|
this.sqlAdapter(`DELETE FROM ${this.uiSchemasTableName} WHERE "x-uid" IN (
|
|
|
|
SELECT descendant FROM ${this.uiSchemaTreePathTableName} WHERE ancestor = :uid
|
|
|
|
)`),
|
2022-01-19 10:09:30 +08:00
|
|
|
{
|
|
|
|
replacements: {
|
|
|
|
uid,
|
|
|
|
},
|
|
|
|
transaction,
|
|
|
|
},
|
|
|
|
);
|
2022-02-20 20:01:07 +08:00
|
|
|
|
2022-01-19 10:09:30 +08:00
|
|
|
await this.database.sequelize.query(
|
|
|
|
`
|
2022-02-20 20:01:07 +08:00
|
|
|
DELETE FROM ${this.uiSchemaTreePathTableName}
|
2022-01-19 10:09:30 +08:00
|
|
|
WHERE descendant IN (
|
|
|
|
select descendant FROM
|
|
|
|
(SELECT descendant
|
2022-02-20 20:01:07 +08:00
|
|
|
FROM ${this.uiSchemaTreePathTableName}
|
2022-01-19 10:09:30 +08:00
|
|
|
WHERE ancestor = :uid)as descendantTable) `,
|
|
|
|
{
|
|
|
|
replacements: {
|
|
|
|
uid,
|
|
|
|
},
|
|
|
|
transaction,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
if (handleTransaction) {
|
|
|
|
await transaction.commit();
|
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
if (handleTransaction) {
|
|
|
|
await transaction.rollback();
|
|
|
|
}
|
|
|
|
throw err;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-18 12:29:03 +08:00
|
|
|
async insertBeside(targetUid: string, schema: any, side: 'before' | 'after', options?: InsertAdjacentOptions) {
|
|
|
|
const targetParent = await this.findParentUid(targetUid);
|
2022-01-19 10:09:30 +08:00
|
|
|
|
|
|
|
const db = this.database;
|
2022-02-18 12:29:03 +08:00
|
|
|
|
2022-02-16 00:22:47 +08:00
|
|
|
const treeTable = this.uiSchemaTreePathTableName;
|
2022-02-18 12:29:03 +08:00
|
|
|
|
2022-01-19 10:09:30 +08:00
|
|
|
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 = {
|
2022-02-18 12:29:03 +08:00
|
|
|
parentUid: targetParent,
|
2022-01-19 10:09:30 +08:00
|
|
|
type: typeQuery[0]['type'],
|
|
|
|
position: {
|
|
|
|
type: side,
|
|
|
|
target: targetUid,
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
2022-02-18 12:29:03 +08:00
|
|
|
const insertedNodes = await this.insertNodes(nodes, options);
|
2022-02-20 01:23:04 +08:00
|
|
|
return await this.getJsonSchema(insertedNodes[0].get('x-uid'));
|
2022-01-19 10:09:30 +08:00
|
|
|
}
|
|
|
|
|
2022-02-18 12:29:03 +08:00
|
|
|
async insertInner(targetUid: string, schema: any, position: 'first' | 'last', options?: InsertAdjacentOptions) {
|
2022-01-19 10:09:30 +08:00
|
|
|
const nodes = UiSchemaRepository.schemaToSingleNodes(schema);
|
|
|
|
const rootNode = nodes[0];
|
2022-02-18 12:29:03 +08:00
|
|
|
|
2022-01-19 10:09:30 +08:00
|
|
|
rootNode.childOptions = {
|
|
|
|
parentUid: targetUid,
|
|
|
|
type: lodash.get(schema, 'x-node-type', 'properties'),
|
|
|
|
position,
|
|
|
|
};
|
|
|
|
|
2022-02-18 12:29:03 +08:00
|
|
|
const insertedNodes = await this.insertNodes(nodes, options);
|
2022-02-20 01:23:04 +08:00
|
|
|
return await this.getJsonSchema(insertedNodes[0].get('x-uid'));
|
2022-01-19 10:09:30 +08:00
|
|
|
}
|
|
|
|
|
2022-02-18 12:29:03 +08:00
|
|
|
async insertAdjacent(
|
|
|
|
position: 'beforeBegin' | 'afterBegin' | 'beforeEnd' | 'afterEnd',
|
|
|
|
target: string,
|
|
|
|
schema: any,
|
|
|
|
options?: InsertAdjacentOptions,
|
|
|
|
) {
|
|
|
|
return await this[`insert${lodash.upperFirst(position)}`](target, schema, options);
|
2022-01-19 10:09:30 +08:00
|
|
|
}
|
|
|
|
|
2022-02-18 12:29:03 +08:00
|
|
|
async insertAfterBegin(targetUid: string, schema: any, options?: InsertAdjacentOptions) {
|
|
|
|
return await this.insertInner(targetUid, schema, 'first', options);
|
2022-01-19 10:09:30 +08:00
|
|
|
}
|
|
|
|
|
2022-02-18 12:29:03 +08:00
|
|
|
async insertBeforeEnd(targetUid: string, schema: any, options?: InsertAdjacentOptions) {
|
|
|
|
return await this.insertInner(targetUid, schema, 'last', options);
|
2022-01-19 10:09:30 +08:00
|
|
|
}
|
|
|
|
|
2022-02-18 12:29:03 +08:00
|
|
|
async insertBeforeBegin(targetUid: string, schema: any, options?: InsertAdjacentOptions) {
|
|
|
|
return await this.insertBeside(targetUid, schema, 'before', options);
|
2022-01-19 10:09:30 +08:00
|
|
|
}
|
|
|
|
|
2022-02-18 12:29:03 +08:00
|
|
|
async insertAfterEnd(targetUid: string, schema: any, options?: InsertAdjacentOptions) {
|
|
|
|
return await this.insertBeside(targetUid, schema, 'after', options);
|
2022-01-19 10:09:30 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
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) {
|
2022-02-18 12:29:03 +08:00
|
|
|
insertedNodes.push(
|
|
|
|
await this.insertSingleNode(node, {
|
|
|
|
...options,
|
|
|
|
transaction,
|
|
|
|
}),
|
|
|
|
);
|
2022-01-19 10:09:30 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
if (handleTransaction) {
|
|
|
|
await transaction.commit();
|
|
|
|
}
|
|
|
|
return insertedNodes;
|
|
|
|
} catch (err) {
|
2022-02-10 19:11:43 +08:00
|
|
|
console.log({ err });
|
2022-01-19 10:09:30 +08:00
|
|
|
if (handleTransaction) {
|
|
|
|
await transaction.rollback();
|
|
|
|
}
|
|
|
|
throw err;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-10 22:53:26 +08:00
|
|
|
async insert(schema: any, options?: TransactionAble) {
|
2022-01-19 10:09:30 +08:00
|
|
|
const nodes = UiSchemaRepository.schemaToSingleNodes(schema);
|
|
|
|
const insertedNodes = await this.insertNodes(nodes, options);
|
2022-02-20 01:23:04 +08:00
|
|
|
return this.getJsonSchema(insertedNodes[0].get('x-uid'), {
|
2022-01-19 10:09:30 +08:00
|
|
|
transaction: options?.transaction,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2022-02-08 16:36:48 +08:00
|
|
|
private async insertSchemaRecord(name, uid, schema, transaction) {
|
2022-02-09 20:24:25 +08:00
|
|
|
const serverHooks = schema['x-server-hooks'] || [];
|
|
|
|
|
2022-02-08 16:36:48 +08:00
|
|
|
const node = await this.create({
|
|
|
|
values: {
|
|
|
|
name,
|
2022-02-15 22:32:02 +08:00
|
|
|
['x-uid']: uid,
|
2022-02-08 16:36:48 +08:00
|
|
|
schema,
|
2022-02-09 20:24:25 +08:00
|
|
|
serverHooks,
|
2022-02-08 16:36:48 +08:00
|
|
|
},
|
|
|
|
transaction,
|
2022-02-10 22:53:26 +08:00
|
|
|
context: {
|
|
|
|
disableInsertHook: true,
|
|
|
|
},
|
2022-02-08 16:36:48 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
return node;
|
|
|
|
}
|
|
|
|
|
2022-02-18 12:29:03 +08:00
|
|
|
async insertSingleNode(schema: SchemaNode, options: TransactionAble & removeParentOptions) {
|
|
|
|
const { transaction } = options;
|
|
|
|
|
2022-01-19 10:09:30 +08:00
|
|
|
const db = this.database;
|
|
|
|
|
|
|
|
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: {
|
2022-02-20 01:23:04 +08:00
|
|
|
'x-uid': uid,
|
2022-01-19 10:09:30 +08:00
|
|
|
},
|
|
|
|
transaction,
|
|
|
|
});
|
|
|
|
|
2022-02-16 00:22:47 +08:00
|
|
|
const treeTable = this.uiSchemaTreePathTableName;
|
2022-01-19 10:09:30 +08:00
|
|
|
|
|
|
|
if (existsNode) {
|
|
|
|
savedNode = existsNode;
|
|
|
|
} else {
|
2022-02-08 16:36:48 +08:00
|
|
|
savedNode = await this.insertSchemaRecord(name, uid, schema, transaction);
|
2022-01-19 10:09:30 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
if (childOptions) {
|
2022-02-18 12:29:03 +08:00
|
|
|
const oldParentUid = await this.findParentUid(uid, transaction);
|
2022-01-19 10:09:30 +08:00
|
|
|
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) {
|
2022-02-18 12:29:03 +08:00
|
|
|
// delete old tree path
|
2022-01-19 10:09:30 +08:00
|
|
|
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,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
2022-02-18 12:29:03 +08:00
|
|
|
// insert new tree path
|
2022-01-19 10:09:30 +08:00
|
|
|
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: {
|
2022-02-20 01:23:04 +08:00
|
|
|
modelKey: savedNode.get('x-uid'),
|
2022-01-19 10:09:30 +08:00
|
|
|
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: {
|
2022-02-20 01:23:04 +08:00
|
|
|
modelKey: savedNode.get('x-uid'),
|
2022-01-19 10:09:30 +08:00
|
|
|
type: childOptions.type,
|
|
|
|
async,
|
|
|
|
},
|
|
|
|
transaction,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
const nodePosition = childOptions.position || 'last';
|
|
|
|
|
|
|
|
let sort;
|
|
|
|
|
|
|
|
// insert at first
|
|
|
|
if (nodePosition === 'first') {
|
|
|
|
sort = 1;
|
2022-02-15 22:32:02 +08:00
|
|
|
|
|
|
|
let updateSql = `UPDATE ${treeTable} as TreeTable
|
2022-01-19 10:09:30 +08:00
|
|
|
SET sort = TreeTable.sort + 1
|
|
|
|
FROM ${treeTable} as NodeInfo
|
|
|
|
WHERE NodeInfo.descendant = TreeTable.descendant and NodeInfo.depth = 0
|
2022-02-15 22:32:02 +08:00
|
|
|
AND TreeTable.depth = 1 AND TreeTable.ancestor = :ancestor and NodeInfo.type = :type`;
|
|
|
|
|
|
|
|
// Compatible with mysql
|
|
|
|
if (this.database.sequelize.getDialect() === 'mysql') {
|
|
|
|
updateSql = `UPDATE ${treeTable} as TreeTable
|
|
|
|
JOIN ${treeTable} as NodeInfo ON (NodeInfo.descendant = TreeTable.descendant and NodeInfo.depth = 0)
|
|
|
|
SET TreeTable.sort = TreeTable.sort + 1
|
|
|
|
WHERE TreeTable.depth = 1 AND TreeTable.ancestor = :ancestor and NodeInfo.type = :type`;
|
|
|
|
}
|
|
|
|
|
|
|
|
// move all child last index
|
|
|
|
await db.sequelize.query(updateSql, {
|
|
|
|
replacements: {
|
|
|
|
ancestor: childOptions.parentUid,
|
|
|
|
type: childOptions.type,
|
2022-01-19 10:09:30 +08:00
|
|
|
},
|
2022-02-15 22:32:02 +08:00
|
|
|
transaction,
|
|
|
|
});
|
2022-01-19 10:09:30 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2022-02-15 22:32:02 +08:00
|
|
|
let updateSql = `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`;
|
|
|
|
|
|
|
|
if (this.database.sequelize.getDialect() === 'mysql') {
|
|
|
|
updateSql = `UPDATE ${treeTable} as TreeTable
|
|
|
|
JOIN ${treeTable} as NodeInfo ON (NodeInfo.descendant = TreeTable.descendant and NodeInfo.depth = 0)
|
|
|
|
SET TreeTable.sort = TreeTable.sort + 1
|
|
|
|
WHERE TreeTable.depth = 1 AND TreeTable.ancestor = :ancestor and TreeTable.sort >= :sort and NodeInfo.type = :type`;
|
|
|
|
}
|
|
|
|
|
|
|
|
await db.sequelize.query(updateSql, {
|
|
|
|
replacements: {
|
|
|
|
ancestor: childOptions.parentUid,
|
|
|
|
sort,
|
|
|
|
type: childOptions.type,
|
2022-01-19 10:09:30 +08:00
|
|
|
},
|
2022-02-15 22:32:02 +08:00
|
|
|
transaction,
|
|
|
|
});
|
2022-01-19 10:09:30 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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,
|
|
|
|
});
|
2022-02-18 12:29:03 +08:00
|
|
|
|
|
|
|
// move node to new parent
|
|
|
|
if (oldParentUid !== null && oldParentUid !== parentUid) {
|
|
|
|
await this.database.emitAsync('uiSchemaMove', savedNode, {
|
|
|
|
transaction,
|
|
|
|
oldParentUid,
|
|
|
|
parentUid,
|
|
|
|
});
|
|
|
|
|
|
|
|
if (options.removeParentsIfNoChildren) {
|
|
|
|
await this.recursivelyRemoveIfNoChildren({
|
|
|
|
transaction,
|
|
|
|
uid: oldParentUid,
|
|
|
|
breakRemoveOn: options.breakRemoveOn,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
2022-01-19 10:09:30 +08:00
|
|
|
} else {
|
|
|
|
// insert root node path
|
|
|
|
await db.sequelize.query(
|
|
|
|
`INSERT INTO ${treeTable}(ancestor, descendant, depth, async) VALUES (:modelKey, :modelKey, 0, :async )`,
|
|
|
|
{
|
|
|
|
type: 'INSERT',
|
|
|
|
replacements: {
|
2022-02-20 01:23:04 +08:00
|
|
|
modelKey: savedNode.get('x-uid'),
|
2022-01-19 10:09:30 +08:00
|
|
|
async,
|
|
|
|
},
|
|
|
|
transaction,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return savedNode;
|
|
|
|
}
|
|
|
|
}
|
2022-02-11 18:13:14 +08:00
|
|
|
|
|
|
|
export default UiSchemaRepository;
|