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,53 +1,53 @@
1
1
  /**
2
2
  * File Handling Plugin for JSON REST API
3
- *
3
+ *
4
4
  * This plugin provides automatic file upload handling based on schema definitions.
5
5
  * It works with any protocol connector (HTTP, Express, WebSocket, etc.) by using
6
6
  * a detector registry pattern.
7
- *
7
+ *
8
8
  * Features:
9
9
  * - Schema-driven: Detects file fields from type: 'file' in schemas
10
10
  * - Protocol-agnostic: Works with any connector that registers a detector
11
11
  * - Storage pluggable: Different fields can use different storage backends
12
12
  * - Zero configuration: Just define file fields in your schema
13
- *
13
+ *
14
14
  * Usage:
15
15
  * ```javascript
16
16
  * // 1. Define schema with file fields
17
17
  * const imageSchema = {
18
18
  * title: { type: 'string' },
19
- * uploadedImage: {
19
+ * uploadedImage: {
20
20
  * type: 'file',
21
21
  * storage: S3Storage,
22
22
  * accepts: ['image/jpeg', 'image/png']
23
23
  * }
24
24
  * };
25
- *
25
+ *
26
26
  * // 2. Use plugins (order matters - file-handling depends on rest-api)
27
27
  * api.use(RestApiPlugin);
28
28
  * api.use(FileHandlingPlugin);
29
29
  * api.use(ExpressPlugin); // Or any other connector
30
- *
30
+ *
31
31
  * // 3. Files are automatically handled!
32
32
  * ```
33
33
  */
34
34
 
35
- import { RestApiValidationError } from '../../lib/rest-api-errors.js';
35
+ import { RestApiValidationError } from '../../lib/rest-api-errors.js'
36
36
 
37
37
  export const FileHandlingPlugin = {
38
38
  name: 'file-handling',
39
39
  dependencies: ['rest-api'],
40
-
41
- install({ addHook, helpers, scopes, log, vars, on, api }) {
40
+
41
+ install ({ addHook, helpers, scopes, log, vars, on, api }) {
42
42
  // Track which scopes have file fields
43
- const fileScopes = new Map(); // scopeName -> fileField[]
44
-
43
+ const fileScopes = new Map() // scopeName -> fileField[]
44
+
45
45
  // Registry of file detectors from various protocols
46
- const detectorRegistry = [];
47
-
46
+ const detectorRegistry = []
47
+
48
48
  /**
49
49
  * Register a file detector from a protocol plugin
50
- *
50
+ *
51
51
  * @param {Object} detector - The detector object
52
52
  * @param {string} detector.name - Name of the detector (e.g., 'http-multipart')
53
53
  * @param {Function} detector.detect - Function to check if this detector applies
@@ -55,25 +55,25 @@ export const FileHandlingPlugin = {
55
55
  */
56
56
  api.rest.registerFileDetector = (detector) => {
57
57
  if (!detector || !detector.name || !detector.detect || !detector.parse) {
58
- throw new Error('File detector must have name, detect(), and parse() properties');
58
+ throw new Error('File detector must have name, detect(), and parse() properties')
59
59
  }
60
-
61
- detectorRegistry.push(detector);
62
- log.debug(`Registered file detector: ${detector.name}`);
63
- };
64
-
60
+
61
+ detectorRegistry.push(detector)
62
+ log.debug(`Registered file detector: ${detector.name}`)
63
+ }
64
+
65
65
  // Store detectors array for inspection
66
- api.rest.fileDetectors = detectorRegistry;
67
-
66
+ api.rest.fileDetectors = detectorRegistry
67
+
68
68
  /**
69
69
  * Analyze a scope's schema to find file fields
70
70
  */
71
71
  const analyzeScopeSchema = (scopeName, scopeOptions) => {
72
- const schema = scopeOptions?.schema;
73
- if (!schema) return;
74
-
75
- const fileFields = [];
76
-
72
+ const schema = scopeOptions?.schema
73
+ if (!schema) return
74
+
75
+ const fileFields = []
76
+
77
77
  // Look for fields with type: 'file'
78
78
  for (const [fieldName, fieldConfig] of Object.entries(schema)) {
79
79
  if (fieldConfig && fieldConfig.type === 'file') {
@@ -83,55 +83,55 @@ export const FileHandlingPlugin = {
83
83
  accepts: fieldConfig.accepts || ['*'],
84
84
  maxSize: fieldConfig.maxSize,
85
85
  required: fieldConfig.required || false
86
- });
86
+ })
87
87
  }
88
88
  }
