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,7 +1,7 @@
1
- import { describe, it, before, beforeEach, after } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import knexLib from 'knex';
4
- import { createBulkOperationsApi } from './fixtures/api-configs.js';
1
+ import { describe, it, before, beforeEach, after } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import knexLib from 'knex'
4
+ import { createBulkOperationsApi } from './fixtures/api-configs.js'
5
5
  import {
6
6
  validateJsonApiStructure,
7
7
  resourceIdentifier,
@@ -9,7 +9,7 @@ import {
9
9
  createJsonApiDocument,
10
10
  createRelationship,
11
11
  createToManyRelationship
12
- } from './helpers/test-utils.js';
12
+ } from './helpers/test-utils.js'
13
13
 
14
14
  // Create Knex instance for tests
15
15
  const knex = knexLib({
@@ -18,87 +18,87 @@ const knex = knexLib({
18
18
  filename: ':memory:'
19
19
  },
20
20
  useNullAsDefault: true
21
- });
21
+ })
22
22
 
23
23
  // API instance that persists across tests
24
- let api;
24
+ let api
25
25
 
26
26
  describe('Bulk Operations', () => {
27
27
  before(async () => {
28
28
  // Initialize API once
29
- api = await createBulkOperationsApi(knex);
30
- });
29
+ api = await createBulkOperationsApi(knex)
30
+ })
31
31
 
32
32
  after(async () => {
33
33
  // Close database connection to allow tests to exit
34
- await knex.destroy();
35
- });
34
+ await knex.destroy()
35
+ })
36
36
 
37
37
  describe('Bulk Create (bulkPost)', () => {
38
38
  beforeEach(async () => {
39
39
  // Clean all tables before each test
40
40
  await cleanTables(knex, [
41
41
  'basic_countries', 'basic_publishers', 'basic_authors', 'basic_books', 'basic_book_authors'
42
- ]);
43
- });
42
+ ])
43
+ })
44
44
 
45
45
  it('should create multiple records atomically', async () => {
46
46
  const records = [
47
47
  createJsonApiDocument('authors', { name: 'Author One' }),
48
48
  createJsonApiDocument('authors', { name: 'Author Two' }),
49
49
  createJsonApiDocument('authors', { name: 'Author Three' })
50
- ];
50
+ ]
51
51
 
52
52
  const result = await api.scopes.authors.bulkPost({
53
53
  inputRecords: records,
54
54
  atomic: true
55
- });
55
+ })
56
56
 
57
57
  // Validate response structure
58
- assert(result.data, 'Should have data array');
59
- assert(Array.isArray(result.data), 'Data should be an array');
60
- assert.equal(result.data.length, 3, 'Should return 3 created records');
58
+ assert(result.data, 'Should have data array')
59
+ assert(Array.isArray(result.data), 'Data should be an array')
60
+ assert.equal(result.data.length, 3, 'Should return 3 created records')
61
61
 
62
62
  // Validate meta
63
- assert.equal(result.meta.total, 3);
64
- assert.equal(result.meta.succeeded, 3);
65
- assert.equal(result.meta.failed, 0);
66
- assert.equal(result.meta.atomic, true);
63
+ assert.equal(result.meta.total, 3)
64
+ assert.equal(result.meta.succeeded, 3)
65
+ assert.equal(result.meta.failed, 0)
66
+ assert.equal(result.meta.atomic, true)
67
67
 
68
68
  // Validate each created record
69
69
  result.data.forEach((record, index) => {
70
- validateJsonApiStructure({ data: record }, false);
71
- assert.equal(record.type, 'authors');
72
- assert(record.id, 'Should have generated ID');
73
- assert.equal(record.attributes.name, `Author ${['One', 'Two', 'Three'][index]}`);
74
- });
70
+ validateJsonApiStructure({ data: record }, false)
71
+ assert.equal(record.type, 'authors')
72
+ assert(record.id, 'Should have generated ID')
73
+ assert.equal(record.attributes.name, `Author ${['One', 'Two', 'Three'][index]}`)
74
+ })
75
75
 
