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,12 +1,12 @@
1
- import { describe, it, before, after, beforeEach } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import knexLib from 'knex';
4
- import {
5
- validateJsonApiStructure,
6
- cleanTables,
1
+ import { describe, it, before, after, beforeEach } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import knexLib from 'knex'
4
+ import {
5
+ validateJsonApiStructure,
6
+ cleanTables,
7
7
  createJsonApiDocument
8
- } from './helpers/test-utils.js';
9
- import { createCursorPaginationApi } from './fixtures/api-configs.js';
8
+ } from './helpers/test-utils.js'
9
+ import { createCursorPaginationApi } from './fixtures/api-configs.js'
10
10
 
11
11
  // Create Knex instance for tests
12
12
  const knex = knexLib({
@@ -15,25 +15,25 @@ const knex = knexLib({
15
15
  filename: ':memory:'
16
16
  },
17
17
  useNullAsDefault: true
18
- });
18
+ })
19
19
 
20
20
  // API instance that persists across tests
21
- let api;
21
+ let api
22
22
 
23
23
  describe('Multi-field Cursor Pagination', () => {
24
24
  before(async () => {
25
25
  // Initialize API once with cursor pagination configuration
26
- api = await createCursorPaginationApi(knex);
27
- });
26
+ api = await createCursorPaginationApi(knex)
27
+ })
28
28
 
29
29
  after(async () => {
30
30
  // Close database connection
31
- await knex.destroy();
32
- });
31
+ await knex.destroy()
32
+ })
33
33
 
34
34
  describe('Basic Multi-field Cursor Pagination', () => {
35
35
  beforeEach(async () => {
36
- await cleanTables(knex, ['cursor_products']);
36
+ await cleanTables(knex, ['cursor_products'])
37
37
 
38
38
  // Create test data with some duplicate categories and brands
39
39
  const products = [
@@ -45,62 +45,62 @@ describe('Multi-field Cursor Pagination', () => {
45
45
  { name: 'Asparagus', category: 'Vegetable', brand: 'Green Gardens', price: 3.50, sku: 'GG-001' },
46
46
  { name: 'Broccoli', category: 'Vegetable', brand: 'Green Gardens', price: 2.00, sku: 'GG-002' },
47
47
  { name: 'Carrot', category: 'Vegetable', brand: 'Root Ranch', price: 1.00, sku: 'RR-001' }
48
- ];
48
+ ]
49
49
 
50
50
  for (const product of products) {
51
51
  await api.resources.products.post({
52
52
  inputRecord: createJsonApiDocument('products', product),
53
53
  simplified: false
54
- });
54
+ })
55
55
  }
56
- });
56
+ })
57
57
 
58
58
  it.skip('should handle cursor pagination with multi-field sorting without skipping records', async () => {
59
59
  // Sort by category first (which has duplicates), then by name
60
- const pageSize = 3;
61
- const allProducts = [];
62
- let cursor = null;
63
- let pageCount = 0;
60
+ const pageSize = 3
61
+ const allProducts = []
62
+ let cursor = null
63
+ let pageCount = 0
64
64
 
65
65
  // Collect all products through cursor pagination
66
66
  while (pageCount < 10) { // Safety limit
67
67
  const result = await api.resources.products.query({
68
68
  queryParams: {
69
- page: {
69
+ page: {
70
70
  size: pageSize,
71
71
  ...(cursor && { after: cursor })
72
72
  },
73
73
  sort: ['category', 'name']
74
74
  },
75
75
  simplified: false
76
- });
76
+ })
77
77
 
78
- validateJsonApiStructure(result, true);
79
- allProducts.push(...result.data);
80
- pageCount++;
78
+ validateJsonApiStructure(result, true)
79
+ allProducts.push(...result.data)
80
+ pageCount++
81
81
 
82
82
  if (!result.meta.pagination.hasMore) {
83
- break;
83
+ break
84
84
  }
85
- cursor = result.meta.pagination.cursor.next;
85
+ cursor = result.meta.pagination.cursor.next
86
86
  }
87
87
 
88
88
  // Verify we got all 8 products
89
- assert.equal(allProducts.length, 8, 'Should retrieve all 8 products');
90
-
89
+ assert.equal(allProducts.length, 8, 'Should retrieve all 8 products')
90
+
91
91
  // Verify the order is correct
92
- const productNames = allProducts.map(p => p.attributes.name);
92
+ const productNames = allProducts.map(p => p.attributes.name)
93
93
  const expectedOrder = [
94
94
  'Apple', 'Banana', 'Cherry', 'Date', 'Elderberry', // All Fruits in alphabetical order
95
95
  'Asparagus', 'Broccoli', 'Carrot' // All Vegetables in alphabetical order
96
- ];
97
- assert.deepEqual(productNames, expectedOrder, 'Products should be in correct order');
96
+ ]
97
+ assert.deepEqual(productNames, expectedOrder, 'Products should be in correct order')
98
98
 