89
-
89
+
90
90
  if (fileFields.length > 0) {
91
- fileScopes.set(scopeName, fileFields);
92
- log.info(`Scope '${scopeName}' has ${fileFields.length} file field(s): ${fileFields.map(f => f.field).join(', ')}`);
91
+ fileScopes.set(scopeName, fileFields)
92
+ log.info(`Scope '${scopeName}' has ${fileFields.length} file field(s): ${fileFields.map(f => f.field).join(', ')}`)
93
93
  }
94
- };
95
-
94
+ }
95
+
96
96
  // Analyze existing scopes
97
97
  for (const [scopeName, scope] of Object.entries(scopes)) {
98
98
  if (scope._scopeOptions) {
99
- analyzeScopeSchema(scopeName, scope._scopeOptions);
99
+ analyzeScopeSchema(scopeName, scope._scopeOptions)
100
100
  }
101
101
  }
102
-
102
+
103
103
  // Listen for new scopes being added
104
104
  addHook('scope:added', 'analyzeFileFields', {}, ({ context }) => {
105
- const { scopeName, scopeOptions } = context;
106
- analyzeScopeSchema(scopeName, scopeOptions);
107
- });
108
-
105
+ const { scopeName, scopeOptions } = context
106
+ analyzeScopeSchema(scopeName, scopeOptions)
107
+ })
108
+
109
109
  /**
110
110
  * Process files for a scope if it has file fields
111
111
  */
112
112
  const processFiles = async (scopeName, params, context) => {
113
- const fileFields = fileScopes.get(scopeName);
113
+ const fileFields = fileScopes.get(scopeName)
114
114
  if (!fileFields || fileFields.length === 0) {
115
- return; // This scope doesn't have file fields
115
+ return // This scope doesn't have file fields
116
116
  }
117
-
117
+
118
118
  // Try each detector to see if we have files
119
- let parsed = null;
120
- let detectorUsed = null;
121
-
119
+ let parsed = null
120
+ let detectorUsed = null
121
+
122
122
  for (const detector of detectorRegistry) {
123
123
  try {
124
124
  if (await detector.detect(params, context)) {
125
- log.debug(`Detector '${detector.name}' matched for scope '${scopeName}'`);
126
- parsed = await detector.parse(params, context);
127
- detectorUsed = detector.name;
128
- break;
125
+ log.debug(`Detector '${detector.name}' matched for scope '${scopeName}'`)
126
+ parsed = await detector.parse(params, context)
127
+ detectorUsed = detector.name
128
+ break
129
129
  }
130
130
  } catch (error) {
131
- log.warn(`Detector '${detector.name}' failed:`, error);
131
+ log.warn(`Detector '${detector.name}' failed:`, error)
132
132
  }
133
133
  }
134
-
134
+
135
135
  if (!parsed) {
136
136
  // No files detected - check if any were required
137
137
  for (const fieldConfig of fileFields) {
@@ -145,19 +145,19 @@ export const FileHandlingPlugin = {
145
145
  message: 'This field is required'
146
146
  }]
147
147
  }
148
- );
148
+ )
149
149
  }
150
150
  }
151
- return;
151
+ return
152
152
  }
153
-
154
- log.debug(`Processing files with detector '${detectorUsed}'`);
155
- const { fields, files } = parsed;
156
-
153
+
154
+ log.debug(`Processing files with detector '${detectorUsed}'`)
155
+ const { fields, files } = parsed
156
+
157
157
  // Process each file field defined in schema
158
158
  for (const fieldConfig of fileFields) {
159
- const file = files[fieldConfig.field];
160
-
159
+ const file = files[fieldConfig.field]
160
+
161
161
  if (!file) {
162
162
  if (fieldConfig.required) {
163
163
  throw new RestApiValidationError(
@@ -169,22 +169,22 @@ export const FileHandlingPlugin = {
169
169
  message: 'This field is required'
170
170
  }]
171
171
  }
172
- );
172
+ )
173
173
  }