76
76
  // Verify via API query
77
- const queryResult = await api.resources.authors.query({ simplified: false });
78
- assert.equal(queryResult.data.length, 3, 'Should have 3 records via query');
79
- });
77
+ const queryResult = await api.resources.authors.query({ simplified: false })
78
+ assert.equal(queryResult.data.length, 3, 'Should have 3 records via query')
79
+ })
80
80
 
81
81
  it('should rollback all records on error in atomic mode', async () => {
82
82
  const records = [
83
83
  createJsonApiDocument('authors', { name: 'Valid Author' }),
84
84
  createJsonApiDocument('authors', {}), // Invalid - missing required name
85
85
  createJsonApiDocument('authors', { name: 'Another Valid' })
86
- ];
86
+ ]
87
87
 
88
88
  try {
89
89
  await api.scopes.authors.bulkPost({
90
90
  inputRecords: records,
91
91
  atomic: true
92
- });
93
- assert.fail('Should have thrown error');
92
+ })
93
+ assert.fail('Should have thrown error')
94
94
  } catch (error) {
95
- assert(error.message.includes('validation failed') || error.message.includes('required'), 'Error should mention validation or required field');
95
+ assert(error.message.includes('validation failed') || error.message.includes('required'), 'Error should mention validation or required field')
96
96
  }
97
97
 
98
98
  // Verify no records were created via API
99
- const queryResult = await api.resources.authors.query({ simplified: false });
100
- assert.equal(queryResult.data.length, 0, 'Should have no records due to rollback');
101
- });
99
+ const queryResult = await api.resources.authors.query({ simplified: false })
100
+ assert.equal(queryResult.data.length, 0, 'Should have no records due to rollback')
101
+ })
102
102
 
103
103
  it('should allow partial success in non-atomic mode', async () => {
104
104
  const records = [
@@ -106,118 +106,118 @@ describe('Bulk Operations', () => {
106
106
  createJsonApiDocument('authors', {}), // Invalid
107
107
  createJsonApiDocument('authors', { name: 'Valid Author 2' }),
108
108
  createJsonApiDocument('authors', { name: 'Valid Author 3' })
109
- ];
109
+ ]
110
110
 
111
111
  const result = await api.scopes.authors.bulkPost({
112
112
  inputRecords: records,
113
113
  atomic: false
114
- });
114
+ })
115
115
 
116
116
  // Check succeeded records
117
- assert.equal(result.data.length, 3, 'Should have 3 successful records');
118
- assert.equal(result.meta.succeeded, 3);
119
- assert.equal(result.meta.failed, 1);
117
+ assert.equal(result.data.length, 3, 'Should have 3 successful records')
118
+ assert.equal(result.meta.succeeded, 3)
119
+ assert.equal(result.meta.failed, 1)
120
120
 
121
121
  // Check errors
122
- assert(result.errors, 'Should have errors array');
123
- assert.equal(result.errors.length, 1);
124
-
122
+ assert(result.errors, 'Should have errors array')
123
+ assert.equal(result.errors.length, 1)
124
+
125
125
  // Error should be for missing name
126
- assert.equal(result.errors[0].index, 1);
127
- assert(result.errors[0].error.message.includes('validation failed') || result.errors[0].error.message.includes('required'));
126
+ assert.equal(result.errors[0].index, 1)
127
+ assert(result.errors[0].error.message.includes('validation failed') || result.errors[0].error.message.includes('required'))
128
128
 
129
129
  // Verify via API
130
- const queryResult = await api.resources.authors.query({ simplified: false });
131
- assert.equal(queryResult.data.length, 3);
132
- });
130
+ const queryResult = await api.resources.authors.query({ simplified: false })
131
+ assert.equal(queryResult.data.length, 3)
132
+ })
133
133
 
