json-rest-api 1.0.10 → 1.0.13

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 (117) hide show
  1. package/.claude/settings.local.json +9 -1
  2. package/AGENTS.md +34 -0
  3. package/CLAUDE.md +26 -1
  4. package/CRUSH.md +55 -0
  5. package/README.md +3 -3
  6. package/TODO.todo +8 -0
  7. package/docs/API.md +17 -18
  8. package/docs/COMPARISON.md +18 -13
  9. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +17 -21
  10. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +33 -2
  11. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +61 -7
  12. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +58 -61
  13. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +37 -37
  14. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +103 -87
  15. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +37 -34
  16. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +45 -42
  17. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +3 -3
  18. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +1 -1
  19. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -5
  20. package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +498 -755
  21. package/docs/GUIDE/GUIDE_6_Positioning.md +1 -3
  22. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +4 -5
  23. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +15 -6
  24. package/docs/GUIDE/GUIDE_X_Cors.md +2 -5
  25. package/docs/GUIDE/GUIDE_X_File_Uploads.md +2 -3
  26. package/docs/GUIDE/GUIDE_X_Multihome.md +1 -4
  27. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -2
  28. package/docs/GUIDE/GUIDE_X_URL_Management.md +317 -0
  29. package/docs/GUIDE/index.md +3 -0
  30. package/docs/QUICKSTART.md +12 -12
  31. package/docs/index.md +2 -2
  32. package/docs/mini-guides/file-uploads-complete.md +0 -1
  33. package/examples/url-override-example.js +137 -0
  34. package/index.js +7 -1
  35. package/package.json +7 -11
  36. package/plugins/core/bulk-operations-plugin.js +2 -0
  37. package/plugins/core/connectors/express-plugin.js +43 -24
  38. package/plugins/core/connectors/lib/request-helpers.js +2 -2
  39. package/plugins/core/lib/anyapi/anyapi-registry.js +821 -0
  40. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +647 -0
  41. package/plugins/core/lib/anyapi/schema-utils.js +186 -0
  42. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +113 -0
  43. package/plugins/core/lib/dbTablesOperations.js +305 -0
  44. package/plugins/core/lib/querying/knex-cross-table-search.js +64 -108
  45. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +21 -27
  46. package/plugins/core/lib/querying/knex-pagination-helpers.js +36 -20
  47. package/plugins/core/lib/querying/knex-query-helpers-base.js +49 -14
  48. package/plugins/core/lib/querying/knex-query-helpers.js +378 -222
  49. package/plugins/core/lib/querying/knex-relationship-includes.js +262 -87
  50. package/plugins/core/lib/querying/url-helpers.js +119 -0
  51. package/plugins/core/lib/querying-writing/compile-schemas.js +36 -28
  52. package/plugins/core/lib/querying-writing/database-capabilities.js +10 -9
  53. package/plugins/core/lib/querying-writing/knex-field-helpers.js +9 -3
  54. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +2 -2
  55. package/plugins/core/lib/querying-writing/payload-validators.js +10 -16
  56. package/plugins/core/lib/querying-writing/schema-helpers.js +52 -60
  57. package/plugins/core/lib/querying-writing/scope-validations.js +78 -22
  58. package/plugins/core/lib/querying-writing/simplified-helpers.js +208 -61
  59. package/plugins/core/lib/storage/storage-adapter.js +160 -0
  60. package/plugins/core/lib/writing/relationship-processor.js +4 -11
  61. package/plugins/core/rest-api-access.js +456 -0
  62. package/plugins/core/rest-api-anyapi-knex-plugin.js +2285 -0
  63. package/plugins/core/rest-api-knex-plugin.js +226 -87
  64. package/plugins/core/rest-api-multihome-plugin.js +89 -27
  65. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +0 -1
  66. package/plugins/core/rest-api-plugin-methods/check-permissions.js +1 -5
  67. package/plugins/core/rest-api-plugin-methods/common.js +46 -10
  68. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +31 -27
  69. package/plugins/core/rest-api-plugin-methods/delete.js +1 -4
  70. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +13 -1
  71. package/plugins/core/rest-api-plugin-methods/get-related.js +114 -56
  72. package/plugins/core/rest-api-plugin-methods/get-relationship.js +4 -3
  73. package/plugins/core/rest-api-plugin-methods/get.js +2 -5
  74. package/plugins/core/rest-api-plugin-methods/patch.js +15 -16
  75. package/plugins/core/rest-api-plugin-methods/post-relationship.js +21 -14
  76. package/plugins/core/rest-api-plugin-methods/post.js +23 -11
  77. package/plugins/core/rest-api-plugin-methods/put.js +18 -21
  78. package/plugins/core/rest-api-plugin-methods/query.js +3 -4
  79. package/plugins/core/rest-api-plugin-methods/release.js +13 -0
  80. package/plugins/core/rest-api-plugin.js +3 -4
  81. package/plugins/core/rest-api-positioning-plugin.js +253 -76
  82. package/plugins/core/socketio-plugin.js +436 -502
  83. package/quickTest.js +2 -2
  84. package/scripts/debug-anyapi-review-filter.mjs +63 -0
  85. package/scripts/inspect-anyapi-descriptor-country.mjs +19 -0
  86. package/scripts/inspect-anyapi-descriptor.mjs +19 -0
  87. package/test-debug.js +59 -0
  88. package/tests/README.md +7 -1
  89. package/tests/anyapi-basic.test.js +102 -0
  90. package/tests/anyapi-cursor-pagination.test.js +144 -0
  91. package/tests/computed-fields.test.js +27 -12
  92. package/tests/cors.test.js +5 -6
  93. package/tests/curl.test.js +1 -1
  94. package/tests/custom-idproperty-relationships.test.js +37 -29
  95. package/tests/field-getters.test.js +25 -7
  96. package/tests/fixtures/api-configs.js +576 -150
  97. package/tests/helpers/storage-mode.js +65 -0
  98. package/tests/helpers/test-utils.js +85 -3
  99. package/tests/include-depth-validation.test.js +35 -1
  100. package/tests/nested-includes.test.js +130 -103
  101. package/tests/pagination-cursor-multifield.test.js +17 -13
  102. package/tests/pagination-enhanced.test.js +19 -24
  103. package/tests/polymorphic-simple.test.js +9 -9
  104. package/tests/positioning-advanced.test.js +19 -19
  105. package/tests/positioning-extended.test.js +25 -25
  106. package/tests/positioning-multifilter.test.js +62 -62
  107. package/tests/positioning.test.js +7 -7
  108. package/tests/queries.test.js +5 -5
  109. package/tests/query-limits.test.js +77 -92
  110. package/tests/rest-api-access.test.js +206 -0
  111. package/tests/searchschema-merge.test.js +182 -0
  112. package/tests/socketio.test.js +33 -14
  113. package/tests/virtual-fields.test.js +0 -1
  114. package/docs/DOCS_ALL.md +0 -21894
  115. package/plugins/core/jwt-auth-plugin.js +0 -897
  116. package/plugins/core/lib/jwt-auth-helpers.js +0 -130
  117. package/tests/jwt-auth.test.js +0 -692