174
- continue;
174
+ continue
175
175
  }
176
-
176
+
177
177
  // Validate mime type
178
178
  if (fieldConfig.accepts[0] !== '*') {
179
179
  const acceptable = fieldConfig.accepts.some(pattern => {
180
180
  if (pattern.endsWith('/*')) {
181
181
  // e.g., 'image/*'
182
- const prefix = pattern.slice(0, -2);
183
- return file.mimetype.startsWith(prefix + '/');
182
+ const prefix = pattern.slice(0, -2)
183
+ return file.mimetype.startsWith(prefix + '/')
184
184
  }
185
- return file.mimetype === pattern;
186
- });
187
-
185
+ return file.mimetype === pattern
186
+ })
187
+
188
188
  if (!acceptable) {
189
189
  throw new RestApiValidationError(
190
190
  `Invalid file type for field '${fieldConfig.field}'`,
@@ -195,13 +195,13 @@ export const FileHandlingPlugin = {
195
195
  message: `Expected ${fieldConfig.accepts.join(' or ')}, got ${file.mimetype}`
196
196
  }]
197
197
  }
198
- );
198
+ )
199
199
  }
200
200
  }
201
-
201
+
202
202
  // Validate file size
203
203
  if (fieldConfig.maxSize) {
204
- const maxBytes = parseSize(fieldConfig.maxSize);
204
+ const maxBytes = parseSize(fieldConfig.maxSize)
205
205
  if (file.size > maxBytes) {
206
206
  throw new RestApiValidationError(
207
207
  `File too large for field '${fieldConfig.field}'`,
@@ -212,29 +212,29 @@ export const FileHandlingPlugin = {
212
212
  message: `Maximum size is ${fieldConfig.maxSize}, got ${formatSize(file.size)}`
213
213
  }]
214
214
  }
215
- );
215
+ )
216
216
  }
217
217
  }
218
-
218
+
219
219
  // Upload to storage
220
220
  if (!fieldConfig.storage) {
221
- throw new Error(`No storage configured for file field '${fieldConfig.field}'`);
221
+ throw new Error(`No storage configured for file field '${fieldConfig.field}'`)
222
222
  }
223
-
223
+
224
224
  try {
225
- const storedUrl = await fieldConfig.storage.upload(file);
226
- fields[fieldConfig.field] = storedUrl;
227
- log.debug(`Uploaded file for field '${fieldConfig.field}' to: ${storedUrl}`);
225
+ const storedUrl = await fieldConfig.storage.upload(file)
226
+ fields[fieldConfig.field] = storedUrl
227
+ log.debug(`Uploaded file for field '${fieldConfig.field}' to: ${storedUrl}`)
228
228
  } catch (error) {
229
229
  // Cleanup if file has cleanup function
230
230
  if (file.cleanup) {
231
231
  try {
232
- await file.cleanup();
232
+ await file.cleanup()
233
233
  } catch (cleanupError) {
234
- log.warn(`Failed to cleanup file after upload error:`, cleanupError);
234
+ log.warn('Failed to cleanup file after upload error:', cleanupError)
235
235
  }
236
236
  }
237
-
237
+
238
238
  throw new RestApiValidationError(
239
239
  `Failed to upload file for field '${fieldConfig.field}': ${error.message}`,
240
240
  {
@@ -244,83 +244,83 @@ export const FileHandlingPlugin = {
244
244
  message: error.message
245
245
  }]
246
246
  }
247
- );
247
+ )
248
248
  }
249
249
  }
250
-
250
+
251
251
  // Replace inputRecord with processed data
252
252
  if (!params.inputRecord) {
253
- params.inputRecord = { data: { attributes: {} } };
253
+ params.inputRecord = { data: { attributes: {} } }
254
254
  }
255
255
  if (!params.inputRecord.data) {
256
- params.inputRecord.data = { attributes: {} };
256
+ params.inputRecord.data = { attributes: {} }
257
257
  }