99
99
  // Verify no duplicates
100
- const ids = allProducts.map(p => p.id);
101
- const uniqueIds = [...new Set(ids)];
102
- assert.equal(ids.length, uniqueIds.length, 'Should have no duplicate products');
103
- });
100
+ const ids = allProducts.map(p => p.id)
101
+ const uniqueIds = [...new Set(ids)]
102
+ assert.equal(ids.length, uniqueIds.length, 'Should have no duplicate products')
103
+ })
104
104
 
105
105
  it.skip('should handle DESC sorting with duplicates correctly', async () => {
106
106
  // Sort by category DESC, then name ASC
@@ -110,28 +110,28 @@ describe('Multi-field Cursor Pagination', () => {
110
110
  sort: ['-category', 'name']
111
111
  },
112
112
  simplified: false
113
- });
113
+ })
114
114
 
115
115
  const secondPage = await api.resources.products.query({
116
116
  queryParams: {
117
- page: {
117
+ page: {
118
118
  size: 3,
119
119
  after: firstPage.meta.pagination.cursor.next
120
120
  },
121
121
  sort: ['-category', 'name']
122
122
  },
123
123
  simplified: false
124
- });
124
+ })
125
125
 
126
126
  // First page should have vegetables (category DESC)
127
- const firstPageCategories = firstPage.data.map(p => p.attributes.category);
128
- assert(firstPageCategories.every(c => c === 'Vegetable'), 'First page should only have Vegetables');
127
+ const firstPageCategories = firstPage.data.map(p => p.attributes.category)
128
+ assert(firstPageCategories.every(c => c === 'Vegetable'), 'First page should only have Vegetables')
129
129
 
130
130
  // Second page should start with remaining vegetables or fruits
131
- const secondPageNames = secondPage.data.map(p => p.attributes.name);
132
- assert(secondPageNames.includes('Apple') || secondPageNames.includes('Banana'),
133
- 'Second page should include fruits that come after vegetables');
134
- });
131
+ const secondPageNames = secondPage.data.map(p => p.attributes.name)
132
+ assert(secondPageNames.includes('Apple') || secondPageNames.includes('Banana'),
133
+ 'Second page should include fruits that come after vegetables')
134
+ })
135
135
 
136
136
  it('should handle three-field sorting with multiple duplicate values', async () => {
137
137
  // Add more products with duplicate category AND brand
@@ -144,50 +144,50 @@ describe('Multi-field Cursor Pagination', () => {
144
144
  sku: 'GG-003'
145
145
  }),
146
146
  simplified: false
147
- });
147
+ })
148
148
 
149
- const pageSize = 2;
150
- const allProducts = [];
151
- let cursor = null;
152
- let iterations = 0;
149
+ const pageSize = 2
150
+ const allProducts = []
151
+ let cursor = null
152
+ let iterations = 0
153
153
 
154
154
  // Sort by category, brand, then name