134
134
  it('should create records with relationships', async () => {
135
135
  // First create a country
136
136
  const countryResult = await api.resources.countries.post({
137
137
  inputRecord: createJsonApiDocument('countries', { name: 'Test Country', code: 'TC' }),
138
138
  simplified: false
139
- });
139
+ })
140
140
 
141
141
  const records = [
142
- createJsonApiDocument('publishers',
142
+ createJsonApiDocument('publishers',
143
143
  { name: 'Publisher One' },
144
144
  { country: createRelationship(resourceIdentifier('countries', countryResult.data.id)) }
145
145
  ),
146
- createJsonApiDocument('publishers',
146
+ createJsonApiDocument('publishers',
147
147
  { name: 'Publisher Two' },
148
148
  { country: createRelationship(resourceIdentifier('countries', countryResult.data.id)) }
149
149
  )
150
- ];
150
+ ]
151
151
 
152
152
  const result = await api.scopes.publishers.bulkPost({
153
153
  inputRecords: records
154
- });
154
+ })
155
155
 
156
- assert.equal(result.data.length, 2);
156
+ assert.equal(result.data.length, 2)
157
157
  result.data.forEach(publisher => {
158
- assert.equal(publisher.relationships.country.data.id, countryResult.data.id);
159
- });
160
- });
158
+ assert.equal(publisher.relationships.country.data.id, countryResult.data.id)
159
+ })
160
+ })
161
161
 
162
162
  it('should enforce max bulk operations limit', async () => {
163
- const tooManyRecords = Array.from({ length: 101 }, (_, i) =>
163
+ const tooManyRecords = Array.from({ length: 101 }, (_, i) =>
164
164
  createJsonApiDocument('authors', { name: `Author ${i}` })
165
- );
165
+ )
166
166
 
167
167
  try {
168
168
  await api.scopes.authors.bulkPost({
169
169
  inputRecords: tooManyRecords
170
- });
171
- assert.fail('Should have thrown error');
170
+ })
171
+ assert.fail('Should have thrown error')
172
172
  } catch (error) {
173
- assert(error.message.includes('limited to 100'));
173
+ assert(error.message.includes('limited to 100'))
174
174
  }
175
- });
175
+ })
176
176
 
177
177
  it('should respect atomic query parameter override', async () => {
178
178
  const records = [
179
179
  createJsonApiDocument('authors', { name: 'Valid' }),
180
180
  createJsonApiDocument('authors', {}) // Invalid
181
- ];
181
+ ]
182
182
 
183
183
  // Override default atomic mode
184
184
  const result = await api.scopes.authors.bulkPost({
185
185
  inputRecords: records,
186
186
  atomic: false // Override to non-atomic
187
- });
187
+ })
188
188
 
189
- assert.equal(result.meta.succeeded, 1);
190
- assert.equal(result.meta.failed, 1);
191
- assert.equal(result.meta.atomic, false);
192
- });
193
- });
189
+ assert.equal(result.meta.succeeded, 1)
190
+ assert.equal(result.meta.failed, 1)
191
+ assert.equal(result.meta.atomic, false)
192
+ })
193
+ })
194
194
 
