json-rest-api 1.0.14 → 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 +11 -2
  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 +1 -1
  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,10 +1,10 @@
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
- import { rebalancePositions, isValidPosition, getUnpositionedItems, assignInitialPositions } from '../plugins/core/lib/fractional-positioning.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
+ import { rebalancePositions, isValidPosition, getUnpositionedItems, assignInitialPositions } from '../plugins/core/lib/fractional-positioning.js'
8
8
 
9
9
  // Create Knex instance for tests
10
10
  const knex = knexLib({
@@ -13,31 +13,31 @@ const knex = knexLib({
13
13
  filename: ':memory:'
14
14
  },
15
15
  useNullAsDefault: true
16
- });
16
+ })
17
17
 
18
18
  describe('Advanced Positioning Plugin Tests', { timeout: 30000 }, () => {
19
- let api;
19
+ let api
20
20
 
21
21
  before(async () => {
22
22
  // Initialize API once
23
23
  api = await createPositioningApi(knex, {
24
24
  apiName: 'positioning-advanced-test',
25
25
  tablePrefix: 'pos_adv'
26
- });
27
-
26
+ })
27
+
28
28
  // Install positioning plugin with default configuration
29
29
  await api.use(PositioningPlugin, {
30
30
  field: 'position',
31
31
  filters: ['category'],
32
32
  defaultPosition: 'last',
33
33
  autoIndex: true
34
- });
35
- });
36
-
34
+ })
35
+ })
36
+
37
37
  after(async () => {
38
- await knex.destroy();
39
- });
40
-
38
+ await knex.destroy()
39
+ })
40
+
41
41
  beforeEach(async () => {
42
42
  // Only clean tables for the main test API
43
43
  await cleanTables(knex, [
@@ -45,8 +45,8 @@ describe('Advanced Positioning Plugin Tests', { timeout: 30000 }, () => {
45
45
  'pos_adv_tasks',
46
46
  'pos_adv_projects',
47
47
  'pos_adv_items'
48
- ]);
49
- });
48
+ ])
49
+ })
50
50
 
51
51
  describe('Position Rebalancing', () => {
52
52
  it('should detect when positions need rebalancing', () => {
@@ -59,24 +59,24 @@ describe('Advanced Positioning Plugin Tests', { timeout: 30000 }, () => {
59
59
  { id: 5, name: 'Item 5', position: 'a0VVVV' },
60
60
  { id: 6, name: 'Item 6', position: 'a0VVVVV' },
61
61
  { id: 7, name: 'Item 7', position: 'a0VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV' } // 51 chars
62
- ];
62
+ ]
63
63
 
64
64
  // Test with default threshold (50)
65
- const rebalanced = rebalancePositions(items, 'position', 50);
66
-
65
+ const rebalanced = rebalancePositions(items, 'position', 50)
66
+
67
67
  // Should have rebalanced because item 7 exceeds threshold
68
- assert.notEqual(rebalanced[6].position, items[6].position);
69
-
68
+ assert.notEqual(rebalanced[6].position, items[6].position)
69
+
70
70
  // All positions should be shorter after rebalancing
71
71
  for (const item of rebalanced) {
72
- assert(item.position.length <= 50);
72
+ assert(item.position.length <= 50)
73
73
  }
74
-
74
+
75
75
  // Order should be preserved
76
76
  for (let i = 1; i < rebalanced.length; i++) {
77
- assert(rebalanced[i-1].position < rebalanced[i].position);
77
+ assert(rebalanced[i - 1].position < rebalanced[i].position)
78
78
  }
79
- });
79
+ })
80
80
 
81
81
  it('should not rebalance when not needed', async () => {
82
82
  const items = [
@@ -84,31 +84,31 @@ describe('Advanced Positioning Plugin Tests', { timeout: 30000 }, () => {
84
84
  { id: 2, name: 'Item 2', position: 'a1' },
85
85
  { id: 3, name: 'Item 3', position: 'a2' },
86
86
  { id: 4, name: 'Item 4', position: 'a3' }
87
- ];
87
+ ]
88
+
89
+ const result = rebalancePositions(items, 'position', 50)
88
90
 
89
- const result = rebalancePositions(items, 'position', 50);
90
-
91
91
  // Should return same items (no rebalancing needed)
92
- assert.deepEqual(result, items);
93
- });
92
+ assert.deepEqual(result, items)
93
+ })
94
94
 
