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.
Files changed (118) hide show
  1. package/connectors_ideas/fastify.js +66 -68
  2. package/connectors_ideas/koa-plugin.js +50 -51
  3. package/eslint.config.js +13 -0
  4. package/examples/url-override-example.js +63 -63
  5. package/index.js +13 -13
  6. package/lib/enhanced-logger.js +51 -51
  7. package/lib/error-formatter.js +61 -61
  8. package/lib/rest-api-errors.js +28 -28
  9. package/package.json +9 -5
  10. package/plugins/core/bulk-operations-plugin.js +142 -145
  11. package/plugins/core/connectors/express-plugin.js +210 -217
  12. package/plugins/core/connectors/lib/busboy-detector.js +84 -84
  13. package/plugins/core/connectors/lib/formidable-detector.js +50 -50
  14. package/plugins/core/connectors/lib/request-helpers.js +74 -74
  15. package/plugins/core/file-handling-plugin.js +125 -125
  16. package/plugins/core/lib/anyapi/anyapi-registry.js +254 -254
  17. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +287 -287
  18. package/plugins/core/lib/anyapi/schema-utils.js +101 -101
  19. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +41 -41
  20. package/plugins/core/lib/dbTablesOperations.js +134 -135
  21. package/plugins/core/lib/fractional-positioning.js +74 -76
  22. package/plugins/core/lib/querying/knex-cross-table-search.js +320 -320
  23. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +144 -144
  24. package/plugins/core/lib/querying/knex-pagination-helpers.js +164 -164
  25. package/plugins/core/lib/querying/knex-process-includes.js +50 -50
  26. package/plugins/core/lib/querying/knex-query-helpers-base.js +31 -31
  27. package/plugins/core/lib/querying/knex-query-helpers.js +451 -452
  28. package/plugins/core/lib/querying/knex-relationship-includes.js +1160 -1170
  29. package/plugins/core/lib/querying/knex-window-queries.js +89 -89
  30. package/plugins/core/lib/querying/url-helpers.js +35 -35
  31. package/plugins/core/lib/querying-writing/compile-schemas.js +97 -98
  32. package/plugins/core/lib/querying-writing/connectors-query-parser.js +31 -31
  33. package/plugins/core/lib/querying-writing/database-capabilities.js +94 -94
  34. package/plugins/core/lib/querying-writing/database-value-normalizers.js +76 -76
  35. package/plugins/core/lib/querying-writing/default-data-helpers.js +27 -27
  36. package/plugins/core/lib/querying-writing/field-utils.js +44 -42
  37. package/plugins/core/lib/querying-writing/knex-constants.js +15 -16
  38. package/plugins/core/lib/querying-writing/knex-field-helpers.js +117 -115
  39. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +41 -41
  40. package/plugins/core/lib/querying-writing/payload-validators.js +275 -275
  41. package/plugins/core/lib/querying-writing/schema-helpers.js +129 -129
  42. package/plugins/core/lib/querying-writing/scope-validations.js +143 -146
  43. package/plugins/core/lib/querying-writing/simplified-helpers.js +133 -129
  44. package/plugins/core/lib/storage/storage-adapter.js +68 -68
  45. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +31 -31
  46. package/plugins/core/lib/writing/many-to-many-manipulations.js +81 -81
  47. package/plugins/core/lib/writing/relationship-processor.js +58 -58
  48. package/plugins/core/rest-api-access.js +8 -8
  49. package/plugins/core/rest-api-anyapi-knex-plugin.js +920 -920
  50. package/plugins/core/rest-api-cors-plugin.js +170 -171
  51. package/plugins/core/rest-api-knex-plugin.js +482 -493
  52. package/plugins/core/rest-api-multihome-plugin.js +247 -251
  53. package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +5 -5
  54. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +20 -21
  55. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +44 -44
  56. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +23 -23
  57. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +15 -15
  58. package/plugins/core/rest-api-plugin-methods/add-route.js +9 -9
  59. package/plugins/core/rest-api-plugin-methods/check-permissions.js +5 -6
  60. package/plugins/core/rest-api-plugin-methods/common.js +295 -297
  61. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +35 -37
  62. package/plugins/core/rest-api-plugin-methods/delete.js +57 -59
  63. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +172 -171
  64. package/plugins/core/rest-api-plugin-methods/get-related.js +96 -96
  65. package/plugins/core/rest-api-plugin-methods/get-relationship.js +16 -18
  66. package/plugins/core/rest-api-plugin-methods/get.js +86 -86
  67. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +16 -17
  68. package/plugins/core/rest-api-plugin-methods/patch.js +63 -64
  69. package/plugins/core/rest-api-plugin-methods/post-relationship.js +54 -55
  70. package/plugins/core/rest-api-plugin-methods/post.js +61 -62
  71. package/plugins/core/rest-api-plugin-methods/put.js +86 -87
  72. package/plugins/core/rest-api-plugin-methods/query.js +88 -88
  73. package/plugins/core/rest-api-plugin-methods/release.js +5 -6
  74. package/plugins/core/rest-api-plugin.js +76 -78
  75. package/plugins/core/rest-api-positioning-plugin.js +251 -252
  76. package/plugins/core/socketio-plugin.js +240 -236
  77. package/plugins/storage/local-storage.js +103 -103
  78. package/plugins/storage/s3-storage.js +41 -42
  79. package/quickTest.js +37 -37
  80. package/scripts/debug-anyapi-review-filter.mjs +18 -18
  81. package/scripts/inspect-anyapi-descriptor-country.mjs +10 -10
  82. package/scripts/inspect-anyapi-descriptor.mjs +10 -10
  83. package/scripts/run-docs.js +112 -113
  84. package/test-debug.js +28 -28
  85. package/tests/TEST_TEMPLATE.test.js +51 -51
  86. package/tests/anyapi-basic.test.js +42 -42
  87. package/tests/anyapi-cursor-pagination.test.js +48 -48
  88. package/tests/bulk-operations.test.js +208 -208
  89. package/tests/computed-fields.test.js +204 -204
  90. package/tests/cors.test.js +412 -413
  91. package/tests/curl.test.js +275 -276
  92. package/tests/custom-idproperty-relationships.test.js +266 -266
  93. package/tests/field-getters.test.js +132 -132
  94. package/tests/field-setters.test.js +110 -110
  95. package/tests/fixtures/api-configs.js +699 -699
  96. package/tests/helpers/storage-mode.js +32 -32
  97. package/tests/helpers/test-utils.js +76 -76
  98. package/tests/include-depth-validation.test.js +76 -76
  99. package/tests/includes.test.js +145 -145
  100. package/tests/multihome.test.js +175 -175
  101. package/tests/nested-includes.test.js +255 -255
  102. package/tests/pagination-cursor-multifield.test.js +164 -164
  103. package/tests/pagination-enhanced.test.js +225 -225
  104. package/tests/polymorphic-relationships.test.js +96 -96
  105. package/tests/polymorphic-simple.test.js +44 -44
  106. package/tests/positioning-advanced.test.js +226 -226
  107. package/tests/positioning-extended.test.js +399 -399
  108. package/tests/positioning-multifilter.test.js +204 -204
  109. package/tests/positioning.test.js +266 -266
  110. package/tests/queries.test.js +184 -184
  111. package/tests/query-limits.test.js +154 -155
  112. package/tests/relationship-endpoints.test.js +127 -127
  113. package/tests/relationships.test.js +178 -179
  114. package/tests/rest-api-access.test.js +86 -86
  115. package/tests/rest-api.test.js +103 -103
  116. package/tests/searchschema-merge.test.js +93 -93
  117. package/tests/socketio.test.js +326 -326
  118. 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,43 +12,43 @@ const knex = knexLib({
12
12
  filename: ':memory:'
13
13
  },
14
14
  useNullAsDefault: true
15
- });
15
+ })
16
16
 
