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,13 +1,13 @@
1
- import { RestApiResourceError } from '../../../../lib/rest-api-errors.js';
2
- import { transformSimplifiedToJsonApi } from '../querying-writing/simplified-helpers.js';
1
+ import { RestApiResourceError } from '../../../../lib/rest-api-errors.js'
2
+ import { transformSimplifiedToJsonApi } from '../querying-writing/simplified-helpers.js'
3
3
 
4
4
  /**
5
5
  * Updates many-to-many relationships intelligently by synchronizing pivot table records
6
- *
6
+ *
7
7
  * @param {Object} scope - The scope object (not used directly, for consistency)
8
8
  * @param {Object} deps - Dependencies object
9
9
  * @returns {Promise<void>}
10
- *
10
+ *
11
11
  * @example
12
12
  * // Input: Article currently has tags [1, 2], want to change to [1, 3]
13
13
  * const deps = {
@@ -26,29 +26,29 @@ import { transformSimplifiedToJsonApi } from '../querying-writing/simplified-hel
26
26
  * transaction: trx
27
27
  * }
28
28
  * };
29
- *
29
+ *
30
30
  * // Before: article_tags table
31
31
  * // article_id | tag_id | created_at
32
- * // 100 | 1 | 2024-01-01
32
+ * // 100 | 1 | 2024-01-01
33
33
  * // 100 | 2 | 2024-01-02
34
- *
34
+ *
35
35
  * await updateManyToManyRelationship(null, deps);
36
- *
37
- * // After: article_tags table
36
+ *
37
+ * // After: article_tags table
38
38
  * // article_id | tag_id | created_at
39
39
  * // 100 | 1 | 2024-01-01 (preserved!)
40
40
  * // 100 | 3 | 2024-12-01 (new)
41
41
  * // Tag 2 was deleted, Tag 1 kept its metadata
42
- *
42
+ *
43
43
  * @example
44
44
  * // Input: Pivot table has extra fields to preserve
45
45
  * // article_tags has: article_id, tag_id, display_order, featured
46
- *
46
+ *
47
47
  * // Current data:
48
48
  * // article_id | tag_id | display_order | featured
49
49
  * // 100 | 1 | 1 | true
50
50
  * // 100 | 2 | 2 | false
51
- *
51
+ *
52
52
  * const deps = {
53
53
  * context: {
54
54
  * resourceId: '100',
@@ -58,14 +58,14 @@ import { transformSimplifiedToJsonApi } from '../querying-writing/simplified-hel
58
58
  * ]
59
59
  * }
60
60
  * };
61
- *
61
+ *
62
62
  * await updateManyToManyRelationship(null, deps);
63
- *
63
+ *
64
64
  * // Result:
65
- * // article_id | tag_id | display_order | featured
65
+ * // article_id | tag_id | display_order | featured
66
66
  * // 100 | 1 | 1 | true (preserved!)
67
67
  * // 100 | 5 | NULL | NULL (new with defaults)
68
- *
68
+ *
69
69
  * @example
70
70
  * // Input: Clear all relationships
71
71
  * const deps = {
@@ -74,23 +74,23 @@ import { transformSimplifiedToJsonApi } from '../querying-writing/simplified-hel
74
74
  * relData: [] // Empty array means remove all
75
75
  * }
76
76
  * };
77
- *
77
+ *
78
78
  * await updateManyToManyRelationship(null, deps);
79
79
  * // All article_tags records for article 100 are deleted
80
- *
80
+ *
81
81
  * @description
82
82
  * Used by:
83
83
  * - relationship-processor.js calls this for many-to-many updates
84
84
  * - Used in PATCH operations to sync relationships
85
85
  * - Also used in PUT operations (replaces deleteExistingPivotRecords pattern)
86
- *
86
+ *
87
87
  * Purpose:
88
88
  * - Intelligently syncs pivot table to match desired state
89
89
  * - Preserves existing pivot records that should remain (with their metadata)
90
90
  * - Only deletes records that should be removed
91
91
  * - Only creates records that are new
92
92
  * - Much better than delete-all-then-recreate pattern
93
- *
93
+ *
94
94
  * Data flow:
95
95
  * 1. Queries existing pivot records for the resource
96
96
  * 2. Compares existing IDs with desired IDs
@@ -102,73 +102,73 @@ import { transformSimplifiedToJsonApi } from '../querying-writing/simplified-hel
102
102
  */