155
155
  while (iterations < 20) {
156
156
  const result = await api.resources.products.query({
157
157
  queryParams: {
158
- page: {
158
+ page: {
159
159
  size: pageSize,
160
160
  ...(cursor && { after: cursor })
161
161
  },
162
162
  sort: ['category', 'brand', 'name']
163
163
  },
164
164
  simplified: false
165
- });
165
+ })
166
166
 
167
- allProducts.push(...result.data);
167
+ allProducts.push(...result.data)
168
168
 
169
169
  if (!result.meta.pagination.hasMore) {
170
- break;
170
+ break
171
171
  }
172
- cursor = result.meta.pagination.cursor.next;
173
- iterations += 1;
172
+ cursor = result.meta.pagination.cursor.next
173
+ iterations += 1
174
174
  }
175
175
 
176
- assert(iterations < 20, 'Cursor pagination should converge within expected page count');
176
+ assert(iterations < 20, 'Cursor pagination should converge within expected page count')
177
177
 
178
178
  // Find all Green Gardens vegetables
179
- const greenGardensVeggies = allProducts.filter(p =>
180
- p.attributes.category === 'Vegetable' &&
179
+ const greenGardensVeggies = allProducts.filter(p =>
180
+ p.attributes.category === 'Vegetable' &&
181
181
  p.attributes.brand === 'Green Gardens'
182
- );
183
-
184
- assert.equal(greenGardensVeggies.length, 3, 'Should have all 3 Green Gardens vegetables');
182
+ )
183
+
184
+ assert.equal(greenGardensVeggies.length, 3, 'Should have all 3 Green Gardens vegetables')
185
185
 
186
186
  // Verify they're in name order
187
- const ggNames = greenGardensVeggies.map(p => p.attributes.name);
188
- assert.deepEqual(ggNames, ['Asparagus', 'Broccoli', 'Zucchini'],
189
- 'Green Gardens vegetables should be in alphabetical order by name');
190
- });
187
+ const ggNames = greenGardensVeggies.map(p => p.attributes.name)
188
+ assert.deepEqual(ggNames, ['Asparagus', 'Broccoli', 'Zucchini'],
189
+ 'Green Gardens vegetables should be in alphabetical order by name')
190
+ })
191
191
 
192
192
  it.skip('should handle cursor pagination with numeric and text fields', async () => {
193
193
  // Sort by price (numeric), then category (text), then name (text)
@@ -197,30 +197,30 @@ describe('Multi-field Cursor Pagination', () => {
197
197
  sort: ['price', 'category', 'name']
198
198
  },
199
199
  simplified: false
200
- });
200
+ })
201
201
 
202
202
  // The cheapest item is Carrot at 1.00
203
- assert.equal(firstPage.data[0].attributes.name, 'Carrot');
204
- assert.equal(firstPage.data[0].attributes.price, 1.00);
203
+ assert.equal(firstPage.data[0].attributes.name, 'Carrot')
204
+ assert.equal(firstPage.data[0].attributes.price, 1.00)
205
205
 
206
206
  // Get second page
207
207
  const secondPage = await api.resources.products.query({
208
208
  queryParams: {
209
- page: {
209
+ page: {
210
210
  size: 3,
211
211
  after: firstPage.meta.pagination.cursor.next
212
212
  },
213
213
  sort: ['price', 'category', 'name']
214
214
  },
215
215
  simplified: false
216
- });
216
+ })
217
217
 
218
218
  // Verify prices are non-decreasing across pages
219
- const lastPriceFirstPage = firstPage.data[firstPage.data.length - 1].attributes.price;
220
- const firstPriceSecondPage = secondPage.data[0].attributes.price;
221
- assert(firstPriceSecondPage >= lastPriceFirstPage,
222
- 'Prices should be non-decreasing across pages');
223
- });
219
+ const lastPriceFirstPage = firstPage.data[firstPage.data.length - 1].attributes.price
220
+ const firstPriceSecondPage = secondPage.data[0].attributes.price
221
+ assert(firstPriceSecondPage >= lastPriceFirstPage,
222
+ 'Prices should be non-decreasing across pages')
223
+ })
224
224
 
