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

View File

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

View File

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

View File

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