17
17
  describe('Multi-Filter Positioning Tests', { timeout: 30000 }, () => {
18
- let api;
18
+ let api
19
19
 
20
20
  before(async () => {
21
21
  // Initialize API
22
- api = await createPositioningApi(knex);
23
-
22
+ api = await createPositioningApi(knex)
23
+
24
24
  // Install positioning plugin with default configuration
25
25
  await api.use(PositioningPlugin, {
26
26
  field: 'position',
27
27
  filters: ['project', 'status'], // Default filters for most tests
28
28
  defaultPosition: 'last'
29
- });
30
- });
31
-
29
+ })
30
+ })
31
+
32
32
  after(async () => {
33
- await knex.destroy();
34
- });
35
-
33
+ await knex.destroy()
34
+ })
35
+
36
36
  beforeEach(async () => {
37
37
  await cleanTables(knex, [
38
38
  'positioning_categories',
39
39
  'positioning_tasks',
40
40
  'positioning_projects',
41
41
  'positioning_items'
42
- ]);
43
- });
42
+ ])
43
+ })
44
44
 
45
45
  describe('Multiple Filter Fields', () => {
46
46
  it('should maintain separate position sequences for each filter combination', async () => {
47
47
  // Plugin already installed with filters in before hook
48
48
 
49
49
  // Create projects
50
- const project1 = await api.resources.projects.post({ name: 'Project 1' });
51
- const project2 = await api.resources.projects.post({ name: 'Project 2' });
50
+ const project1 = await api.resources.projects.post({ name: 'Project 1' })
51
+ const project2 = await api.resources.projects.post({ name: 'Project 2' })
52
52
 
53
53
  // Create items in different filter combinations
54
54
  // Project 1, Active
@@ -56,46 +56,46 @@ describe('Multi-Filter Positioning Tests', { timeout: 30000 }, () => {
56
56
  name: 'P1 Active 1',
57
57
  project: project1.id,
58
58
  status: 'active'
59
- });
59
+ })
60
60
 
61
61
  const p1Active2 = await api.resources.items.post({
62
62
  name: 'P1 Active 2',
63
63
  project: project1.id,
64
64
  status: 'active'
65
- });
65
+ })
66
66
 
67
67
  // Project 1, Archived
68
68
  const p1Archived1 = await api.resources.items.post({
69
69
  name: 'P1 Archived 1',
70
70
  project: project1.id,
71
71
  status: 'archived'
72
- });
72
+ })
73
73
 
74
74
  const p1Archived2 = await api.resources.items.post({
75
75
  name: 'P1 Archived 2',
76
76
  project: project1.id,
77
77
  status: 'archived'
78
- });
78
+ })
79
79
 
80
80
  // Project 2, Active