95
95
  it('should handle position validation', () => {
96
96
  // Valid positions
97
- assert(isValidPosition('a0'));
98
- assert(isValidPosition('Zz'));
99
- assert(isValidPosition('a0V'));
100
- assert(isValidPosition('ABC123xyz'));
101
-
97
+ assert(isValidPosition('a0'))
98
+ assert(isValidPosition('Zz'))
99
+ assert(isValidPosition('a0V'))
100
+ assert(isValidPosition('ABC123xyz'))
101
+
102
102
  // Invalid positions
103
- assert(!isValidPosition(''));
104
- assert(!isValidPosition(null));
105
- assert(!isValidPosition(undefined));
106
- assert(!isValidPosition('a-0')); // Contains hyphen
107
- assert(!isValidPosition('a_0')); // Contains underscore
108
- assert(!isValidPosition('a 0')); // Contains space
109
- assert(!isValidPosition('!@#')); // Special characters
110
- assert(!isValidPosition(123)); // Not a string
111
- });
103
+ assert(!isValidPosition(''))
104
+ assert(!isValidPosition(null))
105
+ assert(!isValidPosition(undefined))
106
+ assert(!isValidPosition('a-0')) // Contains hyphen
107
+ assert(!isValidPosition('a_0')) // Contains underscore
108
+ assert(!isValidPosition('a 0')) // Contains space
109
+ assert(!isValidPosition('!@#')) // Special characters
110
+ assert(!isValidPosition(123)) // Not a string
111
+ })
112
112
 
113
113
  it('should identify unpositioned items', () => {
114
114
  const items = [
@@ -118,16 +118,16 @@ describe('Advanced Positioning Plugin Tests', { timeout: 30000 }, () => {
118
118
  { id: 4, name: 'Item 4', position: '' },
119
119
  { id: 5, name: 'Item 5', position: undefined },
120
120
  { id: 6, name: 'Item 6', position: 'invalid-position' }
121
- ];
121
+ ]
122
122
 
123
- const unpositioned = getUnpositionedItems(items, 'position');
124
-
125
- assert.equal(unpositioned.length, 4);
126
- assert.equal(unpositioned[0].id, 2);
127
- assert.equal(unpositioned[1].id, 4);
128
- assert.equal(unpositioned[2].id, 5);
129
- assert.equal(unpositioned[3].id, 6);
130
- });
123
+ const unpositioned = getUnpositionedItems(items, 'position')
124
+
125
+ assert.equal(unpositioned.length, 4)
126
+ assert.equal(unpositioned[0].id, 2)
127
+ assert.equal(unpositioned[1].id, 4)
128
+ assert.equal(unpositioned[2].id, 5)
129
+ assert.equal(unpositioned[3].id, 6)
130
+ })
131
131
 
132
132
  it('should assign initial positions to unpositioned items', () => {
133
133
  const items = [
@@ -136,63 +136,63 @@ describe('Advanced Positioning Plugin Tests', { timeout: 30000 }, () => {
136
136
  { id: 3, name: 'Item 3', position: 'a2' },
137
137
  { id: 4, name: 'Item 4', position: '' },
138
138
  { id: 5, name: 'Item 5', position: 'a3' }
139
- ];
139
+ ]
140
+
141
+ const result = assignInitialPositions(items, 'position', 'id')
142
+
143
+ assert.equal(result.length, 5)
140
144
 
141
- const result = assignInitialPositions(items, 'position', 'id');
142
-
143
- assert.equal(result.length, 5);
144
-
145
145
  // Previously positioned items should keep their positions
146
- const item1 = result.find(i => i.id === 1);
147
- assert.equal(item1.position, 'a0');
148
-
149
- const item3 = result.find(i => i.id === 3);
150
- assert.equal(item3.position, 'a2');
151
-
152
- const item5 = result.find(i => i.id === 5);
153
- assert.equal(item5.position, 'a3');
154
-
146
+ const item1 = result.find(i => i.id === 1)
147
+ assert.equal(item1.position, 'a0')
148
+
149
+ const item3 = result.find(i => i.id === 3)
150
+ assert.equal(item3.position, 'a2')
151
+
152
+ const item5 = result.find(i => i.id === 5)
153
+ assert.equal(item5.position, 'a3')
154
+
155
155
  // Unpositioned items should have valid positions
156
- const item2 = result.find(i => i.id === 2);
157
- assert(isValidPosition(item2.position));
158
- assert(item2.position > 'a3'); // Should be after last positioned item
159
-
160
- const item4 = result.find(i => i.id === 4);
161
- assert(isValidPosition(item4.position));
162
- assert(item4.position > item2.position); // Should be after item2
163
- });
164
- });
156
+ const item2 = result.find(i => i.id === 2)
157
+ assert(isValidPosition(item2.position))
158
+ assert(item2.position > 'a3') // Should be after last positioned item
159
+
160
+ const item4 = result.find(i => i.id === 4)
161
+ assert(isValidPosition(item4.position))
162
+ assert(item4.position > item2.position) // Should be after item2
163
+ })
164
+ })
165
165
 
