json-rest-api 1.0.13 → 1.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/connectors_ideas/fastify.js +66 -68
  2. package/connectors_ideas/koa-plugin.js +50 -51
  3. package/eslint.config.js +13 -0
  4. package/examples/url-override-example.js +63 -63
  5. package/index.js +13 -13
  6. package/lib/enhanced-logger.js +51 -51
  7. package/lib/error-formatter.js +61 -61
  8. package/lib/rest-api-errors.js +28 -28
  9. package/package.json +9 -5
  10. package/plugins/core/bulk-operations-plugin.js +142 -145
  11. package/plugins/core/connectors/express-plugin.js +210 -217
  12. package/plugins/core/connectors/lib/busboy-detector.js +84 -84
  13. package/plugins/core/connectors/lib/formidable-detector.js +50 -50
  14. package/plugins/core/connectors/lib/request-helpers.js +74 -74
  15. package/plugins/core/file-handling-plugin.js +125 -125
  16. package/plugins/core/lib/anyapi/anyapi-registry.js +254 -254
  17. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +287 -287
  18. package/plugins/core/lib/anyapi/schema-utils.js +101 -101
  19. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +41 -41
  20. package/plugins/core/lib/dbTablesOperations.js +134 -135
  21. package/plugins/core/lib/fractional-positioning.js +74 -76
  22. package/plugins/core/lib/querying/knex-cross-table-search.js +320 -320
  23. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +144 -144
  24. package/plugins/core/lib/querying/knex-pagination-helpers.js +164 -164
  25. package/plugins/core/lib/querying/knex-process-includes.js +50 -50
  26. package/plugins/core/lib/querying/knex-query-helpers-base.js +31 -31
  27. package/plugins/core/lib/querying/knex-query-helpers.js +451 -452
  28. package/plugins/core/lib/querying/knex-relationship-includes.js +1160 -1170
  29. package/plugins/core/lib/querying/knex-window-queries.js +89 -89
  30. package/plugins/core/lib/querying/url-helpers.js +35 -35
  31. package/plugins/core/lib/querying-writing/compile-schemas.js +97 -98
  32. package/plugins/core/lib/querying-writing/connectors-query-parser.js +31 -31
  33. package/plugins/core/lib/querying-writing/database-capabilities.js +94 -94
  34. package/plugins/core/lib/querying-writing/database-value-normalizers.js +76 -76
  35. package/plugins/core/lib/querying-writing/default-data-helpers.js +27 -27
  36. package/plugins/core/lib/querying-writing/field-utils.js +44 -42
  37. package/plugins/core/lib/querying-writing/knex-constants.js +15 -16
  38. package/plugins/core/lib/querying-writing/knex-field-helpers.js +117 -115
  39. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +41 -41
  40. package/plugins/core/lib/querying-writing/payload-validators.js +275 -275
  41. package/plugins/core/lib/querying-writing/schema-helpers.js +129 -129
  42. package/plugins/core/lib/querying-writing/scope-validations.js +143 -146
  43. package/plugins/core/lib/querying-writing/simplified-helpers.js +133 -129
  44. package/plugins/core/lib/storage/storage-adapter.js +68 -68
  45. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +31 -31
  46. package/plugins/core/lib/writing/many-to-many-manipulations.js +81 -81
  47. package/plugins/core/lib/writing/relationship-processor.js +58 -58
  48. package/plugins/core/rest-api-access.js +8 -8
  49. package/plugins/core/rest-api-anyapi-knex-plugin.js +920 -920
  50. package/plugins/core/rest-api-cors-plugin.js +170 -171
  51. package/plugins/core/rest-api-knex-plugin.js +482 -493
  52. package/plugins/core/rest-api-multihome-plugin.js +247 -251
  53. package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +5 -5
  54. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +20 -21
  55. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +44 -44
  56. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +23 -23
  57. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +15 -15
  58. package/plugins/core/rest-api-plugin-methods/add-route.js +9 -9
  59. package/plugins/core/rest-api-plugin-methods/check-permissions.js +5 -6
  60. package/plugins/core/rest-api-plugin-methods/common.js +295 -297
  61. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +35 -37
  62. package/plugins/core/rest-api-plugin-methods/delete.js +57 -59
  63. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +172 -171
  64. package/plugins/core/rest-api-plugin-methods/get-related.js +96 -96
  65. package/plugins/core/rest-api-plugin-methods/get-relationship.js +16 -18
  66. package/plugins/core/rest-api-plugin-methods/get.js +86 -86
  67. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +16 -17
  68. package/plugins/core/rest-api-plugin-methods/patch.js +63 -64
  69. package/plugins/core/rest-api-plugin-methods/post-relationship.js +54 -55
  70. package/plugins/core/rest-api-plugin-methods/post.js +61 -62
  71. package/plugins/core/rest-api-plugin-methods/put.js +86 -87
  72. package/plugins/core/rest-api-plugin-methods/query.js +88 -88
  73. package/plugins/core/rest-api-plugin-methods/release.js +5 -6
  74. package/plugins/core/rest-api-plugin.js +76 -78
  75. package/plugins/core/rest-api-positioning-plugin.js +251 -252
  76. package/plugins/core/socketio-plugin.js +240 -236
  77. package/plugins/storage/local-storage.js +103 -103
  78. package/plugins/storage/s3-storage.js +41 -42
  79. package/quickTest.js +37 -37
  80. package/scripts/debug-anyapi-review-filter.mjs +18 -18
  81. package/scripts/inspect-anyapi-descriptor-country.mjs +10 -10
  82. package/scripts/inspect-anyapi-descriptor.mjs +10 -10
  83. package/scripts/run-docs.js +112 -113
  84. package/test-debug.js +28 -28
  85. package/tests/TEST_TEMPLATE.test.js +51 -51
  86. package/tests/anyapi-basic.test.js +42 -42
  87. package/tests/anyapi-cursor-pagination.test.js +48 -48
  88. package/tests/bulk-operations.test.js +208 -208
  89. package/tests/computed-fields.test.js +204 -204
  90. package/tests/cors.test.js +412 -413
  91. package/tests/curl.test.js +275 -276
  92. package/tests/custom-idproperty-relationships.test.js +266 -266
  93. package/tests/field-getters.test.js +132 -132
  94. package/tests/field-setters.test.js +110 -110
  95. package/tests/fixtures/api-configs.js +699 -699
  96. package/tests/helpers/storage-mode.js +32 -32
  97. package/tests/helpers/test-utils.js +76 -76
  98. package/tests/include-depth-validation.test.js +76 -76
  99. package/tests/includes.test.js +145 -145
  100. package/tests/multihome.test.js +175 -175
  101. package/tests/nested-includes.test.js +255 -255
  102. package/tests/pagination-cursor-multifield.test.js +164 -164
  103. package/tests/pagination-enhanced.test.js +225 -225
  104. package/tests/polymorphic-relationships.test.js +96 -96
  105. package/tests/polymorphic-simple.test.js +44 -44
  106. package/tests/positioning-advanced.test.js +226 -226
  107. package/tests/positioning-extended.test.js +399 -399
  108. package/tests/positioning-multifilter.test.js +204 -204
  109. package/tests/positioning.test.js +266 -266
  110. package/tests/queries.test.js +184 -184
  111. package/tests/query-limits.test.js +154 -155
  112. package/tests/relationship-endpoints.test.js +127 -127
  113. package/tests/relationships.test.js +178 -179
  114. package/tests/rest-api-access.test.js +86 -86
  115. package/tests/rest-api.test.js +103 -103
  116. package/tests/searchschema-merge.test.js +93 -93
  117. package/tests/socketio.test.js +326 -326
  118. package/tests/virtual-fields.test.js +164 -164
