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')
.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');

View File

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

View File

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

View File

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

View File

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