81
81
  const p2Active1 = await api.resources.items.post({
82
82
  name: 'P2 Active 1',
83
83
  project: project2.id,
84
84
  status: 'active'
85
- });
85
+ })
86
86
 
87
87
  const p2Active2 = await api.resources.items.post({
88
88
  name: 'P2 Active 2',
89
89
  project: project2.id,
90
90
  status: 'active'
91
- });
91
+ })
92
92
 
93
93
  // Project 2, Archived
94
94
  const p2Archived1 = await api.resources.items.post({
95
95
  name: 'P2 Archived 1',
96
96
  project: project2.id,
97
97
  status: 'archived'
98
- });
98
+ })
99
99
 
100
100
  // Query each combination
101
101
  const { data: p1ActiveItems } = await api.resources.items.query({
@@ -103,71 +103,71 @@ describe('Multi-Filter Positioning Tests', { timeout: 30000 }, () => {
103
103
  filters: { project: project1.id, status: 'active' },
104
104
  sort: ['position']
105
105
  }
106
- });
106
+ })
107
107
 
108
- assert.equal(p1ActiveItems.length, 2);
109
- assert.equal(p1ActiveItems[0].name, 'P1 Active 1');
110
- assert.equal(p1ActiveItems[1].name, 'P1 Active 2');
108
+ assert.equal(p1ActiveItems.length, 2)
109
+ assert.equal(p1ActiveItems[0].name, 'P1 Active 1')
110
+ assert.equal(p1ActiveItems[1].name, 'P1 Active 2')
111
111
 
112
112
  const { data: p1ArchivedItems } = await api.resources.items.query({
113
113
  queryParams: {
114
114
  filters: { project: project1.id, status: 'archived' },
115
115
  sort: ['position']
116
116
  }
117
- });
117
+ })
118
118
 
119
- assert.equal(p1ArchivedItems.length, 2);
120
- assert.equal(p1ArchivedItems[0].name, 'P1 Archived 1');
121
- assert.equal(p1ArchivedItems[1].name, 'P1 Archived 2');
119
+ assert.equal(p1ArchivedItems.length, 2)
120
+ assert.equal(p1ArchivedItems[0].name, 'P1 Archived 1')
121
+ assert.equal(p1ArchivedItems[1].name, 'P1 Archived 2')
122
122
 
123
123
  const { data: p2ActiveItems } = await api.resources.items.query({
124
124
  queryParams: {
125
125
  filters: { project: project2.id, status: 'active' },
126
126
  sort: ['position']
127
127
  }
128
- });
128
+ })
129
129
 
130
- assert.equal(p2ActiveItems.length, 2);
131
- assert.equal(p2ActiveItems[0].name, 'P2 Active 1');
132
- assert.equal(p2ActiveItems[1].name, 'P2 Active 2');
130
+ assert.equal(p2ActiveItems.length, 2)
131
+ assert.equal(p2ActiveItems[0].name, 'P2 Active 1')
132
+ assert.equal(p2ActiveItems[1].name, 'P2 Active 2')
133
133
 
134
134
  // Verify positions are independent across filter combinations
135
135
  // Items in different groups can have the same position
136
- assert.equal(p1Active1.position, 'a0');
137
- assert.equal(p1Archived1.position, 'a0');
138
- assert.equal(p2Active1.position, 'a0');
139
- assert.equal(p2Archived1.position, 'a0');
140
- });
136
+ assert.equal(p1Active1.position, 'a0')
137
+ assert.equal(p1Archived1.position, 'a0')
138
+ assert.equal(p2Active1.position, 'a0')
139
+ assert.equal(p2Archived1.position, 'a0')
140
+ })
141
141
 
142
142
  it('should handle positioning with null values in multi-filter setup', async () => {
143
143
  // Plugin already installed with filters in before hook
144
144
 
145
- const project = await api.resources.projects.post({ name: 'Project' });
145
+ const project = await api.resources.projects.post({ name: 'Project' })
146
146
 
147
147
  // Create items with various null combinations
148
148
  const item1 = await api.resources.items.post({
149
149
  name: 'Item 1',
150
150
  project: project.id,
151
151
  status: 'active'
152
- });
152
+ })
153
153
 
154
154
  const item2 = await api.resources.items.post({
155
155
  name: 'Item 2',
156
156
  project: project.id,
157
157
  status: null // Null status
158
- });
158
+ })
159
159
 
160
160
  const item3 = await api.resources.items.post({
161
161
  name: 'Item 3',
162
162
  project: null, // Null project
163
163
  status: 'active'
164
- });
164
+ })
165
165
 
166
166
  const item4 = await api.resources.items.post({
167
167
  name: 'Item 4',
168
168
  project: null,
169
169
  status: null // Both null
170
- });
170
+ })
171
171
 
172
172
  // Query different combinations
173
173
  const { data: projectActiveItems } = await api.resources.items.query({
@@ -175,61 +175,61 @@ describe('Multi-Filter Positioning Tests', { timeout: 30000 }, () => {
175
175
  filters: { project: project.id, status: 'active' },
176
176
  sort: ['position']
177
177
  }
178
- });
179
- assert.equal(projectActiveItems.length, 1);
180
- assert.equal(projectActiveItems[0].name, 'Item 1');
178
+ })
179
+ assert.equal(projectActiveItems.length, 1)
180
+ assert.equal(projectActiveItems[0].name, 'Item 1')
181
181
 