166
166
  describe('Default Position Configuration', () => {
167
167
  it('should respect defaultPosition: first configuration', async () => {
168
168
  const api = await createPositioningApi(knex, {
169
169
  apiName: 'positioning-first-default',
170
170
  tablePrefix: 'pos_first_def'
171
- });
171
+ })
172
172
 
173
173
  await api.use(PositioningPlugin, {
174
174
  field: 'position',
175
175
  filters: ['category'],
176
176
  defaultPosition: 'first' // New items go to beginning
177
- });
177
+ })
178
178
 
179
- const category = await api.resources.categories.post({ name: 'Test Category' });
179
+ const category = await api.resources.categories.post({ name: 'Test Category' })
180
180
 
181
181
  // Create tasks - they should be added at the beginning
182
182
  const task1 = await api.resources.tasks.post({
183
183
  title: 'Task 1',
184
184
  category: category.id
185
- });
185
+ })
186
186
 
187
187
  const task2 = await api.resources.tasks.post({
188
188
  title: 'Task 2',
189
189
  category: category.id
190
- });
190
+ })
191
191
 
192
192
  const task3 = await api.resources.tasks.post({
193
193
  title: 'Task 3',
194
194
  category: category.id
195
- });
195
+ })
196
196
 
197
197
  // Query tasks
198
198
  const { data: tasks } = await api.resources.tasks.query({
@@ -200,99 +200,99 @@ describe('Advanced Positioning Plugin Tests', { timeout: 30000 }, () => {
200
200
  filters: { category: category.id },
201
201
  sort: ['position']
202
202
  }
203
- });
203
+ })
204
204
 
205
205
  // With defaultPosition: 'first', the order should be reversed
206
206
  // Task 3 should be first (added last, positioned first)
207
- assert.equal(tasks.length, 3);
208
- assert.equal(tasks[0].title, 'Task 3');
209
- assert.equal(tasks[1].title, 'Task 2');
210
- assert.equal(tasks[2].title, 'Task 1');
207
+ assert.equal(tasks.length, 3)
208
+ assert.equal(tasks[0].title, 'Task 3')
209
+ assert.equal(tasks[1].title, 'Task 2')
210
+ assert.equal(tasks[2].title, 'Task 1')
211
211
 
212
212
  // Verify positions are ordered correctly
213
- assert(tasks[0].position < tasks[1].position);
214
- assert(tasks[1].position < tasks[2].position);
215
- });
213
+ assert(tasks[0].position < tasks[1].position)
214
+ assert(tasks[1].position < tasks[2].position)
215
+ })
216
216
 
217
217
  it('should handle beforeId with defaultPosition: first', async () => {
218
218
  const api = await createPositioningApi(knex, {
219
219
  apiName: 'positioning-first-before',
220
220
  tablePrefix: 'pos_first_bef'
221
- });
221
+ })
222
222
 
223
223
  await api.use(PositioningPlugin, {
224
224
  field: 'position',
225
225
  filters: ['category'],
226
226
  defaultPosition: 'first'
227
- });
227
+ })
228
228
 
229
- const category = await api.resources.categories.post({ name: 'Test Category' });
229
+ const category = await api.resources.categories.post({ name: 'Test Category' })
230
230
 
231
231
  const task1 = await api.resources.tasks.post({
232
232
  title: 'Task 1',
233
233
  category: category.id
234
- });
234
+ })
235
235
 
236
236
  const task2 = await api.resources.tasks.post({
237
237
  title: 'Task 2',
238
238
  category: category.id
239
- });
239
+ })
240
240
 
241
241
  // Create task with explicit beforeId (should override defaultPosition)