@@ -1,8 +1,8 @@
1
- import { describe, it, before, beforeEach, after } from 'node:test';
2
- import assert from 'node:assert/strict';
3
- import knexLib from 'knex';
4
- import { spawn } from 'child_process';
5
- import { createBasicApi } 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 { spawn } from 'child_process'
5
+ import { createBasicApi } from './fixtures/api-configs.js'
6
6
  import {
7
7
  validateJsonApiStructure,
8
8
  resourceIdentifier,
@@ -10,9 +10,9 @@ import {
10
10
  createJsonApiDocument,
11
11
  createRelationship,
12
12
  createToManyRelationship
13
- } from './helpers/test-utils.js';
14
- import express from 'express';
15
- import http from 'http';
13
+ } from './helpers/test-utils.js'
14
+ import express from 'express'
15
+ import http from 'http'
16
16
 
17
17
  // Create Knex instance for tests
18
18
  const knex = knexLib({
@@ -21,147 +21,146 @@ const knex = knexLib({
21
21
  filename: ':memory:'
22
22
  },
23
23
  useNullAsDefault: true
24
- });
24
+ })
25
25
 
26
26
  // API instances that persist across tests
27
- let basicApi;
28
- let app;
29
- let server;
30
- const TEST_PORT = 3456;
27
+ let basicApi
28
+ let app
29
+ let server
30
+ const TEST_PORT = 3456
31
31
 
32
32
  /**
33
33
  * Execute a CURL command and return the response
34
34
  * @param {string[]} args - The curl command arguments
35
35
  * @returns {Promise<{stdout: string, stderr: string, exitCode: number}>}
36
36
  */
37
- async function executeCurl(args) {
37
+ async function executeCurl (args) {
38
38
  return new Promise((resolve, reject) => {
39
39
  const curl = spawn('curl', args, {
40
40
  shell: false
41
- });
42
-
43
- let stdout = '';
44
- let stderr = '';
45
-
41
+ })
42
+
43
+ let stdout = ''
44
+ let stderr = ''
45
+
46
46
  curl.stdout.on('data', (data) => {
47
- stdout += data.toString();
48
- });
49
-
47
+ stdout += data.toString()
48
+ })
49
+
50
50
  curl.stderr.on('data', (data) => {
51
- stderr += data.toString();
52
- });
53
-
51
+ stderr += data.toString()
52
+ })
53
+
54
54
  curl.on('close', (code) => {
55
55
  resolve({
56
56
  stdout,
57
57
  stderr,
58
58
  exitCode: code
59
- });
60
- });
61
-
59
+ })
60
+ })
61
+
62
62
  curl.on('error', (error) => {
63
- reject(error);
64
- });
65
- });
63
+ reject(error)
64
+ })
65
+ })
66
66
  }
67
67
 