225
225
  it.skip('should generate correct cursor values for multi-field sorting', async () => {
226
226
  const result = await api.resources.products.query({
@@ -229,20 +229,20 @@ describe('Multi-field Cursor Pagination', () => {
229
229
  sort: ['category', 'name']
230
230
  },
231
231
  simplified: false
232
- });
232
+ })
233
233
 
234
234
  // Check that cursor is present
235
- assert(result.meta.pagination.cursor?.next, 'Should have next cursor');
236
-
235
+ assert(result.meta.pagination.cursor?.next, 'Should have next cursor')
236
+
237
237
  // The cursor should be parseable (not throwing an error)
238
- const cursor = result.meta.pagination.cursor.next;
239
- assert(typeof cursor === 'string', 'Cursor should be a string');
240
-
238
+ const cursor = result.meta.pagination.cursor.next
239
+ assert(typeof cursor === 'string', 'Cursor should be a string')
240
+
241
241
  // Cursor should encode multiple field values
242
242
  // Since we're using the simplified format, it should contain field:value pairs
243
- assert(cursor.includes('category:'), 'Cursor should include category field');
244
- assert(cursor.includes('name:'), 'Cursor should include name field');
245
- });
243
+ assert(cursor.includes('category:'), 'Cursor should include category field')
244
+ assert(cursor.includes('name:'), 'Cursor should include name field')
245
+ })
246
246
 
247
247
  it.skip('should handle page[before] with multi-field sorting', async () => {
248
248
  // First, get to the middle of the dataset
@@ -252,45 +252,45 @@ describe('Multi-field Cursor Pagination', () => {
252
252
  sort: ['category', 'name']
253
253
  },
254
254
  simplified: false
255
- });
255
+ })
256
256
 
257
257
  const secondPage = await api.resources.products.query({
258
258
  queryParams: {
259
- page: {
259
+ page: {
260
260
  size: 4,
261
261
  after: firstPage.meta.pagination.cursor.next
262
262
  },
263
263
  sort: ['category', 'name']
264
264
  },
265
265
  simplified: false
266
- });
266
+ })
267
267
 
268
268
  // Now go backwards from the second page
269
269
  const previousPage = await api.resources.products.query({
270
270
  queryParams: {
271
- page: {
271
+ page: {
272
272
  size: 4,
273
- before: secondPage.meta.pagination.cursor?.next ||
273
+ before: secondPage.meta.pagination.cursor?.next ||
274
274
  // If no next cursor, create one from the last record
275
275
  `category:${encodeURIComponent(secondPage.data[0].attributes.category)},name:${encodeURIComponent(secondPage.data[0].attributes.name)}`
276
276
  },
277
277
  sort: ['category', 'name']
278
278
  },
279
279
  simplified: false
280
- });
280
+ })
281
281
 
282
282
  // Verify we got different records going backwards
283
- const secondPageIds = secondPage.data.map(p => p.id);
284
- const previousPageIds = previousPage.data.map(p => p.id);
285
- assert(!secondPageIds.some(id => previousPageIds.includes(id)),
286
- 'Previous page should have different records than second page');
287
- });
288
- });
283
+ const secondPageIds = secondPage.data.map(p => p.id)
284
+ const previousPageIds = previousPage.data.map(p => p.id)
285
+ assert(!secondPageIds.some(id => previousPageIds.includes(id)),
286
+ 'Previous page should have different records than second page')
287
+ })
288
+ })
289
289
 
290
290
  describe('Edge Cases and Special Scenarios', () => {
291
291
  beforeEach(async () => {
292
- await cleanTables(knex, ['cursor_products', 'cursor_items']);
293
- });
292
+ await cleanTables(knex, ['cursor_products', 'cursor_items'])
293
+ })
294
294
 