242
242
  const task3 = await api.resources.tasks.post({
243
243
  title: 'Task 3',
244
244
  category: category.id,
245
245
  beforeId: task1.id
246
- });
246
+ })
247
247
 
248
248
  const { data: tasks } = await api.resources.tasks.query({
249
249
  queryParams: {
250
250
  filters: { category: category.id },
251
251
  sort: ['position']
252
252
  }
253
- });
253
+ })
254
254
 
255
255
  // Order should be: Task 3, Task 2, Task 1
256
256
  // (Task 3 explicitly before Task 1, Task 2 at beginning by default)
257
- assert.equal(tasks[0].title, 'Task 2');
258
- assert.equal(tasks[1].title, 'Task 3');
259
- assert.equal(tasks[2].title, 'Task 1');
260
- });
261
- });
257
+ assert.equal(tasks[0].title, 'Task 2')
258
+ assert.equal(tasks[1].title, 'Task 3')
259
+ assert.equal(tasks[2].title, 'Task 1')
260
+ })
261
+ })
262
262
 
263
263
  describe('Custom Field Names', () => {
264
264
  it('should work with custom position field name', async () => {
265
265
  const api = await createPositioningApi(knex, {
266
266
  apiName: 'positioning-custom-field',
267
267
  tablePrefix: 'pos_custom_fld'
268
- });
268
+ })
269
269
 
270
270
  await api.use(PositioningPlugin, {
271
271
  field: 'sort_order', // Custom field name
272
272
  filters: ['project'],
273
273
  defaultPosition: 'last'
274
- });
274
+ })
275
275
 
276
- const project = await api.resources.projects.post({ name: 'Test Project' });
276
+ const project = await api.resources.projects.post({ name: 'Test Project' })
277
277
 
278
278
  const item1 = await api.resources.items.post({
279
279
  name: 'Item 1',
280
280
  project: project.id
281
- });
281
+ })
282
282
 
283
283
  const item2 = await api.resources.items.post({
284
284
  name: 'Item 2',
285
285
  project: project.id
286
- });
286
+ })
287
287
 
288
288
  // Should have sort_order field, not position
289
- assert(item1.sort_order);
290
- assert(!item1.position);
291
- assert(item2.sort_order);
292
- assert(!item2.position);
289
+ assert(item1.sort_order)
290
+ assert(!item1.position)
291
+ assert(item2.sort_order)
292
+ assert(!item2.position)
293
293
 
294
294
  // Should be properly ordered
295
- assert(item1.sort_order < item2.sort_order);
295
+ assert(item1.sort_order < item2.sort_order)
296
296
 
297
297
  // Query should work with custom field
298
298
  const { data: items } = await api.resources.items.query({
@@ -300,77 +300,77 @@ describe('Advanced Positioning Plugin Tests', { timeout: 30000 }, () => {
300
300
  filters: { project: project.id },
301
301
  sort: ['sort_order']
302
302
  }
303
- });
303
+ })
304
304
 
305
- assert.equal(items.length, 2);
306
- assert.equal(items[0].name, 'Item 1');
307
- assert.equal(items[1].name, 'Item 2');
308
- });
305
+ assert.equal(items.length, 2)
306
+ assert.equal(items[0].name, 'Item 1')
307
+ assert.equal(items[1].name, 'Item 2')
308
+ })
309
309
 
310
310
  it('should handle custom beforeId field name', async () => {
311
311
  const api = await createPositioningApi(knex, {
312
312
  apiName: 'positioning-custom-beforeid',
313
313
  tablePrefix: 'pos_custom_bid'
314
- });
314
+ })
315
315
 
316
316
  await api.use(PositioningPlugin, {
317
317
  field: 'position',
318
318
  filters: ['project'],
319
319
  beforeIdField: 'insertBefore', // Custom virtual field name
320
320
  defaultPosition: 'last'
321
- });
321
+ })
322
322
 
323
- const project = await api.resources.projects.post({ name: 'Test Project' });
323
+ const project = await api.resources.projects.post({ name: 'Test Project' })
324
324
 
325
325
  const item1 = await api.resources.items.post({
326
326
  name: 'Item 1',
327
327
  project: project.id
328
- });
328
+ })
329
329
 
330
330
  const item2 = await api.resources.items.post({
331
331
  name: 'Item 2',
332
332
  project: project.id
333
- });
333
+ })
334
334
 
335
335
  // Use custom beforeId field name