68
68
  describe('CURL HTTP Abstraction Layer Tests', () => {
69
69
  before(async () => {
70
70
  // Initialize API with Express plugin
71
- basicApi = await createBasicApi(knex, { includeExpress: true });
72
-
71
+ basicApi = await createBasicApi(knex, { includeExpress: true })
72
+
73
73
  // Create Express app
74
- app = express();
75
-
74
+ app = express()
75
+
76
76
  // Mount the API routes
77
- basicApi.http.express.mount(app, '');
78
-
77
+ basicApi.http.express.mount(app, '')
78
+
79
79
  // Start server
80
- server = http.createServer(app);
80
+ server = http.createServer(app)
81
81
  await new Promise((resolve) => {
82
82
  server.listen(TEST_PORT, () => {
83
- console.log(`Test server started on port ${TEST_PORT}`);
84
- resolve();
85
- });
86
- });
87
-
88
- });
83
+ console.log(`Test server started on port ${TEST_PORT}`)
84
+ resolve()
85
+ })
86
+ })
87
+ })
89
88
 
90
89
  after(async () => {
91
90
  // Close server
92
91
  await new Promise((resolve) => {
93
92
  server.close(() => {
94
- console.log('Test server closed');
95
- resolve();
96
- });
97
- });
98
-
93
+ console.log('Test server closed')
94
+ resolve()
95
+ })
96
+ })
97
+
99
98
  // Close database connection
100
- await knex.destroy();
101
- });
99
+ await knex.destroy()
100
+ })
102
101
 
103
102
  describe('Basic CURL Operations', () => {
104
- let testData = {};
103
+ const testData = {}
105
104
 
106
105
  beforeEach(async () => {
107
106
  // Clean all tables
108
107
  await cleanTables(knex, [
109
108
  'basic_countries', 'basic_publishers', 'basic_authors', 'basic_books', 'basic_book_authors'
110
- ]);
109
+ ])
111
110
 
112
111
  // Create test data using API methods
113
- const countryDoc = createJsonApiDocument('countries', { name: 'CURL Test Country', code: 'CT' });
112
+ const countryDoc = createJsonApiDocument('countries', { name: 'CURL Test Country', code: 'CT' })
114
113
  const countryResult = await basicApi.resources.countries.post({
115
114
  inputRecord: countryDoc,
116
115
  simplified: false
117
- });
118
- testData.country = countryResult.data;
119
- });
116
+ })
117
+ testData.country = countryResult.data
118
+ })
120
119
 
121
120
  it('should GET a resource using CURL', async () => {
122
- const result = await executeCurl(['-s', '-H', 'Accept: application/vnd.api+json', `http://localhost:${TEST_PORT}/api/countries/${testData.country.id}`]);
123
-
124
- assert.equal(result.exitCode, 0, 'CURL should exit successfully');
125
-
126
- const response = JSON.parse(result.stdout);
127
- validateJsonApiStructure(response, false);
128
-
129
- assert.equal(response.data.type, 'countries');
130
- assert.equal(response.data.id, testData.country.id);
131
- assert.equal(response.data.attributes.name, 'CURL Test Country');
132
- assert.equal(response.data.attributes.code, 'CT');
133
- });
121
+ const result = await executeCurl(['-s', '-H', 'Accept: application/vnd.api+json', `http://localhost:${TEST_PORT}/api/countries/${testData.country.id}`])
122
+
123
+ assert.equal(result.exitCode, 0, 'CURL should exit successfully')
124
+
125
+ const response = JSON.parse(result.stdout)
126
+ validateJsonApiStructure(response, false)
127
+
128
+ assert.equal(response.data.type, 'countries')
129
+ assert.equal(response.data.id, testData.country.id)
130
+ assert.equal(response.data.attributes.name, 'CURL Test Country')
131
+ assert.equal(response.data.attributes.code, 'CT')
132
+ })
134
133
 
135
134
  it('should GET collection using CURL', async () => {
136
- const result = await executeCurl(['-s', '-H', 'Accept: application/vnd.api+json', `http://localhost:${TEST_PORT}/api/countries`]);
137
-
138
- assert.equal(result.exitCode, 0, 'CURL should exit successfully');
139
-
140
- const response = JSON.parse(result.stdout);
141
- validateJsonApiStructure(response, true);
142
-
143
- assert(Array.isArray(response.data), 'Response data should be an array');
144
- assert.equal(response.data.length, 1, 'Should have one country');
145
- assert.equal(response.data[0].type, 'countries');
146
- assert.equal(response.data[0].attributes.name, 'CURL Test Country');
147
- });
135
+ const result = await executeCurl(['-s', '-H', 'Accept: application/vnd.api+json', `http://localhost:${TEST_PORT}/api/countries`])
136
+
137
+ assert.equal(result.exitCode, 0, 'CURL should exit successfully')
138
+
139
+ const response = JSON.parse(result.stdout)
140
+ validateJsonApiStructure(response, true)
141
+
142
+ assert(Array.isArray(response.data), 'Response data should be an array')
143
+ assert.equal(response.data.length, 1, 'Should have one country')
144
+ assert.equal(response.data[0].type, 'countries')
145
+ assert.equal(response.data[0].attributes.name, 'CURL Test Country')
146
+ })
148
147
 
