mirror of
https://github.com/nocobase/nocobase
synced 2024-11-15 16:46:31 +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 { omit } from 'lodash';
|
||||||
import { BelongsToManyOptions as SequelizeBelongsToManyOptions } from 'sequelize';
|
import { BelongsToManyOptions as SequelizeBelongsToManyOptions } from 'sequelize';
|
||||||
import { Collection } from '../collection';
|
import { Collection } from '../collection';
|
||||||
import { BaseRelationFieldOptions, RelationField } from './relation-field';
|
import { BaseRelationFieldOptions, MultipleRelationFieldOptions, RelationField } from './relation-field';
|
||||||
|
|
||||||
export class BelongsToManyField extends RelationField {
|
export class BelongsToManyField extends RelationField {
|
||||||
get through() {
|
get through() {
|
||||||
@ -62,7 +62,7 @@ export class BelongsToManyField extends RelationField {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface BelongsToManyFieldOptions
|
export interface BelongsToManyFieldOptions
|
||||||
extends BaseRelationFieldOptions,
|
extends MultipleRelationFieldOptions,
|
||||||
Omit<SequelizeBelongsToManyOptions, 'through'> {
|
Omit<SequelizeBelongsToManyOptions, 'through'> {
|
||||||
type: 'belongsToMany';
|
type: 'belongsToMany';
|
||||||
through?: string;
|
through?: string;
|
||||||
|
@ -5,9 +5,10 @@ import {
|
|||||||
ForeignKeyOptions,
|
ForeignKeyOptions,
|
||||||
HasManyOptions,
|
HasManyOptions,
|
||||||
HasManyOptions as SequelizeHasManyOptions,
|
HasManyOptions as SequelizeHasManyOptions,
|
||||||
Utils
|
Utils,
|
||||||
} from 'sequelize';
|
} from 'sequelize';
|
||||||
import { BaseRelationFieldOptions, RelationField } from './relation-field';
|
|
||||||
|
import { BaseRelationFieldOptions, MultipleRelationFieldOptions, RelationField } from './relation-field';
|
||||||
|
|
||||||
export interface HasManyFieldOptions extends HasManyOptions {
|
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';
|
type: 'hasMany';
|
||||||
target?: string;
|
target?: string;
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,10 @@ import { BaseFieldOptions, Field } from './field';
|
|||||||
|
|
||||||
export interface BaseRelationFieldOptions extends BaseFieldOptions {}
|
export interface BaseRelationFieldOptions extends BaseFieldOptions {}
|
||||||
|
|
||||||
|
export interface MultipleRelationFieldOptions extends BaseRelationFieldOptions {
|
||||||
|
sortBy?: string | string[];
|
||||||
|
}
|
||||||
|
|
||||||
export abstract class RelationField extends Field {
|
export abstract class RelationField extends Field {
|
||||||
/**
|
/**
|
||||||
* target relation name
|
* 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 { Collection } from './collection';
|
||||||
import { Database } from './database';
|
import { Database } from './database';
|
||||||
|
import lodash from 'lodash';
|
||||||
|
import { Field } from './fields';
|
||||||
|
|
||||||
interface IModel {
|
interface IModel {
|
||||||
[key: string]: any;
|
[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>
|
export class Model<TModelAttributes extends {} = any, TCreationAttributes extends {} = TModelAttributes>
|
||||||
extends SequelizeModel<TModelAttributes, TCreationAttributes>
|
extends SequelizeModel<TModelAttributes, TCreationAttributes>
|
||||||
implements IModel
|
implements IModel
|
||||||
{
|
{
|
||||||
public static database: Database;
|
public static database: Database;
|
||||||
public static collection: Collection;
|
public static collection: Collection;
|
||||||
// [key: string]: any;
|
|
||||||
|
|
||||||
private toJsonWithoutHiddenFields(data, { model, collection }): any {
|
public toJSON<T extends TModelAttributes>(): T {
|
||||||
if (!data) {
|
const handleObj = (obj, options: JSONTransformerOptions) => {
|
||||||
|
const handles = [
|
||||||
|
(data) => {
|
||||||
|
if (data instanceof Model) {
|
||||||
|
return data.toJSON();
|
||||||
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
},
|
||||||
if (typeof data.toJSON === 'function') {
|
this.hiddenObjKey,
|
||||||
data = data.toJSON();
|
];
|
||||||
}
|
return handles.reduce((carry, fn) => fn.apply(this, [carry, options]), obj);
|
||||||
const db = (this.constructor as any).database as Database;
|
};
|
||||||
const hidden = [];
|
|
||||||
collection.forEachField((field) => {
|
const handleArray = (arrayOfObj, options: JSONTransformerOptions) => {
|
||||||
if (field.options.hidden) {
|
const handles = [this.sortAssociations];
|
||||||
hidden.push(field.options.name);
|
return handles.reduce((carry, fn) => fn.apply(this, [carry, options]), arrayOfObj);
|
||||||
}
|
};
|
||||||
});
|
|
||||||
const json = {};
|
const opts = {
|
||||||
Object.keys(data).forEach((key) => {
|
model: this.constructor as ModelCtor<any>,
|
||||||
if (hidden.includes(key)) {
|
collection: (this.constructor as any).collection,
|
||||||
return;
|
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)) {
|
if (model.hasAlias(key)) {
|
||||||
const association = model.associations[key];
|
const association = model.associations[key];
|
||||||
const opts = {
|
const opts = {
|
||||||
model: association.target,
|
model: association.target,
|
||||||
collection: db.getCollection(association.target.name),
|
collection: db.getCollection(association.target.name),
|
||||||
|
db,
|
||||||
|
key,
|
||||||
|
field: collection.getField(key),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (['HasMany', 'BelongsToMany'].includes(association.associationType)) {
|
if (['HasMany', 'BelongsToMany'].includes(association.associationType)) {
|
||||||
if (Array.isArray(data[key])) {
|
result[key] = handleArray(data[key], opts).map((item) => traverseJSON(item, opts));
|
||||||
json[key] = data[key].map((item) => this.toJsonWithoutHiddenFields(item, opts));
|
} else {
|
||||||
|
result[key] = traverseJSON(data[key], opts);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
json[key] = this.toJsonWithoutHiddenFields(data[key], opts);
|
result[key] = data[key];
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
json[key] = data[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return json;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public toJSON<T extends TModelAttributes>(): T {
|
return result as T;
|
||||||
return this.toJsonWithoutHiddenFields(super.toJSON(), {
|
};
|
||||||
model: this.constructor,
|
|
||||||
collection: (this.constructor as any).collection,
|
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