336
336
  const item3 = await api.resources.items.post({
337
337
  name: 'Item 3',
338
338
  project: project.id,
339
339
  insertBefore: item2.id
340
- });
340
+ })
341
341
 
342
342
  const { data: items } = await api.resources.items.query({
343
343
  queryParams: {
344
344
  filters: { project: project.id },
345
345
  sort: ['position']
346
346
  }
347
- });
347
+ })
348
348
 
349
- assert.equal(items.length, 3);
350
- assert.equal(items[0].name, 'Item 1');
351
- assert.equal(items[1].name, 'Item 3');
352
- assert.equal(items[2].name, 'Item 2');
353
- });
354
- });
349
+ assert.equal(items.length, 3)
350
+ assert.equal(items[0].name, 'Item 1')
351
+ assert.equal(items[1].name, 'Item 3')
352
+ assert.equal(items[2].name, 'Item 2')
353
+ })
354
+ })
355
355
 
356
356
  describe('Error Recovery and Consistency', () => {
357
357
  it('should handle concurrent inserts at same position gracefully', async () => {
358
358
  // Use the main api instance
359
359
 
360
- const category = await api.resources.categories.post({ name: 'Test Category' });
360
+ const category = await api.resources.categories.post({ name: 'Test Category' })
361
361
 
362
362
  const task1 = await api.resources.tasks.post({
363
363
  title: 'Task 1',
364
364
  category: category.id
365
- });
365
+ })
366
366
 
367
367
  const task2 = await api.resources.tasks.post({
368
368
  title: 'Task 2',
369
369
  category: category.id
370
- });
370
+ })
371
371
 
372
372
  // Simulate concurrent inserts before task2
373
- const promises = [];
373
+ const promises = []
374
374
  for (let i = 0; i < 5; i++) {
375
375
  promises.push(
376
376
  api.resources.tasks.post({
@@ -378,13 +378,13 @@ describe('Advanced Positioning Plugin Tests', { timeout: 30000 }, () => {
378
378
  category: category.id,
379
379
  beforeId: task2.id
380
380
  })
381
- );
381
+ )
382
382
  }
383
383
 
384
- const results = await Promise.all(promises);
384
+ const results = await Promise.all(promises)
385
385
 
386
386
  // All should succeed
387
- assert.equal(results.length, 5);
387
+ assert.equal(results.length, 5)
388
388
 
389
389
  // Query all tasks
390
390
  const { data: tasks } = await api.resources.tasks.query({
@@ -392,65 +392,65 @@ describe('Advanced Positioning Plugin Tests', { timeout: 30000 }, () => {
392
392
  filters: { category: category.id },
393
393
  sort: ['position']
394
394
  }
395
- });
395
+ })
396
396
 
397
- assert.equal(tasks.length, 7);
397
+ assert.equal(tasks.length, 7)
398
398
 
399
399
  // Task 1 should still be first
400
- assert.equal(tasks[0].title, 'Task 1');
400
+ assert.equal(tasks[0].title, 'Task 1')
401
401
 
402
402
  // Task 2 should be last
403
- assert.equal(tasks[6].title, 'Task 2');
403
+ assert.equal(tasks[6].title, 'Task 2')
404
404
 
405
405
  // All positions should be unique
406
- const positions = tasks.map(t => t.position);
407
- const uniquePositions = [...new Set(positions)];
408
- assert.equal(uniquePositions.length, 7);
406
+ const positions = tasks.map(t => t.position)
407
+ const uniquePositions = [...new Set(positions)]
408
+ assert.equal(uniquePositions.length, 7)
409
409
 
410
410
  // All should be properly ordered
411
411
  for (let i = 1; i < tasks.length; i++) {
412
- assert(tasks[i-1].position < tasks[i].position);
412
+ assert(tasks[i - 1].position < tasks[i].position)
413
413
  }
414
- });
414
+ })
415
415
 
416
416
  it('should maintain consistency when moving items rapidly', async () => {
417
417
  // Use the main api instance
418
418
 
419
- const category = await api.resources.categories.post({ name: 'Test Category' });
419
+ const category = await api.resources.categories.post({ name: 'Test Category' })
420
420
 
421
421
  // Create initial items
422
- const items = [];
422
+ const items = []
423
423
  for (let i = 0; i < 10; i++) {
424
424
  const item = await api.resources.tasks.post({
425
425
  title: `Task ${i}`,
426
426
  category: category.id
427
- });
428
- items.push(item);
427
+ })
428
+ items.push(item)
429
429
  }