295
295
  it.skip('should handle all records having the same value in first sort field', async () => {
296
296
  // Create products all in the same category
@@ -300,53 +300,53 @@ describe('Multi-field Cursor Pagination', () => {
300
300
  { name: 'Gamma', category: 'Electronics', brand: 'GadgetInc', price: 150, sku: 'GI-001' },
301
301
  { name: 'Delta', category: 'Electronics', brand: 'GadgetInc', price: 250, sku: 'GI-002' },
302
302
  { name: 'Epsilon', category: 'Electronics', brand: 'TechCo', price: 300, sku: 'TC-003' }
303
- ];
303
+ ]
304
304
 
305
305
  for (const product of products) {
306
306
  await api.resources.products.post({
307
307
  inputRecord: createJsonApiDocument('products', product),
308
308
  simplified: false
309
- });
309
+ })
310
310
  }
311
311
 
312
312
  // Sort by category (all same), then brand, then name
313
- const pageSize = 2;
314
- const allProducts = [];
315
- let cursor = null;
313
+ const pageSize = 2
314
+ const allProducts = []
315
+ let cursor = null
316
316
 
317
317
  while (true) {
318
318
  const result = await api.resources.products.query({
319
319
  queryParams: {
320
- page: {
320
+ page: {
321
321
  size: pageSize,
322
322
  ...(cursor && { after: cursor })
323
323
  },
324
324
  sort: ['category', 'brand', 'name']
325
325
  },
326
326
  simplified: false
327
- });
327
+ })
328
328
 
329
- allProducts.push(...result.data);
329
+ allProducts.push(...result.data)
330
330
 
331
331
  if (!result.meta.pagination.hasMore) {
332
- break;
332
+ break
333
333
  }
334
- cursor = result.meta.pagination.cursor.next;
334
+ cursor = result.meta.pagination.cursor.next
335
335
  }
336
336
 
337
- assert.equal(allProducts.length, 5, 'Should get all 5 products');
338
-
337
+ assert.equal(allProducts.length, 5, 'Should get all 5 products')
338
+
339
339
  // Check order: should be grouped by brand, then by name
340
340
  const productInfo = allProducts.map(p => ({
341
341
  brand: p.attributes.brand,
342
342
  name: p.attributes.name
343
- }));
344
-
343
+ }))
344
+
345
345
  // GadgetInc should come before TechCo
346
- const gadgetIncIndex = productInfo.findIndex(p => p.brand === 'GadgetInc');
347
- const firstTechCoIndex = productInfo.findIndex(p => p.brand === 'TechCo');
348
- assert(gadgetIncIndex < firstTechCoIndex, 'GadgetInc products should come before TechCo');
349
- });
346
+ const gadgetIncIndex = productInfo.findIndex(p => p.brand === 'GadgetInc')
347
+ const firstTechCoIndex = productInfo.findIndex(p => p.brand === 'TechCo')
348
+ assert(gadgetIncIndex < firstTechCoIndex, 'GadgetInc products should come before TechCo')
349
+ })
350
350
 
351
351
  it.skip('should handle empty results with multi-field cursor', async () => {
352
352
  // Create one product
@@ -359,7 +359,7 @@ describe('Multi-field Cursor Pagination', () => {
359
359
  sku: 'GEN-001'
360
360
  }),
361
361
  simplified: false
362
- });
362
+ })
363
363
 
364
364
  // Get the first (and only) page
365
365
  const firstPage = await api.resources.products.query({
@@ -368,12 +368,12 @@ describe('Multi-field Cursor Pagination', () => {
368
368
  sort: ['category', 'name']
369
369
  },
370
370
  simplified: false
371
- });
371
+ })
372
372
 
373
- assert.equal(firstPage.data.length, 1);
374
- assert.equal(firstPage.meta.pagination.hasMore, false);
375
- assert(!firstPage.meta.pagination.cursor?.next, 'Should not have next cursor on last page');
376
- });
373
+ assert.equal(firstPage.data.length, 1)
374
+ assert.equal(firstPage.meta.pagination.hasMore, false)
375
+ assert(!firstPage.meta.pagination.cursor?.next, 'Should not have next cursor on last page')
376
+ })
377
377
 