182
182
  const { data: projectNullStatusItems } = await api.resources.items.query({
183
183
  queryParams: {
184
184
  filters: { project: project.id, status: null },
185
185
  sort: ['position']
186
186
  }
187
- });
188
- assert.equal(projectNullStatusItems.length, 1);
189
- assert.equal(projectNullStatusItems[0].name, 'Item 2');
187
+ })
188
+ assert.equal(projectNullStatusItems.length, 1)
189
+ assert.equal(projectNullStatusItems[0].name, 'Item 2')
190
190
 
191
191
  const { data: nullProjectActiveItems } = await api.resources.items.query({
192
192
  queryParams: {
193
193
  filters: { project: null, status: 'active' },
194
194
  sort: ['position']
195
195
  }
196
- });
197
- assert.equal(nullProjectActiveItems.length, 1);
198
- assert.equal(nullProjectActiveItems[0].name, 'Item 3');
196
+ })
197
+ assert.equal(nullProjectActiveItems.length, 1)
198
+ assert.equal(nullProjectActiveItems[0].name, 'Item 3')
199
199
 
200
200
  const { data: bothNullItems } = await api.resources.items.query({
201
201
  queryParams: {
202
202
  filters: { project: null, status: null },
203
203
  sort: ['position']
204
204
  }
205
- });
206
- assert.equal(bothNullItems.length, 1);
207
- assert.equal(bothNullItems[0].name, 'Item 4');
208
- });
205
+ })
206
+ assert.equal(bothNullItems.length, 1)
207
+ assert.equal(bothNullItems[0].name, 'Item 4')
208
+ })
209
209
 
210
210
  it('should handle beforeId correctly with multi-filter positioning', async () => {
211
211
  // Plugin already installed with filters in before hook
212
212
 
213
- const project = await api.resources.projects.post({ name: 'Project' });
213
+ const project = await api.resources.projects.post({ name: 'Project' })
214
214
 
215
215
  // Create initial items
216
216
  const item1 = await api.resources.items.post({
217
217
  name: 'Item 1',
218
218
  project: project.id,
219
219
  status: 'active'
220
- });
220
+ })
221
221
 
222
222
  const item2 = await api.resources.items.post({
223
223
  name: 'Item 2',
224
224
  project: project.id,
225
225
  status: 'active'
226
- });
226
+ })
227
227
 
228
228
  const item3 = await api.resources.items.post({
229
229
  name: 'Item 3',
230
230
  project: project.id,
231
231
  status: 'active'
232
- });
232
+ })
233
233
 
234
234
  // Create item in different filter group with beforeId from first group
235
235
  const item4 = await api.resources.items.post({
@@ -237,7 +237,7 @@ describe('Multi-Filter Positioning Tests', { timeout: 30000 }, () => {
237
237
  project: project.id,
238
238
  status: 'archived',
239
239
  beforeId: item2.id // This should be ignored since it's in a different filter group
240
- });
240
+ })
241
241
 
242
242
  // Item 4 should be first in its filter group
243
243
  const { data: archivedItems } = await api.resources.items.query({
@@ -245,11 +245,11 @@ describe('Multi-Filter Positioning Tests', { timeout: 30000 }, () => {
245
245
  filters: { project: project.id, status: 'archived' },
246
246
  sort: ['position']
247
247
  }
248
- });
248
+ })
249
249
 
250
- assert.equal(archivedItems.length, 1);
251
- assert.equal(archivedItems[0].name, 'Item 4');
252
- assert.equal(archivedItems[0].position, 'a0'); // First in its group
250
+ assert.equal(archivedItems.length, 1)
251
+ assert.equal(archivedItems[0].name, 'Item 4')
252
+ assert.equal(archivedItems[0].position, 'a0') // First in its group
253
253
 
254
254
  // Now position an item before another in the same filter group
255
255
  const item5 = await api.resources.items.post({
@@ -257,59 +257,59 @@ describe('Multi-Filter Positioning Tests', { timeout: 30000 }, () => {
257
257
  project: project.id,
258
258
  status: 'active',
259
259
  beforeId: item2.id
260
- });
260
+ })
261
261
 
262
262
  const { data: activeItems } = await api.resources.items.query({
263
263
  queryParams: {
264
264
  filters: { project: project.id, status: 'active' },
265
265
  sort: ['position']
266
266
  }
267
- });
267
+ })
268
268
 
269
- assert.equal(activeItems.length, 4);
270
- const item5Index = activeItems.findIndex(i => i.name === 'Item 5');
271
- const item2Index = activeItems.findIndex(i => i.name === 'Item 2');
272
- assert(item5Index < item2Index, 'Item 5 should be before Item 2');
273
- });
269
+ assert.equal(activeItems.length, 4)
270
+ const item5Index = activeItems.findIndex(i => i.name === 'Item 5')
271
+ const item2Index = activeItems.findIndex(i => i.name === 'Item 2')
272
+ assert(item5Index < item2Index, 'Item 5 should be before Item 2')
273
+ })
274
274
 