430
430
 
431
431
  // Rapidly move items around
432
- const updates = [];
433
-
432
+ const updates = []
433
+
434
434
  // Move multiple items to beginning
435
435
  updates.push(
436
436
  api.resources.tasks.patch({ id: items[5].id, beforeId: 'FIRST' }),
437
437
  api.resources.tasks.patch({ id: items[7].id, beforeId: 'FIRST' }),
438
438
  api.resources.tasks.patch({ id: items[9].id, beforeId: 'FIRST' })
439
- );
439
+ )
440
440
 
441
441
  // Move multiple items to end
442
442
  updates.push(
443
443
  api.resources.tasks.patch({ id: items[0].id, beforeId: null }),
444
444
  api.resources.tasks.patch({ id: items[2].id, beforeId: null })
445
- );
445
+ )
446
446
 
447
447
  // Move items to middle positions
448
448
  updates.push(
449
449
  api.resources.tasks.patch({ id: items[1].id, beforeId: items[4].id }),
450
450
  api.resources.tasks.patch({ id: items[3].id, beforeId: items[6].id })
451
- );
451
+ )
452
452
 
453
- await Promise.all(updates);
453
+ await Promise.all(updates)
454
454
 
455
455
  // Verify final state
456
456
  const { data: finalTasks } = await api.resources.tasks.query({
@@ -458,38 +458,38 @@ describe('Advanced Positioning Plugin Tests', { timeout: 30000 }, () => {
458
458
  filters: { category: category.id },
459
459
  sort: ['position']
460
460
  }
461
- });
461
+ })
462
462
 
463
- assert.equal(finalTasks.length, 10);
463
+ assert.equal(finalTasks.length, 10)
464
464
 
465
465
  // All positions should be unique
466
- const positions = finalTasks.map(t => t.position);
467
- const uniquePositions = [...new Set(positions)];
468
- assert.equal(uniquePositions.length, 10);
466
+ const positions = finalTasks.map(t => t.position)
467
+ const uniquePositions = [...new Set(positions)]
468
+ assert.equal(uniquePositions.length, 10)
469
469
 
470
470
  // All should be properly ordered
471
471
  for (let i = 1; i < finalTasks.length; i++) {
472
- assert(finalTasks[i-1].position < finalTasks[i].position);
472
+ assert(finalTasks[i - 1].position < finalTasks[i].position)
473
473
  }
474
- });
474
+ })
475
475
 
476
476
  // Removed test for missing position field - not a realistic scenario
477
- });
477
+ })
478
478
 
479
479
  describe('Special Positioning Values', () => {
480
480
  it('should handle FIRST positioning correctly', async () => {
481
481
  // Use the main api instance
482
482
 
483
- const category = await api.resources.categories.post({ name: 'Test Category' });
483
+ const category = await api.resources.categories.post({ name: 'Test Category' })
484
484
 
485
485
  // Create initial tasks
486
- const tasks = [];
486
+ const tasks = []
487
487
  for (let i = 1; i <= 5; i++) {
488
488
  const task = await api.resources.tasks.post({
489
489
  title: `Task ${i}`,
490
490
  category: category.id
491
- });
492
- tasks.push(task);
491
+ })
492
+ tasks.push(task)
493
493
  }
494
494
 
495
495
  // Position new task at beginning
@@ -497,152 +497,152 @@ describe('Advanced Positioning Plugin Tests', { timeout: 30000 }, () => {
497
497
  title: 'First Task',
498
498
  category: category.id,
499
499
  beforeId: 'FIRST'
500
- });
500
+ })
501
501
 
502
502
  const { data: allTasks } = await api.resources.tasks.query({
503
503
  queryParams: {
504
504
  filters: { category: category.id },
505
505
  sort: ['position']
506
506
  }
507
- });
507
+ })
508
508
 
509
- assert.equal(allTasks[0].title, 'First Task');
510
- assert(firstTask.position < tasks[0].position);
509
+ assert.equal(allTasks[0].title, 'First Task')
510
+ assert(firstTask.position < tasks[0].position)
511
511
 
512
512
  // Update existing task to move to beginning
513
513
  await api.resources.tasks.patch({
514
514
  id: tasks[2].id,
515
515
  beforeId: 'FIRST'
516
- });
516
+ })
517
517
 