@@ -167,16 +167,18 @@ export function ensureSearchFieldsAreIndexed(searchSchema) {
167
167
  * // }
168
168
  *
169
169
  * @example
170
- * // Input: Conflict detection
170
+ * // Input: Explicit searchSchema takes precedence
171
171
  * const schema = {
172
172
  * email: { type: 'string', search: true }
173
173
  * };
174
174
  * const explicitSearchSchema = {
175
175
  * email: { type: 'string', filterOperator: 'contains' }
176
176
  * };
177
- * generateSearchSchemaFromSchema(schema, explicitSearchSchema);
178
- * // Throws: RestApiValidationError
179
- * // "Field 'email' is defined in both schema (with search: true) and explicit searchSchema."
177
+ * const result = generateSearchSchemaFromSchema(schema, explicitSearchSchema);
178
+ * // Output: Explicit searchSchema wins
179
+ * // {
180
+ * // email: { type: 'string', filterOperator: 'contains' } // Uses explicit definition
181
+ * // }
180
182
  *
181
183
  * @example
182
184
  * // Input: Virtual search fields
@@ -215,15 +217,16 @@ export function ensureSearchFieldsAreIndexed(searchSchema) {
215
217
  * - Provides flexible search configuration options
216
218
  * - Enables virtual fields for cross-table searches
217
219
  * - Supports range queries (before/after patterns)
218
- * - Validates no duplicate definitions exist
220
+ * - Merges search:true fields with explicit searchSchema
221
+ * - Explicit searchSchema always takes precedence
219
222
  * - Allows storage plugins to optimize queries
220
223
  *
221
224
  * Data flow:
222
225
  * 1. Starts with explicit searchSchema (if any)
223
226
  * 2. Processes schema fields with 'search' property
224
- * 3. Handles multiple filters from single field
225
- * 4. Processes virtual search definitions
226
- * 5. Detects and throws on conflicts
227
+ * 3. Skips fields already in explicit searchSchema (no conflicts)
228
+ * 4. Handles multiple filters from single field
229
+ * 5. Processes virtual search definitions
227
230
  * 6. Returns merged searchSchema or null
228
231
  */
229
232
  export const generateSearchSchemaFromSchema = (schema, explicitSearchSchema) => {
@@ -240,28 +243,32 @@ export const generateSearchSchemaFromSchema = (schema, explicitSearchSchema) =>
240
243
 
241
244
  if (effectiveSearch) {
242
245
  if (effectiveSearch === true) {
243
- // Check for conflicts with explicit searchSchema
244
- if (searchSchema[fieldName]) {
245
- throw new RestApiValidationError(
246
- `Field '${fieldName}' is defined in both schema (with search: true) and explicit searchSchema. Remove one definition to avoid conflicts.`,
247
- {
248
- fields: [fieldName],
249
- violations: [{
250
- field: fieldName,
251
- rule: 'duplicate_search_field',
252
- message: 'Field cannot be defined in both schema and searchSchema'
253
- }]
254
- }
255
- );
246
+ // For belongsTo relationships, use the relationship name (as property) instead of the field name
247
+ const searchFieldName = (fieldDef.belongsTo && fieldDef.as) ? fieldDef.as : fieldName;
248
+
249
+ // Check if field already exists in explicit searchSchema
250
+ if (searchSchema[searchFieldName]) {
251
+ // Skip - explicit searchSchema takes precedence
252
+ // This allows searchSchema to override fields marked with search:true
253
+ return;
256
254
  }
257
255
 
258
256
  // Simple boolean - copy entire field definition (except search) and add filterOperator
259
257
  const { search, ...fieldDefWithoutSearch } = fieldDef;
260
- searchSchema[fieldName] = {
258
+ const searchEntry = {
261
259
  ...fieldDefWithoutSearch,
262
260
  // Preserve existing filterOperator or default to '='
263
261
  filterOperator: fieldDefWithoutSearch.filterOperator || '='
264
262
  };
263
+
264
+ // For belongsTo relationships, add metadata to map back to the actual field
265
+ if (fieldDef.belongsTo && fieldDef.as) {
266
+ searchEntry.actualField = fieldName; // Maps to the DB column
267
+ searchEntry.isRelationship = true;
268
+ searchEntry.targetResource = fieldDef.belongsTo;
269
+ }
270
+
271
+ searchSchema[searchFieldName] = searchEntry;
265
272
  } else if (typeof effectiveSearch === 'object') {
266
273
  // Check if search defines multiple filter fields
267
274
  const hasNestedFilters = Object.values(effectiveSearch).some(
@@ -271,19 +278,10 @@ export const generateSearchSchemaFromSchema = (schema, explicitSearchSchema) =>
271
278
  if (hasNestedFilters) {
272
279
  // Multiple filters from one field (like published_after/before)
273
280
  Object.entries(effectiveSearch).forEach(([filterName, filterDef]) => {
274
- // Check for conflicts
281
+ // Check if filter already exists in explicit searchSchema
275
282
  if (searchSchema[filterName]) {
276
- throw new RestApiValidationError(
277
- `Field '${filterName}' is defined in both schema (with search) and explicit searchSchema. Remove one definition to avoid conflicts.`,
278
- {
279
- fields: [filterName],
280
- violations: [{
281
- field: filterName,
282
- rule: 'duplicate_search_field',
283
- message: 'Field cannot be defined in both schema and searchSchema'
284
- }]
285
- }
286
- );
283
+ // Skip - explicit searchSchema takes precedence
284
+ return;
287
285
  }
288
286
 
289
287
  searchSchema[filterName] = {
@@ -293,26 +291,29 @@ export const generateSearchSchemaFromSchema = (schema, explicitSearchSchema) =>
293
291
  };
294
292
  });
295
293
  } else {
296
- // Check for conflicts
297
- if (searchSchema[fieldName]) {
298
- throw new RestApiValidationError(
299
- `Field '${fieldName}' is defined in both schema (with search) and explicit searchSchema. Remove one definition to avoid conflicts.`,
300
- {
301
- fields: [fieldName],
302
- violations: [{
303
- field: fieldName,
304
- rule: 'duplicate_search_field',
305
- message: 'Field cannot be defined in both schema and searchSchema'
306
- }]
307
- }
308
- );
294
+ // For belongsTo relationships, use the relationship name (as property) instead of the field name
295
+ const searchFieldName = (fieldDef.belongsTo && fieldDef.as) ? fieldDef.as : fieldName;
296
+
297
+ // Check if field already exists in explicit searchSchema
298
+ if (searchSchema[searchFieldName]) {
299
+ // Skip - explicit searchSchema takes precedence
300
+ return;
309
301
  }
310
302
 
311
303
  // Single filter with config
312
- searchSchema[fieldName] = {
304
+ const searchEntry = {
313
305
  type: fieldDef.type,
314
306
  ...effectiveSearch
315
307
  };
308
+
309
+ // For belongsTo relationships, add metadata to map back to the actual field
310
+ if (fieldDef.belongsTo && fieldDef.as) {
311
+ searchEntry.actualField = fieldName; // Maps to the DB column
312
+ searchEntry.isRelationship = true;
313
+ searchEntry.targetResource = fieldDef.belongsTo;
314
+ }
315
+
316
+ searchSchema[searchFieldName] = searchEntry;
316
317
  }
317
318
  }
318
319
  }
@@ -321,19 +322,10 @@ export const generateSearchSchemaFromSchema = (schema, explicitSearchSchema) =>
321
322
  // Handle _virtual search definitions
322
323
  if (schema._virtual?.search) {
323
324
  Object.entries(schema._virtual.search).forEach(([filterName, filterDef]) => {
324
- // Check for conflicts
325
+ // Check if filter already exists in explicit searchSchema
325
326
  if (searchSchema[filterName]) {
326
- throw new RestApiValidationError(
327
- `Field '${filterName}' is defined in both schema (_virtual.search) and explicit searchSchema. Remove one definition to avoid conflicts.`,
328
- {
329
- fields: [filterName],
330
- violations: [{
331
- field: filterName,
332
- rule: 'duplicate_search_field',
333
- message: 'Field cannot be defined in both schema and searchSchema'
334
- }]
335
- }
336
- );
327
+ // Skip - explicit searchSchema takes precedence
328
+ return;
337
329
  }
338
330
 
339
331
  searchSchema[filterName] = filterDef;
@@ -74,7 +74,8 @@
74
74
  * scopeOptions: {
75
75
  * relationships: {
76
76
  * posts: {
77
- * hasMany: 'posts'
77
+ * type: 'hasMany',
78
+ * target: 'posts'
78
79
  * // Missing: foreignKey
79
80
  * }
80
81
  * }
@@ -154,8 +155,8 @@ export function validateRelationships({ context, scopes }) {
154
155
  }
155
156
  }
156
157
 
157
- // Validate hasMany relationships require foreignKey
158
- if (relDef.hasMany) {
158
+ // Validate based on relationship type
159
+ if (relDef.type === 'hasMany') {
159
160
  const validation = validateHasManyRelationship(relDef, relName, scopeName);
160
161
  if (!validation.valid) {
161
162
  throw new Error(
@@ -164,8 +165,16 @@ export function validateRelationships({ context, scopes }) {
164
165
  }
165
166
  }
166
167
 
167
- // Validate manyToMany relationships require both foreignKey and otherKey
168
- if (relDef.manyToMany) {
168
+ if (relDef.type === 'hasOne') {
169
+ const validation = validateHasOneRelationship(relDef, relName, scopeName);
170
+ if (!validation.valid) {
171
+ throw new Error(
172
+ `Invalid hasOne relationship '${relName}' in scope '${scopeName}': ${validation.error}`
173
+ );
174
+ }
175
+ }
176
+
177
+ if (relDef.type === 'manyToMany') {
169
178
  const validation = validateManyToManyRelationship(relDef, relName, scopeName);
170
179
  if (!validation.valid) {
171
180
  throw new Error(
@@ -290,7 +299,8 @@ const validatePolymorphicRelationship = (relDef, scopeName, scopes) => {
290
299
  * @example
291
300
  * // Input: Valid hasMany configuration
292
301
  * const relDef = {
293
- * hasMany: 'posts',
302
+ * type: 'hasMany',
303
+ * target: 'posts',
294
304
  * foreignKey: 'author_id' // Required!
295
305
  * };
296
306
  * validateHasManyRelationship(relDef, 'posts', 'users');
@@ -299,7 +309,8 @@ const validatePolymorphicRelationship = (relDef, scopeName, scopes) => {
299
309
  * @example
300
310
  * // Input: Missing required foreignKey
301
311
  * const relDef = {
302
- * hasMany: 'posts'
312
+ * type: 'hasMany',
313
+ * target: 'posts'
303
314
  * // Missing: foreignKey
304
315
  * };
305
316
  * validateHasManyRelationship(relDef, 'posts', 'users');
@@ -311,7 +322,8 @@ const validatePolymorphicRelationship = (relDef, scopeName, scopes) => {
311
322
  * @example
312
323
  * // Input: Polymorphic hasMany with 'via' (doesn't need foreignKey)
313
324
  * const relDef = {
314
- * hasMany: 'comments',
325
+ * type: 'hasMany',
326
+ * target: 'comments',
315
327
  * via: 'commentable' // Uses polymorphic relationship
316
328
  * };
317
329
  * validateHasManyRelationship(relDef, 'comments', 'posts');
@@ -320,6 +332,14 @@ const validatePolymorphicRelationship = (relDef, scopeName, scopes) => {
320
332
  * @private
321
333
  */
322
334
  const validateHasManyRelationship = (relDef, relName, scopeName) => {
335
+ // Validate target is specified
336
+ if (!relDef.target) {
337
+ return {
338
+ valid: false,
339
+ error: `hasMany relationship requires 'target' to be specified.`
340
+ };
341
+ }
342
+
323
343
  // Polymorphic hasMany relationships using 'via' don't need foreignKey
324
344
  // They use the polymorphic fields (typeField, idField) from the belongsToPolymorphic relationship
325
345
  if (relDef.via) {
@@ -340,11 +360,49 @@ const validateHasManyRelationship = (relDef, relName, scopeName) => {
340
360
  };
341
361
  }
342
362
 
343
- // If through is specified, also require otherKey for many-to-many
344
- if (relDef.through && !relDef.otherKey) {
363
+ return { valid: true };
364
+ };
365
+
366
+ /**
367
+ * Validates a hasOne relationship definition
368
+ *
369
+ * @param {Object} relDef - Relationship definition with hasOne
370
+ * @param {string} relName - Relationship name
371
+ * @param {string} scopeName - Scope being registered
372
+ * @returns {Object} Validation result {valid: boolean, error?: string}
373
+ *
374
+ * @example
375
+ * // Input: Valid hasOne configuration
376
+ * const relDef = {
377
+ * type: 'hasOne',
378
+ * target: 'profile',
379
+ * foreignKey: 'user_id' // Required!
380
+ * };
381
+ * validateHasOneRelationship(relDef, 'profile', 'users');
382
+ * // Output: { valid: true }
383
+ *
384
+ * @private
385
+ */
386
+ const validateHasOneRelationship = (relDef, relName, scopeName) => {
387
+ // Validate target is specified
388
+ if (!relDef.target) {
389
+ return {
390
+ valid: false,
391
+ error: `hasOne relationship requires 'target' to be specified.`
392
+ };
393
+ }
394
+
395
+ if (!relDef.foreignKey) {
396
+ return {
397
+ valid: false,
398
+ error: `hasOne relationship requires foreignKey to be specified. Add foreignKey: '<field_name>' to the relationship definition.`
399
+ };
400
+ }
401
+
402
+ if (typeof relDef.foreignKey !== 'string') {
345
403
  return {
346
404
  valid: false,
347
- error: `hasMany relationship with 'through' requires both foreignKey and otherKey to be specified`
405
+ error: `hasOne relationship foreignKey must be a string, got ${typeof relDef.foreignKey}`
348
406
  };
349
407
  }
350
408
 
@@ -362,11 +420,10 @@ const validateHasManyRelationship = (relDef, relName, scopeName) => {
362
420
  * @example
363
421
  * // Input: Valid manyToMany configuration
364
422
  * const relDef = {
365
- * manyToMany: {
366
- * through: 'article_tags', // Pivot table
367
- * foreignKey: 'article_id', // This scope's FK
368
- * otherKey: 'tag_id' // Other scope's FK
369
- * }
423
+ * type: 'manyToMany',
424
+ * through: 'article_tags', // Pivot table
425
+ * foreignKey: 'article_id', // This scope's FK
426
+ * otherKey: 'tag_id' // Other scope's FK
370
427
  * };
371
428
  * validateManyToManyRelationship(relDef, 'tags', 'articles');
372
429
  * // Output: { valid: true }
@@ -374,11 +431,10 @@ const validateHasManyRelationship = (relDef, relName, scopeName) => {
374
431
  * @example
375
432
  * // Input: Missing required fields
376
433
  * const relDef = {
377
- * manyToMany: {
378
- * through: 'article_tags',
379
- * foreignKey: 'article_id'
380
- * // Missing: otherKey
381
- * }
434
+ * type: 'manyToMany',
435
+ * through: 'article_tags',
436
+ * foreignKey: 'article_id'
437
+ * // Missing: otherKey
382
438
  * };
383
439
  * validateManyToManyRelationship(relDef, 'tags', 'articles');
384
440
  * // Output: {
@@ -389,7 +445,7 @@ const validateHasManyRelationship = (relDef, relName, scopeName) => {
389
445
  * @private
390
446
  */
391
447
  const validateManyToManyRelationship = (relDef, relName, scopeName) => {
392
- const { through, foreignKey, otherKey } = relDef.manyToMany;
448
+ const { through, foreignKey, otherKey } = relDef;
393
449
 
394
450
  if (!through) {
395
451
  return {