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,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
RestApiValidationError,
|
|
3
|
-
RestApiPayloadError
|
|
4
|
-
} from '../../../lib/rest-api-errors.js'
|
|
5
|
-
import { transformSimplifiedToJsonApi } from '../lib/querying-writing/simplified-helpers.js'
|
|
6
|
-
import { createEnhancedLogger } from '../../../lib/enhanced-logger.js'
|
|
1
|
+
import {
|
|
2
|
+
RestApiValidationError,
|
|
3
|
+
RestApiPayloadError
|
|
4
|
+
} from '../../../lib/rest-api-errors.js'
|
|
5
|
+
import { transformSimplifiedToJsonApi } from '../lib/querying-writing/simplified-helpers.js'
|
|
6
|
+
import { createEnhancedLogger } from '../../../lib/enhanced-logger.js'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Gets an enhanced logger instance with full error details and stack traces
|
|
@@ -11,11 +11,11 @@ import { createEnhancedLogger } from '../../../lib/enhanced-logger.js';
|
|
|
11
11
|
* @returns {Object} Enhanced logger instance
|
|
12
12
|
*/
|
|
13
13
|
export const getEnhancedLogger = (log) => {
|
|
14
|
-
return createEnhancedLogger(log, {
|
|
15
|
-
logFullErrors: true,
|
|
16
|
-
includeStack: true
|
|
17
|
-
})
|
|
18
|
-
}
|
|
14
|
+
return createEnhancedLogger(log, {
|
|
15
|
+
logFullErrors: true,
|
|
16
|
+
includeStack: true
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Cascades configuration values through multiple sources with fallback to default
|
|
@@ -25,22 +25,22 @@ export const getEnhancedLogger = (log) => {
|
|
|
25
25
|
* @returns {*} The first defined value found or the default
|
|
26
26
|
*/
|
|
27
27
|
export const cascadeConfig = (settingName, sources, defaultValue) =>
|
|
28
|
-
sources.find(source => source?.[settingName] !== undefined)?.[settingName] ?? defaultValue
|
|
28
|
+
sources.find(source => source?.[settingName] !== undefined)?.[settingName] ?? defaultValue
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Normalizes return record settings to valid values
|
|
32
32
|
* @param {string|boolean} value - The value to normalize
|
|
33
33
|
* @returns {string} Normalized value: 'no', 'minimal', or 'full'
|
|
34
34
|
*/
|
|
35
|
-
export function normalizeReturnValue(value) {
|
|
36
|
-
if (['no', 'minimal', 'full'].includes(value)) return value
|
|
37
|
-
return 'no'
|
|
35
|
+
export function normalizeReturnValue (value) {
|
|
36
|
+
if (['no', 'minimal', 'full'].includes(value)) return value
|
|
37
|
+
return 'no' // default
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
41
|
* Sets up common request context for REST API methods
|
|
42
42
|
* Handles simplified mode, transaction setup, and initial validation
|
|
43
|
-
*
|
|
43
|
+
*
|
|
44
44
|
* @param {Object} params - The method parameters
|
|
45
45
|
* @param {Object} context - The request context
|
|
46
46
|
* @param {Object} vars - Plugin variables
|
|
@@ -51,47 +51,47 @@ export function normalizeReturnValue(value) {
|
|
|
51
51
|
* @param {Object} helpers - Helper functions
|
|
52
52
|
* @returns {Object} An object containing schema-related shortcuts
|
|
53
53
|
*/
|
|
54
|
-
export async function setupCommonRequest({ params, context, vars, scopes, scopeOptions, scopeName, api, helpers }) {
|
|
54
|
+
export async function setupCommonRequest ({ params, context, vars, scopes, scopeOptions, scopeName, api, helpers }) {
|
|
55
55
|
// Determine which simplified setting to use based on transport
|
|
56
|
-
const isTransport = params.isTransport === true
|
|
57
|
-
|
|
56
|
+
const isTransport = params.isTransport === true
|
|
57
|
+
|
|
58
58
|
// Use vars which automatically cascade from scope to global
|
|
59
|
-
const defaultSimplified = isTransport ? vars.simplifiedTransport : vars.simplifiedApi
|
|
60
|
-
|
|
59
|
+
const defaultSimplified = isTransport ? vars.simplifiedTransport : vars.simplifiedApi
|
|
60
|
+
|
|
61
61
|
// Get simplified setting - from params only (per-call override) or use default
|
|
62
|
-
context.simplified = params.simplified !== undefined ? params.simplified : defaultSimplified
|
|
63
|
-
|
|
62
|
+
context.simplified = params.simplified !== undefined ? params.simplified : defaultSimplified
|
|
63
|
+
|
|
64
64
|
// Special case: if no inputRecord provided, force simplified mode
|
|
65
65
|
if (!params.inputRecord && context.simplified === false) {
|
|
66
|
-
context.simplified = true
|
|
66
|
+
context.simplified = true
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
// Params is totally bypassed in simplified mode
|
|
70
70
|
if (context.simplified) {
|
|
71
71
|
if (params.inputRecord) {
|
|
72
|
-
context.inputRecord = params.inputRecord
|
|
73
|
-
context.params = params
|
|
72
|
+
context.inputRecord = params.inputRecord
|
|
73
|
+
context.params = params
|
|
74
74
|
} else {
|
|
75
|
-
context.inputRecord = params
|
|
75
|
+
context.inputRecord = params
|
|
76
76
|
// Preserve returnFullRecord if specified in params
|
|
77
|
-
context.params = params.returnFullRecord ? { returnFullRecord: params.returnFullRecord } : {}
|
|
77
|
+
context.params = params.returnFullRecord ? { returnFullRecord: params.returnFullRecord } : {}
|
|
78
78
|
}
|
|
79
79
|
} else {
|
|
80
|
-
context.inputRecord = params.inputRecord
|
|
81
|
-
context.params = params
|
|
80
|
+
context.inputRecord = params.inputRecord
|
|
81
|
+
context.params = params
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
// Assign common context properties
|
|
85
|
-
context.schemaInfo = scopes[scopeName].vars.schemaInfo
|
|
86
|
-
|
|
85
|
+
context.schemaInfo = scopes[scopeName].vars.schemaInfo
|
|
86
|
+
|
|
87
87
|
// Use vars which automatically cascade from scope to global
|
|
88
|
-
const defaultReturnFullRecord = isTransport ? vars.returnRecordTransport : vars.returnRecordApi
|
|
89
|
-
|
|
88
|
+
const defaultReturnFullRecord = isTransport ? vars.returnRecordTransport : vars.returnRecordApi
|
|
89
|
+
|
|
90
90
|
// Get return record setting - from params only (per-call override) or use default
|
|
91
|
-
const returnFullRecordRaw = context.params.returnFullRecord !== undefined
|
|
92
|
-
? context.params.returnFullRecord
|
|
93
|
-
: defaultReturnFullRecord
|
|
94
|
-
|
|
91
|
+
const returnFullRecordRaw = context.params.returnFullRecord !== undefined
|
|
92
|
+
? context.params.returnFullRecord
|
|
93
|
+
: defaultReturnFullRecord
|
|
94
|
+
|
|
95
95
|
// Normalize return record setting to always be an object with method keys
|
|
96
96
|
if (typeof returnFullRecordRaw === 'object' && returnFullRecordRaw !== null) {
|
|
97
97
|
// It's already an object, normalize the values
|
|
@@ -99,47 +99,47 @@ export async function setupCommonRequest({ params, context, vars, scopes, scopeO
|
|
|
99
99
|
post: normalizeReturnValue(returnFullRecordRaw.post),
|
|
100
100
|
put: normalizeReturnValue(returnFullRecordRaw.put),
|
|
101
101
|
patch: normalizeReturnValue(returnFullRecordRaw.patch)
|
|
102
|
-
}
|
|
102
|
+
}
|
|
103
103
|
} else {
|
|
104
104
|
// It's a single value (string or boolean), apply to all methods
|
|
105
|
-
const normalized = normalizeReturnValue(returnFullRecordRaw)
|
|
105
|
+
const normalized = normalizeReturnValue(returnFullRecordRaw)
|
|
106
106
|
context.returnRecordSetting = {
|
|
107
107
|
post: normalized,
|
|
108
108
|
put: normalized,
|
|
109
109
|
patch: normalized
|
|
110
|
-
}
|
|
110
|
+
}
|
|
111
111
|
}
|
|
112
|
-
|
|
112
|
+
|
|
113
113
|
// Helper function to normalize return values (same as above)
|
|
114
|
-
function normalizeReturnValue(value) {
|
|
115
|
-
if (['no', 'minimal', 'full'].includes(value)) return value
|
|
116
|
-
return 'no'
|
|
114
|
+
function normalizeReturnValue (value) {
|
|
115
|
+
if (['no', 'minimal', 'full'].includes(value)) return value
|
|
116
|
+
return 'no' // default
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
// These only make sense as parameter per query, not in vars etc.
|
|
120
|
-
context.queryParams = params.queryParams || {}
|
|
121
|
-
context.queryParams.fields = cascadeConfig('fields', [context.queryParams], {})
|
|
122
|
-
context.queryParams.include = cascadeConfig('include', [context.queryParams], [])
|
|
120
|
+
context.queryParams = params.queryParams || {}
|
|
121
|
+
context.queryParams.fields = cascadeConfig('fields', [context.queryParams], {})
|
|
122
|
+
context.queryParams.include = cascadeConfig('include', [context.queryParams], [])
|
|
123
123
|
|
|
124
|
-
context.scopeName = scopeName
|
|
124
|
+
context.scopeName = scopeName
|
|
125
125
|
|
|
126
126
|
// Transaction handling
|
|
127
|
-
context.transaction = params.transaction ||
|
|
128
|
-
(helpers.newTransaction && !params.transaction ? await helpers.newTransaction() : null)
|
|
129
|
-
context.shouldCommit = !params.transaction && !!context.transaction
|
|
130
|
-
context.db = context.transaction || api.knex.instance
|
|
127
|
+
context.transaction = params.transaction ||
|
|
128
|
+
(helpers.newTransaction && !params.transaction ? await helpers.newTransaction() : null)
|
|
129
|
+
context.shouldCommit = !params.transaction && !!context.transaction
|
|
130
|
+
context.db = context.transaction || api.knex.instance
|
|
131
131
|
|
|
132
132
|
// These are just shortcuts used in this function and will be returned
|
|
133
|
-
const schema = context.schemaInfo.schemaInstance
|
|
134
|
-
const schemaStructure = context.schemaInfo.schemaInstance.structure
|
|
135
|
-
const schemaRelationships = context.schemaInfo.schemaRelationships
|
|
133
|
+
const schema = context.schemaInfo.schemaInstance
|
|
134
|
+
const schemaStructure = context.schemaInfo.schemaInstance.structure
|
|
135
|
+
const schemaRelationships = context.schemaInfo.schemaRelationships
|
|
136
136
|
|
|
137
137
|
// Transform input if in simplified mode
|
|
138
138
|
if (context.simplified) {
|
|
139
139
|
context.inputRecord = transformSimplifiedToJsonApi(
|
|
140
140
|
{ inputRecord: context.inputRecord },
|
|
141
141
|
{ context: { scopeName, schemaStructure, schemaRelationships } }
|
|
142
|
-
)
|
|
142
|
+
)
|
|
143
143
|
} else {
|
|
144
144
|
// Strict mode: validate no belongsTo fields in attributes
|
|
145
145
|
if (context.inputRecord?.data?.attributes) {
|
|
@@ -148,7 +148,7 @@ export async function setupCommonRequest({ params, context, vars, scopes, scopeO
|
|
|
148
148
|
throw new RestApiValidationError(
|
|
149
149
|
`Field '${key}' is a foreign key and must be set via relationships, not attributes`,
|
|
150
150
|
{ fields: [`data.attributes.${key}`] }
|
|
151
|
-
)
|
|
151
|
+
)
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
154
|
}
|
|
@@ -157,15 +157,15 @@ export async function setupCommonRequest({ params, context, vars, scopes, scopeO
|
|
|
157
157
|
if (context.inputRecord.data.type !== scopeName) {
|
|
158
158
|
throw new RestApiValidationError(
|
|
159
159
|
`Resource type mismatch. Expected '${scopeName}' but got '${context.inputRecord.data.type}'`,
|
|
160
|
-
{
|
|
161
|
-
fields: ['data.type'],
|
|
162
|
-
violations: [{
|
|
163
|
-
field: 'data.type',
|
|
164
|
-
rule: 'resource_type_match',
|
|
165
|
-
message: `Resource type must be '${scopeName}'`
|
|
166
|
-
}]
|
|
160
|
+
{
|
|
161
|
+
fields: ['data.type'],
|
|
162
|
+
violations: [{
|
|
163
|
+
field: 'data.type',
|
|
164
|
+
rule: 'resource_type_match',
|
|
165
|
+
message: `Resource type must be '${scopeName}'`
|
|
166
|
+
}]
|
|
167
167
|
}
|
|
168
|
-
)
|
|
168
|
+
)
|
|
169
169
|
}
|
|
170
170
|
|
|
171
171
|
// Remove included validation since JSON:API doesn't support it
|
|
@@ -173,7 +173,7 @@ export async function setupCommonRequest({ params, context, vars, scopes, scopeO
|
|
|
173
173
|
throw new RestApiPayloadError(
|
|
174
174
|
context.method + ' requests cannot include an "included" array. JSON:API does not support creating multiple resources in a single request.',
|
|
175
175
|
{ path: 'included', expected: 'undefined', received: 'array' }
|
|
176
|
-
)
|
|
176
|
+
)
|
|
177
177
|
}
|
|
178
178
|
|
|
179
179
|
// If both URL path ID and request body ID are provided, they must match
|
|
@@ -181,24 +181,24 @@ export async function setupCommonRequest({ params, context, vars, scopes, scopeO
|
|
|
181
181
|
if (context.id && context.inputRecord.data.id && String(context.id) !== String(context.inputRecord.data.id)) {
|
|
182
182
|
throw new RestApiValidationError(
|
|
183
183
|
`ID mismatch. URL path ID '${context.id}' does not match request body ID '${context.inputRecord.data.id}'`,
|
|
184
|
-
{
|
|
185
|
-
fields: ['data.id'],
|
|
186
|
-
violations: [{
|
|
187
|
-
field: 'data.id',
|
|
188
|
-
rule: 'id_consistency',
|
|
189
|
-
message:
|
|
190
|
-
}]
|
|
184
|
+
{
|
|
185
|
+
fields: ['data.id'],
|
|
186
|
+
violations: [{
|
|
187
|
+
field: 'data.id',
|
|
188
|
+
rule: 'id_consistency',
|
|
189
|
+
message: 'Request body ID must match URL path ID when both are provided'
|
|
190
|
+
}]
|
|
191
191
|
}
|
|
192
|
-
)
|
|
192
|
+
)
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
// Return key schema-related objects for direct use in the main methods
|
|
196
|
-
return { schema, schemaStructure, schemaRelationships }
|
|
196
|
+
return { schema, schemaStructure, schemaRelationships }
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
/**
|
|
200
200
|
* Handles error cleanup and logging for write methods (POST, PUT, PATCH)
|
|
201
|
-
*
|
|
201
|
+
*
|
|
202
202
|
* @param {Error} error - The error that was caught
|
|
203
203
|
* @param {Object} context - The request context
|
|
204
204
|
* @param {string} method - The HTTP method name (POST, PUT, PATCH)
|
|
@@ -210,26 +210,26 @@ export async function setupCommonRequest({ params, context, vars, scopes, scopeO
|
|
|
210
210
|
export const handleWriteMethodError = async (error, context, method, scopeName, log, runHooks) => {
|
|
211
211
|
// Rollback transaction if we created it
|
|
212
212
|
if (context.shouldCommit) {
|
|
213
|
-
await context.transaction.rollback()
|
|
214
|
-
await runHooks('afterRollback')
|
|
213
|
+
await context.transaction.rollback()
|
|
214
|
+
await runHooks('afterRollback')
|
|
215
215
|
}
|
|
216
|
-
|
|
216
|
+
|
|
217
217
|
// Create enhanced logger for error logging
|
|
218
|
-
const enhancedLog = getEnhancedLogger(log)
|
|
219
|
-
|
|
218
|
+
const enhancedLog = getEnhancedLogger(log)
|
|
219
|
+
|
|
220
220
|
// Log the full error details
|
|
221
221
|
enhancedLog.logError(`Error in ${method} method`, error, {
|
|
222
222
|
scopeName,
|
|
223
223
|
method: method.toLowerCase(),
|
|
224
224
|
inputRecord: context.inputRecord
|
|
225
|
-
})
|
|
226
|
-
|
|
227
|
-
throw error
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
throw error
|
|
228
228
|
}
|
|
229
229
|
|
|
230
230
|
/**
|
|
231
231
|
* Validates that a pivot resource exists for many-to-many relationships
|
|
232
|
-
*
|
|
232
|
+
*
|
|
233
233
|
* @param {Object} scopes - All available scopes/resources
|
|
234
234
|
* @param {Object} relDef - The relationship definition
|
|
235
235
|
* @param {string} relName - The relationship name
|
|
@@ -239,7 +239,7 @@ export const validatePivotResource = (scopes, relDef, relName) => {
|
|
|
239
239
|
if (!scopes[relDef.through]) {
|
|
240
240
|
throw new RestApiValidationError(
|
|
241
241
|
`Pivot resource '${relDef.through}' not found for relationship '${relName}'`,
|
|
242
|
-
{
|
|
242
|
+
{
|
|
243
243
|
fields: [`relationships.${relName}`],
|
|
244
244
|
violations: [{
|
|
245
245
|
field: `relationships.${relName}`,
|
|
@@ -247,23 +247,23 @@ export const validatePivotResource = (scopes, relDef, relName) => {
|
|
|
247
247
|
message: `Pivot resource '${relDef.through}' must be defined`
|
|
248
248
|
}]
|
|
249
249
|
}
|
|
250
|
-
)
|
|
250
|
+
)
|
|
251
251
|
}
|
|
252
252
|
}
|
|
253
253
|
|
|
254
254
|
/**
|
|
255
255
|
* Gets the appropriate hook suffix based on HTTP method
|
|
256
|
-
*
|
|
256
|
+
*
|
|
257
257
|
* @param {string} method - The HTTP method (e.g., 'post', 'get')
|
|
258
258
|
* @returns {string} The capitalized method name for hook naming
|
|
259
259
|
*/
|
|
260
260
|
export const getMethodHookSuffix = (method) => {
|
|
261
|
-
return method.charAt(0).toUpperCase() + method.slice(1)
|
|
261
|
+
return method.charAt(0).toUpperCase() + method.slice(1)
|
|
262
262
|
}
|
|
263
263
|
|
|
264
264
|
/**
|
|
265
265
|
* Validates resource attributes before write operations
|
|
266
|
-
*
|
|
266
|
+
*
|
|
267
267
|
* @param {Object} params - Validation parameters
|
|
268
268
|
* @param {Object} params.context - The request context
|
|
269
269
|
* @param {Object} params.schema - The resource schema
|
|
@@ -272,147 +272,147 @@ export const getMethodHookSuffix = (method) => {
|
|
|
272
272
|
* @param {boolean} params.isPartialValidation - Whether this is partial validation (for PATCH)
|
|
273
273
|
* @throws {RestApiValidationError} If validation fails
|
|
274
274
|
*/
|
|
275
|
-
export const validateResourceAttributesBeforeWrite = async ({
|
|
276
|
-
context,
|
|
277
|
-
schema,
|
|
278
|
-
belongsToUpdates,
|
|
279
|
-
runHooks,
|
|
275
|
+
export const validateResourceAttributesBeforeWrite = async ({
|
|
276
|
+
context,
|
|
277
|
+
schema,
|
|
278
|
+
belongsToUpdates,
|
|
279
|
+
runHooks,
|
|
280
280
|
isPartialValidation = false
|
|
281
281
|
}) => {
|
|
282
282
|
// Dynamically get the method suffix
|
|
283
|
-
const methodSpecificHookSuffix = getMethodHookSuffix(context.method)
|
|
283
|
+
const methodSpecificHookSuffix = getMethodHookSuffix(context.method)
|
|
284
284
|
|
|
285
|
-
await runHooks('beforeSchemaValidate')
|
|
286
|
-
await runHooks(`beforeSchemaValidate${methodSpecificHookSuffix}`)
|
|
285
|
+
await runHooks('beforeSchemaValidate')
|
|
286
|
+
await runHooks(`beforeSchemaValidate${methodSpecificHookSuffix}`)
|
|
287
287
|
|
|
288
288
|
// Store original input attributes before validation adds defaults (primarily for POST)
|
|
289
289
|
// Only if it's not already set and the current method is POST
|
|
290
290
|
if (!context.originalInputAttributes && context.method === 'post') {
|
|
291
|
-
|
|
291
|
+
context.originalInputAttributes = { ...(context.inputRecord.data.attributes || {}) }
|
|
292
292
|
}
|
|
293
|
-
|
|
293
|
+
|
|
294
294
|
// Merge belongsTo updates with attributes for validation
|
|
295
295
|
const attributesToValidate = {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
}
|
|
299
|
-
|
|
296
|
+
...context.inputRecord.data.attributes,
|
|
297
|
+
...belongsToUpdates
|
|
298
|
+
}
|
|
299
|
+
|
|
300
300
|
// Extract computed fields and foreign key fields before validation
|
|
301
|
-
const computedFields = {}
|
|
302
|
-
const foreignKeyFields = {}
|
|
303
|
-
const schemaStructure = context.schemaInfo.schemaStructure || {}
|
|
301
|
+
const computedFields = {}
|
|
302
|
+
const foreignKeyFields = {}
|
|
303
|
+
const schemaStructure = context.schemaInfo.schemaStructure || {}
|
|
304
304
|
Object.entries(attributesToValidate).forEach(([key, value]) => {
|
|
305
|
-
const fieldDef = schemaStructure[key]
|
|
305
|
+
const fieldDef = schemaStructure[key]
|
|
306
306
|
// Computed fields should be stripped from input
|
|
307
307
|
if (fieldDef && fieldDef.computed === true && value !== undefined) {
|
|
308
|
-
computedFields[key] = value
|
|
308
|
+
computedFields[key] = value
|
|
309
309
|
}
|
|
310
310
|
// Foreign key fields (belongsTo with 'as' property) should NOT be in attributes
|
|
311
311
|
// They should only come through relationships or belongsToUpdates
|
|
312
312
|
// Check if key exists in belongsToUpdates (not just truthy value)
|
|
313
313
|
if (fieldDef && fieldDef.belongsTo && fieldDef.as && value !== undefined && !(key in belongsToUpdates)) {
|
|
314
|
-
foreignKeyFields[key] = value
|
|
314
|
+
foreignKeyFields[key] = value
|
|
315
315
|
}
|
|
316
|
-
})
|
|
317
|
-
|
|
316
|
+
})
|
|
317
|
+
|
|
318
318
|
// Warn if computed fields were sent in the input
|
|
319
319
|
if (Object.keys(computedFields).length > 0) {
|
|
320
|
-
const fieldNames = Object.keys(computedFields).join(', ')
|
|
321
|
-
console.warn(`Computed fields [${fieldNames}] were sent in input for resource '${context.scopeName}' but will be ignored as they are output-only`)
|
|
320
|
+
const fieldNames = Object.keys(computedFields).join(', ')
|
|
321
|
+
console.warn(`Computed fields [${fieldNames}] were sent in input for resource '${context.scopeName}' but will be ignored as they are output-only`)
|
|
322
322
|
}
|
|
323
|
-
|
|
323
|
+
|
|
324
324
|
// Reject if foreign key fields were sent directly in attributes
|
|
325
325
|
if (Object.keys(foreignKeyFields).length > 0) {
|
|
326
326
|
const violations = Object.keys(foreignKeyFields).map(field => {
|
|
327
|
-
const fieldDef = schemaStructure[field]
|
|
327
|
+
const fieldDef = schemaStructure[field]
|
|
328
328
|
return {
|
|
329
329
|
field: `data.attributes.${field}`,
|
|
330
330
|
rule: 'foreign_key_in_attributes',
|
|
331
331
|
message: `Foreign key field '${field}' should not be in attributes. Use 'data.relationships.${fieldDef.as}' instead.`
|
|
332
|
-
}
|
|
333
|
-
})
|
|
334
|
-
|
|
332
|
+
}
|
|
333
|
+
})
|
|
334
|
+
|
|
335
335
|
throw new RestApiValidationError(
|
|
336
336
|
'Foreign key fields cannot be set directly in attributes',
|
|
337
|
-
{
|
|
337
|
+
{
|
|
338
338
|
fields: violations.map(v => v.field),
|
|
339
339
|
violations
|
|
340
340
|
}
|
|
341
|
-
)
|
|
341
|
+
)
|
|
342
342
|
}
|
|
343
|
-
|
|
343
|
+
|
|
344
344
|
// Filter out computed fields and foreign key fields from validation
|
|
345
345
|
// Virtual fields MUST go through validation
|
|
346
346
|
const attributesForValidation = Object.entries(attributesToValidate)
|
|
347
347
|
.filter(([key, _]) => {
|
|
348
|
-
const fieldDef = schemaStructure[key]
|
|
348
|
+
const fieldDef = schemaStructure[key]
|
|
349
349
|
// Exclude computed fields
|
|
350
350
|
if (fieldDef && fieldDef.computed === true) {
|
|
351
|
-
return false
|
|
351
|
+
return false
|
|
352
352
|
}
|
|
353
353
|
// Exclude foreign key fields that weren't provided via belongsToUpdates
|
|
354
354
|
if (fieldDef && fieldDef.belongsTo && fieldDef.as && !belongsToUpdates[key]) {
|
|
355
|
-
return false
|
|
355
|
+
return false
|
|
356
356
|
}
|
|
357
|
-
return true
|
|
357
|
+
return true
|
|
358
358
|
})
|
|
359
359
|
.reduce((acc, [key, value]) => {
|
|
360
|
-
acc[key] = value
|
|
361
|
-
return acc
|
|
362
|
-
}, {})
|
|
360
|
+
acc[key] = value
|
|
361
|
+
return acc
|
|
362
|
+
}, {})
|
|
363
363
|
|
|
364
|
-
const validationOptions = isPartialValidation ? { onlyObjectValues: true } : {}
|
|
364
|
+
const validationOptions = isPartialValidation ? { onlyObjectValues: true } : {}
|
|
365
365
|
|
|
366
|
-
const { validatedObject, errors } = await schema.validate(attributesForValidation, validationOptions)
|
|
366
|
+
const { validatedObject, errors } = await schema.validate(attributesForValidation, validationOptions)
|
|
367
367
|
|
|
368
368
|
if (Object.keys(errors).length > 0) {
|
|
369
369
|
// --- START OF MODIFICATION ---
|
|
370
|
-
const schemaStructure = context.schemaInfo.schemaInstance.structure
|
|
370
|
+
const schemaStructure = context.schemaInfo.schemaInstance.structure // Get the schema structure for lookup
|
|
371
371
|
|
|
372
372
|
const violations = Object.entries(errors).map(([field, error]) => {
|
|
373
|
-
|
|
373
|
+
let fieldPath = `data.attributes.${field}` // Default path for attributes
|
|
374
374
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
375
|
+
// Check if this field is a foreign key that has an 'as' alias
|
|
376
|
+
const fieldDef = schemaStructure[field]
|
|
377
|
+
if (fieldDef && fieldDef.belongsTo && fieldDef.as) {
|
|
378
378
|
// If it's a belongsTo field with an alias, rewrite the path to the relationship alias
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
379
|
+
fieldPath = `data.relationships.${fieldDef.as}.data.id`
|
|
380
|
+
}
|
|
381
|
+
// For many-to-many relationships, the original `transformSimplifiedToJsonApi`
|
|
382
|
+
// already puts them under `relationships.relName.data`, so `field` here
|
|
383
|
+
// would already be the relationship name, not a foreign key.
|
|
384
|
+
// However, if a validation error somehow slips through for a pivot table field
|
|
385
|
+
// that doesn't have an 'as' alias but is a foreign key, you might need
|
|
386
|
+
// more sophisticated mapping. For now, this covers belongsTo.
|
|
387
|
+
|
|
388
|
+
return {
|
|
389
|
+
field: fieldPath,
|
|
390
|
+
rule: error.code || 'invalid_value',
|
|
391
|
+
message: error.message
|
|
392
|
+
}
|
|
393
|
+
})
|
|
394
|
+
// --- END OF MODIFICATION ---
|
|
395
|
+
|
|
396
|
+
throw new RestApiValidationError(
|
|
397
|
+
'Schema validation failed for resource attributes',
|
|
398
|
+
{
|
|
399
|
+
fields: violations.map(v => v.field), // Use the potentially rewritten fields
|
|
400
|
+
violations
|
|
401
|
+
}
|
|
402
|
+
)
|
|
403
403
|
}
|
|
404
|
-
|
|
404
|
+
|
|
405
405
|
// Update attributes with validated values
|
|
406
406
|
// Virtual fields have now been validated and cast properly
|
|
407
|
-
context.inputRecord.data.attributes = validatedObject
|
|
407
|
+
context.inputRecord.data.attributes = validatedObject
|
|
408
408
|
|
|
409
|
-
await runHooks(`afterSchemaValidate${methodSpecificHookSuffix}`)
|
|
410
|
-
await runHooks('afterSchemaValidate')
|
|
409
|
+
await runHooks(`afterSchemaValidate${methodSpecificHookSuffix}`)
|
|
410
|
+
await runHooks('afterSchemaValidate')
|
|
411
411
|
}
|
|
412
412
|
|
|
413
413
|
/**
|
|
414
414
|
* Validates that the user has access to all resources referenced in relationships
|
|
415
|
-
*
|
|
415
|
+
*
|
|
416
416
|
* @param {object} context - The context object containing authentication info
|
|
417
417
|
* @param {object} inputRecord - The input record containing relationships to validate
|
|
418
418
|
* @param {object} helpers - Data helpers including dataGetMinimal
|
|
@@ -421,21 +421,21 @@ export const validateResourceAttributesBeforeWrite = async ({
|
|
|
421
421
|
* @throws {Error} If user doesn't have access to any related resource
|
|
422
422
|
*/
|
|
423
423
|
export const validateRelationshipAccess = async (context, inputRecord, helpers, runHooks, api) => {
|
|
424
|
-
if (!inputRecord?.data?.relationships) return
|
|
425
|
-
|
|
424
|
+
if (!inputRecord?.data?.relationships) return
|
|
425
|
+
|
|
426
426
|
for (const [relName, relData] of Object.entries(inputRecord.data.relationships)) {
|
|
427
|
-
if (!relData?.data) continue
|
|
428
|
-
|
|
427
|
+
if (!relData?.data) continue
|
|
428
|
+
|
|
429
429
|
// Handle both single and array relationships
|
|
430
|
-
const relatedItems = Array.isArray(relData.data) ? relData.data : [relData.data]
|
|
431
|
-
|
|
430
|
+
const relatedItems = Array.isArray(relData.data) ? relData.data : [relData.data]
|
|
431
|
+
|
|
432
432
|
for (const item of relatedItems) {
|
|
433
433
|
// Get the scope for the related resource
|
|
434
|
-
const relatedScope = api.resources[item.type]
|
|
434
|
+
const relatedScope = api.resources[item.type]
|
|
435
435
|
if (!relatedScope) {
|
|
436
|
-
throw new Error(`Unknown resource type: ${item.type}`)
|
|
436
|
+
throw new Error(`Unknown resource type: ${item.type}`)
|
|
437
437
|
}
|
|
438
|
-
|
|
438
|
+
|
|
439
439
|
// Create context for dataGetMinimal
|
|
440
440
|
const getContext = {
|
|
441
441
|
...context,
|
|
@@ -444,20 +444,19 @@ export const validateRelationshipAccess = async (context, inputRecord, helpers,
|
|
|
444
444
|
scopeName: item.type,
|
|
445
445
|
method: 'get', // We're checking read permission
|
|
446
446
|
isUpdate: false
|
|
447
|
-
}
|
|
447
|
+
}
|
|
448
448
|
|
|
449
|
-
|
|
450
449
|
// Get the minimal record
|
|
451
|
-
const record = await helpers.dataGetMinimal({
|
|
452
|
-
scopeName: item.type,
|
|
450
|
+
const record = await helpers.dataGetMinimal({
|
|
451
|
+
scopeName: item.type,
|
|
453
452
|
context: getContext,
|
|
454
|
-
runHooks
|
|
455
|
-
})
|
|
456
|
-
|
|
453
|
+
runHooks
|
|
454
|
+
})
|
|
455
|
+
|
|
457
456
|
if (!record) {
|
|
458
|
-
throw new Error(`Cannot create relationship to non-existent ${item.type} with id ${item.id}`)
|
|
457
|
+
throw new Error(`Cannot create relationship to non-existent ${item.type} with id ${item.id}`)
|
|
459
458
|
}
|
|
460
|
-
|
|
459
|
+
|
|
461
460
|
// Check permissions using the related scope's checkPermissions
|
|
462
461
|
await relatedScope.checkPermissions({
|
|
463
462
|
method: 'get',
|
|
@@ -466,7 +465,7 @@ export const validateRelationshipAccess = async (context, inputRecord, helpers,
|
|
|
466
465
|
id: item.id,
|
|
467
466
|
minimalRecord: record
|
|
468
467
|
}
|
|
469
|
-
})
|
|
468
|
+
})
|
|
470
469
|
}
|
|
471
470
|
}
|
|
472
471
|
}
|
|
@@ -474,7 +473,7 @@ export const validateRelationshipAccess = async (context, inputRecord, helpers,
|
|
|
474
473
|
/**
|
|
475
474
|
* Applies field setters to transform attribute values before storage
|
|
476
475
|
* Setters are applied in dependency order to handle interdependent fields
|
|
477
|
-
*
|
|
476
|
+
*
|
|
478
477
|
* @param {object} attributes - The validated attributes to transform
|
|
479
478
|
* @param {object} schemaInfo - Schema information including fieldSetters
|
|
480
479
|
* @param {object} context - The request context
|
|
@@ -483,20 +482,20 @@ export const validateRelationshipAccess = async (context, inputRecord, helpers,
|
|
|
483
482
|
* @returns {object} Transformed attributes ready for storage
|
|
484
483
|
*/
|
|
485
484
|
export const applyFieldSetters = async (attributes, schemaInfo, context, api, helpers) => {
|
|
486
|
-
const fieldSetters = schemaInfo.fieldSetters || {}
|
|
487
|
-
const sortedSetterFields = schemaInfo.sortedSetterFields || []
|
|
488
|
-
|
|
485
|
+
const fieldSetters = schemaInfo.fieldSetters || {}
|
|
486
|
+
const sortedSetterFields = schemaInfo.sortedSetterFields || []
|
|
487
|
+
|
|
489
488
|
// No setters to apply
|
|
490
489
|
if (sortedSetterFields.length === 0) {
|
|
491
|
-
return attributes
|
|
490
|
+
return attributes
|
|
492
491
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
492
|
+
|
|
493
|
+
const transformedAttributes = { ...attributes }
|
|
494
|
+
|
|
496
495
|
for (const fieldName of sortedSetterFields) {
|
|
497
496
|
// Only process fields that exist in the attributes
|
|
498
497
|
if (fieldName in transformedAttributes) {
|
|
499
|
-
const setterInfo = fieldSetters[fieldName]
|
|
498
|
+
const setterInfo = fieldSetters[fieldName]
|
|
500
499
|
try {
|
|
501
500
|
const setterContext = {
|
|
502
501
|
attributes: transformedAttributes, // Current state with previous setters applied
|
|
@@ -508,134 +507,133 @@ export const applyFieldSetters = async (attributes, schemaInfo, context, api, he
|
|
|
508
507
|
api,
|
|
509
508
|
helpers,
|
|
510
509
|
auth: context.auth
|
|
511
|
-
}
|
|
510
|
+
}
|
|
512
511
|
transformedAttributes[fieldName] = await setterInfo.setter(
|
|
513
512
|
transformedAttributes[fieldName],
|
|
514
513
|
setterContext
|
|
515
|
-
)
|
|
514
|
+
)
|
|
516
515
|
} catch (error) {
|
|
517
|
-
console.warn(`Setter for field '${fieldName}' failed:`, error)
|
|
516
|
+
console.warn(`Setter for field '${fieldName}' failed:`, error)
|
|
518
517
|
// Keep original value if setter fails
|
|
519
518
|
}
|
|
520
519
|
}
|
|
521
520
|
}
|
|
522
|
-
|
|
523
|
-
return transformedAttributes
|
|
521
|
+
|
|
522
|
+
return transformedAttributes
|
|
524
523
|
}
|
|
525
524
|
|
|
526
|
-
export async function handleRecordReturnAfterWrite({
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
525
|
+
export async function handleRecordReturnAfterWrite ({
|
|
526
|
+
context,
|
|
527
|
+
scopeName,
|
|
528
|
+
api,
|
|
529
|
+
scopes,
|
|
530
|
+
schemaStructure,
|
|
531
|
+
schemaRelationships,
|
|
532
|
+
scopeOptions,
|
|
533
|
+
vars,
|
|
534
|
+
runHooks,
|
|
535
|
+
helpers,
|
|
536
|
+
log
|
|
538
537
|
}) {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
// Fetch the current state of the record after the write operation
|
|
557
|
-
try {
|
|
558
|
-
const currentRecord = await helpers.dataGetMinimal({
|
|
559
|
-
scopeName,
|
|
560
|
-
context,
|
|
561
|
-
runHooks
|
|
562
|
-
});
|
|
563
|
-
context.minimalRecord = currentRecord;
|
|
564
|
-
} catch (error) {
|
|
565
|
-
enhancedLog.warn(`Could not fetch minimal record after ${context.method} operation`, { error, id: context.id });
|
|
566
|
-
}
|
|
538
|
+
// Create enhanced logger for warnings
|
|
539
|
+
const enhancedLog = getEnhancedLogger(log)
|
|
540
|
+
const methodSpecificHookSuffix = getMethodHookSuffix(context.method)
|
|
541
|
+
|
|
542
|
+
// Step 1: Set up record state for hooks
|
|
543
|
+
// Handle originalMinimalRecord and minimalRecord based on method type
|
|
544
|
+
if (context.method === 'DELETE') {
|
|
545
|
+
// For DELETE, keep the deleted record reference
|
|
546
|
+
if (context.minimalRecord) {
|
|
547
|
+
context.originalMinimalRecord = context.minimalRecord
|
|
548
|
+
}
|
|
549
|
+
} else {
|
|
550
|
+
// For POST, PUT, PATCH - save the original state if it exists
|
|
551
|
+
if (context.minimalRecord) {
|
|
552
|
+
context.originalMinimalRecord = context.minimalRecord
|
|
567
553
|
}
|
|
568
554
|
|
|
569
|
-
//
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
555
|
+
// Fetch the current state of the record after the write operation
|
|
556
|
+
try {
|
|
557
|
+
const currentRecord = await helpers.dataGetMinimal({
|
|
558
|
+
scopeName,
|
|
559
|
+
context,
|
|
560
|
+
runHooks
|
|
561
|
+
})
|
|
562
|
+
context.minimalRecord = currentRecord
|
|
563
|
+
} catch (error) {
|
|
564
|
+
enhancedLog.warn(`Could not fetch minimal record after ${context.method} operation`, { error, id: context.id })
|
|
578
565
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Step 2: Determine what to return based on configuration
|
|
569
|
+
const returnMode = context.returnRecordSetting[context.method]
|
|
570
|
+
|
|
571
|
+
// Case 1: Return nothing (204 No Content)
|
|
572
|
+
if (returnMode === 'no') {
|
|
573
|
+
context.responseRecord = undefined
|
|
574
|
+
await runHooks('finish')
|
|
575
|
+
await runHooks(`finish${methodSpecificHookSuffix}`)
|
|
576
|
+
return undefined
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Case 2: Return minimal record (just type and id)
|
|
580
|
+
if (returnMode === 'minimal') {
|
|
581
|
+
if (context.simplified) {
|
|
582
|
+
context.responseRecord = {
|
|
583
|
+
id: String(context.id),
|
|
584
|
+
type: scopeName
|
|
585
|
+
}
|
|
586
|
+
} else {
|
|
587
|
+
context.responseRecord = {
|
|
588
|
+
data: {
|
|
589
|
+
type: scopeName,
|
|
590
|
+
id: String(context.id)
|
|
594
591
|
}
|
|
595
|
-
|
|
596
|
-
await runHooks(`finish${methodSpecificHookSuffix}`);
|
|
597
|
-
return context.responseRecord;
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
// Case 3: Return full record
|
|
601
|
-
if (returnMode === 'full') {
|
|
602
|
-
// Fetch the complete record using the GET method
|
|
603
|
-
const fullRecord = await api.resources[scopeName].get({
|
|
604
|
-
id: context.id,
|
|
605
|
-
queryParams: context.queryParams,
|
|
606
|
-
transaction: context.transaction,
|
|
607
|
-
simplified: context.simplified
|
|
608
|
-
}, {...context });
|
|
609
|
-
|
|
610
|
-
context.responseRecord = fullRecord || undefined;
|
|
611
|
-
|
|
612
|
-
// Run finish hooks
|
|
613
|
-
await runHooks('finish');
|
|
614
|
-
await runHooks(`finish${methodSpecificHookSuffix}`);
|
|
615
|
-
|
|
616
|
-
// No transformation needed - GET already returns the correct format
|
|
617
|
-
// based on context.simplified
|
|
618
|
-
return context.responseRecord;
|
|
592
|
+
}
|
|
619
593
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
594
|
+
await runHooks('finish')
|
|
595
|
+
await runHooks(`finish${methodSpecificHookSuffix}`)
|
|
596
|
+
return context.responseRecord
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Case 3: Return full record
|
|
600
|
+
if (returnMode === 'full') {
|
|
601
|
+
// Fetch the complete record using the GET method
|
|
602
|
+
const fullRecord = await api.resources[scopeName].get({
|
|
603
|
+
id: context.id,
|
|
604
|
+
queryParams: context.queryParams,
|
|
605
|
+
transaction: context.transaction,
|
|
606
|
+
simplified: context.simplified
|
|
607
|
+
}, { ...context })
|
|
608
|
+
|
|
609
|
+
context.responseRecord = fullRecord || undefined
|
|
610
|
+
|
|
611
|
+
// Run finish hooks
|
|
612
|
+
await runHooks('finish')
|
|
613
|
+
await runHooks(`finish${methodSpecificHookSuffix}`)
|
|
614
|
+
|
|
615
|
+
// No transformation needed - GET already returns the correct format
|
|
616
|
+
// based on context.simplified
|
|
617
|
+
return context.responseRecord
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// This should never be reached, but just in case
|
|
621
|
+
throw new Error(`Invalid returnMode: ${returnMode}`)
|
|
623
622
|
}
|
|
624
623
|
|
|
625
624
|
export const findRelationshipDefinition = (schemaInfo, relationshipName) => {
|
|
626
625
|
// First check schemaRelationships (for relationships defined in relationships object)
|
|
627
|
-
const relDef = schemaInfo.schemaRelationships?.[relationshipName]
|
|
626
|
+
const relDef = schemaInfo.schemaRelationships?.[relationshipName]
|
|
628
627
|
if (relDef) {
|
|
629
|
-
return relDef
|
|
628
|
+
return relDef
|
|
630
629
|
}
|
|
631
|
-
|
|
630
|
+
|
|
632
631
|
// Then check schema fields for belongsTo relationships with matching 'as' property
|
|
633
632
|
for (const [fieldName, fieldDef] of Object.entries(schemaInfo.schemaStructure || {})) {
|
|
634
633
|
if (fieldDef.as === relationshipName && fieldDef.belongsTo) {
|
|
635
|
-
return fieldDef
|
|
634
|
+
return fieldDef
|
|
636
635
|
}
|
|
637
636
|
}
|
|
638
|
-
|
|
639
|
-
return null;
|
|
640
|
-
};
|
|
641
637
|
|
|
638
|
+
return null
|
|
639
|
+
}
|