518
518
  const { data: updatedTasks } = await api.resources.tasks.query({
519
519
  queryParams: {
520
520
  filters: { category: category.id },
521
521
  sort: ['position']
522
522
  }
523
- });
523
+ })
524
524
 
525
- assert.equal(updatedTasks[0].title, 'Task 3');
526
- assert.equal(updatedTasks[1].title, 'First Task');
527
- });
525
+ assert.equal(updatedTasks[0].title, 'Task 3')
526
+ assert.equal(updatedTasks[1].title, 'First Task')
527
+ })
528
528
 
529
529
  it('should handle null beforeId as positioning at end', async () => {
530
530
  // Use the main api instance
531
531
 
532
- const category = await api.resources.categories.post({ name: 'Test Category' });
532
+ const category = await api.resources.categories.post({ name: 'Test Category' })
533
533
 
534
534
  // Create tasks
535
535
  const task1 = await api.resources.tasks.post({
536
536
  title: 'Task 1',
537
537
  category: category.id
538
- });
538
+ })
539
539
 
540
540
  const task2 = await api.resources.tasks.post({
541
541
  title: 'Task 2',
542
542
  category: category.id
543
- });
543
+ })
544
544
 
545
545
  // Move task 1 to end with explicit null
546
546
  await api.resources.tasks.patch({
547
547
  id: task1.id,
548
548
  beforeId: null
549
- });
549
+ })
550
550
 
551
551
  const { data: tasks } = await api.resources.tasks.query({
552
552
  queryParams: {
553
553
  filters: { category: category.id },
554
554
  sort: ['position']
555
555
  }
556
- });
556
+ })
557
557
 
558
- assert.equal(tasks[0].title, 'Task 2');
559
- assert.equal(tasks[1].title, 'Task 1');
560
- });
561
- });
558
+ assert.equal(tasks[0].title, 'Task 2')
559
+ assert.equal(tasks[1].title, 'Task 1')
560
+ })
561
+ })
562
562
 
563
563
  describe('Integration with Other Plugins', () => {
564
564
  it('should work with soft delete scenarios', async () => {
565
565
  const softDeleteApi = await createPositioningApi(knex, {
566
566
  apiName: 'positioning-soft-delete-test',
567
567
  tablePrefix: 'pos_soft'
568
- });
568
+ })
569
569
 
570
570
  await softDeleteApi.use(PositioningPlugin, {
571
571
  field: 'position',
572
572
  filters: ['category'],
573
573
  defaultPosition: 'last',
574
574
  excludeResources: ['system_migrations', 'system_logs', 'categories', 'projects']
575
- });
575
+ })
576
576
 
577
- const category = await softDeleteApi.resources.categories.post({ name: 'Test Category' });
577
+ const category = await softDeleteApi.resources.categories.post({ name: 'Test Category' })
578
578
 
579
579
  // Create tasks
580
- const tasks = [];
580
+ const tasks = []
581
581
  for (let i = 1; i <= 5; i++) {
582
582
  const task = await softDeleteApi.resources.tasks.post({
583
583
  title: `Task ${i}`,
584
584
  category: category.id
585
- });
586
- tasks.push(task);
585
+ })
586
+ tasks.push(task)
587
587
  }
588
588
 
589
589
  // Soft delete task 3
590
590
  await softDeleteApi.resources.tasks.patch({
591
591
  id: tasks[2].id,
592
592
  deleted_at: new Date().toISOString()
593
- });
593
+ })
594
594
 
595
595
  // Create new task - should still position correctly
596
596
  const newTask = await softDeleteApi.resources.tasks.post({
597
597
  title: 'New Task',
598
598
  category: category.id,
599
599
  beforeId: tasks[3].id // Before task 4
600
- });
600
+ })
601
601
 
602
602
  // Query non-deleted tasks
603
603
  const { data: activeTasks } = await softDeleteApi.resources.tasks.query({
604
604
  queryParams: {
605
- filters: {
605
+ filters: {
606
606
  category: category.id,
607
607
  deleted_at: null
608
608
  },
609
609
  sort: ['position']
610
610
  }
611
- });
611
+ })
612
612
 
613
613
  // Should have 5 tasks (original 5 - 1 deleted + 1 new)
614
- assert.equal(activeTasks.length, 5);
614
+ assert.equal(activeTasks.length, 5)
615
615
 
616
616
  // New task should be before Task 4