195
195
  describe('Bulk Update (bulkPatch)', () => {
196
- let testData = {};
196
+ const testData = {}
197
197
 
198
198
  beforeEach(async () => {
199
199
  await cleanTables(knex, [
200
200
  'basic_countries', 'basic_publishers', 'basic_authors', 'basic_books', 'basic_book_authors'
201
- ]);
201
+ ])
202
202
 
203
203
  // Create test data via API
204
204
  const authors = await Promise.all([
205
- api.resources.authors.post({
205
+ api.resources.authors.post({
206
206
  inputRecord: createJsonApiDocument('authors', { name: 'Author One' }),
207
207
  simplified: false
208
208
  }),
209
- api.resources.authors.post({
209
+ api.resources.authors.post({
210
210
  inputRecord: createJsonApiDocument('authors', { name: 'Author Two' }),
211
211
  simplified: false
212
212
  }),
213
- api.resources.authors.post({
213
+ api.resources.authors.post({
214
214
  inputRecord: createJsonApiDocument('authors', { name: 'Author Three' }),
215
215
  simplified: false
216
216
  })
217
- ]);
217
+ ])
218
218
 
219
- testData.authorIds = authors.map(a => a.data.id);
220
- });
219
+ testData.authorIds = authors.map(a => a.data.id)
220
+ })
221
221
 
222
222
  it('should update multiple records atomically', async () => {
223
223
  const operations = [
@@ -237,28 +237,28 @@ describe('Bulk Operations', () => {
237
237
  attributes: { name: 'Updated Author Two' }
238
238
  }
239
239
  }
240
- ];
240
+ ]
241
241
 
242
242
  const result = await api.scopes.authors.bulkPatch({
243
243
  operations,
244
244
  atomic: true
245
- });
245
+ })
246
246
 
247
- assert.equal(result.data.length, 2);
248
- assert.equal(result.meta.succeeded, 2);
249
- assert.equal(result.meta.failed, 0);
247
+ assert.equal(result.data.length, 2)
248
+ assert.equal(result.meta.succeeded, 2)
249
+ assert.equal(result.meta.failed, 0)
250
250
 
251
251
  // Verify updates in response
252
- assert.equal(result.data[0].attributes.name, 'Updated Author One');
253
- assert.equal(result.data[1].attributes.name, 'Updated Author Two');
252
+ assert.equal(result.data[0].attributes.name, 'Updated Author One')
253
+ assert.equal(result.data[1].attributes.name, 'Updated Author Two')
254
254
 
255
255
  // Verify via API
256
- const author1 = await api.resources.authors.get({
256
+ const author1 = await api.resources.authors.get({
257
257
  id: testData.authorIds[0],
258
258
  simplified: false
259
- });
260
- assert.equal(author1.data.attributes.name, 'Updated Author One');
261
- });
259
+ })
260
+ assert.equal(author1.data.attributes.name, 'Updated Author One')
261
+ })
262
262
 
263
263
  it('should handle non-existent IDs in non-atomic mode', async () => {
264
264
  const operations = [
@@ -286,18 +286,18 @@ describe('Bulk Operations', () => {
286
286
  attributes: { name: 'Also Updated' }
287
287
  }
288
288
  }
289
- ];
289
+ ]
290
290
 
291
291
  const result = await api.scopes.authors.bulkPatch({
292
292
  operations,
293
293
  atomic: false
294
- });
294
+ })
295
295
 
296
- assert.equal(result.meta.succeeded, 2);
297
- assert.equal(result.meta.failed, 1);
298
- assert.equal(result.errors.length, 1);
299
- assert.equal(result.errors[0].id, '999999');
300
- });
296
+ assert.equal(result.meta.succeeded, 2)
297
+ assert.equal(result.meta.failed, 1)
298
+ assert.equal(result.errors.length, 1)
299
+ assert.equal(result.errors[0].id, '999999')
300
+ })
301
301
 
302
302
  it('should validate operation structure', async () => {
303
303
  const invalidOperations = [
@@ -311,25 +311,25 @@ describe('Bulk Operations', () => {
311
311
  attributes: { name: 'Valid' }
312
312
  }
313
313
  }
314
- ];
314
+ ]
315
315
 
316
316
  const result = await api.scopes.authors.bulkPatch({
317
317
  operations: invalidOperations,
318
318
  atomic: false
319
- });
319
+ })
320
320
 
321
- assert.equal(result.meta.succeeded, 1);
322
- assert.equal(result.meta.failed, 2);
323
- assert.equal(result.errors[0].error.code, 'INVALID_OPERATION');
324
- assert.equal(result.errors[1].error.code, 'INVALID_OPERATION');
325
- });
321
+ assert.equal(result.meta.succeeded, 1)
322
+ assert.equal(result.meta.failed, 2)
323
+ assert.equal(result.errors[0].error.code, 'INVALID_OPERATION')
324
+ assert.equal(result.errors[1].error.code, 'INVALID_OPERATION')
325
+ })
326
326
 