149
148
  it('should POST a new resource using CURL', async () => {
150
- const newCountryDoc = createJsonApiDocument('countries', { name: 'New CURL Country', code: 'NC' });
151
- const jsonPayload = JSON.stringify(newCountryDoc);
152
-
153
- const result = await executeCurl(['-s', '-X', 'POST', '-H', 'Content-Type: application/vnd.api+json', '-H', 'Accept: application/vnd.api+json', '-d', jsonPayload, `http://localhost:${TEST_PORT}/api/countries`]);
154
-
155
- assert.equal(result.exitCode, 0, 'CURL should exit successfully');
156
-
157
- const response = JSON.parse(result.stdout);
158
- validateJsonApiStructure(response, false);
159
-
160
- assert.equal(response.data.type, 'countries');
161
- assert(response.data.id, 'Should have an ID');
162
- assert.equal(response.data.attributes.name, 'New CURL Country');
163
- assert.equal(response.data.attributes.code, 'NC');
164
- });
149
+ const newCountryDoc = createJsonApiDocument('countries', { name: 'New CURL Country', code: 'NC' })
150
+ const jsonPayload = JSON.stringify(newCountryDoc)
151
+
152
+ const result = await executeCurl(['-s', '-X', 'POST', '-H', 'Content-Type: application/vnd.api+json', '-H', 'Accept: application/vnd.api+json', '-d', jsonPayload, `http://localhost:${TEST_PORT}/api/countries`])
153
+
154
+ assert.equal(result.exitCode, 0, 'CURL should exit successfully')
155
+
156
+ const response = JSON.parse(result.stdout)
157
+ validateJsonApiStructure(response, false)
158
+
159
+ assert.equal(response.data.type, 'countries')
160
+ assert(response.data.id, 'Should have an ID')
161
+ assert.equal(response.data.attributes.name, 'New CURL Country')
162
+ assert.equal(response.data.attributes.code, 'NC')
163
+ })
165
164
 
166
165
  it('should PATCH a resource using CURL', async () => {
167
166
  const patchDoc = {
@@ -172,201 +171,201 @@ describe('CURL HTTP Abstraction Layer Tests', () => {
172
171
  name: 'Updated CURL Country'
173
172
  }
174
173
  }
175
- };
176
- const jsonPayload = JSON.stringify(patchDoc);
177
-
178
- const result = await executeCurl(['-s', '-X', 'PATCH', '-H', 'Content-Type: application/vnd.api+json', '-H', 'Accept: application/vnd.api+json', '-d', jsonPayload, `http://localhost:${TEST_PORT}/api/countries/${testData.country.id}`]);
179
-
180
- assert.equal(result.exitCode, 0, 'CURL should exit successfully');
181
-
182
- const response = JSON.parse(result.stdout);
183
-
174
+ }
175
+ const jsonPayload = JSON.stringify(patchDoc)
176
+
177
+ const result = await executeCurl(['-s', '-X', 'PATCH', '-H', 'Content-Type: application/vnd.api+json', '-H', 'Accept: application/vnd.api+json', '-d', jsonPayload, `http://localhost:${TEST_PORT}/api/countries/${testData.country.id}`])
178
+
179
+ assert.equal(result.exitCode, 0, 'CURL should exit successfully')
180
+
181
+ const response = JSON.parse(result.stdout)
182
+
184
183
  // PATCH is configured to return minimal JSON:API response
185
- assert(response.data, 'Should have data');
186
- assert.equal(response.data.type, 'countries', 'Should have correct type');
187
- assert.equal(response.data.id, String(testData.country.id), 'Should return correct ID');
188
-
184
+ assert(response.data, 'Should have data')
185
+ assert.equal(response.data.type, 'countries', 'Should have correct type')
186
+ assert.equal(response.data.id, String(testData.country.id), 'Should return correct ID')
187
+
189
188
  // Verify the update by fetching the record
190
189
  const verifyResult = await basicApi.resources.countries.get({
191
190
  id: testData.country.id,
192
191
  simplified: false
193
- });
194
- assert.equal(verifyResult.data.attributes.name, 'Updated CURL Country');
195
- assert.equal(verifyResult.data.attributes.code, 'CT', 'Code should remain unchanged');
196
- });
192
+ })
193
+ assert.equal(verifyResult.data.attributes.name, 'Updated CURL Country')
194
+ assert.equal(verifyResult.data.attributes.code, 'CT', 'Code should remain unchanged')
195
+ })
197
196
 
198
197
  it('should DELETE a resource using CURL', async () => {
199
- const result = await executeCurl(['-s', '-X', 'DELETE', '-w', '%{http_code}', '-o', '/dev/null', `http://localhost:${TEST_PORT}/api/countries/${testData.country.id}`]);
200
-
201
- assert.equal(result.exitCode, 0, 'CURL should exit successfully');
202
- assert.equal(result.stdout.trim(), '204', 'Should return 204 No Content');
203
-
198
+ const result = await executeCurl(['-s', '-X', 'DELETE', '-w', '%{http_code}', '-o', '/dev/null', `http://localhost:${TEST_PORT}/api/countries/${testData.country.id}`])
199
+
200
+ assert.equal(result.exitCode, 0, 'CURL should exit successfully')
201
+ assert.equal(result.stdout.trim(), '204', 'Should return 204 No Content')
202
+
204
203
  // Verify deletion
205
204
  try {
206
205
  await basicApi.resources.countries.get({
207
206
  id: testData.country.id,
208
207
  simplified: false
209
- });
210
- assert.fail('Should have thrown not found error');
208
+ })
209
+ assert.fail('Should have thrown not found error')
211
210
  } catch (error) {
212
- assert.equal(error.code, 'REST_API_RESOURCE');
213
- assert.equal(error.subtype, 'not_found');
211
+ assert.equal(error.code, 'REST_API_RESOURCE')
212
+ assert.equal(error.subtype, 'not_found')
214
213
  }
215
- });
214
+ })
216
215
 
