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.
- package/connectors_ideas/fastify.js +66 -68
- package/connectors_ideas/koa-plugin.js +50 -51
- package/eslint.config.js +11 -2
- package/examples/url-override-example.js +63 -63
- package/index.js +13 -13
- package/lib/enhanced-logger.js +51 -51
- package/lib/error-formatter.js +61 -61
- package/lib/rest-api-errors.js +28 -28
- package/package.json +1 -1
- package/plugins/core/bulk-operations-plugin.js +142 -145
- package/plugins/core/connectors/express-plugin.js +210 -217
- package/plugins/core/connectors/lib/busboy-detector.js +84 -84
- package/plugins/core/connectors/lib/formidable-detector.js +50 -50
- package/plugins/core/connectors/lib/request-helpers.js +74 -74
- package/plugins/core/file-handling-plugin.js +125 -125
- package/plugins/core/lib/anyapi/anyapi-registry.js +254 -254
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +287 -287
- package/plugins/core/lib/anyapi/schema-utils.js +101 -101
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +41 -41
- package/plugins/core/lib/dbTablesOperations.js +134 -135
- package/plugins/core/lib/fractional-positioning.js +74 -76
- package/plugins/core/lib/querying/knex-cross-table-search.js +320 -320
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +144 -144
- package/plugins/core/lib/querying/knex-pagination-helpers.js +164 -164
- package/plugins/core/lib/querying/knex-process-includes.js +50 -50
- package/plugins/core/lib/querying/knex-query-helpers-base.js +31 -31
- package/plugins/core/lib/querying/knex-query-helpers.js +451 -452
- package/plugins/core/lib/querying/knex-relationship-includes.js +1160 -1170
- package/plugins/core/lib/querying/knex-window-queries.js +89 -89
- package/plugins/core/lib/querying/url-helpers.js +35 -35
- package/plugins/core/lib/querying-writing/compile-schemas.js +97 -98
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +31 -31
- package/plugins/core/lib/querying-writing/database-capabilities.js +94 -94
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +76 -76
- package/plugins/core/lib/querying-writing/default-data-helpers.js +27 -27
- package/plugins/core/lib/querying-writing/field-utils.js +44 -42
- package/plugins/core/lib/querying-writing/knex-constants.js +15 -16
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +117 -115
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +41 -41
- package/plugins/core/lib/querying-writing/payload-validators.js +275 -275
- package/plugins/core/lib/querying-writing/schema-helpers.js +129 -129
- package/plugins/core/lib/querying-writing/scope-validations.js +143 -146
- package/plugins/core/lib/querying-writing/simplified-helpers.js +133 -129
- package/plugins/core/lib/storage/storage-adapter.js +68 -68
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +31 -31
- package/plugins/core/lib/writing/many-to-many-manipulations.js +81 -81
- package/plugins/core/lib/writing/relationship-processor.js +58 -58
- package/plugins/core/rest-api-access.js +8 -8
- package/plugins/core/rest-api-anyapi-knex-plugin.js +920 -920
- package/plugins/core/rest-api-cors-plugin.js +170 -171
- package/plugins/core/rest-api-knex-plugin.js +482 -493
- package/plugins/core/rest-api-multihome-plugin.js +247 -251
- package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +5 -5
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +20 -21
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +44 -44
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +23 -23
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +15 -15
- package/plugins/core/rest-api-plugin-methods/add-route.js +9 -9
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +5 -6
- package/plugins/core/rest-api-plugin-methods/common.js +295 -297
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +35 -37
- package/plugins/core/rest-api-plugin-methods/delete.js +57 -59
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +172 -171
- package/plugins/core/rest-api-plugin-methods/get-related.js +96 -96
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +16 -18
- package/plugins/core/rest-api-plugin-methods/get.js +86 -86
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +16 -17
- package/plugins/core/rest-api-plugin-methods/patch.js +63 -64
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +54 -55
- package/plugins/core/rest-api-plugin-methods/post.js +61 -62
- package/plugins/core/rest-api-plugin-methods/put.js +86 -87
- package/plugins/core/rest-api-plugin-methods/query.js +88 -88
- package/plugins/core/rest-api-plugin-methods/release.js +5 -6
- package/plugins/core/rest-api-plugin.js +76 -78
- package/plugins/core/rest-api-positioning-plugin.js +251 -252
- package/plugins/core/socketio-plugin.js +240 -236
- package/plugins/storage/local-storage.js +103 -103
- package/plugins/storage/s3-storage.js +41 -42
- package/quickTest.js +37 -37
- package/scripts/debug-anyapi-review-filter.mjs +18 -18
- package/scripts/inspect-anyapi-descriptor-country.mjs +10 -10
- package/scripts/inspect-anyapi-descriptor.mjs +10 -10
- package/scripts/run-docs.js +112 -113
- package/test-debug.js +28 -28
- package/tests/TEST_TEMPLATE.test.js +51 -51
- package/tests/anyapi-basic.test.js +42 -42
- package/tests/anyapi-cursor-pagination.test.js +48 -48
- package/tests/bulk-operations.test.js +208 -208
- package/tests/computed-fields.test.js +204 -204
- package/tests/cors.test.js +412 -413
- package/tests/curl.test.js +275 -276
- package/tests/custom-idproperty-relationships.test.js +266 -266
- package/tests/field-getters.test.js +132 -132
- package/tests/field-setters.test.js +110 -110
- package/tests/fixtures/api-configs.js +699 -699
- package/tests/helpers/storage-mode.js +32 -32
- package/tests/helpers/test-utils.js +76 -76
- package/tests/include-depth-validation.test.js +76 -76
- package/tests/includes.test.js +145 -145
- package/tests/multihome.test.js +175 -175
- package/tests/nested-includes.test.js +255 -255
- package/tests/pagination-cursor-multifield.test.js +164 -164
- package/tests/pagination-enhanced.test.js +225 -225
- package/tests/polymorphic-relationships.test.js +96 -96
- package/tests/polymorphic-simple.test.js +44 -44
- package/tests/positioning-advanced.test.js +226 -226
- package/tests/positioning-extended.test.js +399 -399
- package/tests/positioning-multifilter.test.js +204 -204
- package/tests/positioning.test.js +266 -266
- package/tests/queries.test.js +184 -184
- package/tests/query-limits.test.js +154 -155
- package/tests/relationship-endpoints.test.js +127 -127
- package/tests/relationships.test.js +178 -179
- package/tests/rest-api-access.test.js +86 -86
- package/tests/rest-api.test.js +103 -103
- package/tests/searchschema-merge.test.js +93 -93
- package/tests/socketio.test.js +326 -326
- 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
|
|
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
|
}
|