327
327
  it('should rollback all updates on error in atomic mode', async () => {
328
328
  // Get original name via API
329
- const originalAuthor = await api.resources.authors.get({
329
+ const originalAuthor = await api.resources.authors.get({
330
330
  id: testData.authorIds[0],
331
331
  simplified: false
332
- });
332
+ })
333
333
 
334
334
  const operations = [
335
335
  {
@@ -348,71 +348,71 @@ describe('Bulk Operations', () => {
348
348
  attributes: { name: 'Non-existent' }
349
349
  }
350
350
  }
351
- ];
351
+ ]
352
352
 
353
353
  try {
354
354
  await api.scopes.authors.bulkPatch({
355
355
  operations,
356
356
  atomic: true
357
- });
358
- assert.fail('Should have thrown error');
357
+ })
358
+ assert.fail('Should have thrown error')
359
359
  } catch (error) {
360
360
  // Expected error
361
361
  }
362
362
 
363
363
  // Verify no changes were made via API
364
- const author = await api.resources.authors.get({
364
+ const author = await api.resources.authors.get({
365
365
  id: testData.authorIds[0],
366
366
  simplified: false
367
- });
368
- assert.equal(author.data.attributes.name, originalAuthor.data.attributes.name, 'Name should not have changed');
369
- });
370
- });
367
+ })
368
+ assert.equal(author.data.attributes.name, originalAuthor.data.attributes.name, 'Name should not have changed')
369
+ })
370
+ })
371
371
 
372
372
  describe('Bulk Delete (bulkDelete)', () => {
373
- let testData = {};
373
+ const testData = {}
374
374
 
375
375
  beforeEach(async () => {
376
376
  await cleanTables(knex, [
377
377
  'basic_countries', 'basic_publishers', 'basic_authors', 'basic_books', 'basic_book_authors'
378
- ]);
378
+ ])
379
379
 
380
380
  // Create test data via API
381
381
  const authors = await Promise.all(
382
- Array.from({ length: 5 }, (_, i) =>
383
- api.resources.authors.post({
382
+ Array.from({ length: 5 }, (_, i) =>
383
+ api.resources.authors.post({
384
384
  inputRecord: createJsonApiDocument('authors', { name: `Author ${i + 1}` }),
385
385
  simplified: false
386
386
  })
387
387
  )
388
- );
388
+ )
389
389
 
390
- testData.authorIds = authors.map(a => a.data.id);
391
- });
390
+ testData.authorIds = authors.map(a => a.data.id)
391
+ })
392
392
 
393
393
  it('should delete multiple records atomically', async () => {
394
- const idsToDelete = [testData.authorIds[0], testData.authorIds[2], testData.authorIds[4]];
394
+ const idsToDelete = [testData.authorIds[0], testData.authorIds[2], testData.authorIds[4]]
395
395
 
396
396
  const result = await api.scopes.authors.bulkDelete({
397
397
  ids: idsToDelete,
398
398
  atomic: true
399
- });
399
+ })
400
400
 
401
- assert.equal(result.meta.total, 3);
402
- assert.equal(result.meta.succeeded, 3);
403
- assert.equal(result.meta.failed, 0);
404
- assert.deepEqual(result.meta.deleted, idsToDelete);
401
+ assert.equal(result.meta.total, 3)
402
+ assert.equal(result.meta.succeeded, 3)
403
+ assert.equal(result.meta.failed, 0)
404
+ assert.deepEqual(result.meta.deleted, idsToDelete)
405
405
 
406
406
  // Verify via API query
407
- const queryResult = await api.resources.authors.query({ simplified: false });
408
- assert.equal(queryResult.data.length, 2, 'Should have 2 remaining records');
407
+ const queryResult = await api.resources.authors.query({ simplified: false })
408
+ assert.equal(queryResult.data.length, 2, 'Should have 2 remaining records')
409
409
 