217
216
  it('should handle query parameters using CURL', async () => {
218
217
  // Create additional countries
219
218
  await basicApi.resources.countries.post({
220
219
  inputRecord: createJsonApiDocument('countries', { name: 'Another Country', code: 'AC' }),
221
220
  simplified: false
222
- });
221
+ })
223
222
  await basicApi.resources.countries.post({
224
223
  inputRecord: createJsonApiDocument('countries', { name: 'Third Country', code: 'TC2' }),
225
224
  simplified: false
226
- });
225
+ })
227
226
 
228
227
  // Test with filter (URL encode the space and square brackets)
229
- const result = await executeCurl(['-s', '-H', 'Accept: application/vnd.api+json', `http://localhost:${TEST_PORT}/api/countries?filter%5Bname%5D=Another%20Country`]);
230
-
231
- assert.equal(result.exitCode, 0, 'CURL should exit successfully');
232
-
233
- const response = JSON.parse(result.stdout);
234
- validateJsonApiStructure(response, true);
235
-
236
- assert.equal(response.data.length, 1, 'Should filter to one country');
237
- assert.equal(response.data[0].attributes.name, 'Another Country');
238
- });
228
+ const result = await executeCurl(['-s', '-H', 'Accept: application/vnd.api+json', `http://localhost:${TEST_PORT}/api/countries?filter%5Bname%5D=Another%20Country`])
229
+
230
+ assert.equal(result.exitCode, 0, 'CURL should exit successfully')
231
+
232
+ const response = JSON.parse(result.stdout)
233
+ validateJsonApiStructure(response, true)
234
+
235
+ assert.equal(response.data.length, 1, 'Should filter to one country')
236
+ assert.equal(response.data[0].attributes.name, 'Another Country')
237
+ })
239
238
 
240
239
  it('should handle includes using CURL', async () => {
241
240
  // Create publisher with country relationship
242
241
  const publisherDoc = createJsonApiDocument('publishers',
243
242
  { name: 'CURL Test Publisher' },
244
243
  { country: createRelationship(resourceIdentifier('countries', testData.country.id)) }
245
- );
244
+ )
246
245
  const publisherResult = await basicApi.resources.publishers.post({
247
246
  inputRecord: publisherDoc,
248
247
  simplified: false
249
- });
250
-
251
- const result = await executeCurl(['-s', '-H', 'Accept: application/vnd.api+json', `http://localhost:${TEST_PORT}/api/publishers/${publisherResult.data.id}?include=country`]);
252
-
253
- assert.equal(result.exitCode, 0, 'CURL should exit successfully');
254
-
255
- const response = JSON.parse(result.stdout);
256
- validateJsonApiStructure(response, false);
257
-
258
- assert.equal(response.data.type, 'publishers');
259
- assert(response.included, 'Should have included data');
260
- assert.equal(response.included.length, 1, 'Should include one resource');
261
- assert.equal(response.included[0].type, 'countries');
262
- assert.equal(response.included[0].id, testData.country.id);
263
- });
248
+ })
249
+
250
+ const result = await executeCurl(['-s', '-H', 'Accept: application/vnd.api+json', `http://localhost:${TEST_PORT}/api/publishers/${publisherResult.data.id}?include=country`])
251
+
252
+ assert.equal(result.exitCode, 0, 'CURL should exit successfully')
253
+
254
+ const response = JSON.parse(result.stdout)
255
+ validateJsonApiStructure(response, false)
256
+
257
+ assert.equal(response.data.type, 'publishers')
258
+ assert(response.included, 'Should have included data')
259
+ assert.equal(response.included.length, 1, 'Should include one resource')
260
+ assert.equal(response.included[0].type, 'countries')
261
+ assert.equal(response.included[0].id, testData.country.id)
262
+ })
264
263
 
