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) {
const { fields, filter, appends, except, sort } = params;
return { filter, fields, appends, except, sort };
const { tree, fields, filter, appends, except, sort } = params;
return { tree, filter, fields, appends, except, sort };
}
async function listWithPagination(ctx: Context) {

View File

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

View File

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

View File

@ -1,3 +1,4 @@
export * from './calendar';
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;
/** 默认配置 */
default?: CollectionOptions;
events?: any;
/** UI 可配置的 CollectionOptions 参数(添加或编辑的 Collection 表单的字段) */
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,
SyncOptions,
Transactionable,
Utils,
Utils
} from 'sequelize';
import { Database } from './database';
import { Field, FieldOptions } from './fields';
@ -37,6 +37,8 @@ export interface CollectionOptions extends Omit<ModelOptions, 'name' | 'hooks'>
*/
magicAttribute?: string;
tree?: string;
[key: string]: any;
}
@ -83,6 +85,7 @@ export class Collection<
this.db.modelCollection.set(this.model, this);
this.db.tableNameCollectionMap.set(this.model.tableName, this);
this.treeHook();
if (!options.inherits) {
this.setFields(options.fields);
@ -92,6 +95,51 @@ export class Collection<
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) {
checkIdentifier(options.name);
this.checkTableName();
@ -231,6 +279,7 @@ export class Collection<
this.checkFieldType(name, options);
const { database } = this.context;
this.emit('field.beforeAdd', name, options, { collection: this });
const field = database.buildField(
{ name, ...options },

View File

@ -11,7 +11,7 @@ import {
Op,
Transactionable,
UpdateOptions as SequelizeUpdateOptions,
WhereOperators,
WhereOperators
} from 'sequelize';
import { Collection } from './collection';
import { Database } from './database';
@ -106,6 +106,7 @@ export interface CommonFindOptions extends Transactionable {
except?: Except;
sort?: Sort;
context?: any;
tree?: boolean;
}
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
if (!context) {
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,
beforeCreateForReverseField,
beforeDestroyForeignKey,
beforeInitOptions,
beforeInitOptions
} from './hooks';
import { InheritedCollection } from '@nocobase/database';