275
275
  it('should handle updates that change filter values', async () => {
276
276
  // Plugin already installed with filters in before hook
277
277
 
278
- const project1 = await api.resources.projects.post({ name: 'Project 1' });
279
- const project2 = await api.resources.projects.post({ name: 'Project 2' });
278
+ const project1 = await api.resources.projects.post({ name: 'Project 1' })
279
+ const project2 = await api.resources.projects.post({ name: 'Project 2' })
280
280
 
281
281
  // Create items in project 1
282
282
  const item1 = await api.resources.items.post({
283
283
  name: 'Item 1',
284
284
  project: project1.id,
285
285
  status: 'active'
286
- });
286
+ })
287
287
 
288
288
  const item2 = await api.resources.items.post({
289
289
  name: 'Item 2',
290
290
  project: project1.id,
291
291
  status: 'active'
292
- });
292
+ })
293
293
 
294
294
  const item3 = await api.resources.items.post({
295
295
  name: 'Item 3',
296
296
  project: project1.id,
297
297
  status: 'active'
298
- });
298
+ })
299
299
 
300
300
  // Create items in project 2
301
301
  const item4 = await api.resources.items.post({
302
302
  name: 'Item 4',
303
303
  project: project2.id,
304
304
  status: 'active'
305
- });
305
+ })
306
306
 
307
307
  // Move item2 to project 2, positioning it before item4
308
308
  await api.resources.items.patch({
309
309
  id: item2.id,
310
310
  project: project2.id,
311
311
  beforeId: item4.id
312
- });
312
+ })
313
313
 
314
314
  // Verify project 1 now has 2 items
315
315
  const { data: p1Items } = await api.resources.items.query({
@@ -317,11 +317,11 @@ describe('Multi-Filter Positioning Tests', { timeout: 30000 }, () => {
317
317
  filters: { project: project1.id, status: 'active' },
318
318
  sort: ['position']
319
319
  }
320
- });
320
+ })
321
321
 
322
- assert.equal(p1Items.length, 2);
323
- assert.equal(p1Items[0].name, 'Item 1');
324
- assert.equal(p1Items[1].name, 'Item 3');
322
+ assert.equal(p1Items.length, 2)
323
+ assert.equal(p1Items[0].name, 'Item 1')
324
+ assert.equal(p1Items[1].name, 'Item 3')
325
325
 
326
326
  // Verify project 2 has items in correct order
327
327
  const { data: p2Items } = await api.resources.items.query({
@@ -329,17 +329,17 @@ describe('Multi-Filter Positioning Tests', { timeout: 30000 }, () => {
329
329
  filters: { project: project2.id, status: 'active' },
330
330
  sort: ['position']
331
331
  }
332
- });
332
+ })
333
333
 
334
- assert.equal(p2Items.length, 2);
335
- assert.equal(p2Items[0].name, 'Item 2');
336
- assert.equal(p2Items[1].name, 'Item 4');
334
+ assert.equal(p2Items.length, 2)
335
+ assert.equal(p2Items[0].name, 'Item 2')
336
+ assert.equal(p2Items[1].name, 'Item 4')
337
337
 
338
338
  // Change status of item3 to archived
339
339
  await api.resources.items.patch({
340
340
  id: item3.id,
341
341
  status: 'archived'
342
- });
342
+ })
343
343
 
344
344
  // Verify it's now in a different filter group
345
345
  const { data: p1ActiveItems } = await api.resources.items.query({
@@ -347,25 +347,25 @@ describe('Multi-Filter Positioning Tests', { timeout: 30000 }, () => {
347
347
  filters: { project: project1.id, status: 'active' },
348
348
  sort: ['position']
349
349
  }
350
- });
350
+ })
351
351
 
352
- assert.equal(p1ActiveItems.length, 1);
353
- assert.equal(p1ActiveItems[0].name, 'Item 1');
352
+ assert.equal(p1ActiveItems.length, 1)
353
+ assert.equal(p1ActiveItems[0].name, 'Item 1')
354
354
 
355
355
  const { data: p1ArchivedItems } = await api.resources.items.query({
356
356
  queryParams: {
357
357
  filters: { project: project1.id, status: 'archived' },
358
358
  sort: ['position']
359
359
  }
360
- });
360
+ })
361
361
 
362
- assert.equal(p1ArchivedItems.length, 1);
363
- assert.equal(p1ArchivedItems[0].name, 'Item 3');
362
+ assert.equal(p1ArchivedItems.length, 1)
363
+ assert.equal(p1ArchivedItems[0].name, 'Item 3')
364
364
  // Position should be maintained when moving to new filter group
365
365
  // (the plugin doesn't know where to position it in the new group)
366
- assert(p1ArchivedItems[0].position); // Just verify it has a position
367
- });
368
- });
366
+ assert(p1ArchivedItems[0].position) // Just verify it has a position
367
+ })
368
+ })
369
369
 