265
264
  it('should handle errors using CURL', async () => {
266
265
  // Test 404 error
267
- const result404 = await executeCurl(['-s', '-w', '\\n%{http_code}', '-H', 'Accept: application/vnd.api+json', `http://localhost:${TEST_PORT}/api/countries/999999`]);
268
-
269
- assert.equal(result404.exitCode, 0, 'CURL should exit successfully');
270
-
271
- const lines = result404.stdout.trim().split('\n');
272
- const httpCode = lines[lines.length - 1];
273
- const responseBody = lines.slice(0, -1).join('\n');
274
-
275
- assert.equal(httpCode, '404', 'Should return 404 status');
276
-
277
- const response = JSON.parse(responseBody);
278
- assert(response.errors, 'Should have errors array');
279
- assert.equal(response.errors[0].status, '404');
280
- assert.equal(response.errors[0].title, 'Not Found');
266
+ const result404 = await executeCurl(['-s', '-w', '\\n%{http_code}', '-H', 'Accept: application/vnd.api+json', `http://localhost:${TEST_PORT}/api/countries/999999`])
267
+
268
+ assert.equal(result404.exitCode, 0, 'CURL should exit successfully')
269
+
270
+ const lines = result404.stdout.trim().split('\n')
271
+ const httpCode = lines[lines.length - 1]
272
+ const responseBody = lines.slice(0, -1).join('\n')
273
+
274
+ assert.equal(httpCode, '404', 'Should return 404 status')
275
+
276
+ const response = JSON.parse(responseBody)
277
+ assert(response.errors, 'Should have errors array')
278
+ assert.equal(response.errors[0].status, '404')
279
+ assert.equal(response.errors[0].title, 'Not Found')
281
280
 
282
281
  // Test validation error - omit required name field
283
- const invalidDoc = createJsonApiDocument('countries', { code: 'XX' }); // Missing required name
284
- const jsonPayload = JSON.stringify(invalidDoc);
285
-
286
- const resultValidation = await executeCurl(['-s', '-w', '\n%{http_code}', '-X', 'POST', '-H', 'Content-Type: application/vnd.api+json', '-H', 'Accept: application/vnd.api+json', '-d', jsonPayload, `http://localhost:${TEST_PORT}/api/countries`]);
287
-
288
- const validationLines = resultValidation.stdout.trim().split('\n');
289
- const validationCode = validationLines[validationLines.length - 1];
290
- const validationBody = validationLines.slice(0, -1).join('\n');
291
-
292
- assert.equal(validationCode, '422', 'Should return 422 for validation error');
293
-
294
- const validationResponse = JSON.parse(validationBody);
295
- assert(validationResponse.errors, 'Should have errors array');
296
- assert.equal(validationResponse.errors[0].status, '422');
297
- });
282
+ const invalidDoc = createJsonApiDocument('countries', { code: 'XX' }) // Missing required name
283
+ const jsonPayload = JSON.stringify(invalidDoc)
284
+
285
+ const resultValidation = await executeCurl(['-s', '-w', '\n%{http_code}', '-X', 'POST', '-H', 'Content-Type: application/vnd.api+json', '-H', 'Accept: application/vnd.api+json', '-d', jsonPayload, `http://localhost:${TEST_PORT}/api/countries`])
286
+
287
+ const validationLines = resultValidation.stdout.trim().split('\n')
288
+ const validationCode = validationLines[validationLines.length - 1]
289
+ const validationBody = validationLines.slice(0, -1).join('\n')
290
+
291
+ assert.equal(validationCode, '422', 'Should return 422 for validation error')
292
+
293
+ const validationResponse = JSON.parse(validationBody)
294
+ assert(validationResponse.errors, 'Should have errors array')
295
+ assert.equal(validationResponse.errors[0].status, '422')
296
+ })
298
297
 
299
298
  it('should handle different content types using CURL', async () => {
300
299
  // Test with application/json content type
301
- const newCountryDoc = createJsonApiDocument('countries', { name: 'JSON Country', code: 'JC' });
302
- const jsonPayload = JSON.stringify(newCountryDoc);
303
-
304
- const resultJson = await executeCurl(['-s', '-X', 'POST', '-H', 'Content-Type: application/json', '-H', 'Accept: application/vnd.api+json', '-d', jsonPayload, `http://localhost:${TEST_PORT}/api/countries`]);
305
-
306
- assert.equal(resultJson.exitCode, 0, 'CURL should exit successfully with application/json');
307
-
308
- const responseJson = JSON.parse(resultJson.stdout);
309
- validateJsonApiStructure(responseJson, false);
310
- assert.equal(responseJson.data.attributes.name, 'JSON Country');
300
+ const newCountryDoc = createJsonApiDocument('countries', { name: 'JSON Country', code: 'JC' })
301
+ const jsonPayload = JSON.stringify(newCountryDoc)
302
+
303
+ const resultJson = await executeCurl(['-s', '-X', 'POST', '-H', 'Content-Type: application/json', '-H', 'Accept: application/vnd.api+json', '-d', jsonPayload, `http://localhost:${TEST_PORT}/api/countries`])
304
+
305
+ assert.equal(resultJson.exitCode, 0, 'CURL should exit successfully with application/json')
306
+
307
+ const responseJson = JSON.parse(resultJson.stdout)
308
+ validateJsonApiStructure(responseJson, false)
309
+ assert.equal(responseJson.data.attributes.name, 'JSON Country')
311
310
 
312
311
  // Test with unsupported content type (should fail if strictContentType is enabled)
313
- const resultXml = await executeCurl(['-s', '-w', '\n%{http_code}', '-X', 'POST', '-H', 'Content-Type: application/xml', '-H', 'Accept: application/vnd.api+json', '-d', jsonPayload, `http://localhost:${TEST_PORT}/api/countries`]);
314
-
315
- const xmlLines = resultXml.stdout.trim().split('\n');
316
- const xmlCode = xmlLines[xmlLines.length - 1];
317
-
318
- assert.equal(xmlCode, '415', 'Should return 415 Unsupported Media Type');
319
- });
312
+ const resultXml = await executeCurl(['-s', '-w', '\n%{http_code}', '-X', 'POST', '-H', 'Content-Type: application/xml', '-H', 'Accept: application/vnd.api+json', '-d', jsonPayload, `http://localhost:${TEST_PORT}/api/countries`])
313
+
314
+ const xmlLines = resultXml.stdout.trim().split('\n')
315
+ const xmlCode = xmlLines[xmlLines.length - 1]
316
+
317
+ assert.equal(xmlCode, '415', 'Should return 415 Unsupported Media Type')
318
+ })
320
319
 
