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
This commit is contained in:
Junyi 2020-12-12 16:38:08 +08:00 committed by GitHub
parent 4decab86be
commit 5662509f4c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 144 additions and 68 deletions

View File

@ -90,7 +90,7 @@ describe('get', () => {
.post('/posts:sort/1') .post('/posts:sort/1')
.send({ .send({
field: 'sort', field: 'sort',
targetId: 2, target: { id: 2 },
}); });
const Post = db.getModel('posts'); const Post = db.getModel('posts');
@ -117,7 +117,7 @@ describe('get', () => {
.post('/posts:sort/2') .post('/posts:sort/2')
.send({ .send({
field: 'sort', field: 'sort',
targetId: 1, target: { id: 1 },
}); });
const Post = db.getModel('posts'); const Post = db.getModel('posts');
@ -144,7 +144,7 @@ describe('get', () => {
.post('/posts:sort/1') .post('/posts:sort/1')
.send({ .send({
field: 'sort', field: 'sort',
targetId: 10, target: { id: 10 },
}); });
const Post = db.getModel('posts'); const Post = db.getModel('posts');
@ -173,7 +173,7 @@ describe('get', () => {
.post('/posts:sort/2') .post('/posts:sort/2')
.send({ .send({
field: 'sort_in_status', field: 'sort_in_status',
targetId: 8, target: { id: 8 },
}); });
const Post = db.getModel('posts'); const Post = db.getModel('posts');
@ -198,7 +198,7 @@ describe('get', () => {
.post('/posts:sort/1') .post('/posts:sort/1')
.send({ .send({
field: 'sort_in_status', field: 'sort_in_status',
targetId: 8, target: { id: 8 },
}); });
const Post = db.getModel('posts'); const Post = db.getModel('posts');
@ -218,6 +218,59 @@ describe('get', () => {
{ id: 10, sort_in_status: 6 } { 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', () => { describe('associations', () => {
@ -227,7 +280,7 @@ describe('get', () => {
.post('/users/1/posts:sort/1') .post('/users/1/posts:sort/1')
.send({ .send({
field: 'sort_in_user', field: 'sort_in_user',
targetId: 3, target: { id: 3 },
}); });
const Post = db.getModel('posts'); const Post = db.getModel('posts');

View File

@ -390,8 +390,8 @@ export async function sort(ctx: Context, next: Next) {
const Model = ctx.db.getModel(resourceName); const Model = ctx.db.getModel(resourceName);
const table = ctx.db.getTable(resourceName); const table = ctx.db.getTable(resourceName);
const { field, targetId } = values; const { field, target } = values;
if (!values.field || typeof targetId === 'undefined') { if (!values.field || typeof target === 'undefined') {
return next(); return next();
} }
const sortField = table.getField(field); const sortField = table.getField(field);
@ -420,60 +420,77 @@ export async function sort(ctx: Context, next: Next) {
await transaction.rollback(); await transaction.rollback();
throw new Error(`resource(${resourceKey}) does not exist`); 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) { if (!targetObject) {
await transaction.rollback(); await transaction.rollback();
throw new Error(`resource(${targetId}) does not exist`); 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); const sameScope = whereCompare(sourceScopeWhere, targetScopeWhere);
let increment; const updates = { ...targetScopeWhere };
const updateWhere = { ...targetScopeWhere }; if (targetObject) {
if (sameScope) { let increment: number;
const direction = source[sortAttr] < target[sortAttr] ? { const updateWhere = { ...targetScopeWhere };
sourceOp: Op.gt, if (sameScope) {
targetOp: Op.lte, const direction = source[sortAttr] < targetObject[sortAttr] ? {
increment: -1 sourceOp: Op.gt,
} : { targetOp: Op.lte,
sourceOp: Op.lt, increment: -1
targetOp: Op.gte, } : {
increment: 1 sourceOp: Op.lt,
}; targetOp: Op.gte,
increment: 1
};
increment = direction.increment; increment = direction.increment;
Object.assign(updateWhere, { Object.assign(updateWhere, {
[sortAttr]: { [sortAttr]: {
[direction.sourceOp]: source[sortAttr], [direction.sourceOp]: source[sortAttr],
[direction.targetOp]: target[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 { } else {
increment = 1; Object.assign(updates, {
Object.assign(updateWhere, { [sortAttr]: await sortField.getNextValue({
[sortAttr]: { where: targetScopeWhere,
[Op.gte]: target[sortAttr] transaction
} })
}); });
} }
await Model.increment(sortAttr, { await source.update(updates, { transaction });
by: increment,
where: updateWhere,
transaction
});
await source.update({
[sortAttr]: target[sortAttr],
...targetScopeWhere
}, {
transaction
});
await transaction.commit(); await transaction.commit();

View File

@ -3,7 +3,7 @@ import Database from '../../database';
describe('getScopeWhere', () => { describe('getValuesByFieldNames', () => {
let db: Database; let db: Database;
beforeEach(async () => { beforeEach(async () => {
@ -25,14 +25,14 @@ describe('getScopeWhere', () => {
it('exist column', async () => { it('exist column', async () => {
const Post = db.getModel('posts'); const Post = db.getModel('posts');
const post = await Post.create({ status: 'published' }); const post = await Post.create({ status: 'published' });
const where = post.getScopeWhere(['status']); const where = post.getScopedValues(['status']);
expect(where).toEqual({ status: 'published' }); expect(where).toEqual({ status: 'published' });
}); });
it('non-exist column', async () => { it('non-exist column', async () => {
const Post = db.getModel('posts'); const Post = db.getModel('posts');
const post = await Post.create({}); const post = await Post.create({});
const where = post.getScopeWhere(['whatever']); const where = post.getScopedValues(['whatever']);
expect(where).toEqual({}); expect(where).toEqual({});
}); });
}); });

View File

@ -697,24 +697,22 @@ export class SORT extends NUMBER {
public readonly options: Options.SortOptions; public readonly options: Options.SortOptions;
static async beforeCreateHook(this: SORT, model, options) { static async beforeCreateHook(this: SORT, model, options) {
const { transaction } = options; const { name, scope = [] } = this.options;
const Model = model.constructor; const extremum: number = await this.getNextValue({
const { name, scope = [], next = 'max' } = this.options; ...options,
const where = model.getScopeWhere(scope); where: model.getValuesByFieldNames(scope)
const extremum: number = await Model[next](name, { where, transaction }) || 0; });
model.set(name, extremum + (next === 'max' ? 1 : -1)); model.set(name, extremum);
} }
static async beforeBulkCreateHook(this: SORT, models, options) { static async beforeBulkCreateHook(this: SORT, models, options) {
const { transaction } = options; const { transaction } = options;
const table = this.context.sourceTable;
const Model = table.getModel();
const { name, scope = [], next = 'max' } = this.options; const { name, scope = [], next = 'max' } = this.options;
// 如果未配置范围限定,则可以进行性能优化处理(常用情况)。 // 如果未配置范围限定,则可以进行性能优化处理(常用情况)。
if (!scope.length) { 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) => { 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; return;
} }
@ -722,7 +720,7 @@ export class SORT extends NUMBER {
// 用于存放 where 条件与计算极值 // 用于存放 where 条件与计算极值
const groups = new Map<{ [key: string]: any }, number>(); const groups = new Map<{ [key: string]: any }, number>();
await models.reduce((promise, model) => promise.then(async () => { await models.reduce((promise, model) => promise.then(async () => {
const where = model.getScopeWhere(scope); const where = model.getValuesByFieldNames(scope);
let extremum: number; let extremum: number;
// 以 map 作为 key // 以 map 作为 key
@ -731,18 +729,18 @@ export class SORT extends NUMBER {
for (combo of groups.keys()) { for (combo of groups.keys()) {
if (whereCompare(combo, where)) { if (whereCompare(combo, where)) {
// 如果找到的话则以之前储存的值作为基础极值 // 如果找到的话则以之前储存的值作为基础极值
extremum = groups.get(combo); extremum = groups.get(combo) + (next === 'max' ? 1 : -1);
break; break;
} }
} }
// 如未找到组合 // 如未找到组合
if (typeof extremum === 'undefined') { if (typeof extremum === 'undefined') {
// 则使用 where 条件查询极值 // 则使用 where 条件查询极值
extremum = await Model[next](name, { where, transaction }) || 0; extremum = await this.getNextValue({ where, transaction });
// 且使用 where 条件创建组合 // 且使用 where 条件创建组合
combo = where; combo = where;
} }
const nextValue = extremum + (next === 'max' ? 1 : -1); const nextValue = extremum;
// 设置数据行的排序值 // 设置数据行的排序值
model.setDataValue(name, nextValue); model.setDataValue(name, nextValue);
// 保存新的排序值为对应 where 组合的极值,以供下次计算 // 保存新的排序值为对应 where 组合的极值,以供下次计算
@ -761,4 +759,12 @@ export class SORT extends NUMBER {
public getDataType(): Function { public getDataType(): Function {
return DataTypes.INTEGER; 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);
}
} }

View File

@ -259,9 +259,9 @@ export abstract class Model extends SequelizeModel {
return data; return data;
} }
getScopeWhere(scope: string[] = []) { getValuesByFieldNames(scope = []) {
const Model = this.constructor as ModelCtor<Model>;
const table = this.database.getTable(this.constructor.name); const table = this.database.getTable(this.constructor.name);
const Model = table.getModel();
const associations = table.getAssociations(); const associations = table.getAssociations();
const where = {}; const where = {};
scope.forEach(col => { scope.forEach(col => {