370
370
  describe('Complex Multi-Filter Scenarios', () => {
371
371
  it('should handle three or more filter fields', async () => {
@@ -373,7 +373,7 @@ describe('Multi-Filter Positioning Tests', { timeout: 30000 }, () => {
373
373
  const multiApi = await createPositioningApi(knex, {
374
374
  apiName: 'multi-filter-three',
375
375
  tablePrefix: 'mf3'
376
- });
376
+ })
377
377
 
378
378
  // Priority field is already in the items schema
379
379
 
@@ -381,14 +381,14 @@ describe('Multi-Filter Positioning Tests', { timeout: 30000 }, () => {
381
381
  field: 'position',
382
382
  filters: ['project', 'status', 'priority'],
383
383
  defaultPosition: 'last'
384
- });
384
+ })
385
385
 
386
- const project = await multiApi.resources.projects.post({ name: 'Project' });
386
+ const project = await multiApi.resources.projects.post({ name: 'Project' })
387
387
 
388
388
  // Create items in different combinations
389
- const items = [];
390
- const statuses = ['active', 'pending', 'archived'];
391
- const priorities = ['high', 'medium', 'low'];
389
+ const items = []
390
+ const statuses = ['active', 'pending', 'archived']
391
+ const priorities = ['high', 'medium', 'low']
392
392
 
393
393
  for (const status of statuses) {
394
394
  for (const priority of priorities) {
@@ -397,8 +397,8 @@ describe('Multi-Filter Positioning Tests', { timeout: 30000 }, () => {
397
397
  project: project.id,
398
398
  status,
399
399
  priority
400
- });
401
- items.push(item);
400
+ })
401
+ items.push(item)
402
402
  }
403
403
  }
404
404
 
@@ -407,83 +407,83 @@ describe('Multi-Filter Positioning Tests', { timeout: 30000 }, () => {
407
407
  for (const priority of priorities) {
408
408
  const { data: filtered } = await multiApi.resources.items.query({
409
409
  queryParams: {
410
- filters: {
411
- project: project.id,
412
- status: status,
413
- priority: priority
410
+ filters: {
411
+ project: project.id,
412
+ status,
413
+ priority
414
414
  },
415
415
  sort: ['position']
416
416
  }
417
- });
417
+ })
418
418
 
419
- assert.equal(filtered.length, 1);
420
- assert.equal(filtered[0].name, `Item ${status}-${priority}`);
421
- assert.equal(filtered[0].position, 'a0'); // Each is first in its group
419
+ assert.equal(filtered.length, 1)
420
+ assert.equal(filtered[0].name, `Item ${status}-${priority}`)
421
+ assert.equal(filtered[0].position, 'a0') // Each is first in its group
422
422
  }
423
423
  }
424
424
 
425
425
  // Add more items to one combination
426
- const moreItems = [];
426
+ const moreItems = []
427
427
  for (let i = 2; i <= 5; i++) {
428
428
  const item = await multiApi.resources.items.post({
429
429
  name: `Item active-high ${i}`,
430
430
  project: project.id,
431
431
  status: 'active',
432
432
  priority: 'high'
433
- });
434
- moreItems.push(item);
433
+ })
434
+ moreItems.push(item)
435
435
  }
436
436
 
437
437
  // Verify the active-high combination now has 5 items
438
438
  const { data: activeHighItems } = await multiApi.resources.items.query({
439
439
  queryParams: {
440
- filters: {
441
- project: project.id,
440
+ filters: {
441
+ project: project.id,
442
442
  status: 'active',
443
- priority: 'high'
443
+ priority: 'high'
444
444
  },
445
445
  sort: ['position']
446
446
  }
447
- });
447
+ })
448
+
449
+ assert.equal(activeHighItems.length, 5)
448
450
 
449
- assert.equal(activeHighItems.length, 5);
450
-
451
451
  // Verify they're in order
452
452
  for (let i = 1; i < activeHighItems.length; i++) {
453
- assert(activeHighItems[i-1].position < activeHighItems[i].position);
453
+ assert(activeHighItems[i - 1].position < activeHighItems[i].position)
454
454
  }
455
- });
455
+ })
456
456
 
457
457
  it('should handle partial filter queries correctly', async () => {
458
458
  // Plugin already installed with filters in before hook
459
459
 
460
- const project1 = await api.resources.projects.post({ name: 'Project 1' });
461
- const project2 = await api.resources.projects.post({ name: 'Project 2' });
460
+ const project1 = await api.resources.projects.post({ name: 'Project 1' })
461
+ const project2 = await api.resources.projects.post({ name: 'Project 2' })
462
462
 
463
463
  // Create items
464
464
  await api.resources.items.post({
465
465
  name: 'P1 Active 1',
466
466
  project: project1.id,
467
467
  status: 'active'
468
- });
468
+ })
469
469
 
470
470
  await api.resources.items.post({
471
471
  name: 'P1 Active 2',
472
472
  project: project1.id,
473
473
  status: 'active'
474
- });
474
+ })
475
475
 
476
476
  await api.resources.items.post({
477
477
  name: 'P1 Archived',
478
478
  project: project1.id,
479
479
  status: 'archived'
480
- });
480
+ })
481
481
 
482
482
  await api.resources.items.post({
483
483
  name: 'P2 Active',
484
484
  project: project2.id,
485
485
  status: 'active'
486
- });
486
+ })
487
487
 
488
488
  // Query with only one filter (should return items from multiple position groups)
489
489
  const { data: activeItems } = await api.resources.items.query({
@@ -491,36 +491,36 @@ describe('Multi-Filter Positioning Tests', { timeout: 30000 }, () => {
491
491
  filters: { status: 'active' },
492
492
  sort: ['position']
493
493
  }
494
- });
494
+ })
495
+
496
+ assert.equal(activeItems.length, 3)
495
497
 
496
- assert.equal(activeItems.length, 3);
497
-
498
498
  // Items from different projects might have same position