378
378
  it.skip('should handle special characters in multi-field cursors', async () => {
379
379
  // Create products with special characters
@@ -381,13 +381,13 @@ describe('Multi-field Cursor Pagination', () => {
381
381
  { name: 'Product: Special', category: 'Cat:1', brand: 'Brand,A', price: 10, sku: 'SP-001' },
382
382
  { name: 'Product: Extra', category: 'Cat:1', brand: 'Brand,B', price: 20, sku: 'SP-002' },
383
383
  { name: 'Product: Normal', category: 'Cat:2', brand: 'Brand,A', price: 15, sku: 'SP-003' }
384
- ];
384
+ ]
385
385
 
386
386
  for (const product of products) {
387
387
  await api.resources.products.post({
388
388
  inputRecord: createJsonApiDocument('products', product),
389
389
  simplified: false
390
- });
390
+ })
391
391
  }
392
392
 
393
393
  // Navigate through pages
@@ -397,26 +397,26 @@ describe('Multi-field Cursor Pagination', () => {
397
397
  sort: ['category', 'brand', 'name']
398
398
  },
399
399
  simplified: false
400
- });
400
+ })
401
401
 
402
- assert(firstPage.meta.pagination.cursor?.next, 'Should have cursor');
402
+ assert(firstPage.meta.pagination.cursor?.next, 'Should have cursor')
403
403
 
404
404
  // Use the cursor to get next page - should not throw
405
405
  const secondPage = await api.resources.products.query({
406
406
  queryParams: {
407
- page: {
407
+ page: {
408
408
  size: 1,
409
409
  after: firstPage.meta.pagination.cursor.next
410
410
  },
411
411
  sort: ['category', 'brand', 'name']
412
412
  },
413
413
  simplified: false
414
- });
414
+ })
415
415
 
416
- assert.equal(secondPage.data.length, 1);
417
- assert.notEqual(secondPage.data[0].id, firstPage.data[0].id,
418
- 'Should get different record on second page');
419
- });
416
+ assert.equal(secondPage.data.length, 1)
417
+ assert.notEqual(secondPage.data[0].id, firstPage.data[0].id,
418
+ 'Should get different record on second page')
419
+ })
420
420
 
421
421
  it.skip('should maintain stable sorting with custom id property', async () => {
422
422
  // Create items using the resource with custom ID property
@@ -425,13 +425,13 @@ describe('Multi-field Cursor Pagination', () => {
425
425
  { code: 'A002', name: 'Apricot', category: 'Fruit', type: 'Fresh' },
426
426
  { code: 'B001', name: 'Banana', category: 'Fruit', type: 'Fresh' },
427
427
  { code: 'C001', name: 'Carrot', category: 'Vegetable', type: 'Root' }
428
- ];
428
+ ]
429
429
 
430
430
  for (const item of items) {
431
431
  await api.resources.items.post({
432
432
  inputRecord: createJsonApiDocument('items', item),
433
433
  simplified: false
434
- });
434
+ })
435
435
  }
436
436
 
437
437
  // Query with multi-field sort
@@ -441,46 +441,46 @@ describe('Multi-field Cursor Pagination', () => {
441
441
  sort: ['category', 'name']
442
442
  },
443
443
  simplified: false
444
- });
444
+ })
445
445
 
446
446
  // Should use 'item_id' as the ID field in responses
447
- assert(result.data[0].id, 'Should have id field');
448
- assert(typeof result.data[0].id === 'string', 'ID should be a string');
447
+ assert(result.data[0].id, 'Should have id field')
448
+ assert(typeof result.data[0].id === 'string', 'ID should be a string')
449
449
 
450
450
  // Cursor should work with custom ID