321
320
  it('should handle headers correctly using CURL', async () => {
322
321
  // Test with custom headers (-v outputs to stderr)
323
- const result = await executeCurl(['-s', '-H', 'Accept: application/vnd.api+json', '-H', 'X-Custom-Header: test-value', '-H', 'User-Agent: CURL-Test/1.0', '-v', `http://localhost:${TEST_PORT}/api/countries/${testData.country.id}`]);
324
-
325
- assert.equal(result.exitCode, 0, 'CURL should exit successfully');
326
-
322
+ const result = await executeCurl(['-s', '-H', 'Accept: application/vnd.api+json', '-H', 'X-Custom-Header: test-value', '-H', 'User-Agent: CURL-Test/1.0', '-v', `http://localhost:${TEST_PORT}/api/countries/${testData.country.id}`])
323
+
324
+ assert.equal(result.exitCode, 0, 'CURL should exit successfully')
325
+
327
326
  // Check that headers were sent (visible in verbose output on stderr)
328
- assert(result.stderr.includes('X-Custom-Header: test-value'), 'Should send custom header');
329
- assert(result.stderr.includes('User-Agent: CURL-Test/1.0'), 'Should send custom user agent');
330
- });
331
- });
327
+ assert(result.stderr.includes('X-Custom-Header: test-value'), 'Should send custom header')
328
+ assert(result.stderr.includes('User-Agent: CURL-Test/1.0'), 'Should send custom user agent')
329
+ })
330
+ })
332
331
 
333
332
  describe('Complex CURL Operations', () => {
334
- let testData = {};
333
+ const testData = {}
335
334
 
336
335
  beforeEach(async () => {
337
336
  await cleanTables(knex, [
338
337
  'basic_countries', 'basic_publishers', 'basic_authors', 'basic_books', 'basic_book_authors'
339
- ]);
338
+ ])
340
339
 
341
340
  // Create complex test data
342
- const countryDoc = createJsonApiDocument('countries', { name: 'Complex Country', code: 'CC' });
341
+ const countryDoc = createJsonApiDocument('countries', { name: 'Complex Country', code: 'CC' })
343
342
  const countryResult = await basicApi.resources.countries.post({
344
343
  inputRecord: countryDoc,
345
344
  simplified: false
346
- });
347
- testData.country = countryResult.data;
345
+ })
346
+ testData.country = countryResult.data
348
347
 
349
348
  const publisherDoc = createJsonApiDocument('publishers',
350
349
  { name: 'Complex Publisher' },
351
350
  { country: createRelationship(resourceIdentifier('countries', testData.country.id)) }
352
- );
351
+ )
353
352
  const publisherResult = await basicApi.resources.publishers.post({
354
353
  inputRecord: publisherDoc,
355
354
  simplified: false
356
- });
357
- testData.publisher = publisherResult.data;
355
+ })
356
+ testData.publisher = publisherResult.data
358
357
 
359
- const author1Doc = createJsonApiDocument('authors', { name: 'Complex Author One' });
360
- const author2Doc = createJsonApiDocument('authors', { name: 'Complex Author Two' });
358
+ const author1Doc = createJsonApiDocument('authors', { name: 'Complex Author One' })
359
+ const author2Doc = createJsonApiDocument('authors', { name: 'Complex Author Two' })
361
360
  const author1Result = await basicApi.resources.authors.post({
362
361
  inputRecord: author1Doc,
363
362
  simplified: false
364
- });
363
+ })
365
364
  const author2Result = await basicApi.resources.authors.post({
366
365
  inputRecord: author2Doc,
367
366
  simplified: false
368
- });
369
- testData.authors = [author1Result.data, author2Result.data];
367
+ })
368
+ testData.authors = [author1Result.data, author2Result.data]
370
369
 
371
370
  const bookDoc = createJsonApiDocument('books',
372
371
  { title: 'Complex Book' },
@@ -378,33 +377,33 @@ describe('CURL HTTP Abstraction Layer Tests', () => {
378
377
  resourceIdentifier('authors', testData.authors[1].id)
379
378
  ])
380
379
  }
381
- );
380
+ )
382
381
  const bookResult = await basicApi.resources.books.post({
383
382
  inputRecord: bookDoc,
384
383
  simplified: false
385
- });
386
- testData.book = bookResult.data;
387
- });
384
+ })
385
+ testData.book = bookResult.data
386
+ })
388
387
 
389
388
  it('should handle nested includes using CURL', async () => {
390
- const result = await executeCurl(['-s', '-H', 'Accept: application/vnd.api+json', `http://localhost:${TEST_PORT}/api/books/${testData.book.id}?include=publisher.country,authors`]);
391
-
392
- assert.equal(result.exitCode, 0, 'CURL should exit successfully');
393
-
394
- const response = JSON.parse(result.stdout);
395
- validateJsonApiStructure(response, false);
396
-
397
- assert.equal(response.data.type, 'books');
398
- assert(response.included, 'Should have included data');
399
-
389
+ const result = await executeCurl(['-s', '-H', 'Accept: application/vnd.api+json', `http://localhost:${TEST_PORT}/api/books/${testData.book.id}?include=publisher.country,authors`])
390
+
391
+ assert.equal(result.exitCode, 0, 'CURL should exit successfully')
392
+
393
+ const response = JSON.parse(result.stdout)
394
+ validateJsonApiStructure(response, false)
395
+
396
+ assert.equal(response.data.type, 'books')
397
+ assert(response.included, 'Should have included data')
398
+
400
399
  // Should include publisher, country, and 2 authors
401
- assert.equal(response.included.length, 4, 'Should include 4 resources');
402
-
403
- const includedTypes = response.included.map(r => r.type);
404
- assert(includedTypes.includes('publishers'), 'Should include publisher');
405
- assert(includedTypes.includes('countries'), 'Should include country');
406
- assert.equal(includedTypes.filter(t => t === 'authors').length, 2, 'Should include 2 authors');
407
- });
400
+ assert.equal(response.included.length, 4, 'Should include 4 resources')
401
+
402
+ const includedTypes = response.included.map(r => r.type)
403
+ assert(includedTypes.includes('publishers'), 'Should include publisher')
404
+ assert(includedTypes.includes('countries'), 'Should include country')
405
+ assert.equal(includedTypes.filter(t => t === 'authors').length, 2, 'Should include 2 authors')
406
+ })
408
407
 