410
410
  // Verify specific records were deleted
411
- const remainingIds = queryResult.data.map(r => r.id);
412
- assert(!remainingIds.includes(testData.authorIds[0]));
413
- assert(remainingIds.includes(testData.authorIds[1]));
414
- assert(!remainingIds.includes(testData.authorIds[2]));
415
- });
411
+ const remainingIds = queryResult.data.map(r => r.id)
412
+ assert(!remainingIds.includes(testData.authorIds[0]))
413
+ assert(remainingIds.includes(testData.authorIds[1]))
414
+ assert(!remainingIds.includes(testData.authorIds[2]))
415
+ })
416
416
 
417
417
  it('should handle mixed valid/invalid IDs in non-atomic mode', async () => {
418
418
  const idsToDelete = [
@@ -421,66 +421,66 @@ describe('Bulk Operations', () => {
421
421
  testData.authorIds[2],
422
422
  '888888', // Non-existent
423
423
  testData.authorIds[4]
424
- ];
424
+ ]
425
425
 
426
426
  const result = await api.scopes.authors.bulkDelete({
427
427
  ids: idsToDelete,
428
428
  atomic: false
429
- });
429
+ })
430
430
 
431
- assert.equal(result.meta.total, 5);
432
- assert.equal(result.meta.succeeded, 3);
433
- assert.equal(result.meta.failed, 2);
434
- assert.equal(result.meta.deleted.length, 3);
431
+ assert.equal(result.meta.total, 5)
432
+ assert.equal(result.meta.succeeded, 3)
433
+ assert.equal(result.meta.failed, 2)
434
+ assert.equal(result.meta.deleted.length, 3)
435
435
 
436
- assert.equal(result.errors.length, 2);
437
- assert.equal(result.errors[0].id, '999999');
438
- assert.equal(result.errors[1].id, '888888');
436
+ assert.equal(result.errors.length, 2)
437
+ assert.equal(result.errors[0].id, '999999')
438
+ assert.equal(result.errors[1].id, '888888')
439
439
 
440
440
  // Verify correct records were deleted via API
441
- const queryResult = await api.resources.authors.query({ simplified: false });
442
- assert.equal(queryResult.data.length, 2);
443
- });
441
+ const queryResult = await api.resources.authors.query({ simplified: false })
442
+ assert.equal(queryResult.data.length, 2)
443
+ })
444
444
 
445
445
  it('should rollback all deletes on error in atomic mode', async () => {
446
446
  const idsToDelete = [
447
447
  testData.authorIds[0],
448
448
  testData.authorIds[1],
449
449
  '999999' // Will fail
450
- ];
450
+ ]
451
451
 
452
452
  try {
453
453
  await api.scopes.authors.bulkDelete({
454
454
  ids: idsToDelete,
455
455
  atomic: true
456
- });
457
- assert.fail('Should have thrown error');
456
+ })
457
+ assert.fail('Should have thrown error')
458
458
  } catch (error) {
459
459
  // Expected error
460
460
  }
461
461
 
462
462
  // Verify no records were deleted via API
463
- const queryResult = await api.resources.authors.query({ simplified: false });
464
- assert.equal(queryResult.data.length, 5, 'Should still have all 5 records');
465
- });
463
+ const queryResult = await api.resources.authors.query({ simplified: false })
464
+ assert.equal(queryResult.data.length, 5, 'Should still have all 5 records')
465
+ })
466
466
 
467
467
  it('should handle empty ID array', async () => {
468
468
  try {
469
469
  await api.scopes.authors.bulkDelete({
470
470
  ids: []
471
- });
472
- assert.fail('Should have thrown error');
471
+ })
472
+ assert.fail('Should have thrown error')
473
473
  } catch (error) {
474
- assert(error.message.includes('array'));
474
+ assert(error.message.includes('array'))
475
475
  }
476
- });
476
+ })
477
477
 