103
103
  export const updateManyToManyRelationship = async (scope, deps) => {
104
104
  // Extract values from deps
105
- const { api, context } = deps;
106
- const { resourceId, relDef, relData, transaction: trx } = context;
107
-
105
+ const { api, context } = deps
106
+ const { resourceId, relDef, relData, transaction: trx } = context
107
+
108
108
  // Get the knex instance from the pivot scope
109
- const pivotScope = api.resources[relDef.through];
109
+ const pivotScope = api.resources[relDef.through]
110
110
  if (!pivotScope) {
111
- throw new Error(`Pivot table resource '${relDef.through}' not found`);
111
+ throw new Error(`Pivot table resource '${relDef.through}' not found`)
112
112
  }
113
-
113
+
114
114
  // Get the actual database table name (might be different from scope name)
115
- const tableName = pivotScope.vars.schemaInfo.tableName || relDef.through;
116
-
115
+ const tableName = pivotScope.vars.schemaInfo.tableName || relDef.through
116
+
117
117
  // Get existing pivot records directly from database
118
118
  const existingRecords = await trx(tableName)
119
119
  .where(relDef.foreignKey, resourceId)
120
- .select(relDef.otherKey);
121
-
120
+ .select(relDef.otherKey)
121
+
122
122
  // Create sets for efficient comparison
123
- const existingIds = new Set(existingRecords.map(r => String(r[relDef.otherKey])));
124
- const newIds = new Set(relData.map(r => String(r.id)));
125
-
123
+ const existingIds = new Set(existingRecords.map(r => String(r[relDef.otherKey])))
124
+ const newIds = new Set(relData.map(r => String(r.id)))
125
+
126
126
  // Determine what to delete and add
127
- const toDelete = [...existingIds].filter(id => !newIds.has(id));
128
- const toAdd = [...newIds].filter(id => !existingIds.has(id));
129
-
127
+ const toDelete = [...existingIds].filter(id => !newIds.has(id))
128
+ const toAdd = [...newIds].filter(id => !existingIds.has(id))
129
+
130
130
  // Validate related resources exist if needed (do this before any changes)
131
131
  if (relDef.validateExists !== false && toAdd.length > 0) {
132
132
  for (const relatedId of toAdd) {
133
- const related = relData.find(r => String(r.id) === relatedId);
133
+ const related = relData.find(r => String(r.id) === relatedId)
134
134
  try {
135
135
  await api.resources[related.type].get({
136
136
  id: related.id,
137
137
  transaction: trx
138
- });
138
+ })
139
139
  } catch (error) {
140
140
  throw new RestApiResourceError(
141
141
  `Related ${related.type} with id ${related.id} not found`,
142
- {
142
+ {
143
143
  subtype: 'not_found',
144
- resourceType: related.type,
145
- resourceId: related.id
144
+ resourceType: related.type,
145
+ resourceId: related.id
146
146
  }
147
- );
147
+ )
148
148
  }
149
149
  }
150
150
  }
151
-
151
+
152
152
  // Bulk delete records that should be removed
153
153
  if (toDelete.length > 0) {
154
154
  await trx(tableName)
155
155
  .where(relDef.foreignKey, resourceId)
156
156
  .whereIn(relDef.otherKey, toDelete)
157
- .delete();
157
+ .delete()
158
158
  }
159
-
159
+
160
160
  // Bulk insert new records
161
161
  if (toAdd.length > 0) {
162
162
  const recordsToInsert = toAdd.map(relatedId => ({
163
163
  [relDef.foreignKey]: resourceId,
164
164
  [relDef.otherKey]: relatedId
165
- }));
166
-
167
- await trx(tableName).insert(recordsToInsert);
165
+ }))
166
+
167
+ await trx(tableName).insert(recordsToInsert)
168
168
  }
