mirror of
https://github.com/nocobase/nocobase
synced 2024-11-15 07:25:15 +00:00
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:
parent
4decab86be
commit
5662509f4c
@ -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');
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
@ -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({});
|
||||||
});
|
});
|
||||||
});
|
});
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 => {
|
||||||
|
Loading…
Reference in New Issue
Block a user