From b3301e19de499f19488b58cf4e0e2c4562bdf7ce Mon Sep 17 00:00:00 2001 From: sealday Date: Wed, 24 Apr 2024 21:46:05 +0800 Subject: [PATCH] feat: support sort m2m & o2m fields (#768) Co-authored-by: sealday Reviewed-on: https://git.daoyoucloud.com/daoyoucloud/tachybase/pulls/768 --- packages/core/actions/src/actions/list.ts | 31 +++++++++- .../src/__tests__/repository/find.test.ts | 58 +++++++++++++++++++ .../src/eager-loading/eager-loading-tree.ts | 18 +++++- 3 files changed, 103 insertions(+), 4 deletions(-) diff --git a/packages/core/actions/src/actions/list.ts b/packages/core/actions/src/actions/list.ts index cb7187e2d..b83175d4b 100644 --- a/packages/core/actions/src/actions/list.ts +++ b/packages/core/actions/src/actions/list.ts @@ -3,6 +3,7 @@ import { Context } from '..'; import { getRepositoryFromParams, pageArgsToLimitArgs } from '../utils'; import { DEFAULT_PAGE, DEFAULT_PER_PAGE } from '../constants'; import { Op, QueryTypes } from 'sequelize'; +import qs from 'querystring'; function totalPage(total, pageSize): number { return Math.ceil(total / pageSize); @@ -11,6 +12,32 @@ function totalPage(total, pageSize): number { function findArgs(ctx: Context) { const resourceName = ctx.action.resourceName; const params = ctx.action.params; + // 处理 sort 字段 + const includeSort = params.sort?.filter((item) => item.split('.').length > 1) ?? []; + const sortItems = []; + includeSort.forEach((sort) => { + const parts = sort[0] === '-' ? sort.slice(1, sort.length).split('.') : sort.split('.'); + const prefix = parts.slice(0, -1).join('.'); + const sign = sort[0] === '-' ? '-' : ''; + const sortItem = sign + parts.slice(-1).join('.'); + const findIndex = sortItems.findIndex((item) => item.prefix === prefix); + if (findIndex !== -1) { + sortItems[findIndex].items.push(sortItem); + } else { + sortItems.push({ + prefix, + sortItems: [sortItem], + }); + } + }); + sortItems.forEach((item) => { + const i = params.appends?.findIndex((append) => append === item.prefix) ?? -1; + if (i !== -1) { + params.appends[i] = `${item.prefix}(${qs.stringify({ sort: item.sortItems })})`; + } + }); + params.sort = params.sort?.filter((item) => item.split('.').length == 1) ?? []; + if (params.tree) { const [collectionName, associationName] = resourceName.split('.'); const collection = ctx.db.getCollection(resourceName); @@ -81,10 +108,10 @@ async function listWithPagination(ctx: Context) { ) SELECT DISTINCT * FROM ( - SELECT * + SELECT * FROM tree1 UNION ALL - SELECT * + SELECT * FROM tree2 );`; const filterTreeDatas = await ctx.db.sequelize.query(query, { diff --git a/packages/core/database/src/__tests__/repository/find.test.ts b/packages/core/database/src/__tests__/repository/find.test.ts index 13af2511f..7588db56a 100644 --- a/packages/core/database/src/__tests__/repository/find.test.ts +++ b/packages/core/database/src/__tests__/repository/find.test.ts @@ -1,6 +1,7 @@ import { mockDatabase } from '../index'; import Database from '@nocobase/database'; import { Collection } from '../../collection'; +import qs from 'qs'; describe('find with associations', () => { let db: Database; @@ -412,6 +413,63 @@ describe('find with associations', () => { expect(findResult[0].get('name')).toEqual('u1'); }); + it('should find with associations with sort params', async () => { + const User = db.collection({ + name: 'users', + fields: [ + { type: 'string', name: 'name' }, + { + type: 'hasMany', + name: 'posts', + }, + ], + }); + + const Post = db.collection({ + name: 'posts', + fields: [ + { type: 'string', name: 'title' }, + { type: 'belongsTo', name: 'user' }, + ], + }); + + await db.sync(); + + await User.repository.create({ + values: [ + { + name: 'u1', + posts: [ + { + title: 'u1p1', + }, + { + title: 'u1p2', + }, + ], + }, + { + name: 'u2', + posts: [ + { + title: 'u2p1', + }, + { + title: 'u2p2', + }, + ], + }, + ], + }); + + const appendArgs = [`posts(${qs.stringify({ sort: ['-id'] })})`]; + const users = await User.repository.find({ + appends: appendArgs, + }); + + expect(users[0].get('name')).toEqual('u1'); + expect(users[0].get('posts')[0].get('title')).toEqual('u1p2'); + }); }); describe('repository find', () => { diff --git a/packages/core/database/src/eager-loading/eager-loading-tree.ts b/packages/core/database/src/eager-loading/eager-loading-tree.ts index a0ea7b640..5ae9bcaa1 100644 --- a/packages/core/database/src/eager-loading/eager-loading-tree.ts +++ b/packages/core/database/src/eager-loading/eager-loading-tree.ts @@ -257,6 +257,20 @@ export class EagerLoadingTree { const association = node.association; const associationType = association.associationType; + let params: any = {}; + + const otherFindOptions = lodash.pick(node.includeOption, ['sort']) || {}; + + const collection = this.db.modelCollection.get(node.model); + + if (collection && !lodash.isEmpty(otherFindOptions)) { + const parser = new OptionsParser(otherFindOptions, { + collection, + }); + + params = parser.toSequelizeParams(); + } + if (associationType == 'HasOne' || associationType == 'HasMany') { const foreignKey = association.foreignKey; const foreignKeyValues = node.parent.instances.map((instance) => instance.get(association.sourceKey)); @@ -271,7 +285,7 @@ export class EagerLoadingTree { const findOptions = { where, attributes: node.attributes, - order: orderOption(association), + order: params.order || orderOption(association), transaction, }; @@ -358,7 +372,7 @@ export class EagerLoadingTree { }, }, ], - order: orderOption(association), + order: params.order || orderOption(association), }); } }