feat: adjacency list

This commit is contained in:
chenos 2023-02-20 14:25:46 +08:00
parent 4fbad75ea9
commit 30a8678036
12 changed files with 268 additions and 10 deletions

View File

@ -23,8 +23,8 @@ function totalPage(total, pageSize): number {
} }
function findArgs(params: ActionParams) { function findArgs(params: ActionParams) {
const { fields, filter, appends, except, sort } = params; const { tree, fields, filter, appends, except, sort } = params;
return { filter, fields, appends, except, sort }; return { tree, filter, fields, appends, except, sort };
} }
async function listWithPagination(ctx: Context) { async function listWithPagination(ctx: Context) {

View File

@ -84,7 +84,7 @@ const getSchema = (schema, category, compile): ISchema => {
'x-component': 'Action', 'x-component': 'Action',
'x-component-props': { 'x-component-props': {
type: 'primary', type: 'primary',
useAction: () => useCreateCollection(), useAction: () => useCreateCollection(schema),
}, },
}, },
}, },
@ -188,16 +188,19 @@ const useDefaultCollectionFields = (values) => {
return defaults; return defaults;
}; };
const useCreateCollection = () => { const useCreateCollection = (schema?: any) => {
const form = useForm(); const form = useForm();
const { refreshCM } = useCollectionManager(); const { refreshCM } = useCollectionManager();
const ctx = useActionContext(); const ctx = useActionContext();
const { refresh } = useResourceActionContext(); const { refresh } = useResourceActionContext();
const { resource } = useResourceContext(); const { resource, collection } = useResourceContext();
return { return {
async run() { async run() {
await form.submit(); await form.submit();
const values = cloneDeep(form.values); const values = cloneDeep(form.values);
if (schema?.events?.beforeSubmit) {
schema.events.beforeSubmit(values);
}
const fields = useDefaultCollectionFields(values); const fields = useDefaultCollectionFields(values);
if (values.autoCreateReverseField) { if (values.autoCreateReverseField) {
} else { } else {

View File

@ -5,7 +5,7 @@ import {
recordPickerSelector, recordPickerSelector,
recordPickerViewer, recordPickerViewer,
relationshipType, relationshipType,
reverseFieldProperties, reverseFieldProperties
} from './properties'; } from './properties';
import { IField } from './types'; import { IField } from './types';

View File

@ -1,3 +1,4 @@
export * from './calendar'; export * from './calendar';
export * from './general'; export * from './general';
export * from './tree';

View File

@ -0,0 +1,72 @@
import { getConfigurableProperties } from './properties';
import { ICollectionTemplate } from './types';
export const tree: ICollectionTemplate = {
name: 'tree',
title: '{{t("Tree collection")}}',
order: 3,
color: 'blue',
default: {
tree: 'adjacencyList',
fields: [
{
interface: 'integer',
name: 'parentId',
type: 'bigInt',
isForeignKey: true,
uiSchema: {
type: 'number',
title: '{{t("Parent ID")}}',
'x-component': 'InputNumber',
'x-read-pretty': true,
},
},
{
interface: 'm2o',
type: 'belongsTo',
name: 'parent',
foreignKey: 'parentId',
uiSchema: {
title: '{{t("Parent")}}',
'x-component': 'RecordPicker',
'x-component-props': {
// mode: 'tags',
multiple: false,
fieldNames: {
label: 'id',
value: 'id',
},
},
},
},
{
interface: 'o2m',
type: 'hasMany',
name: 'children',
foreignKey: 'parentId',
uiSchema: {
title: '{{t("Children")}}',
'x-component': 'RecordPicker',
'x-component-props': {
// mode: 'tags',
multiple: true,
fieldNames: {
label: 'id',
value: 'id',
},
},
},
},
],
},
events: {
beforeSubmit(values) {
if (Array.isArray(values?.fields)) {
values?.fields.map((f) => {
f.target = values.name;
});
}
},
},
configurableProperties: getConfigurableProperties('title', 'name', 'inherits', 'category', 'moreOptions'),
};

View File

@ -9,6 +9,7 @@ export interface ICollectionTemplate {
order?: number; order?: number;
/** 默认配置 */ /** 默认配置 */
default?: CollectionOptions; default?: CollectionOptions;
events?: any;
/** UI 可配置的 CollectionOptions 参数(添加或编辑的 Collection 表单的字段) */ /** UI 可配置的 CollectionOptions 参数(添加或编辑的 Collection 表单的字段) */
configurableProperties?: Record<string, ISchema>; configurableProperties?: Record<string, ISchema>;
/** 当前模板可用的字段类型 */ /** 当前模板可用的字段类型 */

View File

@ -0,0 +1,123 @@
import { Database } from '../database';
import { mockDatabase } from './';
describe('sort', function () {
let db: Database;
beforeEach(async () => {
db = mockDatabase();
});
afterEach(async () => {
await db.close();
});
it('should be auto completed', () => {
const collection = db.collection({
name: 'categories',
tree: 'adjacency-list',
fields: [
{
type: 'belongsTo',
name: 'parent',
},
{
type: 'hasMany',
name: 'children',
},
],
});
expect(collection.getField('parent').options.target).toBe('categories');
expect(collection.getField('parent').options.foreignKey).toBe('parentId');
expect(collection.getField('children').options.target).toBe('categories');
expect(collection.getField('children').options.foreignKey).toBe('parentId');
});
it('should be tree', async () => {
db.collection({
name: 'categories',
tree: 'adjacency-list',
fields: [
{
type: 'string',
name: 'name',
},
{
type: 'string',
name: 'description',
},
{
type: 'belongsTo',
name: 'parent',
},
{
type: 'hasMany',
name: 'children',
},
],
});
await db.sync();
const values = [
{
name: '1',
children: [
{
name: '1-1',
children: [
{
name: '1-1-1',
children: [
{
name: '1-1-1-1',
},
],
},
],
},
],
},
{
name: '2',
children: [
{
name: '2-1',
children: [
{
name: '2-1-1',
children: [
{
name: '2-1-1-1',
},
],
},
],
},
],
},
];
await db.getRepository('categories').create({
values,
});
const instances = await db.getRepository('categories').find({
filter: {
parentId: null,
},
tree: true,
fields: ['id', 'name'],
appends: ['parent', 'children'],
sort: 'id',
});
expect(instances.map((i) => i.toJSON())).toMatchObject(values);
const instance = await db.getRepository('categories').findOne({
filterByTk: 1,
tree: true,
fields: ['id', 'name'],
});
expect(instance.toJSON()).toMatchObject(values[0]);
});
});

View File

@ -7,7 +7,7 @@ import {
QueryInterfaceDropTableOptions, QueryInterfaceDropTableOptions,
SyncOptions, SyncOptions,
Transactionable, Transactionable,
Utils, Utils
} from 'sequelize'; } from 'sequelize';
import { Database } from './database'; import { Database } from './database';
import { Field, FieldOptions } from './fields'; import { Field, FieldOptions } from './fields';
@ -37,6 +37,8 @@ export interface CollectionOptions extends Omit<ModelOptions, 'name' | 'hooks'>
*/ */
magicAttribute?: string; magicAttribute?: string;
tree?: string;
[key: string]: any; [key: string]: any;
} }
@ -83,6 +85,7 @@ export class Collection<
this.db.modelCollection.set(this.model, this); this.db.modelCollection.set(this.model, this);
this.db.tableNameCollectionMap.set(this.model.tableName, this); this.db.tableNameCollectionMap.set(this.model.tableName, this);
this.treeHook();
if (!options.inherits) { if (!options.inherits) {
this.setFields(options.fields); this.setFields(options.fields);
@ -92,6 +95,51 @@ export class Collection<
this.setSortable(options.sortable); this.setSortable(options.sortable);
} }
treeHook() {
if (!this.options.tree) {
return;
}
this.on('field.beforeAdd', (name, opts, { collection }) => {
if (!collection.options.tree) {
return;
}
if (name === 'parent' || name === 'children') {
opts.target = collection.name;
opts.foreignKey = 'parentId';
}
});
this.model.afterFind(async (instances, options: any) => {
if (!options.tree) {
return;
}
const arr: Model[] = Array.isArray(instances) ? instances : [instances];
let index = 0;
for (const instance of arr) {
const opts = {
...lodash.pick(options, ['tree', 'fields', 'appends', 'except', 'sort']),
};
let __index = `${index++}`;
if (options.parentIndex) {
__index = `${options.parentIndex}.${__index}`;
}
instance.setDataValue('__index', __index);
const children = await this.repository.find({
filter: {
parentId: instance.id,
},
transaction: options.transaction,
...opts,
// @ts-ignore
parentIndex: `${__index}.children`,
context: options.context,
});
if (children?.length > 0) {
instance.setDataValue('children', children);
}
}
});
}
private checkOptions(options: CollectionOptions) { private checkOptions(options: CollectionOptions) {
checkIdentifier(options.name); checkIdentifier(options.name);
this.checkTableName(); this.checkTableName();
@ -231,6 +279,7 @@ export class Collection<
this.checkFieldType(name, options); this.checkFieldType(name, options);
const { database } = this.context; const { database } = this.context;
this.emit('field.beforeAdd', name, options, { collection: this });
const field = database.buildField( const field = database.buildField(
{ name, ...options }, { name, ...options },

View File

@ -11,7 +11,7 @@ import {
Op, Op,
Transactionable, Transactionable,
UpdateOptions as SequelizeUpdateOptions, UpdateOptions as SequelizeUpdateOptions,
WhereOperators, WhereOperators
} from 'sequelize'; } from 'sequelize';
import { Collection } from './collection'; import { Collection } from './collection';
import { Database } from './database'; import { Database } from './database';
@ -106,6 +106,7 @@ export interface CommonFindOptions extends Transactionable {
except?: Except; except?: Except;
sort?: Sort; sort?: Sort;
context?: any; context?: any;
tree?: boolean;
} }
export type FindOneOptions = Omit<FindOptions, 'limit'>; export type FindOneOptions = Omit<FindOptions, 'limit'>;

View File

@ -99,7 +99,7 @@ export function afterCreateForForeignKeyField(db: Database) {
}); });
} }
return async (model, { transaction, context }) => { const hook = async (model, { transaction, context }) => {
// skip if no app context // skip if no app context
if (!context) { if (!context) {
return; return;
@ -172,4 +172,12 @@ export function afterCreateForForeignKeyField(db: Database) {
}); });
} }
}; };
return async (model, options) => {
try {
await hook(model, options);
} catch (error) {
}
};
} }

View File

@ -14,7 +14,7 @@ import {
beforeCreateForChildrenCollection, beforeCreateForChildrenCollection,
beforeCreateForReverseField, beforeCreateForReverseField,
beforeDestroyForeignKey, beforeDestroyForeignKey,
beforeInitOptions, beforeInitOptions
} from './hooks'; } from './hooks';
import { InheritedCollection } from '@nocobase/database'; import { InheritedCollection } from '@nocobase/database';