499
- const p1ActiveItems = activeItems.filter(i => i.name.startsWith('P1'));
500
- const p2ActiveItems = activeItems.filter(i => i.name.startsWith('P2'));
501
-
502
- assert.equal(p1ActiveItems.length, 2);
503
- assert.equal(p2ActiveItems.length, 1);
504
-
499
+ const p1ActiveItems = activeItems.filter(i => i.name.startsWith('P1'))
500
+ const p2ActiveItems = activeItems.filter(i => i.name.startsWith('P2'))
501
+
502
+ assert.equal(p1ActiveItems.length, 2)
503
+ assert.equal(p2ActiveItems.length, 1)
504
+
505
505
  // Within same project, positions should be unique and ordered
506
- assert(p1ActiveItems[0].position < p1ActiveItems[1].position);
507
- });
506
+ assert(p1ActiveItems[0].position < p1ActiveItems[1].position)
507
+ })
508
508
 
509
509
  it('should handle filter changes during bulk operations', async () => {
510
510
  // Plugin already installed with filters in before hook
511
511
 
512
- const project1 = await api.resources.projects.post({ name: 'Project 1' });
513
- const project2 = await api.resources.projects.post({ name: 'Project 2' });
512
+ const project1 = await api.resources.projects.post({ name: 'Project 1' })
513
+ const project2 = await api.resources.projects.post({ name: 'Project 2' })
514
514
 
515
515
  // Create multiple items
516
- const items = [];
516
+ const items = []
517
517
  for (let i = 1; i <= 10; i++) {
518
518
  const item = await api.resources.items.post({
519
519
  name: `Item ${i}`,
520
520
  project: project1.id,
521
521
  status: 'active'
522
- });
523
- items.push(item);
522
+ })
523
+ items.push(item)
524
524
  }
525
525
 
526
526
  // Move multiple items to different filter groups concurrently
@@ -529,20 +529,20 @@ describe('Multi-Filter Positioning Tests', { timeout: 30000 }, () => {
529
529
  api.resources.items.patch({ id: items[0].id, status: 'archived' }),
530
530
  api.resources.items.patch({ id: items[1].id, status: 'archived' }),
531
531
  api.resources.items.patch({ id: items[2].id, status: 'archived' }),
532
-
532
+
533
533
  // Move to project 2
534
534
  api.resources.items.patch({ id: items[3].id, project: project2.id }),
535
535
  api.resources.items.patch({ id: items[4].id, project: project2.id }),
536
-
536
+
537
537
  // Move to project 2 and archived
538
- api.resources.items.patch({
539
- id: items[5].id,
540
- project: project2.id,
541
- status: 'archived'
538
+ api.resources.items.patch({
539
+ id: items[5].id,
540
+ project: project2.id,
541
+ status: 'archived'
542
542
  })
543
- ];
543
+ ]
544
544
 
545
- await Promise.all(updates);
545
+ await Promise.all(updates)
546
546
 
547
547
  // Verify distributions
548
548
  const { data: p1Active } = await api.resources.items.query({
@@ -550,73 +550,73 @@ describe('Multi-Filter Positioning Tests', { timeout: 30000 }, () => {
550
550
  filters: { project: project1.id, status: 'active' },
551
551
  sort: ['position']
552
552
  }
553
- });
554
- assert.equal(p1Active.length, 4); // 10 - 3 archived - 3 moved to p2
553
+ })
554
+ assert.equal(p1Active.length, 4) // 10 - 3 archived - 3 moved to p2
555
555
 
556
556
  const { data: p1Archived } = await api.resources.items.query({
557
557
  queryParams: {
558
558
  filters: { project: project1.id, status: 'archived' },
559
559
  sort: ['position']
560
560
  }
561
- });
562
- assert.equal(p1Archived.length, 3);
561
+ })
562
+ assert.equal(p1Archived.length, 3)
563
563
 
564
564
  const { data: p2Active } = await api.resources.items.query({
565
565
  queryParams: {
566
566
  filters: { project: project2.id, status: 'active' },
567
567
  sort: ['position']
568
568
  }
569
- });
570
- assert.equal(p2Active.length, 2);
569
+ })
570
+ assert.equal(p2Active.length, 2)
571
571
 
572
572
  const { data: p2Archived } = await api.resources.items.query({
573
573
  queryParams: {
574
574
  filters: { project: project2.id, status: 'archived' },
575
575
  sort: ['position']
576
576
  }
577
- });
578
- assert.equal(p2Archived.length, 1);
577
+ })
578
+ assert.equal(p2Archived.length, 1)
579
579
 
580
580
  // Verify all groups have proper positioning
581
- const allGroups = [p1Active, p1Archived, p2Active, p2Archived];
581
+ const allGroups = [p1Active, p1Archived, p2Active, p2Archived]
582
582
  for (const group of allGroups) {
583
583
  if (group.length > 0) {
584
584
  // All items should have positions and be properly ordered
585
585
  for (let i = 0; i < group.length; i++) {
586
- assert(group[i].position, `Item ${i} should have a position`);
586
+ assert(group[i].position, `Item ${i} should have a position`)
587
587
  if (i > 0) {
588
- assert(group[i-1].position < group[i].position, `Items should be ordered`);
588
+ assert(group[i - 1].position < group[i].position, 'Items should be ordered')
589
589
  }
590
590
  }
591
591
  }
592
592
  }
593
- });
594
- });
593
+ })
594
+ })
595
595
 
