nocobase/packages/plugin-ui-schema-storage/src/repository.ts

646 lines
20 KiB
TypeScript
Raw Normal View History

2022-02-10 05:58:26 +00:00
import { Repository, TransactionAble } 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,
},
);
}
2022-02-10 05:58:26 +00:00
async remove(uid: string, options?: TransactionAble) {
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,
});
}
2022-02-08 08:36:48 +00:00
private async insertSchemaRecord(name, uid, schema, transaction) {
2022-02-09 12:24:25 +00:00
const serverHooks = schema['x-server-hooks'] || [];
2022-02-08 08:36:48 +00:00
const node = await this.create({
values: {
name,
uid,
schema,
2022-02-09 12:24:25 +00:00
serverHooks,
2022-02-08 08:36:48 +00:00
},
transaction,
hooks: false,
});
return node;
}
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 {
2022-02-08 08:36:48 +00:00
savedNode = await this.insertSchemaRecord(name, uid, schema, transaction);
}
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;
}
}