From 5662509f4cd7e7831b3a6462a72beac3104d7e10 Mon Sep 17 00:00:00 2001 From: Junyi Date: Sat, 12 Dec 2020 16:38:08 +0800 Subject: [PATCH] Feature/sort (#38) * refactor: change sort strategy from offset to targetId * fix: remove unnecessary query to optimize performance * refactor: change sort api to allow object * refactor: change function member positions * fix: test case names * fix: static to instance --- packages/actions/src/__tests__/sort.test.ts | 65 ++++++++++- packages/actions/src/actions/common.ts | 103 ++++++++++-------- ...Where.test.ts => getValuesByFieldNames.ts} | 6 +- packages/database/src/fields/field-types.ts | 34 +++--- packages/database/src/model.ts | 4 +- 5 files changed, 144 insertions(+), 68 deletions(-) rename packages/database/src/__tests__/model/{getScopeWhere.test.ts => getValuesByFieldNames.ts} (82%) diff --git a/packages/actions/src/__tests__/sort.test.ts b/packages/actions/src/__tests__/sort.test.ts index b5a6479aab..7d864d6b01 100644 --- a/packages/actions/src/__tests__/sort.test.ts +++ b/packages/actions/src/__tests__/sort.test.ts @@ -90,7 +90,7 @@ describe('get', () => { .post('/posts:sort/1') .send({ field: 'sort', - targetId: 2, + target: { id: 2 }, }); const Post = db.getModel('posts'); @@ -117,7 +117,7 @@ describe('get', () => { .post('/posts:sort/2') .send({ field: 'sort', - targetId: 1, + target: { id: 1 }, }); const Post = db.getModel('posts'); @@ -144,7 +144,7 @@ describe('get', () => { .post('/posts:sort/1') .send({ field: 'sort', - targetId: 10, + target: { id: 10 }, }); const Post = db.getModel('posts'); @@ -173,7 +173,7 @@ describe('get', () => { .post('/posts:sort/2') .send({ field: 'sort_in_status', - targetId: 8, + target: { id: 8 }, }); const Post = db.getModel('posts'); @@ -198,7 +198,7 @@ describe('get', () => { .post('/posts:sort/1') .send({ field: 'sort_in_status', - targetId: 8, + target: { id: 8 }, }); const Post = db.getModel('posts'); @@ -218,6 +218,59 @@ describe('get', () => { { id: 10, sort_in_status: 6 } ]); }); + + it('move id=1 to new empty list of scope', async () => { + await agent + .post('/posts:sort/1') + .send({ + field: 'sort_in_status', + target: { status: 'archived' }, + }); + + const Post = db.getModel('posts'); + const posts = await Post.findAll({ + attributes: ['id', 'sort_in_status'], + order: [['id', 'ASC']] + }); + expect(posts.map(item => item.get())).toEqual([ + { id: 1, sort_in_status: 1 }, + { id: 2, sort_in_status: 1 }, + { id: 3, sort_in_status: 2 }, + { id: 4, sort_in_status: 2 }, + { id: 5, sort_in_status: 3 }, + { id: 6, sort_in_status: 3 }, + { id: 7, sort_in_status: 4 }, + { id: 8, sort_in_status: 4 }, + { id: 9, sort_in_status: 5 }, + { id: 10, sort_in_status: 5 } + ]); + }); + + it('move id=1 to scope without target primary key', async () => { + await agent + .post('/posts:sort/1') + .send({ + field: 'sort_in_status', + target: { status: 'publish' }, + }); + + const Post = db.getModel('posts'); + const posts = await Post.findAll({ + where: { + status: 'publish' + }, + attributes: ['id', 'sort_in_status'], + order: [['id', 'ASC']] + }); + expect(posts.map(item => item.get())).toEqual([ + { id: 1, sort_in_status: 6 }, + { id: 2, sort_in_status: 1 }, + { id: 4, sort_in_status: 2 }, + { id: 6, sort_in_status: 3 }, + { id: 8, sort_in_status: 4 }, + { id: 10, sort_in_status: 5 } + ]); + }); }); describe('associations', () => { @@ -227,7 +280,7 @@ describe('get', () => { .post('/users/1/posts:sort/1') .send({ field: 'sort_in_user', - targetId: 3, + target: { id: 3 }, }); const Post = db.getModel('posts'); diff --git a/packages/actions/src/actions/common.ts b/packages/actions/src/actions/common.ts index b325203328..a7c9d140db 100644 --- a/packages/actions/src/actions/common.ts +++ b/packages/actions/src/actions/common.ts @@ -390,8 +390,8 @@ export async function sort(ctx: Context, next: Next) { const Model = ctx.db.getModel(resourceName); const table = ctx.db.getTable(resourceName); - const { field, targetId } = values; - if (!values.field || typeof targetId === 'undefined') { + const { field, target } = values; + if (!values.field || typeof target === 'undefined') { return next(); } const sortField = table.getField(field); @@ -420,60 +420,77 @@ export async function sort(ctx: Context, next: Next) { await transaction.rollback(); throw new Error(`resource(${resourceKey}) does not exist`); } + const sourceScopeWhere = source.getValuesByFieldNames(scope); - const target = await Model.findByPk(targetId, { transaction }); + let targetScopeWhere: any; + let targetObject; + const { [primaryKeyAttribute]: targetId } = target; + if (targetId) { + targetObject = await Model.findByPk(targetId, { transaction }); - if (!target) { - await transaction.rollback(); - throw new Error(`resource(${targetId}) does not exist`); + if (!targetObject) { + await transaction.rollback(); + throw new Error(`resource(${targetId}) does not exist`); + } + + targetScopeWhere = targetObject.getValuesByFieldNames(scope); + } else { + targetScopeWhere = { ...sourceScopeWhere, ...target }; } - const sourceScopeWhere = source.getScopeWhere(scope); - const targetScopeWhere = target.getScopeWhere(scope); const sameScope = whereCompare(sourceScopeWhere, targetScopeWhere); - let increment; - const updateWhere = { ...targetScopeWhere }; - if (sameScope) { - const direction = source[sortAttr] < target[sortAttr] ? { - sourceOp: Op.gt, - targetOp: Op.lte, - increment: -1 - } : { - sourceOp: Op.lt, - targetOp: Op.gte, - increment: 1 - }; + const updates = { ...targetScopeWhere }; + if (targetObject) { + let increment: number; + const updateWhere = { ...targetScopeWhere }; + if (sameScope) { + const direction = source[sortAttr] < targetObject[sortAttr] ? { + sourceOp: Op.gt, + targetOp: Op.lte, + increment: -1 + } : { + sourceOp: Op.lt, + targetOp: Op.gte, + increment: 1 + }; - increment = direction.increment; + increment = direction.increment; - Object.assign(updateWhere, { - [sortAttr]: { - [direction.sourceOp]: source[sortAttr], - [direction.targetOp]: target[sortAttr] - } + Object.assign(updateWhere, { + [sortAttr]: { + [direction.sourceOp]: source[sortAttr], + [direction.targetOp]: targetObject[sortAttr] + } + }); + } else { + increment = 1; + Object.assign(updateWhere, { + [sortAttr]: { + [Op.gte]: targetObject[sortAttr] + } + }); + } + + await Model.increment(sortAttr, { + by: increment, + where: updateWhere, + transaction + }); + + Object.assign(updates, { + [sortAttr]: targetObject[sortAttr] }); } else { - increment = 1; - Object.assign(updateWhere, { - [sortAttr]: { - [Op.gte]: target[sortAttr] - } + Object.assign(updates, { + [sortAttr]: await sortField.getNextValue({ + where: targetScopeWhere, + transaction + }) }); } - await Model.increment(sortAttr, { - by: increment, - where: updateWhere, - transaction - }); - - await source.update({ - [sortAttr]: target[sortAttr], - ...targetScopeWhere - }, { - transaction - }); + await source.update(updates, { transaction }); await transaction.commit(); diff --git a/packages/database/src/__tests__/model/getScopeWhere.test.ts b/packages/database/src/__tests__/model/getValuesByFieldNames.ts similarity index 82% rename from packages/database/src/__tests__/model/getScopeWhere.test.ts rename to packages/database/src/__tests__/model/getValuesByFieldNames.ts index 1a84b040e9..0ccfb1ec37 100644 --- a/packages/database/src/__tests__/model/getScopeWhere.test.ts +++ b/packages/database/src/__tests__/model/getValuesByFieldNames.ts @@ -3,7 +3,7 @@ import Database from '../../database'; -describe('getScopeWhere', () => { +describe('getValuesByFieldNames', () => { let db: Database; beforeEach(async () => { @@ -25,14 +25,14 @@ describe('getScopeWhere', () => { it('exist column', async () => { const Post = db.getModel('posts'); const post = await Post.create({ status: 'published' }); - const where = post.getScopeWhere(['status']); + const where = post.getScopedValues(['status']); expect(where).toEqual({ status: 'published' }); }); it('non-exist column', async () => { const Post = db.getModel('posts'); const post = await Post.create({}); - const where = post.getScopeWhere(['whatever']); + const where = post.getScopedValues(['whatever']); expect(where).toEqual({}); }); }); diff --git a/packages/database/src/fields/field-types.ts b/packages/database/src/fields/field-types.ts index 8ae69dfced..bff236a53c 100644 --- a/packages/database/src/fields/field-types.ts +++ b/packages/database/src/fields/field-types.ts @@ -697,24 +697,22 @@ export class SORT extends NUMBER { public readonly options: Options.SortOptions; static async beforeCreateHook(this: SORT, model, options) { - const { transaction } = options; - const Model = model.constructor; - const { name, scope = [], next = 'max' } = this.options; - const where = model.getScopeWhere(scope); - const extremum: number = await Model[next](name, { where, transaction }) || 0; - model.set(name, extremum + (next === 'max' ? 1 : -1)); + const { name, scope = [] } = this.options; + const extremum: number = await this.getNextValue({ + ...options, + where: model.getValuesByFieldNames(scope) + }); + model.set(name, extremum); } static async beforeBulkCreateHook(this: SORT, models, options) { const { transaction } = options; - const table = this.context.sourceTable; - const Model = table.getModel(); const { name, scope = [], next = 'max' } = this.options; // 如果未配置范围限定,则可以进行性能优化处理(常用情况)。 if (!scope.length) { - const extremum: number = await Model[next](name, { transaction }) || 0; + const extremum: number = await this.getNextValue({ where: {}, transaction }); models.forEach((model, i: number) => { - model.setDataValue(name, extremum + (i + 1) * (next === 'max' ? 1 : -1)); + model.setDataValue(name, extremum + i * (next === 'max' ? 1 : -1)); }); return; } @@ -722,7 +720,7 @@ export class SORT extends NUMBER { // 用于存放 where 条件与计算极值 const groups = new Map<{ [key: string]: any }, number>(); await models.reduce((promise, model) => promise.then(async () => { - const where = model.getScopeWhere(scope); + const where = model.getValuesByFieldNames(scope); let extremum: number; // 以 map 作为 key @@ -731,18 +729,18 @@ export class SORT extends NUMBER { for (combo of groups.keys()) { if (whereCompare(combo, where)) { // 如果找到的话则以之前储存的值作为基础极值 - extremum = groups.get(combo); + extremum = groups.get(combo) + (next === 'max' ? 1 : -1); break; } } // 如未找到组合 if (typeof extremum === 'undefined') { // 则使用 where 条件查询极值 - extremum = await Model[next](name, { where, transaction }) || 0; + extremum = await this.getNextValue({ where, transaction }); // 且使用 where 条件创建组合 combo = where; } - const nextValue = extremum + (next === 'max' ? 1 : -1); + const nextValue = extremum; // 设置数据行的排序值 model.setDataValue(name, nextValue); // 保存新的排序值为对应 where 组合的极值,以供下次计算 @@ -761,4 +759,12 @@ export class SORT extends NUMBER { public getDataType(): Function { return DataTypes.INTEGER; } + + public async getNextValue(this: SORT, { where, transaction }) { + const table = this.context.sourceTable; + const Model = table.getModel(); + const { name, next = 'max' } = this.options; + const extremum: number = await Model[next](name, { where, transaction }) || 0; + return extremum + (next === 'max' ? 1 : -1); + } } diff --git a/packages/database/src/model.ts b/packages/database/src/model.ts index 360fd1b19b..86e0ccd5a5 100644 --- a/packages/database/src/model.ts +++ b/packages/database/src/model.ts @@ -259,9 +259,9 @@ export abstract class Model extends SequelizeModel { return data; } - getScopeWhere(scope: string[] = []) { - const Model = this.constructor as ModelCtor; + getValuesByFieldNames(scope = []) { const table = this.database.getTable(this.constructor.name); + const Model = table.getModel(); const associations = table.getAssociations(); const where = {}; scope.forEach(col => {