478
478
  it('should respect relationships when deleting', async () => {
479
479
  // Create country and publishers with relationship
480
480
  const country = await api.resources.countries.post({
481
481
  inputRecord: createJsonApiDocument('countries', { name: 'Test Country', code: 'TC' }),
482
482
  simplified: false
483
- });
483
+ })
484
484
 
485
485
  const publishers = await Promise.all([
486
486
  api.resources.publishers.post({
@@ -497,7 +497,7 @@ describe('Bulk Operations', () => {
497
497
  ),
498
498
  simplified: false
499
499
  })
500
- ]);
500
+ ])
501
501
 
502
502
  // Create books referencing publishers
503
503
  const book = await api.resources.books.post({
@@ -509,70 +509,70 @@ describe('Bulk Operations', () => {
509
509
  }
510
510
  ),
511
511
  simplified: false
512
- });
512
+ })
513
513
 
514
514
  // Try to delete publisher that has books
515
- const publisherIds = publishers.map(p => p.data.id);
516
-
515
+ const publisherIds = publishers.map(p => p.data.id)
516
+
517
517
  const result = await api.scopes.publishers.bulkDelete({
518
518
  ids: publisherIds,
519
519
  atomic: false
520
- });
520
+ })
521
521
 
522
522
  // At least one should succeed (the one without books)
523
- assert(result.meta.succeeded >= 1);
524
- });
525
- });
523
+ assert(result.meta.succeeded >= 1)
524
+ })
525
+ })
526
526
 
527
527
  describe('Batch Processing', () => {
528
528
  beforeEach(async () => {
529
529
  await cleanTables(knex, [
530
530
  'basic_countries', 'basic_publishers', 'basic_authors', 'basic_books', 'basic_book_authors'
531
- ]);
532
- });
531
+ ])
532
+ })
533
533
 
534
534
  it('should process large batches correctly', async () => {
535
535
  // Create 50 records to test batching (batch size is 10 in config)
536
- const records = Array.from({ length: 50 }, (_, i) =>
536
+ const records = Array.from({ length: 50 }, (_, i) =>
537
537
  createJsonApiDocument('authors', { name: `Batch Author ${i + 1}` })
538
- );
538
+ )
539
539
 
540
540
  const result = await api.scopes.authors.bulkPost({
541
541
  inputRecords: records,
542
542
  atomic: true
543
- });
543
+ })
544
544
 
545
- assert.equal(result.meta.total, 50);
546
- assert.equal(result.meta.succeeded, 50);
547
- assert.equal(result.data.length, 50);
545
+ assert.equal(result.meta.total, 50)
546
+ assert.equal(result.meta.succeeded, 50)
547
+ assert.equal(result.data.length, 50)
548
548
 
549
549
  // Verify via API with increased page size
550
- const queryResult = await api.resources.authors.query({
550
+ const queryResult = await api.resources.authors.query({
551
551
  simplified: false,
552
552
  queryParams: { page: { size: 100 } }
553
- });
554
- assert.equal(queryResult.data.length, 50);
555
- });
556
- });
553
+ })
554
+ assert.equal(queryResult.data.length, 50)
555
+ })
556
+ })
557
557
 
558
558
  describe('Complex Scenarios', () => {
559
559
  beforeEach(async () => {
560
560
  await cleanTables(knex, [
561
561
  'basic_countries', 'basic_publishers', 'basic_authors', 'basic_books', 'basic_book_authors'
562
- ]);
563
- });
562
+ ])
563
+ })
564
564
 
565
565
  it('should handle mixed operations workflow', async () => {
566
566
  // Step 1: Bulk create authors
567
- const authorRecords = Array.from({ length: 5 }, (_, i) =>
567
+ const authorRecords = Array.from({ length: 5 }, (_, i) =>
568
568
  createJsonApiDocument('authors', { name: `Author ${i + 1}` })
569
- );
569
+ )
570
570
 
571
571
  const createResult = await api.scopes.authors.bulkPost({
572
572
  inputRecords: authorRecords
573
- });
573
+ })
574
574
 