617
- const newTaskIndex = activeTasks.findIndex(t => t.title === 'New Task');
618
- const task4Index = activeTasks.findIndex(t => t.title === 'Task 4');
619
- assert(newTaskIndex < task4Index);
620
- });
617
+ const newTaskIndex = activeTasks.findIndex(t => t.title === 'New Task')
618
+ const task4Index = activeTasks.findIndex(t => t.title === 'Task 4')
619
+ assert(newTaskIndex < task4Index)
620
+ })
621
621
 
622
622
  it('should handle versioning scenarios', async () => {
623
623
  const versionApi = await createPositioningApi(knex, {
624
624
  apiName: 'positioning-version-test',
625
625
  tablePrefix: 'pos_ver'
626
- });
626
+ })
627
627
 
628
628
  await versionApi.use(PositioningPlugin, {
629
629
  field: 'position',
630
630
  filters: ['category', 'version'],
631
631
  defaultPosition: 'last',
632
632
  excludeResources: ['system_migrations', 'system_logs', 'categories', 'projects']
633
- });
633
+ })
634
634
 
635
- const category = await versionApi.resources.categories.post({ name: 'Test Category' });
635
+ const category = await versionApi.resources.categories.post({ name: 'Test Category' })
636
636
 
637
637
  // Create v1 tasks
638
- const v1Tasks = [];
638
+ const v1Tasks = []
639
639
  for (let i = 1; i <= 3; i++) {
640
640
  const task = await versionApi.resources.tasks.post({
641
641
  title: `Task ${i} v1`,
642
642
  category: category.id,
643
643
  version: 1
644
- });
645
- v1Tasks.push(task);
644
+ })
645
+ v1Tasks.push(task)
646
646
  }
647
647
 
648
648
  // Create v2 of task 2
@@ -650,14 +650,14 @@ describe('Advanced Positioning Plugin Tests', { timeout: 30000 }, () => {
650
650
  title: 'Task 2 v2',
651
651
  category: category.id,
652
652
  version: 2
653
- });
653
+ })
654
654
 
655
655
  // Create v2 of task 1
656
656
  const task1v2 = await versionApi.resources.tasks.post({
657
657
  title: 'Task 1 v2',
658
658
  category: category.id,
659
659
  version: 2
660
- });
660
+ })
661
661
 
662
662
  // Query v1 tasks
663
663
  const { data: v1Items } = await versionApi.resources.tasks.query({
@@ -665,12 +665,12 @@ describe('Advanced Positioning Plugin Tests', { timeout: 30000 }, () => {
665
665
  filters: { category: category.id, version: 1 },
666
666
  sort: ['position']
667
667
  }
668
- });
668
+ })
669
669
 
670
- assert.equal(v1Items.length, 3);
671
- assert.equal(v1Items[0].title, 'Task 1 v1');
672
- assert.equal(v1Items[1].title, 'Task 2 v1');
673
- assert.equal(v1Items[2].title, 'Task 3 v1');
670
+ assert.equal(v1Items.length, 3)
671
+ assert.equal(v1Items[0].title, 'Task 1 v1')
672
+ assert.equal(v1Items[1].title, 'Task 2 v1')
673
+ assert.equal(v1Items[2].title, 'Task 3 v1')
674
674
 
675
675
  // Query v2 tasks
676
676
  const { data: v2Items } = await versionApi.resources.tasks.query({
@@ -678,15 +678,15 @@ describe('Advanced Positioning Plugin Tests', { timeout: 30000 }, () => {
678
678
  filters: { category: category.id, version: 2 },
679
679
  sort: ['position']
680
680
  }
681
- });
681
+ })
682
682
 
683
- assert.equal(v2Items.length, 2);
684
- assert.equal(v2Items[0].title, 'Task 2 v2');
685
- assert.equal(v2Items[1].title, 'Task 1 v2');
683
+ assert.equal(v2Items.length, 2)
684
+ assert.equal(v2Items[0].title, 'Task 2 v2')
685
+ assert.equal(v2Items[1].title, 'Task 1 v2')
686
686
 
687
687
  // Positions should be independent between versions
688
- assert.equal(v1Tasks[0].position, 'a0');
689
- assert.equal(task2v2.position, 'a0'); // First in v2 group
690
- });
691
- });
692
- });
688
+ assert.equal(v1Tasks[0].position, 'a0')
689
+ assert.equal(task2v2.position, 'a0') // First in v2 group
690
+ })
691
+ })
692
+ })