409
408
  it('should handle pagination using CURL', async () => {
410
409
  // Create multiple books
@@ -418,51 +417,51 @@ describe('CURL HTTP Abstraction Layer Tests', () => {
418
417
  }
419
418
  ),
420
419
  simplified: false
421
- });
420
+ })
422
421
  }
423
422
 
424
423
  // Test pagination (URL encode square brackets)
425
- const result = await executeCurl(['-s', '-H', 'Accept: application/vnd.api+json', `http://localhost:${TEST_PORT}/api/books?page%5Bsize%5D=3&page%5Bnumber%5D=1`]);
426
-
427
- assert.equal(result.exitCode, 0, 'CURL should exit successfully');
428
-
429
- const response = JSON.parse(result.stdout);
430
- validateJsonApiStructure(response, true);
431
-
432
- assert.equal(response.data.length, 3, 'Should return 3 books per page');
424
+ const result = await executeCurl(['-s', '-H', 'Accept: application/vnd.api+json', `http://localhost:${TEST_PORT}/api/books?page%5Bsize%5D=3&page%5Bnumber%5D=1`])
425
+
426
+ assert.equal(result.exitCode, 0, 'CURL should exit successfully')
427
+
428
+ const response = JSON.parse(result.stdout)
429
+ validateJsonApiStructure(response, true)
430
+
431
+ assert.equal(response.data.length, 3, 'Should return 3 books per page')
433
432
  // Note: Links are now always included, using relative URLs whenreturnBasePath isn't configured
434
- });
433
+ })
435
434
 
436
435
  it('should handle sorting using CURL', async () => {
437
- const result = await executeCurl(['-s', '-H', 'Accept: application/vnd.api+json', `http://localhost:${TEST_PORT}/api/books?sort=-title`]);
438
-
439
- assert.equal(result.exitCode, 0, 'CURL should exit successfully');
440
-
441
- const response = JSON.parse(result.stdout);
442
- validateJsonApiStructure(response, true);
443
-
436
+ const result = await executeCurl(['-s', '-H', 'Accept: application/vnd.api+json', `http://localhost:${TEST_PORT}/api/books?sort=-title`])
437
+
438
+ assert.equal(result.exitCode, 0, 'CURL should exit successfully')
439
+
440
+ const response = JSON.parse(result.stdout)
441
+ validateJsonApiStructure(response, true)
442
+
444
443
  // Books should be sorted by title descending
445
- const titles = response.data.map(book => book.attributes.title);
446
- const sortedTitles = [...titles].sort().reverse();
447
- assert.deepEqual(titles, sortedTitles, 'Books should be sorted by title descending');
448
- });
444
+ const titles = response.data.map(book => book.attributes.title)
445
+ const sortedTitles = [...titles].sort().reverse()
446
+ assert.deepEqual(titles, sortedTitles, 'Books should be sorted by title descending')
447
+ })
449
448
 
450
449
  it('should handle sparse fieldsets using CURL', async () => {
451
- const result = await executeCurl(['-s', '-H', 'Accept: application/vnd.api+json', `http://localhost:${TEST_PORT}/api/books/${testData.book.id}?fields%5Bbooks%5D=title&fields%5Bpublishers%5D=name&include=publisher`]);
452
-
453
- assert.equal(result.exitCode, 0, 'CURL should exit successfully');
454
-
455
- const response = JSON.parse(result.stdout);
456
- validateJsonApiStructure(response, false);
457
-
450
+ const result = await executeCurl(['-s', '-H', 'Accept: application/vnd.api+json', `http://localhost:${TEST_PORT}/api/books/${testData.book.id}?fields%5Bbooks%5D=title&fields%5Bpublishers%5D=name&include=publisher`])
451
+
452
+ assert.equal(result.exitCode, 0, 'CURL should exit successfully')
453
+
454
+ const response = JSON.parse(result.stdout)
455
+ validateJsonApiStructure(response, false)
456
+
458
457
  // Main resource should only have title attribute
459
- assert.equal(Object.keys(response.data.attributes).length, 1, 'Should only have title attribute');
460
- assert(response.data.attributes.title, 'Should have title');
461
-
458
+ assert.equal(Object.keys(response.data.attributes).length, 1, 'Should only have title attribute')
459
+ assert(response.data.attributes.title, 'Should have title')
460
+
462
461
  // Included publisher should only have name attribute
463
- const publisher = response.included.find(r => r.type === 'publishers');
464
- assert.equal(Object.keys(publisher.attributes).length, 1, 'Publisher should only have name attribute');
465
- assert(publisher.attributes.name, 'Publisher should have name');
466
- });
467
- });
468
- });
462
+ const publisher = response.included.find(r => r.type === 'publishers')
463
+ assert.equal(Object.keys(publisher.attributes).length, 1, 'Publisher should only have name attribute')
464
+ assert(publisher.attributes.name, 'Publisher should have name')
465
+ })
466
+ })
467
+ })