From d2ff34e17e1e42fd75922ec30e40431b6ac9a016 Mon Sep 17 00:00:00 2001 From: aaaaaajie Date: Sun, 30 Mar 2025 21:58:42 +0800 Subject: [PATCH] feat: field sort plugin in mssql --- .../src/server/__tests__/move-action.test.ts | 116 +++++++++--------- .../server/__tests__/sort-collection.test.ts | 68 +++++----- .../src/server/__tests__/sort.test.ts | 40 +++--- .../plugin-field-sort/src/server/action.ts | 4 +- .../src/server/sort-field.ts | 19 +++ 5 files changed, 140 insertions(+), 107 deletions(-) diff --git a/packages/plugins/@nocobase/plugin-field-sort/src/server/__tests__/move-action.test.ts b/packages/plugins/@nocobase/plugin-field-sort/src/server/__tests__/move-action.test.ts index 8c85622d07..037af2ff54 100644 --- a/packages/plugins/@nocobase/plugin-field-sort/src/server/__tests__/move-action.test.ts +++ b/packages/plugins/@nocobase/plugin-field-sort/src/server/__tests__/move-action.test.ts @@ -27,6 +27,10 @@ describe('sort action', () => { return api.destroy(); }); + function parseSortValue(value) { + return api.db.options.dialect === 'mssql' ? value.toString() : value; + } + describe('associations', () => { let UserCollection: Collection; @@ -204,19 +208,19 @@ describe('sort action', () => { data: [ { title: 't2', - sort: 1, + sort: parseSortValue(1), }, { title: 't3', - sort: 2, + sort: parseSortValue(2), }, { title: 't1', - sort: 3, + sort: parseSortValue(3), }, { title: 't4', - sort: 4, + sort: parseSortValue(4), }, ], }); @@ -239,19 +243,19 @@ describe('sort action', () => { data: [ { title: 't3', - sort: 1, + sort: parseSortValue(1), }, { title: 't1', - sort: 2, + sort: parseSortValue(2), }, { title: 't2', - sort: 3, + sort: parseSortValue(3), }, { title: 't4', - sort: 4, + sort: parseSortValue(4), }, ], }); @@ -274,19 +278,19 @@ describe('sort action', () => { data: [ { title: 't2', - sort2: 1, + sort2: parseSortValue(1), }, { title: 't3', - sort2: 2, + sort2: parseSortValue(2), }, { title: 't1', - sort2: 3, + sort2: parseSortValue(3), }, { title: 't4', - sort2: 4, + sort2: parseSortValue(4), }, ], }); @@ -308,19 +312,19 @@ describe('sort action', () => { data: [ { title: 't3', - sort: 0, + sort: parseSortValue(0), }, { title: 't1', - sort: 1, + sort: parseSortValue(1), }, { title: 't2', - sort: 2, + sort: parseSortValue(2), }, { title: 't4', - sort: 4, + sort: parseSortValue(4), }, ], }); @@ -492,15 +496,15 @@ describe('sort action', () => { data: [ { title: 't12', - sort: 2, + sort: parseSortValue(2), }, { title: 't13', - sort: 3, + sort: parseSortValue(3), }, { title: 't14', - sort: 4, + sort: parseSortValue(4), }, ], }); @@ -517,23 +521,23 @@ describe('sort action', () => { data: [ { title: 't21', - sort: 1, + sort: parseSortValue(1), }, { title: 't11', - sort: 2, + sort: parseSortValue(2), }, { title: 't22', - sort: 3, + sort: parseSortValue(3), }, { title: 't23', - sort: 4, + sort: parseSortValue(4), }, { title: 't24', - sort: 5, + sort: parseSortValue(5), }, ], }); @@ -558,15 +562,15 @@ describe('sort action', () => { data: [ { title: 't12', - sort: 2, + sort: parseSortValue(2), }, { title: 't13', - sort: 3, + sort: parseSortValue(3), }, { title: 't14', - sort: 4, + sort: parseSortValue(4), }, ], }); @@ -581,23 +585,23 @@ describe('sort action', () => { data: [ { title: 't21', - sort: 1, + sort: parseSortValue(1), }, { title: 't22', - sort: 2, + sort: parseSortValue(2), }, { title: 't11', - sort: 3, + sort: parseSortValue(3), }, { title: 't23', - sort: 4, + sort: parseSortValue(4), }, { title: 't24', - sort: 5, + sort: parseSortValue(5), }, ], }); @@ -619,23 +623,23 @@ describe('sort action', () => { data: [ { title: 't11', - sort: 1, + sort: parseSortValue(1), }, { title: 't22', - sort: 2, + sort: parseSortValue(2), }, { title: 't12', - sort: 3, + sort: parseSortValue(3), }, { title: 't13', - sort: 4, + sort: parseSortValue(4), }, { title: 't14', - sort: 5, + sort: parseSortValue(5), }, ], }); @@ -650,15 +654,15 @@ describe('sort action', () => { data: [ { title: 't21', - sort: 1, + sort: parseSortValue(1), }, { title: 't23', - sort: 3, + sort: parseSortValue(3), }, { title: 't24', - sort: 4, + sort: parseSortValue(4), }, ], }); @@ -681,23 +685,23 @@ describe('sort action', () => { data: [ { title: 't11', - sort: 1, + sort: parseSortValue(1), }, { title: 't12', - sort: 2, + sort: parseSortValue(2), }, { title: 't22', - sort: 3, + sort: parseSortValue(3), }, { title: 't13', - sort: 4, + sort: parseSortValue(4), }, { title: 't14', - sort: 5, + sort: parseSortValue(5), }, ], }); @@ -712,15 +716,15 @@ describe('sort action', () => { data: [ { title: 't21', - sort: 1, + sort: parseSortValue(1), }, { title: 't23', - sort: 3, + sort: parseSortValue(3), }, { title: 't24', - sort: 4, + sort: parseSortValue(4), }, ], }); @@ -747,15 +751,15 @@ describe('sort action', () => { data: [ { title: 't12', - sort: 2, + sort: parseSortValue(2), }, { title: 't13', - sort: 3, + sort: parseSortValue(3), }, { title: 't14', - sort: 4, + sort: parseSortValue(4), }, ], }); @@ -770,23 +774,23 @@ describe('sort action', () => { data: [ { title: 't21', - sort: 1, + sort: parseSortValue(1), }, { title: 't22', - sort: 2, + sort: parseSortValue(2), }, { title: 't23', - sort: 3, + sort: parseSortValue(3), }, { title: 't24', - sort: 4, + sort: parseSortValue(4), }, { title: 't11', - sort: 5, + sort: parseSortValue(5), }, ], }); diff --git a/packages/plugins/@nocobase/plugin-field-sort/src/server/__tests__/sort-collection.test.ts b/packages/plugins/@nocobase/plugin-field-sort/src/server/__tests__/sort-collection.test.ts index a89c7c9199..1b6da3e041 100644 --- a/packages/plugins/@nocobase/plugin-field-sort/src/server/__tests__/sort-collection.test.ts +++ b/packages/plugins/@nocobase/plugin-field-sort/src/server/__tests__/sort-collection.test.ts @@ -30,6 +30,10 @@ describe('sort collections', () => { await app.destroy(); }); + function parseSortValue(value) { + return db.options.dialect === 'mssql' ? value.toString() : value; + } + describe('sort collection', () => { beforeEach(async () => { Post = app.db.collection({ @@ -68,7 +72,7 @@ describe('sort collections', () => { await db.sync(); const instance = await model.create(); expect(model.rawAttributes['sort']).toBeDefined(); - expect(instance.get('sort')).toBe(1); + expect(instance.get('sort')).toBe(parseSortValue(1)); }); test('sortable=string', async () => { @@ -82,7 +86,7 @@ describe('sort collections', () => { await db.sync(); const instance = await model.create(); expect(model.rawAttributes['order']).toBeDefined(); - expect(instance.get('order')).toBe(1); + expect(instance.get('order')).toBe(parseSortValue(1)); }); test('sortable=object', async () => { @@ -102,10 +106,10 @@ describe('sort collections', () => { const t3 = await Test.model.create({ status: 'draft' }); const t4 = await Test.model.create({ status: 'draft' }); - expect(t1.get('sort')).toBe(1); - expect(t2.get('sort')).toBe(2); - expect(t3.get('sort')).toBe(1); - expect(t4.get('sort')).toBe(2); + expect(t1.get('sort')).toBe(parseSortValue(1)); + expect(t2.get('sort')).toBe(parseSortValue(2)); + expect(t3.get('sort')).toBe(parseSortValue(1)); + expect(t4.get('sort')).toBe(parseSortValue(2)); }); test('forward insert', async () => { @@ -133,11 +137,11 @@ describe('sort collections', () => { }); expect(results).toEqual([ - { title: 't1', sort: 1 }, - { title: 't3', sort: 2 }, - { title: 't4', sort: 3 }, - { title: 't2', sort: 4 }, - { title: 't5', sort: 5 }, + { title: 't1', sort: parseSortValue(1) }, + { title: 't3', sort: parseSortValue(2) }, + { title: 't4', sort: parseSortValue(3) }, + { title: 't2', sort: parseSortValue(4) }, + { title: 't5', sort: parseSortValue(5) }, ]); }); @@ -166,11 +170,11 @@ describe('sort collections', () => { }); expect(results).toEqual([ - { title: 't1', sort: 1 }, - { title: 't4', sort: 2 }, - { title: 't2', sort: 3 }, - { title: 't3', sort: 4 }, - { title: 't5', sort: 5 }, + { title: 't1', sort: parseSortValue(1) }, + { title: 't4', sort: parseSortValue(2) }, + { title: 't2', sort: parseSortValue(3) }, + { title: 't3', sort: parseSortValue(4) }, + { title: 't5', sort: parseSortValue(5) }, ]); }); }); @@ -245,11 +249,11 @@ describe('sort collections', () => { }); expect(results).toEqual([ - { title: 's1:t1', sort: 1 }, - { title: 's1:t3', sort: 2 }, - { title: 's1:t4', sort: 3 }, - { title: 's1:t2', sort: 4 }, - { title: 's1:t5', sort: 5 }, + { title: 's1:t1', sort: parseSortValue(1) }, + { title: 's1:t3', sort: parseSortValue(2) }, + { title: 's1:t4', sort: parseSortValue(3) }, + { title: 's1:t2', sort: parseSortValue(4) }, + { title: 's1:t5', sort: parseSortValue(5) }, ]); const s2results = ( @@ -264,11 +268,11 @@ describe('sort collections', () => { }); expect(s2results).toEqual([ - { title: 's2:t1', sort: 1 }, - { title: 's2:t2', sort: 2 }, - { title: 's2:t3', sort: 3 }, - { title: 's2:t4', sort: 4 }, - { title: 's2:t5', sort: 5 }, + { title: 's2:t1', sort: parseSortValue(1) }, + { title: 's2:t2', sort: parseSortValue(2) }, + { title: 's2:t3', sort: parseSortValue(3) }, + { title: 's2:t4', sort: parseSortValue(4) }, + { title: 's2:t5', sort: parseSortValue(5) }, ]); }); @@ -319,12 +323,12 @@ describe('sort collections', () => { }); expect(results).toEqual([ - { title: 's2:t1', sort: 1 }, - { title: 's2:t2', sort: 2 }, - { title: 's1:t1', sort: 3 }, - { title: 's2:t3', sort: 4 }, - { title: 's2:t4', sort: 5 }, - { title: 's2:t5', sort: 6 }, + { title: 's2:t1', sort: parseSortValue(1) }, + { title: 's2:t2', sort: parseSortValue(2) }, + { title: 's1:t1', sort: parseSortValue(3) }, + { title: 's2:t3', sort: parseSortValue(4) }, + { title: 's2:t4', sort: parseSortValue(5) }, + { title: 's2:t5', sort: parseSortValue(6) }, ]); }); }); diff --git a/packages/plugins/@nocobase/plugin-field-sort/src/server/__tests__/sort.test.ts b/packages/plugins/@nocobase/plugin-field-sort/src/server/__tests__/sort.test.ts index 85ce42325a..47d78fad19 100644 --- a/packages/plugins/@nocobase/plugin-field-sort/src/server/__tests__/sort.test.ts +++ b/packages/plugins/@nocobase/plugin-field-sort/src/server/__tests__/sort.test.ts @@ -48,6 +48,10 @@ describe('sort field', () => { await app.destroy(); }); + function parseSortValue(value) { + return db.options.dialect === 'mssql' ? value.toString() : value; + } + describe('main data source', () => { it('should init with camelCase scope key', async () => { const Test = db.collection({ @@ -213,9 +217,9 @@ describe('sort field', () => { const records = await Test.repository.find({}); const r3 = records.find((r) => r.get('name') === 'r3'); - expect(r3.get('sort')).toBe(2); + expect(r3.get('sort')).toBe(parseSortValue(2)); const r5 = records.find((r) => r.get('name') === 'r5'); - expect(r5.get('sort')).toBe(1); + expect(r5.get('sort')).toBe(parseSortValue(1)); }); it('should init sorted value by createdAt when primaryKey not exists', async () => { @@ -249,11 +253,11 @@ describe('sort field', () => { await db.sync(); const test1 = await Test.model.create(); - expect(test1.sort).toBe(1); + expect(test1.sort).toBe(parseSortValue(1)); const test2 = await Test.model.create(); - expect(test2.sort).toBe(2); + expect(test2.sort).toBe(parseSortValue(2)); const test3 = await Test.model.create(); - expect(test3.sort).toBe(3); + expect(test3.sort).toBe(parseSortValue(3)); }); it('should init sort value on data already exits', async () => { @@ -292,7 +296,9 @@ describe('sort field', () => { const items = await db.getRepository('tests').find({ order: ['id'], }); - expect(items.map((item) => item.get('sort'))).toEqual([1, 2, 3]); + expect(items[0].get('sort')).toBe(parseSortValue(1)); + expect(items[1].get('sort')).toBe(parseSortValue(2)); + expect(items[2].get('sort')).toBe(parseSortValue(3)); }); test.skip('simultaneously create ', async () => { @@ -311,7 +317,7 @@ describe('sort field', () => { await Promise.all(promise); const tests = await Test.model.findAll(); const sortValues = tests.map((t) => t.get('sort')).sort(); - expect(sortValues).toEqual([1, 2, 3]); + expect(sortValues).toEqual(db.options.dialect === 'mssql' ? ['1', '2', '3'] : [1, 2, 3]); }); it('skip if sort value not empty', async () => { @@ -321,11 +327,11 @@ describe('sort field', () => { }); await db.sync(); const test1 = await Test.model.create({ sort: 3 }); - expect(test1.sort).toBe(3); + expect(test1.sort).toBe(parseSortValue(3)); const test2 = await Test.model.create(); - expect(test2.sort).toBe(4); + expect(test2.sort).toBe(parseSortValue(4)); const test3 = await Test.model.create(); - expect(test3.sort).toBe(5); + expect(test3.sort).toBe(parseSortValue(5)); }); it('scopeKey', async () => { @@ -343,16 +349,16 @@ describe('sort field', () => { const t3 = await Test.model.create({ status: 'draft' }); const t4 = await Test.model.create({ status: 'draft' }); - expect(t1.get('sort')).toBe(1); - expect(t2.get('sort')).toBe(2); - expect(t3.get('sort')).toBe(1); - expect(t4.get('sort')).toBe(2); + expect(t1.get('sort')).toBe(parseSortValue(1)); + expect(t2.get('sort')).toBe(parseSortValue(2)); + expect(t3.get('sort')).toBe(parseSortValue(1)); + expect(t4.get('sort')).toBe(parseSortValue(2)); t1.set('status', 'draft'); await t1.save(); await t1.reload(); - expect(t1.get('sort')).toBe(3); + expect(t1.get('sort')).toBe(parseSortValue(3)); }); }); @@ -374,8 +380,8 @@ describe('sort field', () => { const p2 = await anotherDB.getRepository('posts').create({ values: { title: 'p2' }, }); - expect(p1.sort).toBe(1); - expect(p2.sort).toBe(2); + expect(p1.sort).toBe(parseSortValue(1)); + expect(p2.sort).toBe(parseSortValue(2)); }); }); }); diff --git a/packages/plugins/@nocobase/plugin-field-sort/src/server/action.ts b/packages/plugins/@nocobase/plugin-field-sort/src/server/action.ts index f01435bc1d..1e93f5780d 100644 --- a/packages/plugins/@nocobase/plugin-field-sort/src/server/action.ts +++ b/packages/plugins/@nocobase/plugin-field-sort/src/server/action.ts @@ -132,8 +132,8 @@ export class SortableCollection { async sameScopeMove(sourceInstance: Model, targetInstance: Model, options: MoveOptions) { const fieldName = this.field.get('name'); - const sourceSort = sourceInstance.get(fieldName); - let targetSort = targetInstance.get(fieldName); + const sourceSort = Number(sourceInstance.get(fieldName)); + let targetSort = Number(targetInstance.get(fieldName)); if (options.insertAfter) { targetSort = targetSort + 1; diff --git a/packages/plugins/@nocobase/plugin-field-sort/src/server/sort-field.ts b/packages/plugins/@nocobase/plugin-field-sort/src/server/sort-field.ts index e2d1a320cb..edcc997a76 100644 --- a/packages/plugins/@nocobase/plugin-field-sort/src/server/sort-field.ts +++ b/packages/plugins/@nocobase/plugin-field-sort/src/server/sort-field.ts @@ -171,6 +171,25 @@ export class SortField extends Field { ${whereClause} ) AS ordered_table ON ${this.collection.quotedTableName()}.${quotedOrderField} = ordered_table.${quotedOrderField} SET ${this.collection.quotedTableName()}.${sortColumnName} = ordered_table.new_sequence_number; + `; + } else if (this.collection.db.inDialect('mssql')) { + // TODO: This MSSQL support is intended for external data sources + // Since the core database doesn't support MSSQL, this logic needs to be implemented through an extension mechanism + // Potential solutions: + // 1. Abstract database dialect-specific logic into separate modules + // 2. Consider implementing a dialect adapter pattern for better extensibility + sql = ` + WITH ordered_table AS ( + SELECT *, ROW_NUMBER() OVER (${ + scopeKey ? `PARTITION BY ${queryInterface.quoteIdentifier(scopeKey)}` : '' + } ORDER BY ${quotedOrderField}) AS new_sequence_number + FROM ${this.collection.quotedTableName()} + ${whereClause} + ) + UPDATE t + SET ${sortColumnName} = ot.new_sequence_number + FROM ${this.collection.quotedTableName()} t + INNER JOIN ordered_table ot ON t.${quotedOrderField} = ot.${quotedOrderField}; `; } await this.collection.db.sequelize.query(sql, {