mirror of
https://github.com/nocobase/nocobase
synced 2024-11-15 11:26:55 +00:00
feat: sort in collection fields (#207)
* feat: sort in collection fields * fix: sort should call before hidden * mov: test file * refactor: toJSON with traverseJSON * fix: toJSON test * fix: sortBy with hidden field
This commit is contained in:
parent
c28a1e34ec
commit
3be12644ed
160
packages/database/src/__tests__/field-options/sort-by.test.ts
Normal file
160
packages/database/src/__tests__/field-options/sort-by.test.ts
Normal file
@ -0,0 +1,160 @@
|
||||
import { mockDatabase } from '@nocobase/test';
|
||||
import { Database } from '../../index';
|
||||
|
||||
describe('associated field order', () => {
|
||||
let db: Database;
|
||||
|
||||
afterEach(async () => {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
db = mockDatabase();
|
||||
await db.clean({ drop: true });
|
||||
|
||||
db.collection({
|
||||
name: 'users',
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'name',
|
||||
},
|
||||
{
|
||||
type: 'hasMany',
|
||||
name: 'posts',
|
||||
sortBy: 'title',
|
||||
},
|
||||
{
|
||||
type: 'hasMany',
|
||||
name: 'records',
|
||||
sortBy: 'count',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
db.collection({
|
||||
name: 'records',
|
||||
fields: [
|
||||
{
|
||||
type: 'integer',
|
||||
name: 'count',
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'name',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
db.collection({
|
||||
name: 'posts',
|
||||
fields: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'title',
|
||||
},
|
||||
{
|
||||
type: 'belongsTo',
|
||||
name: 'user',
|
||||
},
|
||||
{
|
||||
type: 'belongsToMany',
|
||||
name: 'tags',
|
||||
sortBy: 'name',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
db.collection({
|
||||
name: 'tags',
|
||||
fields: [
|
||||
{ type: 'string', name: 'name' },
|
||||
{
|
||||
type: 'belongsToMany',
|
||||
name: 'posts',
|
||||
},
|
||||
],
|
||||
});
|
||||
await db.sync();
|
||||
});
|
||||
|
||||
it('should sort hasMany association', async () => {
|
||||
await db.getRepository('users').create({
|
||||
values: {
|
||||
name: 'u1',
|
||||
posts: [{ title: 'c' }, { title: 'b' }, { title: 'a' }],
|
||||
},
|
||||
});
|
||||
|
||||
const u1 = await db.getRepository('users').findOne({
|
||||
appends: ['posts'],
|
||||
});
|
||||
|
||||
const u1Json = u1.toJSON();
|
||||
|
||||
const u1Posts = u1Json['posts'];
|
||||
expect(u1Posts.map((p) => p['title'])).toEqual(['a', 'b', 'c']);
|
||||
});
|
||||
|
||||
it('should sort belongsToMany association', async () => {
|
||||
await db.getRepository('posts').create({
|
||||
values: {
|
||||
title: 'p1',
|
||||
tags: [{ name: 'c' }, { name: 'b' }, { name: 'a' }],
|
||||
},
|
||||
});
|
||||
|
||||
const p1 = await db.getRepository('posts').findOne({
|
||||
appends: ['tags'],
|
||||
});
|
||||
|
||||
const p1JSON = p1.toJSON();
|
||||
|
||||
const p1Tags = p1JSON['tags'];
|
||||
expect(p1Tags.map((p) => p['name'])).toEqual(['a', 'b', 'c']);
|
||||
});
|
||||
|
||||
it('should sort nested associations', async () => {
|
||||
await db.getRepository('users').create({
|
||||
values: {
|
||||
name: 'u1',
|
||||
posts: [{ title: 'c', tags: [{ name: 'c' }, { name: 'b' }, { name: 'a' }] }, { title: 'b' }, { title: 'a' }],
|
||||
},
|
||||
});
|
||||
|
||||
const u1 = await db.getRepository('users').findOne({
|
||||
appends: ['posts.tags'],
|
||||
});
|
||||
|
||||
const u1Json = u1.toJSON();
|
||||
const u1Posts = u1Json['posts'];
|
||||
expect(u1Posts.map((p) => p['title'])).toEqual(['a', 'b', 'c']);
|
||||
|
||||
const postCTags = u1Posts[2]['tags'];
|
||||
expect(postCTags.map((p) => p['name'])).toEqual(['a', 'b', 'c']);
|
||||
});
|
||||
|
||||
it('should sortBy hidden field', async () => {
|
||||
await db.getRepository('users').create({
|
||||
values: {
|
||||
name: 'u1',
|
||||
records: [
|
||||
{ count: 3, name: 'c' },
|
||||
{ count: 2, name: 'b' },
|
||||
{ count: 1, name: 'a' },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const u1 = await db.getRepository('users').findOne({
|
||||
appends: ['records'],
|
||||
});
|
||||
|
||||
const u1Json = u1.toJSON();
|
||||
|
||||
const u1Records = u1Json['records'];
|
||||
expect(u1Records[0].count).toBeUndefined();
|
||||
expect(u1Records.map((p) => p['name'])).toEqual(['a', 'b', 'c']);
|
||||
});
|
||||
});
|
@ -1,7 +1,7 @@
|
||||
import { omit } from 'lodash';
|
||||
import { BelongsToManyOptions as SequelizeBelongsToManyOptions } from 'sequelize';
|
||||
import { Collection } from '../collection';
|
||||
import { BaseRelationFieldOptions, RelationField } from './relation-field';
|
||||
import { BaseRelationFieldOptions, MultipleRelationFieldOptions, RelationField } from './relation-field';
|
||||
|
||||
export class BelongsToManyField extends RelationField {
|
||||
get through() {
|
||||
@ -62,7 +62,7 @@ export class BelongsToManyField extends RelationField {
|
||||
}
|
||||
|
||||
export interface BelongsToManyFieldOptions
|
||||
extends BaseRelationFieldOptions,
|
||||
extends MultipleRelationFieldOptions,
|
||||
Omit<SequelizeBelongsToManyOptions, 'through'> {
|
||||
type: 'belongsToMany';
|
||||
through?: string;
|
||||
|
@ -5,9 +5,10 @@ import {
|
||||
ForeignKeyOptions,
|
||||
HasManyOptions,
|
||||
HasManyOptions as SequelizeHasManyOptions,
|
||||
Utils
|
||||
Utils,
|
||||
} from 'sequelize';
|
||||
import { BaseRelationFieldOptions, RelationField } from './relation-field';
|
||||
|
||||
import { BaseRelationFieldOptions, MultipleRelationFieldOptions, RelationField } from './relation-field';
|
||||
|
||||
export interface HasManyFieldOptions extends HasManyOptions {
|
||||
/**
|
||||
@ -136,7 +137,7 @@ export class HasManyField extends RelationField {
|
||||
}
|
||||
}
|
||||
|
||||
export interface HasManyFieldOptions extends BaseRelationFieldOptions, SequelizeHasManyOptions {
|
||||
export interface HasManyFieldOptions extends MultipleRelationFieldOptions, SequelizeHasManyOptions {
|
||||
type: 'hasMany';
|
||||
target?: string;
|
||||
}
|
||||
|
@ -2,6 +2,10 @@ import { BaseFieldOptions, Field } from './field';
|
||||
|
||||
export interface BaseRelationFieldOptions extends BaseFieldOptions {}
|
||||
|
||||
export interface MultipleRelationFieldOptions extends BaseRelationFieldOptions {
|
||||
sortBy?: string | string[];
|
||||
}
|
||||
|
||||
export abstract class RelationField extends Field {
|
||||
/**
|
||||
* target relation name
|
||||
|
@ -1,62 +1,112 @@
|
||||
import { Model as SequelizeModel } from 'sequelize';
|
||||
import { Model as SequelizeModel, ModelCtor } from 'sequelize';
|
||||
import { Collection } from './collection';
|
||||
import { Database } from './database';
|
||||
import lodash from 'lodash';
|
||||
import { Field } from './fields';
|
||||
|
||||
interface IModel {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface JSONTransformerOptions {
|
||||
model: ModelCtor<any>;
|
||||
collection: Collection;
|
||||
db: Database;
|
||||
key?: string;
|
||||
field?: Field;
|
||||
}
|
||||
|
||||
export class Model<TModelAttributes extends {} = any, TCreationAttributes extends {} = TModelAttributes>
|
||||
extends SequelizeModel<TModelAttributes, TCreationAttributes>
|
||||
implements IModel
|
||||
{
|
||||
public static database: Database;
|
||||
public static collection: Collection;
|
||||
// [key: string]: any;
|
||||
|
||||
private toJsonWithoutHiddenFields(data, { model, collection }): any {
|
||||
if (!data) {
|
||||
return data;
|
||||
}
|
||||
if (typeof data.toJSON === 'function') {
|
||||
data = data.toJSON();
|
||||
}
|
||||
const db = (this.constructor as any).database as Database;
|
||||
const hidden = [];
|
||||
collection.forEachField((field) => {
|
||||
if (field.options.hidden) {
|
||||
hidden.push(field.options.name);
|
||||
}
|
||||
});
|
||||
const json = {};
|
||||
Object.keys(data).forEach((key) => {
|
||||
if (hidden.includes(key)) {
|
||||
return;
|
||||
}
|
||||
if (model.hasAlias(key)) {
|
||||
const association = model.associations[key];
|
||||
const opts = {
|
||||
model: association.target,
|
||||
collection: db.getCollection(association.target.name),
|
||||
};
|
||||
if (['HasMany', 'BelongsToMany'].includes(association.associationType)) {
|
||||
if (Array.isArray(data[key])) {
|
||||
json[key] = data[key].map((item) => this.toJsonWithoutHiddenFields(item, opts));
|
||||
}
|
||||
} else {
|
||||
json[key] = this.toJsonWithoutHiddenFields(data[key], opts);
|
||||
}
|
||||
} else {
|
||||
json[key] = data[key];
|
||||
}
|
||||
});
|
||||
return json;
|
||||
}
|
||||
|
||||
public toJSON<T extends TModelAttributes>(): T {
|
||||
return this.toJsonWithoutHiddenFields(super.toJSON(), {
|
||||
model: this.constructor,
|
||||
const handleObj = (obj, options: JSONTransformerOptions) => {
|
||||
const handles = [
|
||||
(data) => {
|
||||
if (data instanceof Model) {
|
||||
return data.toJSON();
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
this.hiddenObjKey,
|
||||
];
|
||||
return handles.reduce((carry, fn) => fn.apply(this, [carry, options]), obj);
|
||||
};
|
||||
|
||||
const handleArray = (arrayOfObj, options: JSONTransformerOptions) => {
|
||||
const handles = [this.sortAssociations];
|
||||
return handles.reduce((carry, fn) => fn.apply(this, [carry, options]), arrayOfObj);
|
||||
};
|
||||
|
||||
const opts = {
|
||||
model: this.constructor as ModelCtor<any>,
|
||||
collection: (this.constructor as any).collection,
|
||||
db: (this.constructor as any).database as Database,
|
||||
};
|
||||
|
||||
const traverseJSON = (data: T, options: JSONTransformerOptions): T => {
|
||||
const { model, db, collection } = options;
|
||||
// handle Object
|
||||
data = handleObj(data, options);
|
||||
|
||||
const result = {};
|
||||
for (const key of Object.keys(data)) {
|
||||
// @ts-ignore
|
||||
if (model.hasAlias(key)) {
|
||||
const association = model.associations[key];
|
||||
const opts = {
|
||||
model: association.target,
|
||||
collection: db.getCollection(association.target.name),
|
||||
db,
|
||||
key,
|
||||
field: collection.getField(key),
|
||||
};
|
||||
|
||||
if (['HasMany', 'BelongsToMany'].includes(association.associationType)) {
|
||||
result[key] = handleArray(data[key], opts).map((item) => traverseJSON(item, opts));
|
||||
} else {
|
||||
result[key] = traverseJSON(data[key], opts);
|
||||
}
|
||||
} else {
|
||||
result[key] = data[key];
|
||||
}
|
||||
}
|
||||
|
||||
return result as T;
|
||||
};
|
||||
|
||||
return traverseJSON(super.toJSON(), opts);
|
||||
}
|
||||
|
||||
private hiddenObjKey(obj, options: JSONTransformerOptions) {
|
||||
const hiddenFields = Array.from(options.collection.fields.values())
|
||||
.filter((field) => field.options.hidden)
|
||||
.map((field) => field.options.name);
|
||||
|
||||
return lodash.omit(obj, hiddenFields);
|
||||
}
|
||||
|
||||
private sortAssociations(data, { field }: JSONTransformerOptions): any {
|
||||
const sortBy = field.options.sortBy;
|
||||
return sortBy ? this.sortArray(data, sortBy) : data;
|
||||
}
|
||||
|
||||
private sortArray(data, sortBy: string | string[]) {
|
||||
if (!lodash.isArray(sortBy)) {
|
||||
sortBy = [sortBy];
|
||||
}
|
||||
|
||||
const orders = sortBy.map((sortItem) => {
|
||||
const direction = sortItem.startsWith('-') ? 'desc' : 'asc';
|
||||
sortItem.replace('-', '');
|
||||
return [sortItem, direction];
|
||||
});
|
||||
|
||||
return lodash.sortBy(data, ...orders);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user