169
-
169
+
170
170
  // Records that exist in both are automatically preserved with their pivot data
171
- };
171
+ }
172
172
 
173
173
  // Note: deleteExistingPivotRecords has been removed in favor of using
174
174
  // updateManyToManyRelationship for all sync operations (including PUT).
@@ -177,14 +177,14 @@ export const updateManyToManyRelationship = async (scope, deps) => {
177
177
 
178
178
  /**
179
179
  * Creates new pivot table records for many-to-many relationships
180
- *
180
+ *
181
181
  * @param {Object} api - The API instance with access to resources
182
182
  * @param {string|number} resourceId - The ID of the primary resource
183
183
  * @param {Object} relDef - The relationship definition
184
184
  * @param {Array} relData - Array of related resources to link
185
185
  * @param {Object} trx - Database transaction object
186
186
  * @returns {Promise<void>}
187
- *
187
+ *
188
188
  * @example
189
189
  * // Input: Create article-tag relationships
190
190
  * const relDef = {
@@ -197,21 +197,21 @@ export const updateManyToManyRelationship = async (scope, deps) => {
197
197
  * { type: 'tags', id: '20' },
198
198
  * { type: 'tags', id: '30' }
199
199
  * ];
200
- *
200
+ *
201
201
  * await createPivotRecords(api, '100', relDef, relData, trx);
202
- *
202
+ *
203
203
  * // Result: 3 new records in article_tags table
204
204
  * // article_id | tag_id
205
205
  * // 100 | 10
206
- * // 100 | 20
206
+ * // 100 | 20
207
207
  * // 100 | 30
208
- *
208
+ *
209
209
  * @example
210
210
  * // Input: Validation ensures related resources exist
211
211
  * const relData = [
212
212
  * { type: 'tags', id: '999' } // Non-existent tag
213
213
  * ];
214
- *
214
+ *
215
215
  * try {
216
216
  * await createPivotRecords(api, '100', relDef, relData, trx);
217
217
  * } catch (error) {
@@ -219,35 +219,35 @@ export const updateManyToManyRelationship = async (scope, deps) => {
219
219
  * // "Related tags with id 999 not found"
220
220
  * // Transaction rolled back, no records created
221
221
  * }
222
- *
222
+ *
223
223
  * @example
224
224
  * // Input: Skip validation for performance
225
225
  * const relDef = {
226
226
  * through: 'user_permissions',
227
- * foreignKey: 'user_id',
227
+ * foreignKey: 'user_id',
228
228
  * otherKey: 'permission_id',
229
229
  * validateExists: false // Skip GET requests
230
230
  * };
231
- *
231
+ *
232
232
  * // With 100 permissions, saves 100 GET requests
233
233
  * await createPivotRecords(api, userId, relDef, permissions, trx);
234
- *
234
+ *
235
235
  * // Risk: Could create orphaned relationships if permissions don't exist
236
236
  * // Benefit: Much faster for bulk operations when you trust the data
237
- *
237
+ *
238
238
  * @description
239
239
  * Used by:
240
240
  * - relationship-processor.js for POST operations
241
241
  * - updateManyToManyRelationship internally for new relationships
242
242
  * - Any code that needs to create pivot records
243
- *
243
+ *
244
244
  * Purpose:
245
245
  * - Creates pivot table records to link resources
246
246
  * - Validates related resources exist by default (referential integrity)
247
247
  * - Supports bulk insert for efficiency
248
248
  * - Works within transactions for atomicity
249
249
  * - Allows skipping validation when performance matters
250
- *
250
+ *
251
251
  * Data flow:
252
252
  * 1. Validates pivot table resource exists
253
253
  * 2. Gets actual database table name
@@ -257,17 +257,17 @@ export const updateManyToManyRelationship = async (scope, deps) => {
257
257
  * 6. Returns (no data returned, throws on error)
258
258
  */
259
259
  export const createPivotRecords = async (api, resourceId, relDef, relData, trx) => {
260
- if (relData.length === 0) return; // Early exit if nothing to create
261
-
260
+ if (relData.length === 0) return // Early exit if nothing to create
261
+
262
262
  // Get pivot table info
263
- const pivotScope = api.resources[relDef.through];
263
+ const pivotScope = api.resources[relDef.through]
264
264
  if (!pivotScope) {
265
- throw new Error(`Pivot table resource '${relDef.through}' not found`);
265
+ throw new Error(`Pivot table resource '${relDef.through}' not found`)
266
266
  }
267
-
267
+
268
268
  // Get the actual database table name (might be different from scope name)
269
- const tableName = pivotScope.vars.schemaInfo.tableName || relDef.through;
270
-
269
+ const tableName = pivotScope.vars.schemaInfo.tableName || relDef.through
270
+
271
271
  // Validate all related resources exist if needed (do this before any inserts)
272
272
  if (relDef.validateExists !== false) {
273
273
  for (const related of relData) {
@@ -275,26 +275,26 @@ export const createPivotRecords = async (api, resourceId, relDef, relData, trx)
275
275
  await api.resources[related.type].get({
276
276
  id: related.id,
277
277
  transaction: trx
278
- });
278
+ })
279
279
  } catch (error) {
280
280
  throw new RestApiResourceError(
281
281
  `Related ${related.type} with id ${related.id} not found`,
282
- {
282
+ {
283
283
  subtype: 'not_found',
284
- resourceType: related.type,
285
- resourceId: related.id
284
+ resourceType: related.type,
285
+ resourceId: related.id
286
286
  }
287
- );
287
+ )
288
288
  }
289
289
  }