575
- const authorIds = createResult.data.map(a => a.id);
575
+ const authorIds = createResult.data.map(a => a.id)
576
576
 
577
577
  // Step 2: Bulk update some of them
578
578
  const updateOperations = [
@@ -592,29 +592,29 @@ describe('Bulk Operations', () => {
592
592
  attributes: { name: 'Senior Author 3' }
593
593
  }
594
594
  }
595
- ];
595
+ ]
596
596
 
597
597
  const updateResult = await api.scopes.authors.bulkPatch({
598
598
  operations: updateOperations
599
- });
599
+ })
600
600
 
601
- assert.equal(updateResult.meta.succeeded, 2);
601
+ assert.equal(updateResult.meta.succeeded, 2)
602
602
 
603
603
  // Step 3: Bulk delete some others
604
604
  const deleteResult = await api.scopes.authors.bulkDelete({
605
605
  ids: [authorIds[3], authorIds[4]]
606
- });
606
+ })
607
607
 
608
- assert.equal(deleteResult.meta.succeeded, 2);
608
+ assert.equal(deleteResult.meta.succeeded, 2)
609
609
 
610
610
  // Verify final state via API
611
- const finalQuery = await api.resources.authors.query({ simplified: false });
612
- assert.equal(finalQuery.data.length, 3);
611
+ const finalQuery = await api.resources.authors.query({ simplified: false })
612
+ assert.equal(finalQuery.data.length, 3)
613
613
 
614
614
  // Check for senior authors
615
- const seniorAuthors = finalQuery.data.filter(a => a.attributes.name.includes('Senior'));
616
- assert.equal(seniorAuthors.length, 2);
617
- });
615
+ const seniorAuthors = finalQuery.data.filter(a => a.attributes.name.includes('Senior'))
616
+ assert.equal(seniorAuthors.length, 2)
617
+ })
618
618
 
619
619
  it('should maintain data integrity with relationships', async () => {
620
620
  // Create countries
@@ -623,9 +623,9 @@ describe('Bulk Operations', () => {
623
623
  createJsonApiDocument('countries', { name: 'USA', code: 'US' }),
624
624
  createJsonApiDocument('countries', { name: 'UK', code: 'GB' })
625
625
  ]
626
- });
626
+ })
627
627
 
628
- const countryIds = countryResult.data.map(c => c.id);
628
+ const countryIds = countryResult.data.map(c => c.id)
629
629
 
630
630
  // Create publishers with country relationships
631
631
  const publisherRecords = [
@@ -637,21 +637,21 @@ describe('Bulk Operations', () => {
637
637
  { name: 'UK Publisher' },
638
638
  { country: createRelationship(resourceIdentifier('countries', countryIds[1])) }
639
639
  )
640
- ];
640
+ ]
641
641
 
642
642
  const publisherResult = await api.scopes.publishers.bulkPost({
643
643
  inputRecords: publisherRecords
644
- });
644
+ })
645
645
 
646
- assert.equal(publisherResult.data.length, 2);
646
+ assert.equal(publisherResult.data.length, 2)
647
647
 
648
648
  // Verify relationships were set correctly via API
649
- const allPublishers = await api.resources.publishers.query({
649
+ const allPublishers = await api.resources.publishers.query({
650
650
  simplified: false
651
- });
652
- const usPublisher = allPublishers.data.find(p => p.attributes.name === 'US Publisher');
653
- assert(usPublisher, 'Should find US Publisher');
654
- assert.equal(usPublisher.relationships.country.data.id, countryIds[0]);
655
- });
656
- });
657
- });
651
+ })
652
+ const usPublisher = allPublishers.data.find(p => p.attributes.name === 'US Publisher')
653
+ assert(usPublisher, 'Should find US Publisher')
654
+ assert.equal(usPublisher.relationships.country.data.id, countryIds[0])
655
+ })
656
+ })
657
+ })