258
258
  if (!params.inputRecord.data.attributes) {
259
- params.inputRecord.data.attributes = {};
259
+ params.inputRecord.data.attributes = {}
260
260
  }
261
-
261
+
262
262
  // Merge fields into attributes
263
- Object.assign(params.inputRecord.data.attributes, fields);
264
-
263
+ Object.assign(params.inputRecord.data.attributes, fields)
264
+
265
265
  // Cleanup any remaining temp files
266
266
  for (const file of Object.values(files)) {
267
267
  if (file.cleanup) {
268
268
  try {
269
- await file.cleanup();
269
+ await file.cleanup()
270
270
  } catch (error) {
271
- log.warn(`Failed to cleanup temp file:`, error);
271
+ log.warn('Failed to cleanup temp file:', error)
272
272
  }
273
273
  }
274
274
  }
275
- };
276
-
275
+ }
276
+
277
277
  /**
278
278
  * Hook into REST API methods to process files
279
279
  */
280
280
  addHook('beforeProcessing', 'processFiles', {}, async ({ context }) => {
281
- const method = context.method;
282
- const scopeName = context.scopeName;
283
- const params = context.params;
284
-
281
+ const method = context.method
282
+ const scopeName = context.scopeName
283
+ const params = context.params
284
+
285
285
  // Only process for mutation methods
286
286
  if (!['post', 'put', 'patch'].includes(method)) {
287
- return;
287
+ return
288
288
  }
289
-
289
+
290
290
  // Process files if this scope has file fields
291
- await processFiles(scopeName, params, context);
292
- });
293
-
294
- log.info('File handling plugin initialized successfully');
291
+ await processFiles(scopeName, params, context)
292
+ })
293
+
294
+ log.info('File handling plugin initialized successfully')
295
295
  }
296
- };
296
+ }
297
297
 
298
298
  /**
299
299
  * Parse size string to bytes
300
300
  * @param {string} size - Size string like '10mb', '1.5GB'
301
301
  * @returns {number} Size in bytes
302
302
  */
303
- function parseSize(size) {
303
+ function parseSize (size) {
304
304
  const units = {
305
305
  b: 1,
306
306
  kb: 1024,
307
307
  mb: 1024 * 1024,
308
308
  gb: 1024 * 1024 * 1024
309
- };
310
-
311
- const match = size.toLowerCase().match(/^(\d+(?:\.\d+)?)\s*([a-z]+)$/);
309
+ }
310
+
311
+ const match = size.toLowerCase().match(/^(\d+(?:\.\d+)?)\s*([a-z]+)$/)
312
312
  if (!match) {
313
- throw new Error(`Invalid size format: ${size}`);
313
+ throw new Error(`Invalid size format: ${size}`)
314
314
  }
315
-
316
- const [, num, unit] = match;
317
- const multiplier = units[unit];
318
-
315
+
316
+ const [, num, unit] = match
317
+ const multiplier = units[unit]
318
+
319
319
  if (!multiplier) {
320
- throw new Error(`Unknown size unit: ${unit}`);
320
+ throw new Error(`Unknown size unit: ${unit}`)
321
321
  }
322
-
323
- return parseFloat(num) * multiplier;
322
+
323
+ return parseFloat(num) * multiplier
324
324
  }
325
325
 
326
326
  /**
@@ -328,15 +328,15 @@ function parseSize(size) {
328
328
  * @param {number} bytes - Size in bytes
329
329
  * @returns {string} Human readable size
330
330
  */
331
- function formatSize(bytes) {
332
- const units = ['B', 'KB', 'MB', 'GB'];
333
- let size = bytes;
334
- let unitIndex = 0;
335
-
331
+ function formatSize (bytes) {
332
+ const units = ['B', 'KB', 'MB', 'GB']
333
+ let size = bytes
334
+ let unitIndex = 0
335
+
336
336
  while (size >= 1024 && unitIndex < units.length - 1) {
337
- size /= 1024;
338
- unitIndex++;
337
+ size /= 1024
338
+ unitIndex++
339
339
  }
340
-
341
- return `${size.toFixed(1)}${units[unitIndex]}`;
342
- }
340
+
341
+ return `${size.toFixed(1)}${units[unitIndex]}`
342
+ }