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:
ChengLei Shao 2022-02-26 15:12:18 +08:00 committed by GitHub
parent c28a1e34ec
commit 3be12644ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 263 additions and 48 deletions

View 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']);
});
});

View File

@ -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;

View File

@ -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;
} }

View File

@ -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

View File

@ -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);
} }
} }