596
596
  describe('Edge Cases with Multi-Filter', () => {
597
597
  it('should handle empty string vs null in filters', async () => {
598
598
  // Plugin already installed with filters in before hook
599
599
 
600
- const project = await api.resources.projects.post({ name: 'Project' });
600
+ const project = await api.resources.projects.post({ name: 'Project' })
601
601
 
602
602
  // Create items with various empty/null combinations
603
603
  const item1 = await api.resources.items.post({
604
604
  name: 'Item 1',
605
605
  project: project.id,
606
606
  status: '' // Empty string
607
- });
607
+ })
608
608
 
609
609
  const item2 = await api.resources.items.post({
610
610
  name: 'Item 2',
611
611
  project: project.id,
612
612
  status: null // Null
613
- });
613
+ })
614
614
 
615
615
  const item3 = await api.resources.items.post({
616
616
  name: 'Item 3',
617
617
  project: project.id,
618
618
  status: 'active'
619
- });
619
+ })
620
620
 
621
621
  // Query with empty string filter
622
622
  const { data: emptyStatusItems } = await api.resources.items.query({
@@ -624,10 +624,10 @@ describe('Multi-Filter Positioning Tests', { timeout: 30000 }, () => {
624
624
  filters: { project: project.id, status: '' },
625
625
  sort: ['position']
626
626
  }
627
- });
627
+ })
628
628
 
629
- assert.equal(emptyStatusItems.length, 1);
630
- assert.equal(emptyStatusItems[0].name, 'Item 1');
629
+ assert.equal(emptyStatusItems.length, 1)
630
+ assert.equal(emptyStatusItems[0].name, 'Item 1')
631
631
 
632
632
  // Query with null filter
633
633
  const { data: nullStatusItems } = await api.resources.items.query({
@@ -635,40 +635,40 @@ describe('Multi-Filter Positioning Tests', { timeout: 30000 }, () => {
635
635
  filters: { project: project.id, status: null },
636
636
  sort: ['position']
637
637
  }
638
- });
638
+ })
639
639
 
640
- assert.equal(nullStatusItems.length, 1);
641
- assert.equal(nullStatusItems[0].name, 'Item 2');
640
+ assert.equal(nullStatusItems.length, 1)
641
+ assert.equal(nullStatusItems[0].name, 'Item 2')
642
642
 
643
643
  // Verify they're in different position groups
644
- assert.equal(item1.position, 'a0');
645
- assert.equal(item2.position, 'a0');
646
- assert.equal(item3.position, 'a0');
647
- });
644
+ assert.equal(item1.position, 'a0')
645
+ assert.equal(item2.position, 'a0')
646
+ assert.equal(item3.position, 'a0')
647
+ })
648
648
 
649
649
  it('should handle numeric vs string filter values', async () => {
650
650
  // Plugin already installed with filters in before hook
651
651
 
652
- const project = await api.resources.projects.post({ name: 'Project' });
652
+ const project = await api.resources.projects.post({ name: 'Project' })
653
653
 
654
654
  // Create items with numeric-like status values
655
655
  const item1 = await api.resources.items.post({
656
656
  name: 'Item 1',
657
657
  project: project.id,
658
658
  status: '1'
659
- });
659
+ })
660
660
 
661
661
  const item2 = await api.resources.items.post({
662
662
  name: 'Item 2',
663
663
  project: project.id,
664
664
  status: '01' // Different string representation
665
- });
665
+ })
666
666
 
667
667
  const item3 = await api.resources.items.post({
668
668
  name: 'Item 3',
669
669
  project: project.id,
670
670
  status: '1'
671
- });
671
+ })
672
672
 
673
673
  // Query with string '1'
674
674
  const { data: status1Items } = await api.resources.items.query({
@@ -676,12 +676,12 @@ describe('Multi-Filter Positioning Tests', { timeout: 30000 }, () => {
676
676
  filters: { project: project.id, status: '1' },
677
677
  sort: ['position']
678
678
  }
679
- });
679
+ })
680
680
 
681
- assert.equal(status1Items.length, 2);
682
- assert.equal(status1Items[0].name, 'Item 1');
683
- assert.equal(status1Items[1].name, 'Item 3');
684
- assert(status1Items[0].position < status1Items[1].position);
681
+ assert.equal(status1Items.length, 2)
682
+ assert.equal(status1Items[0].name, 'Item 1')
683
+ assert.equal(status1Items[1].name, 'Item 3')
684
+ assert(status1Items[0].position < status1Items[1].position)
685
685
 
686
686
  // Query with string '01'
687
687
  const { data: status01Items } = await api.resources.items.query({
@@ -689,11 +689,11 @@ describe('Multi-Filter Positioning Tests', { timeout: 30000 }, () => {
689
689
  filters: { project: project.id, status: '01' },
690
690
  sort: ['position']
691
691
  }
692
- });
693
-
694
- assert.equal(status01Items.length, 1);
695
- assert.equal(status01Items[0].name, 'Item 2');
696
- assert.equal(status01Items[0].position, 'a0');
697
- });
698
- });
699
- });
692
+ })
693
+
694
+ assert.equal(status01Items.length, 1)
695
+ assert.equal(status01Items[0].name, 'Item 2')
696
+ assert.equal(status01Items[0].position, 'a0')
697
+ })
698
+ })
699
+ })