json-rest-api 1.0.13 → 1.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/connectors_ideas/fastify.js +66 -68
- package/connectors_ideas/koa-plugin.js +50 -51
- package/eslint.config.js +13 -0
- package/examples/url-override-example.js +63 -63
- package/index.js +13 -13
- package/lib/enhanced-logger.js +51 -51
- package/lib/error-formatter.js +61 -61
- package/lib/rest-api-errors.js +28 -28
- package/package.json +9 -5
- package/plugins/core/bulk-operations-plugin.js +142 -145
- package/plugins/core/connectors/express-plugin.js +210 -217
- package/plugins/core/connectors/lib/busboy-detector.js +84 -84
- package/plugins/core/connectors/lib/formidable-detector.js +50 -50
- package/plugins/core/connectors/lib/request-helpers.js +74 -74
- package/plugins/core/file-handling-plugin.js +125 -125
- package/plugins/core/lib/anyapi/anyapi-registry.js +254 -254
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +287 -287
- package/plugins/core/lib/anyapi/schema-utils.js +101 -101
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +41 -41
- package/plugins/core/lib/dbTablesOperations.js +134 -135
- package/plugins/core/lib/fractional-positioning.js +74 -76
- package/plugins/core/lib/querying/knex-cross-table-search.js +320 -320
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +144 -144
- package/plugins/core/lib/querying/knex-pagination-helpers.js +164 -164
- package/plugins/core/lib/querying/knex-process-includes.js +50 -50
- package/plugins/core/lib/querying/knex-query-helpers-base.js +31 -31
- package/plugins/core/lib/querying/knex-query-helpers.js +451 -452
- package/plugins/core/lib/querying/knex-relationship-includes.js +1160 -1170
- package/plugins/core/lib/querying/knex-window-queries.js +89 -89
- package/plugins/core/lib/querying/url-helpers.js +35 -35
- package/plugins/core/lib/querying-writing/compile-schemas.js +97 -98
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +31 -31
- package/plugins/core/lib/querying-writing/database-capabilities.js +94 -94
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +76 -76
- package/plugins/core/lib/querying-writing/default-data-helpers.js +27 -27
- package/plugins/core/lib/querying-writing/field-utils.js +44 -42
- package/plugins/core/lib/querying-writing/knex-constants.js +15 -16
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +117 -115
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +41 -41
- package/plugins/core/lib/querying-writing/payload-validators.js +275 -275
- package/plugins/core/lib/querying-writing/schema-helpers.js +129 -129
- package/plugins/core/lib/querying-writing/scope-validations.js +143 -146
- package/plugins/core/lib/querying-writing/simplified-helpers.js +133 -129
- package/plugins/core/lib/storage/storage-adapter.js +68 -68
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +31 -31
- package/plugins/core/lib/writing/many-to-many-manipulations.js +81 -81
- package/plugins/core/lib/writing/relationship-processor.js +58 -58
- package/plugins/core/rest-api-access.js +8 -8
- package/plugins/core/rest-api-anyapi-knex-plugin.js +920 -920
- package/plugins/core/rest-api-cors-plugin.js +170 -171
- package/plugins/core/rest-api-knex-plugin.js +482 -493
- package/plugins/core/rest-api-multihome-plugin.js +247 -251
- package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +5 -5
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +20 -21
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +44 -44
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +23 -23
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +15 -15
- package/plugins/core/rest-api-plugin-methods/add-route.js +9 -9
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +5 -6
- package/plugins/core/rest-api-plugin-methods/common.js +295 -297
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +35 -37
- package/plugins/core/rest-api-plugin-methods/delete.js +57 -59
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +172 -171
- package/plugins/core/rest-api-plugin-methods/get-related.js +96 -96
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +16 -18
- package/plugins/core/rest-api-plugin-methods/get.js +86 -86
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +16 -17
- package/plugins/core/rest-api-plugin-methods/patch.js +63 -64
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +54 -55
- package/plugins/core/rest-api-plugin-methods/post.js +61 -62
- package/plugins/core/rest-api-plugin-methods/put.js +86 -87
- package/plugins/core/rest-api-plugin-methods/query.js +88 -88
- package/plugins/core/rest-api-plugin-methods/release.js +5 -6
- package/plugins/core/rest-api-plugin.js +76 -78
- package/plugins/core/rest-api-positioning-plugin.js +251 -252
- package/plugins/core/socketio-plugin.js +240 -236
- package/plugins/storage/local-storage.js +103 -103
- package/plugins/storage/s3-storage.js +41 -42
- package/quickTest.js +37 -37
- package/scripts/debug-anyapi-review-filter.mjs +18 -18
- package/scripts/inspect-anyapi-descriptor-country.mjs +10 -10
- package/scripts/inspect-anyapi-descriptor.mjs +10 -10
- package/scripts/run-docs.js +112 -113
- package/test-debug.js +28 -28
- package/tests/TEST_TEMPLATE.test.js +51 -51
- package/tests/anyapi-basic.test.js +42 -42
- package/tests/anyapi-cursor-pagination.test.js +48 -48
- package/tests/bulk-operations.test.js +208 -208
- package/tests/computed-fields.test.js +204 -204
- package/tests/cors.test.js +412 -413
- package/tests/curl.test.js +275 -276
- package/tests/custom-idproperty-relationships.test.js +266 -266
- package/tests/field-getters.test.js +132 -132
- package/tests/field-setters.test.js +110 -110
- package/tests/fixtures/api-configs.js +699 -699
- package/tests/helpers/storage-mode.js +32 -32
- package/tests/helpers/test-utils.js +76 -76
- package/tests/include-depth-validation.test.js +76 -76
- package/tests/includes.test.js +145 -145
- package/tests/multihome.test.js +175 -175
- package/tests/nested-includes.test.js +255 -255
- package/tests/pagination-cursor-multifield.test.js +164 -164
- package/tests/pagination-enhanced.test.js +225 -225
- package/tests/polymorphic-relationships.test.js +96 -96
- package/tests/polymorphic-simple.test.js +44 -44
- package/tests/positioning-advanced.test.js +226 -226
- package/tests/positioning-extended.test.js +399 -399
- package/tests/positioning-multifilter.test.js +204 -204
- package/tests/positioning.test.js +266 -266
- package/tests/queries.test.js +184 -184
- package/tests/query-limits.test.js +154 -155
- package/tests/relationship-endpoints.test.js +127 -127
- package/tests/relationships.test.js +178 -179
- package/tests/rest-api-access.test.js +86 -86
- package/tests/rest-api.test.js +103 -103
- package/tests/searchschema-merge.test.js +93 -93
- package/tests/socketio.test.js +326 -326
- package/tests/virtual-fields.test.js +164 -164
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { describe, it, before, after, beforeEach } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import knexLib from 'knex'
|
|
4
|
-
import { cleanTables } from './helpers/test-utils.js'
|
|
5
|
-
import { createPositioningApi } from './fixtures/api-configs.js'
|
|
6
|
-
import { PositioningPlugin } from '../plugins/core/rest-api-positioning-plugin.js'
|
|
1
|
+
import { describe, it, before, after, beforeEach } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import knexLib from 'knex'
|
|
4
|
+
import { cleanTables } from './helpers/test-utils.js'
|
|
5
|
+
import { createPositioningApi } from './fixtures/api-configs.js'
|
|
6
|
+
import { PositioningPlugin } from '../plugins/core/rest-api-positioning-plugin.js'
|
|
7
7
|
|
|
8
8
|
// Create Knex instance for tests
|
|
9
9
|
const knex = knexLib({
|
|
@@ -12,486 +12,486 @@ const knex = knexLib({
|
|
|
12
12
|
filename: ':memory:'
|
|
13
13
|
},
|
|
14
14
|
useNullAsDefault: true
|
|
15
|
-
})
|
|
15
|
+
})
|
|
16
16
|
|
|
17
17
|
// API instance that persists across tests
|
|
18
|
-
let api
|
|
18
|
+
let api
|
|
19
19
|
|
|
20
20
|
describe('Extended Positioning Plugin Tests', { timeout: 30000 }, () => {
|
|
21
21
|
before(async () => {
|
|
22
22
|
// Initialize API once
|
|
23
|
-
api = await createPositioningApi(knex)
|
|
24
|
-
|
|
23
|
+
api = await createPositioningApi(knex)
|
|
24
|
+
|
|
25
25
|
// Install positioning plugin with default configuration
|
|
26
26
|
await api.use(PositioningPlugin, {
|
|
27
27
|
field: 'position',
|
|
28
28
|
filters: ['category'],
|
|
29
29
|
defaultPosition: 'last',
|
|
30
30
|
autoIndex: true
|
|
31
|
-
})
|
|
32
|
-
})
|
|
33
|
-
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
34
|
after(async () => {
|
|
35
|
-
await knex.destroy()
|
|
36
|
-
})
|
|
37
|
-
|
|
35
|
+
await knex.destroy()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
38
|
beforeEach(async () => {
|
|
39
39
|
await cleanTables(knex, [
|
|
40
40
|
'positioning_categories',
|
|
41
41
|
'positioning_tasks',
|
|
42
42
|
'positioning_projects',
|
|
43
43
|
'positioning_items'
|
|
44
|
-
])
|
|
45
|
-
})
|
|
44
|
+
])
|
|
45
|
+
})
|
|
46
46
|
|
|
47
47
|
describe('Edge Cases for Positioning', () => {
|
|
48
48
|
it('should handle positioning multiple items before the same target in sequence', async () => {
|
|
49
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
50
|
-
|
|
49
|
+
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
50
|
+
|
|
51
51
|
// Create initial task
|
|
52
52
|
const task1 = await api.resources.tasks.post({
|
|
53
53
|
title: 'Task 1',
|
|
54
54
|
category: category.id
|
|
55
|
-
})
|
|
56
|
-
|
|
55
|
+
})
|
|
56
|
+
|
|
57
57
|
// Position multiple tasks before task1
|
|
58
58
|
const taskA = await api.resources.tasks.post({
|
|
59
59
|
title: 'Task A',
|
|
60
60
|
category: category.id,
|
|
61
61
|
beforeId: task1.id
|
|
62
|
-
})
|
|
63
|
-
|
|
62
|
+
})
|
|
63
|
+
|
|
64
64
|
const taskB = await api.resources.tasks.post({
|
|
65
65
|
title: 'Task B',
|
|
66
66
|
category: category.id,
|
|
67
67
|
beforeId: task1.id
|
|
68
|
-
})
|
|
69
|
-
|
|
68
|
+
})
|
|
69
|
+
|
|
70
70
|
const taskC = await api.resources.tasks.post({
|
|
71
71
|
title: 'Task C',
|
|
72
72
|
category: category.id,
|
|
73
73
|
beforeId: task1.id
|
|
74
|
-
})
|
|
75
|
-
|
|
74
|
+
})
|
|
75
|
+
|
|
76
76
|
// Query and verify order
|
|
77
77
|
const { data: tasks } = await api.resources.tasks.query({
|
|
78
78
|
queryParams: {
|
|
79
79
|
filters: { category: category.id },
|
|
80
80
|
sort: ['position']
|
|
81
81
|
}
|
|
82
|
-
})
|
|
83
|
-
|
|
82
|
+
})
|
|
83
|
+
|
|
84
84
|
// Should be: A, B, C, 1 (in creation order when positioned before same target)
|
|
85
|
-
assert.equal(tasks.length, 4)
|
|
86
|
-
assert.equal(tasks[0].title, 'Task A')
|
|
87
|
-
assert.equal(tasks[1].title, 'Task B')
|
|
88
|
-
assert.equal(tasks[2].title, 'Task C')
|
|
89
|
-
assert.equal(tasks[3].title, 'Task 1')
|
|
90
|
-
|
|
85
|
+
assert.equal(tasks.length, 4)
|
|
86
|
+
assert.equal(tasks[0].title, 'Task A')
|
|
87
|
+
assert.equal(tasks[1].title, 'Task B')
|
|
88
|
+
assert.equal(tasks[2].title, 'Task C')
|
|
89
|
+
assert.equal(tasks[3].title, 'Task 1')
|
|
90
|
+
|
|
91
91
|
// Verify positions are unique
|
|
92
|
-
const positions = tasks.map(t => t.position)
|
|
93
|
-
const uniquePositions = [...new Set(positions)]
|
|
94
|
-
assert.equal(uniquePositions.length, 4, 'All positions should be unique')
|
|
95
|
-
})
|
|
92
|
+
const positions = tasks.map(t => t.position)
|
|
93
|
+
const uniquePositions = [...new Set(positions)]
|
|
94
|
+
assert.equal(uniquePositions.length, 4, 'All positions should be unique')
|
|
95
|
+
})
|
|
96
96
|
|
|
97
97
|
it('should handle positioning between very close positions', async () => {
|
|
98
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
99
|
-
|
|
98
|
+
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
99
|
+
|
|
100
100
|
// Create initial tasks
|
|
101
101
|
const task1 = await api.resources.tasks.post({
|
|
102
102
|
title: 'Task 1',
|
|
103
103
|
category: category.id
|
|
104
|
-
})
|
|
105
|
-
|
|
104
|
+
})
|
|
105
|
+
|
|
106
106
|
const task2 = await api.resources.tasks.post({
|
|
107
107
|
title: 'Task 2',
|
|
108
108
|
category: category.id
|
|
109
|
-
})
|
|
110
|
-
|
|
109
|
+
})
|
|
110
|
+
|
|
111
111
|
// Insert many tasks between 1 and 2
|
|
112
|
-
const insertedTasks = []
|
|
112
|
+
const insertedTasks = []
|
|
113
113
|
for (let i = 0; i < 10; i++) {
|
|
114
114
|
const task = await api.resources.tasks.post({
|
|
115
115
|
title: `Inserted Task ${i}`,
|
|
116
116
|
category: category.id,
|
|
117
117
|
beforeId: task2.id
|
|
118
|
-
})
|
|
119
|
-
insertedTasks.push(task)
|
|
118
|
+
})
|
|
119
|
+
insertedTasks.push(task)
|
|
120
120
|
}
|
|
121
|
-
|
|
121
|
+
|
|
122
122
|
// Query and verify all are properly ordered
|
|
123
123
|
const { data: tasks } = await api.resources.tasks.query({
|
|
124
124
|
queryParams: {
|
|
125
125
|
filters: { category: category.id },
|
|
126
126
|
sort: ['position']
|
|
127
127
|
}
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
assert.equal(tasks.length, 12)
|
|
131
|
-
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
assert.equal(tasks.length, 12)
|
|
131
|
+
|
|
132
132
|
// Verify positions are in correct order
|
|
133
133
|
for (let i = 1; i < tasks.length; i++) {
|
|
134
|
-
assert(tasks[i-1].position < tasks[i].position,
|
|
135
|
-
`Task ${tasks[i-1].title} should come before ${tasks[i].title}`)
|
|
134
|
+
assert(tasks[i - 1].position < tasks[i].position,
|
|
135
|
+
`Task ${tasks[i - 1].title} should come before ${tasks[i].title}`)
|
|
136
136
|
}
|
|
137
|
-
})
|
|
137
|
+
})
|
|
138
138
|
|
|
139
139
|
it('should handle beforeId pointing to non-existent item in same category', async () => {
|
|
140
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
141
|
-
|
|
140
|
+
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
141
|
+
|
|
142
142
|
const task1 = await api.resources.tasks.post({
|
|
143
143
|
title: 'Task 1',
|
|
144
144
|
category: category.id
|
|
145
|
-
})
|
|
146
|
-
|
|
145
|
+
})
|
|
146
|
+
|
|
147
147
|
// Create task with non-existent beforeId
|
|
148
148
|
const task2 = await api.resources.tasks.post({
|
|
149
149
|
title: 'Task 2',
|
|
150
150
|
category: category.id,
|
|
151
151
|
beforeId: '99999'
|
|
152
|
-
})
|
|
153
|
-
|
|
152
|
+
})
|
|
153
|
+
|
|
154
154
|
// Should position at end since beforeId doesn't exist
|
|
155
155
|
const { data: tasks } = await api.resources.tasks.query({
|
|
156
156
|
queryParams: {
|
|
157
157
|
filters: { category: category.id },
|
|
158
158
|
sort: ['position']
|
|
159
159
|
}
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
assert.equal(tasks.length, 2)
|
|
163
|
-
assert.equal(tasks[0].title, 'Task 1')
|
|
164
|
-
assert.equal(tasks[1].title, 'Task 2')
|
|
165
|
-
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
assert.equal(tasks.length, 2)
|
|
163
|
+
assert.equal(tasks[0].title, 'Task 1')
|
|
164
|
+
assert.equal(tasks[1].title, 'Task 2')
|
|
165
|
+
})
|
|
166
166
|
|
|
167
167
|
it('should handle beforeId from different category', async () => {
|
|
168
|
-
const category1 = await api.resources.categories.post({ name: 'Category 1' })
|
|
169
|
-
const category2 = await api.resources.categories.post({ name: 'Category 2' })
|
|
170
|
-
|
|
168
|
+
const category1 = await api.resources.categories.post({ name: 'Category 1' })
|
|
169
|
+
const category2 = await api.resources.categories.post({ name: 'Category 2' })
|
|
170
|
+
|
|
171
171
|
const task1 = await api.resources.tasks.post({
|
|
172
172
|
title: 'Task 1',
|
|
173
173
|
category: category1.id
|
|
174
|
-
})
|
|
175
|
-
|
|
174
|
+
})
|
|
175
|
+
|
|
176
176
|
const task2 = await api.resources.tasks.post({
|
|
177
177
|
title: 'Task 2',
|
|
178
178
|
category: category2.id,
|
|
179
179
|
beforeId: task1.id // Task from different category
|
|
180
|
-
})
|
|
181
|
-
|
|
180
|
+
})
|
|
181
|
+
|
|
182
182
|
// Task 2 should be positioned normally in its category (ignoring beforeId from other category)
|
|
183
183
|
const { data: cat2Tasks } = await api.resources.tasks.query({
|
|
184
184
|
queryParams: {
|
|
185
185
|
filters: { category: category2.id },
|
|
186
186
|
sort: ['position']
|
|
187
187
|
}
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
assert.equal(cat2Tasks.length, 1)
|
|
191
|
-
assert.equal(cat2Tasks[0].title, 'Task 2')
|
|
192
|
-
assert(cat2Tasks[0].position)
|
|
193
|
-
})
|
|
194
|
-
})
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
assert.equal(cat2Tasks.length, 1)
|
|
191
|
+
assert.equal(cat2Tasks[0].title, 'Task 2')
|
|
192
|
+
assert(cat2Tasks[0].position) // Should have a position
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
195
|
|
|
196
196
|
describe('Complex Update Scenarios', () => {
|
|
197
197
|
it('should handle moving items multiple times', async () => {
|
|
198
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
199
|
-
|
|
198
|
+
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
199
|
+
|
|
200
200
|
// Create initial tasks
|
|
201
|
-
const tasks = []
|
|
201
|
+
const tasks = []
|
|
202
202
|
for (let i = 1; i <= 5; i++) {
|
|
203
203
|
const task = await api.resources.tasks.post({
|
|
204
204
|
title: `Task ${i}`,
|
|
205
205
|
category: category.id
|
|
206
|
-
})
|
|
207
|
-
tasks.push(task)
|
|
206
|
+
})
|
|
207
|
+
tasks.push(task)
|
|
208
208
|
}
|
|
209
|
-
|
|
209
|
+
|
|
210
210
|
// Move task 5 before task 3
|
|
211
211
|
await api.resources.tasks.patch({
|
|
212
212
|
id: tasks[4].id,
|
|
213
213
|
beforeId: tasks[2].id
|
|
214
|
-
})
|
|
215
|
-
|
|
214
|
+
})
|
|
215
|
+
|
|
216
216
|
// Move task 1 to the end
|
|
217
217
|
await api.resources.tasks.patch({
|
|
218
218
|
id: tasks[0].id,
|
|
219
219
|
beforeId: null
|
|
220
|
-
})
|
|
221
|
-
|
|
220
|
+
})
|
|
221
|
+
|
|
222
222
|
// Move task 3 to the beginning
|
|
223
223
|
await api.resources.tasks.patch({
|
|
224
224
|
id: tasks[2].id,
|
|
225
225
|
beforeId: 'FIRST'
|
|
226
|
-
})
|
|
227
|
-
|
|
226
|
+
})
|
|
227
|
+
|
|
228
228
|
// Query final order
|
|
229
229
|
const { data: finalTasks } = await api.resources.tasks.query({
|
|
230
230
|
queryParams: {
|
|
231
231
|
filters: { category: category.id },
|
|
232
232
|
sort: ['position']
|
|
233
233
|
}
|
|
234
|
-
})
|
|
235
|
-
|
|
234
|
+
})
|
|
235
|
+
|
|
236
236
|
// Expected order: 3, 2, 5, 4, 1
|
|
237
|
-
assert.equal(finalTasks[0].title, 'Task 3')
|
|
238
|
-
assert.equal(finalTasks[1].title, 'Task 2')
|
|
239
|
-
assert.equal(finalTasks[2].title, 'Task 5')
|
|
240
|
-
assert.equal(finalTasks[3].title, 'Task 4')
|
|
241
|
-
assert.equal(finalTasks[4].title, 'Task 1')
|
|
242
|
-
})
|
|
237
|
+
assert.equal(finalTasks[0].title, 'Task 3')
|
|
238
|
+
assert.equal(finalTasks[1].title, 'Task 2')
|
|
239
|
+
assert.equal(finalTasks[2].title, 'Task 5')
|
|
240
|
+
assert.equal(finalTasks[3].title, 'Task 4')
|
|
241
|
+
assert.equal(finalTasks[4].title, 'Task 1')
|
|
242
|
+
})
|
|
243
243
|
|
|
244
244
|
it('should handle category changes with positioning', async () => {
|
|
245
|
-
const category1 = await api.resources.categories.post({ name: 'Category 1' })
|
|
246
|
-
const category2 = await api.resources.categories.post({ name: 'Category 2' })
|
|
247
|
-
|
|
245
|
+
const category1 = await api.resources.categories.post({ name: 'Category 1' })
|
|
246
|
+
const category2 = await api.resources.categories.post({ name: 'Category 2' })
|
|
247
|
+
|
|
248
248
|
// Create tasks in category 1
|
|
249
249
|
const task1 = await api.resources.tasks.post({
|
|
250
250
|
title: 'Task 1',
|
|
251
251
|
category: category1.id
|
|
252
|
-
})
|
|
253
|
-
|
|
252
|
+
})
|
|
253
|
+
|
|
254
254
|
const task2 = await api.resources.tasks.post({
|
|
255
255
|
title: 'Task 2',
|
|
256
256
|
category: category1.id
|
|
257
|
-
})
|
|
258
|
-
|
|
257
|
+
})
|
|
258
|
+
|
|
259
259
|
// Create tasks in category 2
|
|
260
260
|
const task3 = await api.resources.tasks.post({
|
|
261
261
|
title: 'Task 3',
|
|
262
262
|
category: category2.id
|
|
263
|
-
})
|
|
264
|
-
|
|
263
|
+
})
|
|
264
|
+
|
|
265
265
|
// Move task 1 to category 2, positioning it before task 3
|
|
266
266
|
await api.resources.tasks.patch({
|
|
267
267
|
id: task1.id,
|
|
268
268
|
category: category2.id,
|
|
269
269
|
beforeId: task3.id
|
|
270
|
-
})
|
|
271
|
-
|
|
270
|
+
})
|
|
271
|
+
|
|
272
272
|
// Verify category 1 only has task 2
|
|
273
273
|
const { data: cat1Tasks } = await api.resources.tasks.query({
|
|
274
274
|
queryParams: {
|
|
275
275
|
filters: { category: category1.id },
|
|
276
276
|
sort: ['position']
|
|
277
277
|
}
|
|
278
|
-
})
|
|
279
|
-
|
|
280
|
-
assert.equal(cat1Tasks.length, 1)
|
|
281
|
-
assert.equal(cat1Tasks[0].title, 'Task 2')
|
|
282
|
-
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
assert.equal(cat1Tasks.length, 1)
|
|
281
|
+
assert.equal(cat1Tasks[0].title, 'Task 2')
|
|
282
|
+
|
|
283
283
|
// Verify category 2 has tasks in correct order
|
|
284
284
|
const { data: cat2Tasks } = await api.resources.tasks.query({
|
|
285
285
|
queryParams: {
|
|
286
286
|
filters: { category: category2.id },
|
|
287
287
|
sort: ['position']
|
|
288
288
|
}
|
|
289
|
-
})
|
|
290
|
-
|
|
291
|
-
assert.equal(cat2Tasks.length, 2)
|
|
292
|
-
assert.equal(cat2Tasks[0].title, 'Task 1')
|
|
293
|
-
assert.equal(cat2Tasks[1].title, 'Task 3')
|
|
294
|
-
})
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
assert.equal(cat2Tasks.length, 2)
|
|
292
|
+
assert.equal(cat2Tasks[0].title, 'Task 1')
|
|
293
|
+
assert.equal(cat2Tasks[1].title, 'Task 3')
|
|
294
|
+
})
|
|
295
295
|
|
|
296
296
|
it('should maintain position when updating other fields', async () => {
|
|
297
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
298
|
-
|
|
297
|
+
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
298
|
+
|
|
299
299
|
// Create tasks
|
|
300
|
-
const tasks = []
|
|
300
|
+
const tasks = []
|
|
301
301
|
for (let i = 1; i <= 3; i++) {
|
|
302
302
|
const task = await api.resources.tasks.post({
|
|
303
303
|
title: `Task ${i}`,
|
|
304
304
|
category: category.id
|
|
305
|
-
})
|
|
306
|
-
tasks.push(task)
|
|
305
|
+
})
|
|
306
|
+
tasks.push(task)
|
|
307
307
|
}
|
|
308
|
-
|
|
308
|
+
|
|
309
309
|
// Update title without beforeId
|
|
310
|
-
const originalPosition = tasks[1].position
|
|
310
|
+
const originalPosition = tasks[1].position
|
|
311
311
|
await api.resources.tasks.patch({
|
|
312
312
|
id: tasks[1].id,
|
|
313
313
|
title: 'Updated Task 2'
|
|
314
|
-
})
|
|
315
|
-
|
|
314
|
+
})
|
|
315
|
+
|
|
316
316
|
// Get updated task
|
|
317
|
-
const updatedTask = await api.resources.tasks.get({ id: tasks[1].id })
|
|
318
|
-
|
|
319
|
-
assert.equal(updatedTask.title, 'Updated Task 2')
|
|
320
|
-
assert.equal(updatedTask.position, originalPosition, 'Position should not change')
|
|
321
|
-
|
|
317
|
+
const updatedTask = await api.resources.tasks.get({ id: tasks[1].id })
|
|
318
|
+
|
|
319
|
+
assert.equal(updatedTask.title, 'Updated Task 2')
|
|
320
|
+
assert.equal(updatedTask.position, originalPosition, 'Position should not change')
|
|
321
|
+
|
|
322
322
|
// Verify order is maintained
|
|
323
323
|
const { data: allTasks } = await api.resources.tasks.query({
|
|
324
324
|
queryParams: {
|
|
325
325
|
filters: { category: category.id },
|
|
326
326
|
sort: ['position']
|
|
327
327
|
}
|
|
328
|
-
})
|
|
329
|
-
|
|
330
|
-
assert.equal(allTasks[0].title, 'Task 1')
|
|
331
|
-
assert.equal(allTasks[1].title, 'Updated Task 2')
|
|
332
|
-
assert.equal(allTasks[2].title, 'Task 3')
|
|
333
|
-
})
|
|
334
|
-
})
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
assert.equal(allTasks[0].title, 'Task 1')
|
|
331
|
+
assert.equal(allTasks[1].title, 'Updated Task 2')
|
|
332
|
+
assert.equal(allTasks[2].title, 'Task 3')
|
|
333
|
+
})
|
|
334
|
+
})
|
|
335
335
|
|
|
336
336
|
describe('Bulk Operations', () => {
|
|
337
337
|
it('should handle bulk creation with positioning', async () => {
|
|
338
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
339
|
-
|
|
338
|
+
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
339
|
+
|
|
340
340
|
// Create initial task
|
|
341
341
|
const task1 = await api.resources.tasks.post({
|
|
342
342
|
title: 'Task 1',
|
|
343
343
|
category: category.id
|
|
344
|
-
})
|
|
345
|
-
|
|
344
|
+
})
|
|
345
|
+
|
|
346
346
|
// Create multiple tasks in parallel (simulating bulk creation)
|
|
347
|
-
const promises = []
|
|
347
|
+
const promises = []
|
|
348
348
|
for (let i = 2; i <= 10; i++) {
|
|
349
349
|
promises.push(api.resources.tasks.post({
|
|
350
350
|
title: `Task ${i}`,
|
|
351
351
|
category: category.id
|
|
352
|
-
}))
|
|
352
|
+
}))
|
|
353
353
|
}
|
|
354
|
-
|
|
355
|
-
await Promise.all(promises)
|
|
356
|
-
|
|
354
|
+
|
|
355
|
+
await Promise.all(promises)
|
|
356
|
+
|
|
357
357
|
// Query all tasks
|
|
358
358
|
const { data: tasks } = await api.resources.tasks.query({
|
|
359
359
|
queryParams: {
|
|
360
360
|
filters: { category: category.id },
|
|
361
361
|
sort: ['position']
|
|
362
362
|
}
|
|
363
|
-
})
|
|
364
|
-
|
|
365
|
-
assert.equal(tasks.length, 10)
|
|
366
|
-
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
assert.equal(tasks.length, 10)
|
|
366
|
+
|
|
367
367
|
// Verify all have unique positions
|
|
368
|
-
const positions = tasks.map(t => t.position)
|
|
369
|
-
const uniquePositions = [...new Set(positions)]
|
|
370
|
-
assert.equal(uniquePositions.length, 10, 'All positions should be unique')
|
|
371
|
-
|
|
368
|
+
const positions = tasks.map(t => t.position)
|
|
369
|
+
const uniquePositions = [...new Set(positions)]
|
|
370
|
+
assert.equal(uniquePositions.length, 10, 'All positions should be unique')
|
|
371
|
+
|
|
372
372
|
// Verify they're in order
|
|
373
373
|
for (let i = 1; i < tasks.length; i++) {
|
|
374
|
-
assert(tasks[i-1].position < tasks[i].position)
|
|
374
|
+
assert(tasks[i - 1].position < tasks[i].position)
|
|
375
375
|
}
|
|
376
|
-
})
|
|
376
|
+
})
|
|
377
377
|
|
|
378
378
|
it('should handle bulk positioning before same target', async () => {
|
|
379
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
380
|
-
|
|
379
|
+
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
380
|
+
|
|
381
381
|
// Create initial tasks
|
|
382
382
|
const task1 = await api.resources.tasks.post({
|
|
383
383
|
title: 'Task 1',
|
|
384
384
|
category: category.id
|
|
385
|
-
})
|
|
386
|
-
|
|
385
|
+
})
|
|
386
|
+
|
|
387
387
|
const task2 = await api.resources.tasks.post({
|
|
388
388
|
title: 'Task 2',
|
|
389
389
|
category: category.id
|
|
390
|
-
})
|
|
391
|
-
|
|
390
|
+
})
|
|
391
|
+
|
|
392
392
|
// Create multiple tasks before task2 in parallel
|
|
393
|
-
const promises = []
|
|
393
|
+
const promises = []
|
|
394
394
|
for (let i = 1; i <= 5; i++) {
|
|
395
395
|
promises.push(api.resources.tasks.post({
|
|
396
396
|
title: `Inserted Task ${i}`,
|
|
397
397
|
category: category.id,
|
|
398
398
|
beforeId: task2.id
|
|
399
|
-
}))
|
|
399
|
+
}))
|
|
400
400
|
}
|
|
401
|
-
|
|
402
|
-
await Promise.all(promises)
|
|
403
|
-
|
|
401
|
+
|
|
402
|
+
await Promise.all(promises)
|
|
403
|
+
|
|
404
404
|
// Query all tasks
|
|
405
405
|
const { data: tasks } = await api.resources.tasks.query({
|
|
406
406
|
queryParams: {
|
|
407
407
|
filters: { category: category.id },
|
|
408
408
|
sort: ['position']
|
|
409
409
|
}
|
|
410
|
-
})
|
|
411
|
-
|
|
412
|
-
assert.equal(tasks.length, 7)
|
|
413
|
-
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
assert.equal(tasks.length, 7)
|
|
413
|
+
|
|
414
414
|
// Task 1 should still be first
|
|
415
|
-
assert.equal(tasks[0].title, 'Task 1')
|
|
416
|
-
|
|
415
|
+
assert.equal(tasks[0].title, 'Task 1')
|
|
416
|
+
|
|
417
417
|
// All inserted tasks should come before Task 2
|
|
418
|
-
const task2Index = tasks.findIndex(t => t.title === 'Task 2')
|
|
419
|
-
assert.equal(task2Index, 6, 'Task 2 should be last')
|
|
420
|
-
|
|
418
|
+
const task2Index = tasks.findIndex(t => t.title === 'Task 2')
|
|
419
|
+
assert.equal(task2Index, 6, 'Task 2 should be last')
|
|
420
|
+
|
|
421
421
|
// All positions should be unique
|
|
422
|
-
const positions = tasks.map(t => t.position)
|
|
423
|
-
const uniquePositions = [...new Set(positions)]
|
|
424
|
-
assert.equal(uniquePositions.length, 7, 'All positions should be unique')
|
|
425
|
-
})
|
|
426
|
-
})
|
|
422
|
+
const positions = tasks.map(t => t.position)
|
|
423
|
+
const uniquePositions = [...new Set(positions)]
|
|
424
|
+
assert.equal(uniquePositions.length, 7, 'All positions should be unique')
|
|
425
|
+
})
|
|
426
|
+
})
|
|
427
427
|
|
|
428
428
|
describe('Query and Filtering', () => {
|
|
429
429
|
it('should sort correctly with mixed position values', async () => {
|
|
430
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
431
|
-
|
|
430
|
+
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
431
|
+
|
|
432
432
|
// Create tasks in random order
|
|
433
433
|
const taskC = await api.resources.tasks.post({
|
|
434
434
|
title: 'Task C',
|
|
435
435
|
category: category.id
|
|
436
|
-
})
|
|
437
|
-
|
|
436
|
+
})
|
|
437
|
+
|
|
438
438
|
const taskA = await api.resources.tasks.post({
|
|
439
439
|
title: 'Task A',
|
|
440
440
|
category: category.id,
|
|
441
441
|
beforeId: taskC.id
|
|
442
|
-
})
|
|
443
|
-
|
|
442
|
+
})
|
|
443
|
+
|
|
444
444
|
const taskE = await api.resources.tasks.post({
|
|
445
445
|
title: 'Task E',
|
|
446
446
|
category: category.id
|
|
447
|
-
})
|
|
448
|
-
|
|
447
|
+
})
|
|
448
|
+
|
|
449
449
|
const taskB = await api.resources.tasks.post({
|
|
450
450
|
title: 'Task B',
|
|
451
451
|
category: category.id,
|
|
452
452
|
beforeId: taskC.id
|
|
453
|
-
})
|
|
454
|
-
|
|
453
|
+
})
|
|
454
|
+
|
|
455
455
|
const taskD = await api.resources.tasks.post({
|
|
456
456
|
title: 'Task D',
|
|
457
457
|
category: category.id,
|
|
458
458
|
beforeId: taskE.id
|
|
459
|
-
})
|
|
460
|
-
|
|
459
|
+
})
|
|
460
|
+
|
|
461
461
|
// Query with ascending sort
|
|
462
462
|
const { data: ascTasks } = await api.resources.tasks.query({
|
|
463
463
|
queryParams: {
|
|
464
464
|
filters: { category: category.id },
|
|
465
465
|
sort: ['position']
|
|
466
466
|
}
|
|
467
|
-
})
|
|
468
|
-
|
|
467
|
+
})
|
|
468
|
+
|
|
469
469
|
// Should be alphabetical: A, B, C, D, E
|
|
470
|
-
assert.equal(ascTasks.map(t => t.title).join(','), 'Task A,Task B,Task C,Task D,Task E')
|
|
471
|
-
|
|
470
|
+
assert.equal(ascTasks.map(t => t.title).join(','), 'Task A,Task B,Task C,Task D,Task E')
|
|
471
|
+
|
|
472
472
|
// Query with descending sort
|
|
473
473
|
const { data: descTasks } = await api.resources.tasks.query({
|
|
474
474
|
queryParams: {
|
|
475
475
|
filters: { category: category.id },
|
|
476
476
|
sort: ['-position']
|
|
477
477
|
}
|
|
478
|
-
})
|
|
479
|
-
|
|
478
|
+
})
|
|
479
|
+
|
|
480
480
|
// Should be reverse: E, D, C, B, A
|
|
481
|
-
assert.equal(descTasks.map(t => t.title).join(','), 'Task E,Task D,Task C,Task B,Task A')
|
|
482
|
-
})
|
|
481
|
+
assert.equal(descTasks.map(t => t.title).join(','), 'Task E,Task D,Task C,Task B,Task A')
|
|
482
|
+
})
|
|
483
483
|
|
|
484
484
|
it('should handle pagination with position sorting', async () => {
|
|
485
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
486
|
-
|
|
485
|
+
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
486
|
+
|
|
487
487
|
// Create 20 tasks
|
|
488
488
|
for (let i = 1; i <= 20; i++) {
|
|
489
489
|
await api.resources.tasks.post({
|
|
490
490
|
title: `Task ${i.toString().padStart(2, '0')}`,
|
|
491
491
|
category: category.id
|
|
492
|
-
})
|
|
492
|
+
})
|
|
493
493
|
}
|
|
494
|
-
|
|
494
|
+
|
|
495
495
|
// Query first page
|
|
496
496
|
const { data: page1 } = await api.resources.tasks.query({
|
|
497
497
|
queryParams: {
|
|
@@ -499,12 +499,12 @@ describe('Extended Positioning Plugin Tests', { timeout: 30000 }, () => {
|
|
|
499
499
|
sort: ['position'],
|
|
500
500
|
page: { size: 5, number: 1 }
|
|
501
501
|
}
|
|
502
|
-
})
|
|
503
|
-
|
|
504
|
-
assert.equal(page1.length, 5)
|
|
505
|
-
assert.equal(page1[0].title, 'Task 01')
|
|
506
|
-
assert.equal(page1[4].title, 'Task 05')
|
|
507
|
-
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
assert.equal(page1.length, 5)
|
|
505
|
+
assert.equal(page1[0].title, 'Task 01')
|
|
506
|
+
assert.equal(page1[4].title, 'Task 05')
|
|
507
|
+
|
|
508
508
|
// Query second page
|
|
509
509
|
const { data: page2 } = await api.resources.tasks.query({
|
|
510
510
|
queryParams: {
|
|
@@ -512,80 +512,80 @@ describe('Extended Positioning Plugin Tests', { timeout: 30000 }, () => {
|
|
|
512
512
|
sort: ['position'],
|
|
513
513
|
page: { size: 5, number: 2 }
|
|
514
514
|
}
|
|
515
|
-
})
|
|
516
|
-
|
|
517
|
-
assert.equal(page2.length, 5)
|
|
518
|
-
assert.equal(page2[0].title, 'Task 06')
|
|
519
|
-
assert.equal(page2[4].title, 'Task 10')
|
|
520
|
-
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
assert.equal(page2.length, 5)
|
|
518
|
+
assert.equal(page2[0].title, 'Task 06')
|
|
519
|
+
assert.equal(page2[4].title, 'Task 10')
|
|
520
|
+
|
|
521
521
|
// Verify no overlap and correct ordering
|
|
522
|
-
assert(page1[4].position < page2[0].position)
|
|
523
|
-
})
|
|
522
|
+
assert(page1[4].position < page2[0].position)
|
|
523
|
+
})
|
|
524
524
|
|
|
525
525
|
it('should combine position sort with other sorts', async () => {
|
|
526
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
527
|
-
|
|
526
|
+
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
527
|
+
|
|
528
528
|
// Create tasks with same titles
|
|
529
|
-
const positions = []
|
|
529
|
+
const positions = []
|
|
530
530
|
for (let i = 0; i < 3; i++) {
|
|
531
531
|
const task = await api.resources.tasks.post({
|
|
532
532
|
title: 'Task A',
|
|
533
533
|
category: category.id
|
|
534
|
-
})
|
|
535
|
-
positions.push(task)
|
|
534
|
+
})
|
|
535
|
+
positions.push(task)
|
|
536
536
|
}
|
|
537
|
-
|
|
537
|
+
|
|
538
538
|
for (let i = 0; i < 3; i++) {
|
|
539
539
|
const task = await api.resources.tasks.post({
|
|
540
540
|
title: 'Task B',
|
|
541
541
|
category: category.id
|
|
542
|
-
})
|
|
543
|
-
positions.push(task)
|
|
542
|
+
})
|
|
543
|
+
positions.push(task)
|
|
544
544
|
}
|
|
545
|
-
|
|
545
|
+
|
|
546
546
|
// Sort by title then position
|
|
547
547
|
const { data: tasks } = await api.resources.tasks.query({
|
|
548
548
|
queryParams: {
|
|
549
549
|
filters: { category: category.id },
|
|
550
550
|
sort: ['title', 'position']
|
|
551
551
|
}
|
|
552
|
-
})
|
|
553
|
-
|
|
552
|
+
})
|
|
553
|
+
|
|
554
554
|
// First 3 should be Task A, last 3 should be Task B
|
|
555
|
-
assert.equal(tasks.slice(0, 3).every(t => t.title === 'Task A'), true)
|
|
556
|
-
assert.equal(tasks.slice(3, 6).every(t => t.title === 'Task B'), true)
|
|
557
|
-
|
|
555
|
+
assert.equal(tasks.slice(0, 3).every(t => t.title === 'Task A'), true)
|
|
556
|
+
assert.equal(tasks.slice(3, 6).every(t => t.title === 'Task B'), true)
|
|
557
|
+
|
|
558
558
|
// Within each group, should be sorted by position
|
|
559
559
|
for (let i = 1; i < 3; i++) {
|
|
560
|
-
assert(tasks[i-1].position < tasks[i].position)
|
|
561
|
-
assert(tasks[i+2].position < tasks[i+3].position)
|
|
560
|
+
assert(tasks[i - 1].position < tasks[i].position)
|
|
561
|
+
assert(tasks[i + 2].position < tasks[i + 3].position)
|
|
562
562
|
}
|
|
563
|
-
})
|
|
564
|
-
})
|
|
563
|
+
})
|
|
564
|
+
})
|
|
565
565
|
|
|
566
566
|
describe('Special Characters and Extreme Values', () => {
|
|
567
567
|
it('should handle special position values correctly', async () => {
|
|
568
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
569
|
-
|
|
568
|
+
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
569
|
+
|
|
570
570
|
// Create many tasks to test various position values
|
|
571
|
-
const tasks = []
|
|
571
|
+
const tasks = []
|
|
572
572
|
for (let i = 0; i < 50; i++) {
|
|
573
573
|
const task = await api.resources.tasks.post({
|
|
574
574
|
title: `Task ${i}`,
|
|
575
575
|
category: category.id
|
|
576
|
-
})
|
|
577
|
-
tasks.push(task)
|
|
576
|
+
})
|
|
577
|
+
tasks.push(task)
|
|
578
578
|
}
|
|
579
|
-
|
|
579
|
+
|
|
580
580
|
// Insert tasks between existing ones
|
|
581
581
|
for (let i = 0; i < 10; i++) {
|
|
582
582
|
await api.resources.tasks.post({
|
|
583
583
|
title: `Inserted ${i}`,
|
|
584
584
|
category: category.id,
|
|
585
585
|
beforeId: tasks[i * 5].id
|
|
586
|
-
})
|
|
586
|
+
})
|
|
587
587
|
}
|
|
588
|
-
|
|
588
|
+
|
|
589
589
|
// Query all and verify ordering
|
|
590
590
|
const { data: allTasks } = await api.resources.tasks.query({
|
|
591
591
|
queryParams: {
|
|
@@ -593,241 +593,241 @@ describe('Extended Positioning Plugin Tests', { timeout: 30000 }, () => {
|
|
|
593
593
|
sort: ['position'],
|
|
594
594
|
page: { size: 100 }
|
|
595
595
|
}
|
|
596
|
-
})
|
|
597
|
-
|
|
598
|
-
assert.equal(allTasks.length, 60)
|
|
599
|
-
|
|
596
|
+
})
|
|
597
|
+
|
|
598
|
+
assert.equal(allTasks.length, 60)
|
|
599
|
+
|
|
600
600
|
// Verify all positions are valid strings
|
|
601
601
|
for (const task of allTasks) {
|
|
602
|
-
assert.equal(typeof task.position, 'string')
|
|
603
|
-
assert(task.position.length > 0)
|
|
602
|
+
assert.equal(typeof task.position, 'string')
|
|
603
|
+
assert(task.position.length > 0)
|
|
604
604
|
// Fractional indexing uses alphanumeric characters
|
|
605
|
-
assert.match(task.position, /^[a-zA-Z0-9]+$/)
|
|
605
|
+
assert.match(task.position, /^[a-zA-Z0-9]+$/)
|
|
606
606
|
}
|
|
607
|
-
|
|
607
|
+
|
|
608
608
|
// Verify ordering
|
|
609
609
|
for (let i = 1; i < allTasks.length; i++) {
|
|
610
|
-
assert(allTasks[i-1].position < allTasks[i].position)
|
|
610
|
+
assert(allTasks[i - 1].position < allTasks[i].position)
|
|
611
611
|
}
|
|
612
|
-
})
|
|
612
|
+
})
|
|
613
613
|
|
|
614
614
|
it('should handle rapid position updates', async () => {
|
|
615
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
616
|
-
|
|
615
|
+
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
616
|
+
|
|
617
617
|
// Create initial tasks
|
|
618
|
-
const tasks = []
|
|
618
|
+
const tasks = []
|
|
619
619
|
for (let i = 0; i < 10; i++) {
|
|
620
620
|
const task = await api.resources.tasks.post({
|
|
621
621
|
title: `Task ${i}`,
|
|
622
622
|
category: category.id
|
|
623
|
-
})
|
|
624
|
-
tasks.push(task)
|
|
623
|
+
})
|
|
624
|
+
tasks.push(task)
|
|
625
625
|
}
|
|
626
|
-
|
|
626
|
+
|
|
627
627
|
// Rapidly move last task to different positions
|
|
628
|
-
const lastTask = tasks[9]
|
|
629
|
-
|
|
628
|
+
const lastTask = tasks[9]
|
|
629
|
+
|
|
630
630
|
// Move to beginning
|
|
631
631
|
await api.resources.tasks.patch({
|
|
632
632
|
id: lastTask.id,
|
|
633
633
|
beforeId: tasks[0].id
|
|
634
|
-
})
|
|
635
|
-
|
|
634
|
+
})
|
|
635
|
+
|
|
636
636
|
// Move to middle
|
|
637
637
|
await api.resources.tasks.patch({
|
|
638
638
|
id: lastTask.id,
|
|
639
639
|
beforeId: tasks[5].id
|
|
640
|
-
})
|
|
641
|
-
|
|
640
|
+
})
|
|
641
|
+
|
|
642
642
|
// Move to near end
|
|
643
643
|
await api.resources.tasks.patch({
|
|
644
644
|
id: lastTask.id,
|
|
645
645
|
beforeId: tasks[8].id
|
|
646
|
-
})
|
|
647
|
-
|
|
646
|
+
})
|
|
647
|
+
|
|
648
648
|
// Move back to beginning
|
|
649
649
|
await api.resources.tasks.patch({
|
|
650
650
|
id: lastTask.id,
|
|
651
651
|
beforeId: 'FIRST'
|
|
652
|
-
})
|
|
653
|
-
|
|
652
|
+
})
|
|
653
|
+
|
|
654
654
|
// Verify final state
|
|
655
655
|
const { data: finalTasks } = await api.resources.tasks.query({
|
|
656
656
|
queryParams: {
|
|
657
657
|
filters: { category: category.id },
|
|
658
658
|
sort: ['position']
|
|
659
659
|
}
|
|
660
|
-
})
|
|
661
|
-
|
|
662
|
-
assert.equal(finalTasks[0].title, 'Task 9')
|
|
663
|
-
|
|
660
|
+
})
|
|
661
|
+
|
|
662
|
+
assert.equal(finalTasks[0].title, 'Task 9')
|
|
663
|
+
|
|
664
664
|
// Verify all positions are still unique and ordered
|
|
665
|
-
const positions = finalTasks.map(t => t.position)
|
|
666
|
-
const uniquePositions = [...new Set(positions)]
|
|
667
|
-
assert.equal(uniquePositions.length, 10)
|
|
668
|
-
|
|
665
|
+
const positions = finalTasks.map(t => t.position)
|
|
666
|
+
const uniquePositions = [...new Set(positions)]
|
|
667
|
+
assert.equal(uniquePositions.length, 10)
|
|
668
|
+
|
|
669
669
|
for (let i = 1; i < finalTasks.length; i++) {
|
|
670
|
-
assert(finalTasks[i-1].position < finalTasks[i].position)
|
|
670
|
+
assert(finalTasks[i - 1].position < finalTasks[i].position)
|
|
671
671
|
}
|
|
672
|
-
})
|
|
673
|
-
})
|
|
672
|
+
})
|
|
673
|
+
})
|
|
674
674
|
|
|
675
675
|
describe('Null and Empty Filter Values', () => {
|
|
676
676
|
it('should handle tasks with null category separately', async () => {
|
|
677
677
|
// Create tasks with categories
|
|
678
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
679
|
-
|
|
678
|
+
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
679
|
+
|
|
680
680
|
const taskWithCat1 = await api.resources.tasks.post({
|
|
681
681
|
title: 'Task with Category 1',
|
|
682
682
|
category: category.id
|
|
683
|
-
})
|
|
684
|
-
|
|
683
|
+
})
|
|
684
|
+
|
|
685
685
|
const taskWithCat2 = await api.resources.tasks.post({
|
|
686
686
|
title: 'Task with Category 2',
|
|
687
687
|
category: category.id
|
|
688
|
-
})
|
|
689
|
-
|
|
688
|
+
})
|
|
689
|
+
|
|
690
690
|
// Create tasks without category - don't send category_id at all for null
|
|
691
691
|
const taskNoCat1 = await api.resources.tasks.post({
|
|
692
692
|
title: 'Task without Category 1'
|
|
693
|
-
})
|
|
694
|
-
|
|
693
|
+
})
|
|
694
|
+
|
|
695
695
|
const taskNoCat2 = await api.resources.tasks.post({
|
|
696
696
|
title: 'Task without Category 2'
|
|
697
|
-
})
|
|
698
|
-
|
|
697
|
+
})
|
|
698
|
+
|
|
699
699
|
const taskNoCat3 = await api.resources.tasks.post({
|
|
700
700
|
title: 'Task without Category 3',
|
|
701
701
|
beforeId: taskNoCat1.id
|
|
702
|
-
})
|
|
703
|
-
|
|
702
|
+
})
|
|
703
|
+
|
|
704
704
|
// Query tasks with category
|
|
705
705
|
const { data: tasksWithCat } = await api.resources.tasks.query({
|
|
706
706
|
queryParams: {
|
|
707
707
|
filters: { category: category.id },
|
|
708
708
|
sort: ['position']
|
|
709
709
|
}
|
|
710
|
-
})
|
|
711
|
-
|
|
712
|
-
assert.equal(tasksWithCat.length, 2)
|
|
713
|
-
assert(tasksWithCat[0].position < tasksWithCat[1].position)
|
|
714
|
-
|
|
710
|
+
})
|
|
711
|
+
|
|
712
|
+
assert.equal(tasksWithCat.length, 2)
|
|
713
|
+
assert(tasksWithCat[0].position < tasksWithCat[1].position)
|
|
714
|
+
|
|
715
715
|
// Query tasks without category
|
|
716
716
|
const { data: tasksNoCat } = await api.resources.tasks.query({
|
|
717
717
|
queryParams: {
|
|
718
718
|
filters: { category: null },
|
|
719
719
|
sort: ['position']
|
|
720
720
|
}
|
|
721
|
-
})
|
|
722
|
-
|
|
721
|
+
})
|
|
722
|
+
|
|
723
723
|
// Debug: Check what we actually have
|
|
724
724
|
const { data: allTasks } = await api.resources.tasks.query({
|
|
725
725
|
queryParams: {
|
|
726
726
|
sort: ['position']
|
|
727
727
|
}
|
|
728
|
-
})
|
|
729
|
-
console.log('All tasks:', allTasks.map(t => ({ title: t.title, category: t.category_id })))
|
|
730
|
-
console.log('Tasks with null category:', tasksNoCat.length)
|
|
731
|
-
|
|
728
|
+
})
|
|
729
|
+
console.log('All tasks:', allTasks.map(t => ({ title: t.title, category: t.category_id })))
|
|
730
|
+
console.log('Tasks with null category:', tasksNoCat.length)
|
|
731
|
+
|
|
732
732
|
// Try querying without filter
|
|
733
733
|
const { data: tasksNoFilter } = await api.resources.tasks.query({
|
|
734
734
|
queryParams: {
|
|
735
735
|
sort: ['position']
|
|
736
736
|
}
|
|
737
|
-
})
|
|
738
|
-
const actualNullCategoryTasks = tasksNoFilter.filter(t => t.category_id === null || t.category_id === undefined)
|
|
739
|
-
console.log('Actual tasks without category:', actualNullCategoryTasks.length)
|
|
740
|
-
|
|
741
|
-
assert.equal(tasksNoCat.length, 3)
|
|
742
|
-
assert.equal(tasksNoCat[0].title, 'Task without Category 3')
|
|
743
|
-
assert.equal(tasksNoCat[1].title, 'Task without Category 1')
|
|
744
|
-
assert.equal(tasksNoCat[2].title, 'Task without Category 2')
|
|
745
|
-
})
|
|
746
|
-
})
|
|
737
|
+
})
|
|
738
|
+
const actualNullCategoryTasks = tasksNoFilter.filter(t => t.category_id === null || t.category_id === undefined)
|
|
739
|
+
console.log('Actual tasks without category:', actualNullCategoryTasks.length)
|
|
740
|
+
|
|
741
|
+
assert.equal(tasksNoCat.length, 3)
|
|
742
|
+
assert.equal(tasksNoCat[0].title, 'Task without Category 3') // Positioned before 1
|
|
743
|
+
assert.equal(tasksNoCat[1].title, 'Task without Category 1')
|
|
744
|
+
assert.equal(tasksNoCat[2].title, 'Task without Category 2')
|
|
745
|
+
})
|
|
746
|
+
})
|
|
747
747
|
|
|
748
748
|
describe('Position Field Validation', () => {
|
|
749
749
|
it('should ignore manually set position values', async () => {
|
|
750
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
751
|
-
|
|
750
|
+
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
751
|
+
|
|
752
752
|
// Try to set position manually on create
|
|
753
753
|
const task1 = await api.resources.tasks.post({
|
|
754
754
|
title: 'Task 1',
|
|
755
755
|
category: category.id,
|
|
756
756
|
position: 'ZZZZ' // Should be ignored
|
|
757
|
-
})
|
|
758
|
-
|
|
759
|
-
assert.notEqual(task1.position, 'ZZZZ')
|
|
760
|
-
assert.equal(task1.position, 'a0')
|
|
761
|
-
|
|
757
|
+
})
|
|
758
|
+
|
|
759
|
+
assert.notEqual(task1.position, 'ZZZZ')
|
|
760
|
+
assert.equal(task1.position, 'a0') // First item gets 'a0'
|
|
761
|
+
|
|
762
762
|
// Try to set position manually on update
|
|
763
763
|
const task2 = await api.resources.tasks.post({
|
|
764
764
|
title: 'Task 2',
|
|
765
765
|
category: category.id
|
|
766
|
-
})
|
|
767
|
-
|
|
768
|
-
const originalPosition = task2.position
|
|
769
|
-
|
|
766
|
+
})
|
|
767
|
+
|
|
768
|
+
const originalPosition = task2.position
|
|
769
|
+
|
|
770
770
|
await api.resources.tasks.patch({
|
|
771
771
|
id: task2.id,
|
|
772
772
|
position: 'AAAA' // Should be ignored
|
|
773
|
-
})
|
|
774
|
-
|
|
775
|
-
const updated = await api.resources.tasks.get({ id: task2.id })
|
|
776
|
-
assert.equal(updated.position, originalPosition)
|
|
777
|
-
})
|
|
773
|
+
})
|
|
774
|
+
|
|
775
|
+
const updated = await api.resources.tasks.get({ id: task2.id })
|
|
776
|
+
assert.equal(updated.position, originalPosition) // Position unchanged
|
|
777
|
+
})
|
|
778
778
|
|
|
779
779
|
it('should handle both position and beforeId in update', async () => {
|
|
780
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
781
|
-
|
|
780
|
+
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
781
|
+
|
|
782
782
|
const task1 = await api.resources.tasks.post({
|
|
783
783
|
title: 'Task 1',
|
|
784
784
|
category: category.id
|
|
785
|
-
})
|
|
786
|
-
|
|
785
|
+
})
|
|
786
|
+
|
|
787
787
|
const task2 = await api.resources.tasks.post({
|
|
788
788
|
title: 'Task 2',
|
|
789
789
|
category: category.id
|
|
790
|
-
})
|
|
791
|
-
|
|
790
|
+
})
|
|
791
|
+
|
|
792
792
|
const task3 = await api.resources.tasks.post({
|
|
793
793
|
title: 'Task 3',
|
|
794
794
|
category: category.id
|
|
795
|
-
})
|
|
796
|
-
|
|
795
|
+
})
|
|
796
|
+
|
|
797
797
|
// Update with both position (ignored) and beforeId (used)
|
|
798
798
|
await api.resources.tasks.patch({
|
|
799
799
|
id: task3.id,
|
|
800
800
|
position: 'XXXX', // Should be ignored
|
|
801
801
|
beforeId: task1.id // Should be used
|
|
802
|
-
})
|
|
803
|
-
|
|
802
|
+
})
|
|
803
|
+
|
|
804
804
|
const { data: tasks } = await api.resources.tasks.query({
|
|
805
805
|
queryParams: {
|
|
806
806
|
filters: { category: category.id },
|
|
807
807
|
sort: ['position']
|
|
808
808
|
}
|
|
809
|
-
})
|
|
810
|
-
|
|
809
|
+
})
|
|
810
|
+
|
|
811
811
|
// Task 3 should now be before Task 1
|
|
812
|
-
assert.equal(tasks[0].title, 'Task 3')
|
|
813
|
-
assert.equal(tasks[1].title, 'Task 1')
|
|
814
|
-
assert.equal(tasks[2].title, 'Task 2')
|
|
815
|
-
assert.notEqual(tasks[0].position, 'XXXX')
|
|
816
|
-
})
|
|
817
|
-
})
|
|
812
|
+
assert.equal(tasks[0].title, 'Task 3')
|
|
813
|
+
assert.equal(tasks[1].title, 'Task 1')
|
|
814
|
+
assert.equal(tasks[2].title, 'Task 2')
|
|
815
|
+
assert.notEqual(tasks[0].position, 'XXXX')
|
|
816
|
+
})
|
|
817
|
+
})
|
|
818
818
|
|
|
819
819
|
describe('Performance and Stress Tests', () => {
|
|
820
820
|
it('should handle large number of tasks efficiently', async () => {
|
|
821
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
822
|
-
|
|
821
|
+
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
822
|
+
|
|
823
823
|
// Create 100 tasks
|
|
824
824
|
for (let i = 0; i < 100; i++) {
|
|
825
825
|
await api.resources.tasks.post({
|
|
826
826
|
title: `Task ${i.toString().padStart(3, '0')}`,
|
|
827
827
|
category: category.id
|
|
828
|
-
})
|
|
828
|
+
})
|
|
829
829
|
}
|
|
830
|
-
|
|
830
|
+
|
|
831
831
|
// Query with position sort
|
|
832
832
|
const { data: tasks } = await api.resources.tasks.query({
|
|
833
833
|
queryParams: {
|
|
@@ -835,22 +835,22 @@ describe('Extended Positioning Plugin Tests', { timeout: 30000 }, () => {
|
|
|
835
835
|
sort: ['position'],
|
|
836
836
|
page: { size: 100 }
|
|
837
837
|
}
|
|
838
|
-
})
|
|
839
|
-
|
|
840
|
-
assert.equal(tasks.length, 100)
|
|
841
|
-
|
|
838
|
+
})
|
|
839
|
+
|
|
840
|
+
assert.equal(tasks.length, 100)
|
|
841
|
+
|
|
842
842
|
// Verify all are in correct order
|
|
843
843
|
for (let i = 0; i < tasks.length; i++) {
|
|
844
|
-
assert.equal(tasks[i].title, `Task ${i.toString().padStart(3, '0')}`)
|
|
844
|
+
assert.equal(tasks[i].title, `Task ${i.toString().padStart(3, '0')}`)
|
|
845
845
|
}
|
|
846
|
-
|
|
846
|
+
|
|
847
847
|
// Insert task in the middle
|
|
848
848
|
const middleTask = await api.resources.tasks.post({
|
|
849
849
|
title: 'Middle Task',
|
|
850
850
|
category: category.id,
|
|
851
851
|
beforeId: tasks[50].id
|
|
852
|
-
})
|
|
853
|
-
|
|
852
|
+
})
|
|
853
|
+
|
|
854
854
|
// Verify it's positioned correctly
|
|
855
855
|
const { data: updatedTasks } = await api.resources.tasks.query({
|
|
856
856
|
queryParams: {
|
|
@@ -858,27 +858,27 @@ describe('Extended Positioning Plugin Tests', { timeout: 30000 }, () => {
|
|
|
858
858
|
sort: ['position'],
|
|
859
859
|
page: { size: 102 }
|
|
860
860
|
}
|
|
861
|
-
})
|
|
862
|
-
|
|
863
|
-
const middleIndex = updatedTasks.findIndex(t => t.title === 'Middle Task')
|
|
864
|
-
assert.equal(middleIndex, 50)
|
|
865
|
-
assert(updatedTasks[49].position < middleTask.position)
|
|
866
|
-
assert(middleTask.position < updatedTasks[51].position)
|
|
867
|
-
})
|
|
861
|
+
})
|
|
862
|
+
|
|
863
|
+
const middleIndex = updatedTasks.findIndex(t => t.title === 'Middle Task')
|
|
864
|
+
assert.equal(middleIndex, 50)
|
|
865
|
+
assert(updatedTasks[49].position < middleTask.position)
|
|
866
|
+
assert(middleTask.position < updatedTasks[51].position)
|
|
867
|
+
})
|
|
868
868
|
|
|
869
869
|
it('should maintain consistency under concurrent updates', async () => {
|
|
870
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
871
|
-
|
|
870
|
+
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
871
|
+
|
|
872
872
|
// Create initial tasks
|
|
873
|
-
const tasks = []
|
|
873
|
+
const tasks = []
|
|
874
874
|
for (let i = 0; i < 20; i++) {
|
|
875
875
|
const task = await api.resources.tasks.post({
|
|
876
876
|
title: `Task ${i}`,
|
|
877
877
|
category: category.id
|
|
878
|
-
})
|
|
879
|
-
tasks.push(task)
|
|
878
|
+
})
|
|
879
|
+
tasks.push(task)
|
|
880
880
|
}
|
|
881
|
-
|
|
881
|
+
|
|
882
882
|
// Perform concurrent position updates
|
|
883
883
|
const updates = [
|
|
884
884
|
api.resources.tasks.patch({ id: tasks[5].id, beforeId: tasks[0].id }),
|
|
@@ -886,30 +886,30 @@ describe('Extended Positioning Plugin Tests', { timeout: 30000 }, () => {
|
|
|
886
886
|
api.resources.tasks.patch({ id: tasks[15].id, beforeId: tasks[0].id }),
|
|
887
887
|
api.resources.tasks.patch({ id: tasks[19].id, beforeId: 'FIRST' }),
|
|
888
888
|
api.resources.tasks.patch({ id: tasks[1].id, beforeId: null }) // Move to end
|
|
889
|
-
]
|
|
890
|
-
|
|
891
|
-
await Promise.all(updates)
|
|
892
|
-
|
|
889
|
+
]
|
|
890
|
+
|
|
891
|
+
await Promise.all(updates)
|
|
892
|
+
|
|
893
893
|
// Verify final state
|
|
894
894
|
const { data: finalTasks } = await api.resources.tasks.query({
|
|
895
895
|
queryParams: {
|
|
896
896
|
filters: { category: category.id },
|
|
897
897
|
sort: ['position']
|
|
898
898
|
}
|
|
899
|
-
})
|
|
900
|
-
|
|
899
|
+
})
|
|
900
|
+
|
|
901
901
|
// All tasks should still exist
|
|
902
|
-
assert.equal(finalTasks.length, 20)
|
|
903
|
-
|
|
902
|
+
assert.equal(finalTasks.length, 20)
|
|
903
|
+
|
|
904
904
|
// All positions should be unique
|
|
905
|
-
const positions = finalTasks.map(t => t.position)
|
|
906
|
-
const uniquePositions = [...new Set(positions)]
|
|
907
|
-
assert.equal(uniquePositions.length, 20)
|
|
908
|
-
|
|
905
|
+
const positions = finalTasks.map(t => t.position)
|
|
906
|
+
const uniquePositions = [...new Set(positions)]
|
|
907
|
+
assert.equal(uniquePositions.length, 20)
|
|
908
|
+
|
|
909
909
|
// Verify ordering is consistent
|
|
910
910
|
for (let i = 1; i < finalTasks.length; i++) {
|
|
911
|
-
assert(finalTasks[i-1].position < finalTasks[i].position)
|
|
911
|
+
assert(finalTasks[i - 1].position < finalTasks[i].position)
|
|
912
912
|
}
|
|
913
|
-
})
|
|
914
|
-
})
|
|
915
|
-
})
|
|
913
|
+
})
|
|
914
|
+
})
|
|
915
|
+
})
|