451
451
  if (result.meta.pagination.cursor?.next) {
452
452
  const secondPage = await api.resources.items.query({
453
453
  queryParams: {
454
- page: {
454
+ page: {
455
455
  size: 2,
456
456
  after: result.meta.pagination.cursor.next
457
457
  },
458
458
  sort: ['category', 'name']
459
459
  },
460
460
  simplified: false
461
- });
462
-
463
- assert(secondPage.data.length > 0, 'Should get results on second page');
461
+ })
462
+
463
+ assert(secondPage.data.length > 0, 'Should get results on second page')
464
464
  }
465
- });
465
+ })
466
466
 
467
467
  it.skip('should handle the exact bug scenario - multiple fruits with same category', async () => {
468
468
  // This is the exact scenario from the bug report
469
- await cleanTables(knex, ['cursor_products']);
470
-
469
+ await cleanTables(knex, ['cursor_products'])
470
+
471
471
  const products = [
472
472
  { name: 'Apple', category: 'Fruit', brand: 'Farm Fresh', price: 2.00, sku: 'FRUIT-001' },
473
473
  { name: 'Banana', category: 'Fruit', brand: 'Farm Fresh', price: 1.50, sku: 'FRUIT-002' },
474
474
  { name: 'Cherry', category: 'Fruit', brand: 'Farm Fresh', price: 3.00, sku: 'FRUIT-003' },
475
475
  { name: 'Date', category: 'Fruit', brand: 'Desert Farm', price: 4.00, sku: 'FRUIT-004' },
476
476
  { name: 'Asparagus', category: 'Vegetable', brand: 'Green Farm', price: 2.50, sku: 'VEG-001' }
477
- ];
477
+ ]
478
478
 
479
479
  for (const product of products) {
480
480
  await api.resources.products.post({
481
481
  inputRecord: createJsonApiDocument('products', product),
482
482
  simplified: false
483
- });
483
+ })
484
484
  }
485
485
 
486
486
  // Get first page with size 3 (should get Apple, Banana, Cherry)
@@ -490,29 +490,29 @@ describe('Multi-field Cursor Pagination', () => {
490
490
  sort: ['category', 'name']
491
491
  },
492
492
  simplified: false
493
- });
493
+ })
494
494
 
495
- assert.equal(firstPage.data.length, 3);
496
- const firstPageNames = firstPage.data.map(p => p.attributes.name);
497
- assert.deepEqual(firstPageNames, ['Apple', 'Banana', 'Cherry']);
495
+ assert.equal(firstPage.data.length, 3)
496
+ const firstPageNames = firstPage.data.map(p => p.attributes.name)
497
+ assert.deepEqual(firstPageNames, ['Apple', 'Banana', 'Cherry'])
498
498
 
499
499
  // Get second page - MUST include Date (the bug would skip it)
500
500
  const secondPage = await api.resources.products.query({
501
501
  queryParams: {
502
- page: {
502
+ page: {
503
503
  size: 3,
504
504
  after: firstPage.meta.pagination.cursor.next
505
505
  },
506
506
  sort: ['category', 'name']
507
507
  },
508
508
  simplified: false
509
- });
510
-
511
- const secondPageNames = secondPage.data.map(p => p.attributes.name);
512
- assert(secondPageNames.includes('Date'),
513
- 'Second page MUST include Date - this was the bug where it would be skipped');
514
- assert.deepEqual(secondPageNames, ['Date', 'Asparagus'],
515
- 'Second page should have Date and Asparagus in that order');
516
- });
517
- });
518
- });
509
+ })
510
+
511
+ const secondPageNames = secondPage.data.map(p => p.attributes.name)
512
+ assert(secondPageNames.includes('Date'),
513
+ 'Second page MUST include Date - this was the bug where it would be skipped')
514
+ assert.deepEqual(secondPageNames, ['Date', 'Asparagus'],
515
+ 'Second page should have Date and Asparagus in that order')
516
+ })
517
+ })
518
+ })