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 - always use SQLite in-memory
|
|
9
9
|
const knex = knexLib({
|
|
@@ -12,32 +12,32 @@ 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('Positioning Plugin Tests', { timeout: 30000 }, () => {
|
|
21
21
|
// IMPORTANT: before() runs ONCE for the entire test suite
|
|
22
22
|
before(async () => {
|
|
23
23
|
// Initialize API once
|
|
24
|
-
api = await createPositioningApi(knex)
|
|
25
|
-
|
|
24
|
+
api = await createPositioningApi(knex)
|
|
25
|
+
|
|
26
26
|
// Install positioning plugin with default configuration
|
|
27
27
|
await api.use(PositioningPlugin, {
|
|
28
28
|
field: 'position',
|
|
29
29
|
filters: ['category'], // Tasks will be grouped by category
|
|
30
30
|
defaultPosition: 'last',
|
|
31
31
|
autoIndex: true
|
|
32
|
-
})
|
|
33
|
-
})
|
|
34
|
-
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
35
|
// IMPORTANT: after() cleans up resources
|
|
36
36
|
after(async () => {
|
|
37
37
|
// Always destroy knex connection to allow tests to exit
|
|
38
|
-
await knex.destroy()
|
|
39
|
-
})
|
|
40
|
-
|
|
38
|
+
await knex.destroy()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
41
|
// IMPORTANT: beforeEach() cleans data but does NOT recreate API
|
|
42
42
|
beforeEach(async () => {
|
|
43
43
|
// Clean all tables - list ALL tables your tests use
|
|
@@ -46,515 +46,515 @@ describe('Positioning Plugin Tests', { timeout: 30000 }, () => {
|
|
|
46
46
|
'positioning_tasks',
|
|
47
47
|
'positioning_projects',
|
|
48
48
|
'positioning_items'
|
|
49
|
-
])
|
|
50
|
-
})
|
|
49
|
+
])
|
|
50
|
+
})
|
|
51
51
|
|
|
52
52
|
describe('Basic Positioning', () => {
|
|
53
|
-
|
|
53
|
+
it('should add position field to first item automatically', async () => {
|
|
54
54
|
// Create a category first
|
|
55
55
|
const category = await api.resources.categories.post({
|
|
56
56
|
name: 'Work Tasks'
|
|
57
|
-
})
|
|
58
|
-
|
|
57
|
+
})
|
|
58
|
+
|
|
59
59
|
// Create first task
|
|
60
60
|
const result = await api.resources.tasks.post({
|
|
61
61
|
title: 'First Task',
|
|
62
62
|
category: category.id
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
assert.equal(result.title, 'First Task')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
assert.equal(result.title, 'First Task')
|
|
66
66
|
// Should have position assigned
|
|
67
|
-
assert(result.position, 'Position should be assigned')
|
|
68
|
-
assert.equal(typeof result.position, 'string')
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
|
|
67
|
+
assert(result.position, 'Position should be assigned')
|
|
68
|
+
assert.equal(typeof result.position, 'string')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('should position items at the end by default', async () => {
|
|
72
72
|
// Create a category
|
|
73
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
74
|
-
|
|
73
|
+
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
74
|
+
|
|
75
75
|
// Create multiple tasks
|
|
76
76
|
const task1 = await api.resources.tasks.post({
|
|
77
77
|
title: 'Task 1',
|
|
78
78
|
category: category.id
|
|
79
|
-
})
|
|
80
|
-
|
|
79
|
+
})
|
|
80
|
+
|
|
81
81
|
const task2 = await api.resources.tasks.post({
|
|
82
82
|
title: 'Task 2',
|
|
83
83
|
category: category.id
|
|
84
|
-
})
|
|
85
|
-
|
|
84
|
+
})
|
|
85
|
+
|
|
86
86
|
const task3 = await api.resources.tasks.post({
|
|
87
87
|
title: 'Task 3',
|
|
88
88
|
category: category.id
|
|
89
|
-
})
|
|
90
|
-
|
|
89
|
+
})
|
|
90
|
+
|
|
91
91
|
// Verify positions are in order
|
|
92
|
-
const pos1 = task1.position
|
|
93
|
-
const pos2 = task2.position
|
|
94
|
-
const pos3 = task3.position
|
|
95
|
-
|
|
96
|
-
assert(pos1 < pos2, 'Task 1 should be before Task 2')
|
|
97
|
-
assert(pos2 < pos3, 'Task 2 should be before Task 3')
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
|
|
92
|
+
const pos1 = task1.position
|
|
93
|
+
const pos2 = task2.position
|
|
94
|
+
const pos3 = task3.position
|
|
95
|
+
|
|
96
|
+
assert(pos1 < pos2, 'Task 1 should be before Task 2')
|
|
97
|
+
assert(pos2 < pos3, 'Task 2 should be before Task 3')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('should maintain separate position sequences per filter group', async () => {
|
|
101
101
|
// Create two categories
|
|
102
|
-
const category1 = await api.resources.categories.post({ name: 'Category 1' })
|
|
103
|
-
const category2 = await api.resources.categories.post({ name: 'Category 2' })
|
|
104
|
-
|
|
102
|
+
const category1 = await api.resources.categories.post({ name: 'Category 1' })
|
|
103
|
+
const category2 = await api.resources.categories.post({ name: 'Category 2' })
|
|
104
|
+
|
|
105
105
|
// Create tasks in different categories
|
|
106
106
|
const task1Cat1 = await api.resources.tasks.post({
|
|
107
107
|
title: 'Cat1 Task 1',
|
|
108
108
|
category: category1.id
|
|
109
|
-
})
|
|
110
|
-
|
|
109
|
+
})
|
|
110
|
+
|
|
111
111
|
const task1Cat2 = await api.resources.tasks.post({
|
|
112
112
|
title: 'Cat2 Task 1',
|
|
113
113
|
category: category2.id
|
|
114
|
-
})
|
|
115
|
-
|
|
114
|
+
})
|
|
115
|
+
|
|
116
116
|
const task2Cat1 = await api.resources.tasks.post({
|
|
117
117
|
title: 'Cat1 Task 2',
|
|
118
118
|
category: category1.id
|
|
119
|
-
})
|
|
120
|
-
|
|
119
|
+
})
|
|
120
|
+
|
|
121
121
|
// Positions should be independent per category
|
|
122
|
-
assert(task1Cat1.position)
|
|
123
|
-
assert(task1Cat2.position)
|
|
124
|
-
assert(task2Cat1.position)
|
|
125
|
-
|
|
122
|
+
assert(task1Cat1.position)
|
|
123
|
+
assert(task1Cat2.position)
|
|
124
|
+
assert(task2Cat1.position)
|
|
125
|
+
|
|
126
126
|
// Task 2 in category 1 should be after task 1 in category 1
|
|
127
|
-
assert(task1Cat1.position < task2Cat1.position)
|
|
128
|
-
|
|
127
|
+
assert(task1Cat1.position < task2Cat1.position)
|
|
128
|
+
|
|
129
129
|
// But positions in different categories can be similar
|
|
130
130
|
// (they're independent sequences)
|
|
131
|
-
})
|
|
132
|
-
})
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
133
|
|
|
134
134
|
describe('Positioning with beforeId', () => {
|
|
135
|
-
|
|
135
|
+
it('should position item before specified id', async () => {
|
|
136
136
|
// Create category
|
|
137
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
138
|
-
|
|
137
|
+
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
138
|
+
|
|
139
139
|
// Create initial tasks
|
|
140
140
|
const task1 = await api.resources.tasks.post({
|
|
141
141
|
title: 'Task 1',
|
|
142
142
|
category: category.id
|
|
143
|
-
})
|
|
144
|
-
|
|
143
|
+
})
|
|
144
|
+
|
|
145
145
|
const task2 = await api.resources.tasks.post({
|
|
146
146
|
title: 'Task 2',
|
|
147
147
|
category: category.id
|
|
148
|
-
})
|
|
149
|
-
|
|
148
|
+
})
|
|
149
|
+
|
|
150
150
|
// Create task that should go between 1 and 2
|
|
151
151
|
const taskMiddle = await api.resources.tasks.post({
|
|
152
152
|
title: 'Middle Task',
|
|
153
153
|
category: category.id,
|
|
154
154
|
beforeId: task2.id
|
|
155
|
-
})
|
|
156
|
-
|
|
155
|
+
})
|
|
156
|
+
|
|
157
157
|
// Verify positioning
|
|
158
|
-
const pos1 = task1.position
|
|
159
|
-
const posMiddle = taskMiddle.position
|
|
160
|
-
const pos2 = task2.position
|
|
161
|
-
|
|
162
|
-
assert(pos1 < posMiddle, 'Task 1 should be before Middle Task')
|
|
163
|
-
assert(posMiddle < pos2, 'Middle Task should be before Task 2')
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
|
|
158
|
+
const pos1 = task1.position
|
|
159
|
+
const posMiddle = taskMiddle.position
|
|
160
|
+
const pos2 = task2.position
|
|
161
|
+
|
|
162
|
+
assert(pos1 < posMiddle, 'Task 1 should be before Middle Task')
|
|
163
|
+
assert(posMiddle < pos2, 'Middle Task should be before Task 2')
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('should position at beginning when beforeId is first item', async () => {
|
|
167
167
|
// Create category
|
|
168
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
169
|
-
|
|
168
|
+
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
169
|
+
|
|
170
170
|
// Create initial tasks
|
|
171
171
|
const task1 = await api.resources.tasks.post({
|
|
172
172
|
title: 'Task 1',
|
|
173
173
|
category: category.id
|
|
174
|
-
})
|
|
175
|
-
|
|
174
|
+
})
|
|
175
|
+
|
|
176
176
|
const task2 = await api.resources.tasks.post({
|
|
177
177
|
title: 'Task 2',
|
|
178
178
|
category: category.id
|
|
179
|
-
})
|
|
180
|
-
|
|
179
|
+
})
|
|
180
|
+
|
|
181
181
|
// Create task at beginning
|
|
182
182
|
const taskFirst = await api.resources.tasks.post({
|
|
183
183
|
title: 'First Task',
|
|
184
184
|
category: category.id,
|
|
185
185
|
beforeId: task1.id
|
|
186
|
-
})
|
|
187
|
-
|
|
186
|
+
})
|
|
187
|
+
|
|
188
188
|
// Verify it's first
|
|
189
|
-
const posFirst = taskFirst.position
|
|
190
|
-
const pos1 = task1.position
|
|
191
|
-
const pos2 = task2.position
|
|
192
|
-
|
|
193
|
-
assert(posFirst < pos1, 'First Task should be before Task 1')
|
|
194
|
-
assert(pos1 < pos2, 'Task 1 should be before Task 2')
|
|
195
|
-
})
|
|
196
|
-
})
|
|
189
|
+
const posFirst = taskFirst.position
|
|
190
|
+
const pos1 = task1.position
|
|
191
|
+
const pos2 = task2.position
|
|
192
|
+
|
|
193
|
+
assert(posFirst < pos1, 'First Task should be before Task 1')
|
|
194
|
+
assert(pos1 < pos2, 'Task 1 should be before Task 2')
|
|
195
|
+
})
|
|
196
|
+
})
|
|
197
197
|
|
|
198
198
|
describe('Position Updates', () => {
|
|
199
|
-
|
|
199
|
+
it('should reposition item when updated with beforeId', async () => {
|
|
200
200
|
// Create category
|
|
201
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
202
|
-
|
|
201
|
+
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
202
|
+
|
|
203
203
|
// Create three tasks
|
|
204
204
|
const task1 = await api.resources.tasks.post({
|
|
205
205
|
title: 'Task 1',
|
|
206
206
|
category: category.id
|
|
207
|
-
})
|
|
208
|
-
|
|
207
|
+
})
|
|
208
|
+
|
|
209
209
|
const task2 = await api.resources.tasks.post({
|
|
210
210
|
title: 'Task 2',
|
|
211
211
|
category: category.id
|
|
212
|
-
})
|
|
213
|
-
|
|
212
|
+
})
|
|
213
|
+
|
|
214
214
|
const task3 = await api.resources.tasks.post({
|
|
215
215
|
title: 'Task 3',
|
|
216
216
|
category: category.id
|
|
217
|
-
})
|
|
218
|
-
|
|
217
|
+
})
|
|
218
|
+
|
|
219
219
|
// Move task 3 before task 2
|
|
220
220
|
const updated = await api.resources.tasks.patch({
|
|
221
221
|
id: task3.id,
|
|
222
222
|
beforeId: task2.id
|
|
223
|
-
})
|
|
224
|
-
|
|
223
|
+
})
|
|
224
|
+
|
|
225
225
|
// Get fresh data
|
|
226
226
|
const { data: allTasks } = await api.resources.tasks.query({
|
|
227
227
|
queryParams: {
|
|
228
228
|
filters: { category: category.id },
|
|
229
229
|
sort: ['position']
|
|
230
230
|
}
|
|
231
|
-
})
|
|
232
|
-
|
|
231
|
+
})
|
|
232
|
+
|
|
233
233
|
// Verify order is now: Task 1, Task 3, Task 2
|
|
234
|
-
assert.equal(allTasks[0].title, 'Task 1')
|
|
235
|
-
assert.equal(allTasks[1].title, 'Task 3')
|
|
236
|
-
assert.equal(allTasks[2].title, 'Task 2')
|
|
237
|
-
})
|
|
238
|
-
|
|
239
|
-
|
|
234
|
+
assert.equal(allTasks[0].title, 'Task 1')
|
|
235
|
+
assert.equal(allTasks[1].title, 'Task 3')
|
|
236
|
+
assert.equal(allTasks[2].title, 'Task 2')
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it('should maintain position when updating without beforeId', async () => {
|
|
240
240
|
// Create category
|
|
241
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
242
|
-
|
|
241
|
+
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
242
|
+
|
|
243
243
|
// Create task
|
|
244
244
|
const task = await api.resources.tasks.post({
|
|
245
245
|
title: 'Original Title',
|
|
246
246
|
category: category.id
|
|
247
|
-
})
|
|
248
|
-
|
|
249
|
-
const originalPosition = task.position
|
|
250
|
-
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
const originalPosition = task.position
|
|
250
|
+
|
|
251
251
|
// Update title without beforeId
|
|
252
252
|
const updated = await api.resources.tasks.patch({
|
|
253
253
|
id: task.id,
|
|
254
254
|
title: 'Updated Title'
|
|
255
|
-
})
|
|
256
|
-
|
|
255
|
+
})
|
|
256
|
+
|
|
257
257
|
// Position should remain the same
|
|
258
|
-
assert.equal(updated.position, originalPosition)
|
|
259
|
-
assert.equal(updated.title, 'Updated Title')
|
|
260
|
-
})
|
|
261
|
-
})
|
|
258
|
+
assert.equal(updated.position, originalPosition)
|
|
259
|
+
assert.equal(updated.title, 'Updated Title')
|
|
260
|
+
})
|
|
261
|
+
})
|
|
262
262
|
|
|
263
263
|
describe('Manual Position Override', () => {
|
|
264
|
-
|
|
264
|
+
it('should ignore manual position setting', async () => {
|
|
265
265
|
// Create category
|
|
266
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
267
|
-
|
|
266
|
+
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
267
|
+
|
|
268
268
|
// Create task with manual position attempt
|
|
269
269
|
const task = await api.resources.tasks.post({
|
|
270
270
|
title: 'Manual Position Task',
|
|
271
271
|
category: category.id,
|
|
272
272
|
position: 'zzz' // Manual position should be ignored
|
|
273
|
-
})
|
|
274
|
-
|
|
273
|
+
})
|
|
274
|
+
|
|
275
275
|
// Position should be automatically assigned, not 'zzz'
|
|
276
|
-
assert.notEqual(task.position, 'zzz')
|
|
277
|
-
assert(task.position)
|
|
278
|
-
assert.equal(task.position, 'a0')
|
|
279
|
-
})
|
|
280
|
-
|
|
281
|
-
|
|
276
|
+
assert.notEqual(task.position, 'zzz')
|
|
277
|
+
assert(task.position) // Should have a position
|
|
278
|
+
assert.equal(task.position, 'a0') // First item gets 'a0'
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
it('should ignore manual position and use beforeId', async () => {
|
|
282
282
|
// Create category
|
|
283
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
284
|
-
|
|
283
|
+
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
284
|
+
|
|
285
285
|
// Create initial task
|
|
286
286
|
const task1 = await api.resources.tasks.post({
|
|
287
287
|
title: 'Task 1',
|
|
288
288
|
category: category.id
|
|
289
|
-
})
|
|
290
|
-
|
|
289
|
+
})
|
|
290
|
+
|
|
291
291
|
// Create task with both manual position and beforeId
|
|
292
292
|
const task2 = await api.resources.tasks.post({
|
|
293
293
|
title: 'Task 2',
|
|
294
294
|
category: category.id,
|
|
295
295
|
position: 'zzz', // Should be ignored
|
|
296
296
|
beforeId: task1.id // Should be used
|
|
297
|
-
})
|
|
298
|
-
|
|
297
|
+
})
|
|
298
|
+
|
|
299
299
|
// Manual position should be ignored, beforeId should work
|
|
300
|
-
assert.notEqual(task2.position, 'zzz')
|
|
301
|
-
assert(task2.position < task1.position)
|
|
302
|
-
})
|
|
303
|
-
})
|
|
300
|
+
assert.notEqual(task2.position, 'zzz')
|
|
301
|
+
assert(task2.position < task1.position) // Should be positioned before task1
|
|
302
|
+
})
|
|
303
|
+
})
|
|
304
304
|
|
|
305
305
|
describe('Position Configuration', () => {
|
|
306
|
-
|
|
306
|
+
it('should handle different field name configuration', async () => {
|
|
307
307
|
// Create a new API with different position field name
|
|
308
308
|
const customApi = await createPositioningApi(knex, {
|
|
309
309
|
apiName: 'positioning-custom-field-test',
|
|
310
310
|
tablePrefix: 'pos_custom'
|
|
311
|
-
})
|
|
312
|
-
|
|
311
|
+
})
|
|
312
|
+
|
|
313
313
|
await customApi.use(PositioningPlugin, {
|
|
314
314
|
field: 'sort_order', // Different field name
|
|
315
315
|
filters: ['project'],
|
|
316
316
|
defaultPosition: 'last'
|
|
317
|
-
})
|
|
318
|
-
|
|
317
|
+
})
|
|
318
|
+
|
|
319
319
|
// Create project
|
|
320
|
-
const project = await customApi.resources.projects.post({ name: 'Test Project' })
|
|
321
|
-
|
|
320
|
+
const project = await customApi.resources.projects.post({ name: 'Test Project' })
|
|
321
|
+
|
|
322
322
|
// Create item
|
|
323
323
|
const item = await customApi.resources.items.post({
|
|
324
324
|
name: 'Test Item',
|
|
325
325
|
project: project.id
|
|
326
|
-
})
|
|
327
|
-
|
|
326
|
+
})
|
|
327
|
+
|
|
328
328
|
// Should have sort_order field, not position
|
|
329
|
-
assert(item.sort_order, 'Should have sort_order field')
|
|
330
|
-
assert(!item.position, 'Should not have position field')
|
|
331
|
-
})
|
|
332
|
-
|
|
333
|
-
|
|
329
|
+
assert(item.sort_order, 'Should have sort_order field')
|
|
330
|
+
assert(!item.position, 'Should not have position field')
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
it('should handle multiple filter fields', async () => {
|
|
334
334
|
// Create a new API with multiple filters
|
|
335
335
|
const multiFilterApi = await createPositioningApi(knex, {
|
|
336
336
|
apiName: 'positioning-multi-filter-test',
|
|
337
337
|
tablePrefix: 'pos_multi'
|
|
338
|
-
})
|
|
339
|
-
|
|
338
|
+
})
|
|
339
|
+
|
|
340
340
|
await multiFilterApi.use(PositioningPlugin, {
|
|
341
341
|
field: 'position',
|
|
342
342
|
filters: ['project', 'status'], // Multiple filters
|
|
343
343
|
defaultPosition: 'last'
|
|
344
|
-
})
|
|
345
|
-
|
|
344
|
+
})
|
|
345
|
+
|
|
346
346
|
// Create project
|
|
347
|
-
const project = await multiFilterApi.resources.projects.post({ name: 'Multi Filter Project' })
|
|
348
|
-
|
|
347
|
+
const project = await multiFilterApi.resources.projects.post({ name: 'Multi Filter Project' })
|
|
348
|
+
|
|
349
349
|
// Create items with different statuses
|
|
350
350
|
const item1Active = await multiFilterApi.resources.items.post({
|
|
351
351
|
name: 'Active Item 1',
|
|
352
352
|
project: project.id,
|
|
353
353
|
status: 'active'
|
|
354
|
-
})
|
|
355
|
-
|
|
354
|
+
})
|
|
355
|
+
|
|
356
356
|
const item2Active = await multiFilterApi.resources.items.post({
|
|
357
357
|
name: 'Active Item 2',
|
|
358
358
|
project: project.id,
|
|
359
359
|
status: 'active'
|
|
360
|
-
})
|
|
361
|
-
|
|
360
|
+
})
|
|
361
|
+
|
|
362
362
|
const item1Archived = await multiFilterApi.resources.items.post({
|
|
363
363
|
name: 'Archived Item 1',
|
|
364
364
|
project: project.id,
|
|
365
365
|
status: 'archived'
|
|
366
|
-
})
|
|
367
|
-
|
|
366
|
+
})
|
|
367
|
+
|
|
368
368
|
// Active items should be in sequence
|
|
369
|
-
assert(item1Active.position < item2Active.position)
|
|
370
|
-
|
|
369
|
+
assert(item1Active.position < item2Active.position)
|
|
370
|
+
|
|
371
371
|
// Archived item should have its own sequence
|
|
372
372
|
// (its position might be similar to active item 1, but they're in different groups)
|
|
373
|
-
})
|
|
374
|
-
|
|
375
|
-
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
it('should handle defaultPosition first configuration', async () => {
|
|
376
376
|
// Create API with defaultPosition: 'first'
|
|
377
377
|
const firstApi = await createPositioningApi(knex, {
|
|
378
378
|
apiName: 'positioning-first-test',
|
|
379
379
|
tablePrefix: 'pos_first'
|
|
380
|
-
})
|
|
381
|
-
|
|
380
|
+
})
|
|
381
|
+
|
|
382
382
|
await firstApi.use(PositioningPlugin, {
|
|
383
383
|
field: 'position',
|
|
384
384
|
filters: ['category'],
|
|
385
385
|
defaultPosition: 'first' // Items go to beginning by default
|
|
386
|
-
})
|
|
387
|
-
|
|
386
|
+
})
|
|
387
|
+
|
|
388
388
|
// Create category
|
|
389
|
-
const category = await firstApi.resources.categories.post({ name: 'First Default Category' })
|
|
390
|
-
|
|
389
|
+
const category = await firstApi.resources.categories.post({ name: 'First Default Category' })
|
|
390
|
+
|
|
391
391
|
// Create tasks - they should be added at the beginning
|
|
392
392
|
const task1 = await firstApi.resources.tasks.post({
|
|
393
393
|
title: 'Task 1',
|
|
394
394
|
category: category.id
|
|
395
|
-
})
|
|
396
|
-
|
|
395
|
+
})
|
|
396
|
+
|
|
397
397
|
const task2 = await firstApi.resources.tasks.post({
|
|
398
398
|
title: 'Task 2',
|
|
399
399
|
category: category.id
|
|
400
|
-
})
|
|
401
|
-
|
|
400
|
+
})
|
|
401
|
+
|
|
402
402
|
const task3 = await firstApi.resources.tasks.post({
|
|
403
403
|
title: 'Task 3',
|
|
404
404
|
category: category.id
|
|
405
|
-
})
|
|
406
|
-
|
|
405
|
+
})
|
|
406
|
+
|
|
407
407
|
// With defaultPosition: 'first', the order should be reversed
|
|
408
408
|
// Task 3 should be first, then Task 2, then Task 1
|
|
409
|
-
assert(task3.position < task2.position)
|
|
410
|
-
assert(task2.position < task1.position)
|
|
411
|
-
})
|
|
412
|
-
})
|
|
409
|
+
assert(task3.position < task2.position)
|
|
410
|
+
assert(task2.position < task1.position)
|
|
411
|
+
})
|
|
412
|
+
})
|
|
413
413
|
|
|
414
414
|
describe('Error Handling', () => {
|
|
415
|
-
|
|
415
|
+
it('should handle invalid beforeId gracefully', async () => {
|
|
416
416
|
// Create category
|
|
417
|
-
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
418
|
-
|
|
417
|
+
const category = await api.resources.categories.post({ name: 'Test Category' })
|
|
418
|
+
|
|
419
419
|
// Create task with non-existent beforeId
|
|
420
420
|
const task = await api.resources.tasks.post({
|
|
421
421
|
title: 'Task with Invalid BeforeId',
|
|
422
422
|
category: category.id,
|
|
423
423
|
beforeId: '99999' // Non-existent ID
|
|
424
|
-
})
|
|
425
|
-
|
|
424
|
+
})
|
|
425
|
+
|
|
426
426
|
// Should still create the task (positioned at end)
|
|
427
|
-
assert(task.position)
|
|
428
|
-
assert.equal(task.title, 'Task with Invalid BeforeId')
|
|
429
|
-
})
|
|
430
|
-
|
|
431
|
-
|
|
427
|
+
assert(task.position)
|
|
428
|
+
assert.equal(task.title, 'Task with Invalid BeforeId')
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
it('should handle null filter values', async () => {
|
|
432
432
|
// Create task without category (null filter)
|
|
433
433
|
const task = await api.resources.tasks.post({
|
|
434
434
|
title: 'Task without Category',
|
|
435
435
|
category: null
|
|
436
|
-
})
|
|
437
|
-
|
|
436
|
+
})
|
|
437
|
+
|
|
438
438
|
// Should still get position
|
|
439
|
-
assert(task.position)
|
|
440
|
-
|
|
439
|
+
assert(task.position)
|
|
440
|
+
|
|
441
441
|
// Create another task without category
|
|
442
442
|
const task2 = await api.resources.tasks.post({
|
|
443
443
|
title: 'Another Task without Category',
|
|
444
444
|
category: null
|
|
445
|
-
})
|
|
446
|
-
|
|
445
|
+
})
|
|
446
|
+
|
|
447
447
|
// They should be in sequence
|
|
448
|
-
assert(task.position < task2.position)
|
|
449
|
-
})
|
|
450
|
-
})
|
|
448
|
+
assert(task.position < task2.position)
|
|
449
|
+
})
|
|
450
|
+
})
|
|
451
451
|
|
|
452
452
|
describe('Excluded Resources', () => {
|
|
453
|
-
|
|
453
|
+
it('should not add positioning to excluded resources', async () => {
|
|
454
454
|
// Create API with excluded resources
|
|
455
455
|
const excludeApi = await createPositioningApi(knex, {
|
|
456
456
|
apiName: 'positioning-exclude-test',
|
|
457
457
|
tablePrefix: 'pos_exclude'
|
|
458
|
-
})
|
|
459
|
-
|
|
458
|
+
})
|
|
459
|
+
|
|
460
460
|
await excludeApi.use(PositioningPlugin, {
|
|
461
461
|
field: 'position',
|
|
462
462
|
filters: [],
|
|
463
463
|
excludeResources: ['categories'] // Exclude categories from positioning
|
|
464
|
-
})
|
|
465
|
-
|
|
464
|
+
})
|
|
465
|
+
|
|
466
466
|
// Create category - should not have position
|
|
467
|
-
const category = await excludeApi.resources.categories.post({ name: 'Excluded Category' })
|
|
468
|
-
|
|
469
|
-
assert(!category.position, 'Category should not have position')
|
|
470
|
-
|
|
467
|
+
const category = await excludeApi.resources.categories.post({ name: 'Excluded Category' })
|
|
468
|
+
|
|
469
|
+
assert(!category.position, 'Category should not have position')
|
|
470
|
+
|
|
471
471
|
// Create task - should have position
|
|
472
472
|
const task = await excludeApi.resources.tasks.post({
|
|
473
473
|
title: 'Task with Position',
|
|
474
474
|
category: category.id
|
|
475
|
-
})
|
|
476
|
-
|
|
477
|
-
assert(task.position, 'Task should have position')
|
|
478
|
-
})
|
|
479
|
-
})
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
assert(task.position, 'Task should have position')
|
|
478
|
+
})
|
|
479
|
+
})
|
|
480
480
|
|
|
481
481
|
describe('Simplified API Mode', () => {
|
|
482
|
-
|
|
482
|
+
it('should handle positioning in simplified mode', async () => {
|
|
483
483
|
// We're already using simplified mode throughout these tests!
|
|
484
484
|
// This test just verifies it works as expected
|
|
485
|
-
|
|
485
|
+
|
|
486
486
|
// Create category
|
|
487
|
-
const category = await api.resources.categories.post({ name: 'Simplified Category' })
|
|
488
|
-
|
|
487
|
+
const category = await api.resources.categories.post({ name: 'Simplified Category' })
|
|
488
|
+
|
|
489
489
|
// Create tasks in simplified mode
|
|
490
490
|
const task1 = await api.resources.tasks.post({
|
|
491
491
|
title: 'Simplified Task 1',
|
|
492
492
|
category: category.id
|
|
493
|
-
})
|
|
494
|
-
|
|
493
|
+
})
|
|
494
|
+
|
|
495
495
|
const task2 = await api.resources.tasks.post({
|
|
496
496
|
title: 'Simplified Task 2',
|
|
497
497
|
category: category.id,
|
|
498
498
|
beforeId: task1.id
|
|
499
|
-
})
|
|
500
|
-
|
|
499
|
+
})
|
|
500
|
+
|
|
501
501
|
// Verify positioning
|
|
502
|
-
assert(task1.position, 'Task 1 should have position')
|
|
503
|
-
assert(task2.position, 'Task 2 should have position')
|
|
504
|
-
assert(task2.position < task1.position, 'Task 2 should be before Task 1')
|
|
505
|
-
})
|
|
506
|
-
})
|
|
502
|
+
assert(task1.position, 'Task 1 should have position')
|
|
503
|
+
assert(task2.position, 'Task 2 should have position')
|
|
504
|
+
assert(task2.position < task1.position, 'Task 2 should be before Task 1')
|
|
505
|
+
})
|
|
506
|
+
})
|
|
507
507
|
|
|
508
508
|
describe('Query Ordering', () => {
|
|
509
|
-
|
|
509
|
+
it('should allow sorting by position field', async () => {
|
|
510
510
|
// Create category
|
|
511
|
-
const category = await api.resources.categories.post({ name: 'Sort Test Category' })
|
|
512
|
-
|
|
511
|
+
const category = await api.resources.categories.post({ name: 'Sort Test Category' })
|
|
512
|
+
|
|
513
513
|
// Create tasks in specific order
|
|
514
514
|
const taskC = await api.resources.tasks.post({
|
|
515
515
|
title: 'C Task',
|
|
516
516
|
category: category.id
|
|
517
|
-
})
|
|
518
|
-
|
|
517
|
+
})
|
|
518
|
+
|
|
519
519
|
const taskA = await api.resources.tasks.post({
|
|
520
520
|
title: 'A Task',
|
|
521
521
|
category: category.id,
|
|
522
522
|
beforeId: taskC.id
|
|
523
|
-
})
|
|
524
|
-
|
|
523
|
+
})
|
|
524
|
+
|
|
525
525
|
const taskB = await api.resources.tasks.post({
|
|
526
526
|
title: 'B Task',
|
|
527
527
|
category: category.id,
|
|
528
528
|
beforeId: taskC.id
|
|
529
|
-
})
|
|
530
|
-
|
|
529
|
+
})
|
|
530
|
+
|
|
531
531
|
// Query with position sort
|
|
532
532
|
const { data: sorted } = await api.resources.tasks.query({
|
|
533
533
|
queryParams: {
|
|
534
534
|
filters: { category: category.id },
|
|
535
535
|
sort: ['position']
|
|
536
536
|
}
|
|
537
|
-
})
|
|
538
|
-
|
|
537
|
+
})
|
|
538
|
+
|
|
539
539
|
// Should be in position order: A, B, C
|
|
540
540
|
// Note: When both A and B are positioned before C, they maintain creation order
|
|
541
|
-
assert.equal(sorted.length, 3)
|
|
542
|
-
assert.equal(sorted[0].title, 'A Task')
|
|
543
|
-
assert.equal(sorted[1].title, 'B Task')
|
|
544
|
-
assert.equal(sorted[2].title, 'C Task')
|
|
545
|
-
|
|
541
|
+
assert.equal(sorted.length, 3)
|
|
542
|
+
assert.equal(sorted[0].title, 'A Task')
|
|
543
|
+
assert.equal(sorted[1].title, 'B Task')
|
|
544
|
+
assert.equal(sorted[2].title, 'C Task')
|
|
545
|
+
|
|
546
546
|
// Query with reverse position sort
|
|
547
547
|
const { data: reverseSorted } = await api.resources.tasks.query({
|
|
548
548
|
queryParams: {
|
|
549
549
|
filters: { category: category.id },
|
|
550
550
|
sort: ['-position']
|
|
551
551
|
}
|
|
552
|
-
})
|
|
553
|
-
|
|
552
|
+
})
|
|
553
|
+
|
|
554
554
|
// Should be in reverse order: C, B, A
|
|
555
|
-
assert.equal(reverseSorted[0].title, 'C Task')
|
|
556
|
-
assert.equal(reverseSorted[1].title, 'B Task')
|
|
557
|
-
assert.equal(reverseSorted[2].title, 'A Task')
|
|
558
|
-
})
|
|
559
|
-
})
|
|
560
|
-
})
|
|
555
|
+
assert.equal(reverseSorted[0].title, 'C Task')
|
|
556
|
+
assert.equal(reverseSorted[1].title, 'B Task')
|
|
557
|
+
assert.equal(reverseSorted[2].title, 'A Task')
|
|
558
|
+
})
|
|
559
|
+
})
|
|
560
|
+
})
|