290
290
  }
291
-
291
+
292
292
  // Prepare records for bulk insert
293
293
  const recordsToInsert = relData.map(related => ({
294
294
  [relDef.foreignKey]: resourceId,
295
295
  [relDef.otherKey]: related.id
296
- }));
297
-
296
+ }))
297
+
298
298
  // Bulk insert all pivot records in a single query
299
- await trx(tableName).insert(recordsToInsert);
300
- };
299
+ await trx(tableName).insert(recordsToInsert)
300
+ }
@@ -1,12 +1,12 @@
1
- import { RestApiValidationError } from '../../../../lib/rest-api-errors.js';
1
+ import { RestApiValidationError } from '../../../../lib/rest-api-errors.js'
2
2
 
3
3
  /**
4
4
  * Processes JSON:API relationship data and converts to database operations
5
- *
5
+ *
6
6
  * @param {Object} scope - The scope object containing schema info
7
7
  * @param {Object} deps - Dependencies object
8
8
  * @returns {Object} Object with belongsToUpdates and manyToManyRelationships arrays
9
- *
9
+ *
10
10
  * @example
11
11
  * // Input: Simple belongsTo relationship
12
12
  * const inputRecord = {
@@ -23,13 +23,13 @@ import { RestApiValidationError } from '../../../../lib/rest-api-errors.js';
23
23
  * }
24
24
  * }
25
25
  * };
26
- *
26
+ *
27
27
  * // Schema has:
28
28
  * // author_id: { belongsTo: 'users', as: 'author' }
29
29
  * // category_id: { belongsTo: 'categories', as: 'category' }
30
- *
30
+ *
31
31
  * const result = processRelationships(scope, { context: { inputRecord } });
32
- *
32
+ *
33
33
  * // Output: Foreign keys extracted
34
34
  * // {
35
35
  * // belongsToUpdates: {
@@ -38,7 +38,7 @@ import { RestApiValidationError } from '../../../../lib/rest-api-errors.js';
38
38
  * // },
39
39
  * // manyToManyRelationships: []
40
40
  * // }
41
- *
41
+ *
42
42
  * @example
43
43
  * // Input: Polymorphic relationship
44
44
  * const inputRecord = {
@@ -51,7 +51,7 @@ import { RestApiValidationError } from '../../../../lib/rest-api-errors.js';
51
51
  * }
52
52
  * }
53
53
  * };
54
- *
54
+ *
55
55
  * // Schema relationships has:
56
56
  * // commentable: {
57
57
  * // belongsToPolymorphic: {
@@ -60,9 +60,9 @@ import { RestApiValidationError } from '../../../../lib/rest-api-errors.js';
60
60
  * // idField: 'commentable_id'
61
61
  * // }
62
62
  * // }
63
- *
63
+ *
64
64
  * const result = processRelationships(scope, { context: { inputRecord } });
65
- *
65
+ *
66
66
  * // Output: Both type and id fields set
67
67
  * // {
68
68
  * // belongsToUpdates: {
@@ -71,12 +71,12 @@ import { RestApiValidationError } from '../../../../lib/rest-api-errors.js';
71
71
  * // },
72
72
  * // manyToManyRelationships: []
73
73
  * // }
74
- *
74
+ *
75
75
  * @example
76
76
  * // Input: Many-to-many relationship
77
77
  * const inputRecord = {
78
78
  * data: {
79
- * type: 'articles',
79
+ * type: 'articles',
80
80
  * relationships: {
81
81
  * tags: {
82
82
  * data: [
@@ -87,7 +87,7 @@ import { RestApiValidationError } from '../../../../lib/rest-api-errors.js';
87
87
  * }
88
88
  * }
89
89
  * };
90
- *
90
+ *
91
91
  * // Schema relationships has:
92
92
  * // tags: {
93
93
  * // type: 'manyToMany',
@@ -95,9 +95,9 @@ import { RestApiValidationError } from '../../../../lib/rest-api-errors.js';
95
95
  * // foreignKey: 'article_id',
96
96
  * // otherKey: 'tag_id'
97
97
  * // }
98
- *
98
+ *
99
99
  * const result = processRelationships(scope, { context: { inputRecord } });
100
- *
100
+ *
101
101
  * // Output: Many-to-many data collected
102
102
  * // {
103
103
  * // belongsToUpdates: {},
@@ -114,7 +114,7 @@ import { RestApiValidationError } from '../../../../lib/rest-api-errors.js';
114
114
  * // ]
115
115
  * // }]
116
116
  * // }
117
- *
117
+ *
118
118
  * @example
119
119
  * // Input: Clearing relationships
120
120
  * const inputRecord = {
@@ -125,9 +125,9 @@ import { RestApiValidationError } from '../../../../lib/rest-api-errors.js';
125
125
  * }
126
126
  * }
127
127
  * };
128
- *
128
+ *
129
129
  * const result = processRelationships(scope, { context: { inputRecord } });
130
- *
130
+ *
131
131
  * // Output: Null for belongsTo, empty array for many-to-many
132
132
  * // {
133
133
  * // belongsToUpdates: { author_id: null },
@@ -137,18 +137,18 @@ import { RestApiValidationError } from '../../../../lib/rest-api-errors.js';
137
137
  * // relData: [] // Will delete all pivot records
138
138
  * // }]
139
139
  * // }
140
- *
140
+ *
141
141
  * @description
142
142
  * Used by:
143
143
  * - rest-api-knex-plugin's dataPut and dataPatch methods
144
144
  * - Called before database writes to prepare relationship updates
145
- *
145
+ *
146
146
  * Purpose:
147
147
  * - Converts JSON:API relationship format to database operations
148
148
  * - Handles all relationship types: belongsTo, polymorphic, many-to-many
149
149
  * - Validates polymorphic types against allowed values
150
150
  * - Separates concerns: foreign keys vs pivot table operations
151
- *
151
+ *
152
152
  * Data flow:
153
153
  * 1. Receives JSON:API document with relationships section
154
154
  * 2. Analyzes schema to determine relationship types
@@ -159,58 +159,58 @@ import { RestApiValidationError } from '../../../../lib/rest-api-errors.js';
159
159
  */
160
160
  export const processRelationships = (scope, deps) => {
161
161
  // Extract values from scope
162
- const {
163
- vars: {
162
+ const {
163
+ vars: {
164
164
  schemaInfo: { schemaStructure: schemaFields, schemaRelationships: relationships }
165
165
  }
166
- } = scope;
167
-
166
+ } = scope
167
+
168
168
  // Extract values from deps
169
- const { context } = deps;
170
- const { inputRecord } = context;
171
- const belongsToUpdates = {};
172
- const manyToManyRelationships = [];
173
-
169
+ const { context } = deps
170
+ const { inputRecord } = context
171
+ const belongsToUpdates = {}
172
+ const manyToManyRelationships = []
173
+
174
174
  if (!inputRecord.data.relationships) {
175
- return { belongsToUpdates, manyToManyRelationships };
175
+ return { belongsToUpdates, manyToManyRelationships }
176
176
  }
177
-
177
+
178
178
  for (const [relName, relData] of Object.entries(inputRecord.data.relationships)) {
179
- const relDef = relationships?.[relName];
180
-
179
+ const relDef = relationships?.[relName]
180
+
181
181
  // Find the schema field that defines this relationship
182
- const schemaField = Object.entries(schemaFields).find(([fieldName, fieldDef]) =>
182
+ const schemaField = Object.entries(schemaFields).find(([fieldName, fieldDef]) =>
183
183
  fieldDef.as === relName
184
- );
185
-
184
+ )
185
+
186
186
  if (schemaField) {
187
- const [fieldName, fieldDef] = schemaField;
188
-
187
+ const [fieldName, fieldDef] = schemaField
188
+
189
189
  // Handle regular belongsTo (1:1)
190
190
  if (fieldDef.belongsTo) {
191
191
  if (relData.data === null) {
192
- belongsToUpdates[fieldName] = null;
192
+ belongsToUpdates[fieldName] = null
193
193
  } else if (relData.data?.id) {
194
- belongsToUpdates[fieldName] = relData.data.id;
194
+ belongsToUpdates[fieldName] = relData.data.id
195
195
  }
196
196
  }
197
197
  }
198
-
198
+
199
199
  // Check for polymorphic belongsTo defined in relationships object (not as schema field)
200
200
  if (!schemaField && relDef?.belongsToPolymorphic && relData.data !== undefined) {
201
201
  if (relData.data === null) {
202
- const { typeField, idField } = relDef.belongsToPolymorphic;
203
- belongsToUpdates[typeField] = null;
204
- belongsToUpdates[idField] = null;
202
+ const { typeField, idField } = relDef.belongsToPolymorphic
203
+ belongsToUpdates[typeField] = null
204
+ belongsToUpdates[idField] = null
205
205
  } else if (relData.data) {
206
- const { type, id } = relData.data;
207
- const { types, typeField, idField } = relDef.belongsToPolymorphic;
208
-
206
+ const { type, id } = relData.data
207
+ const { types, typeField, idField } = relDef.belongsToPolymorphic
208
+
209
209
  // Validate type is allowed
210
210
  if (!types.includes(type)) {
211
211
  throw new RestApiValidationError(
212
212
  `Invalid type '${type}' for polymorphic relationship '${relName}'. Allowed types: ${types.join(', ')}`,
213
- {
213
+ {
214
214
  fields: [`data.relationships.${relName}.data.type`],
215
215
  violations: [{
216
216
  field: `data.relationships.${relName}.data.type`,
@@ -218,14 +218,14 @@ export const processRelationships = (scope, deps) => {
218
218
  message: `Type must be one of: ${types.join(', ')}`
219
219
  }]
220
220
  }
221
- );
221
+ )
222
222
  }
223
-
224
- belongsToUpdates[typeField] = type;
225
- belongsToUpdates[idField] = id;
223
+
224
+ belongsToUpdates[typeField] = type
225
+ belongsToUpdates[idField] = id
226
226
  }
227
227
  }
228
-
228
+
229
229
  // Check relationship type and process accordingly
230
230
  if (relDef?.type === 'manyToMany' && relData.data !== undefined) {
231
231
  manyToManyRelationships.push({
@@ -236,10 +236,10 @@ export const processRelationships = (scope, deps) => {
236
236
  otherKey: relDef.otherKey
237
237
  },
238
238
  relData: relData.data || [] // null means empty array for many-to-many
239
- });
239
+ })
240
240
  }
241
241
  // Note: hasOne and hasMany don't need processing here as they don't update the current record
242
242
  }
243
-
244
- return { belongsToUpdates, manyToManyRelationships };
245
- };
243
+
244
+ return { belongsToUpdates, manyToManyRelationships }
245
+ }
@@ -4,7 +4,7 @@ export const AccessPlugin = {
4
4
  name: 'rest-api-access',
5
5
  dependencies: ['rest-api'],
6
6
 
7
- async install({ addHook, helpers, log, pluginOptions = {}, scopes }) {
7
+ async install ({ addHook, helpers, log, pluginOptions = {}, scopes }) {
8
8
  const ownershipOptions = pluginOptions.ownership || pluginOptions.autoOwnership || {}
9
9
 
10
10
  const config = {
@@ -105,7 +105,7 @@ export const AccessPlugin = {
105
105
  return storageAdapter.translateFilterValue(field, value)
106
106
  }
107
107
 
108
- function registerBuiltinCheckers() {
108
+ function registerBuiltinCheckers () {
109
109
  state.authCheckers.set('public', () => true)
110
110
 
111
111
  state.authCheckers.set('authenticated', (context) => {
@@ -188,7 +188,7 @@ export const AccessPlugin = {
188
188
  }
189
189
  })
190
190
 
191
- function setOwnerOnInput({ context, scopeName, scopes, scopeOptions }) {
191
+ function setOwnerOnInput ({ context, scopeName, scopes, scopeOptions }) {
192
192
  if (!config.ownership.enabled) return
193
193
  if (config.ownership.excludeResources.includes(scopeName)) return
194
194
  if (!context.auth?.userId) {
@@ -384,7 +384,7 @@ export const AccessPlugin = {
384
384
  helpers.auth = {}
385
385
  }
386
386
 
387
- helpers.auth.requireAuth = function requireAuth(context) {
387
+ helpers.auth.requireAuth = function requireAuth (context) {
388
388
  if (!context.auth?.userId) {
389
389
  const error = new Error('Authentication required')
390
390
  error.statusCode = 401
@@ -393,7 +393,7 @@ export const AccessPlugin = {
393
393
  return context.auth
394
394
  }
395
395
 
396
- helpers.auth.requireOwnership = function requireOwnership(context, resourceOrUserId) {
396
+ helpers.auth.requireOwnership = function requireOwnership (context, resourceOrUserId) {
397
397
  const auth = helpers.auth.requireAuth(context)
398
398
  const field = ownershipField()
399
399
 
@@ -424,11 +424,11 @@ export const AccessPlugin = {
424
424
  return auth
425
425
  }
426
426
 
427
- helpers.auth.registerChecker = function registerChecker(name, checkerFn) {
427
+ helpers.auth.registerChecker = function registerChecker (name, checkerFn) {
428
428
  state.authCheckers.set(name, checkerFn)
429
429
  }
430
430
 
431
- helpers.auth.checkPermission = async function checkPermission(context, rules, options = {}) {
431
+ helpers.auth.checkPermission = async function checkPermission (context, rules, options = {}) {
432
432
  if (!rules || rules.length === 0) return true
433
433
 
434
434
  const { existingRecord, scopeVars } = options
@@ -447,7 +447,7 @@ export const AccessPlugin = {
447
447
  return false
448
448
  }
449
449
 
450
- helpers.auth.cleanup = function cleanup() {
450
+ helpers.auth.cleanup = function cleanup () {
451
451
  state.authCheckers.clear()
452
452
  }
453
453
  }