json-rest-api 1.0.21 → 1.0.22
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/audit/00-charter.md +103 -0
- package/audit/01-scope-map.md +84 -0
- package/audit/02-coverage-matrix.csv +174 -0
- package/audit/03-invariants.md +123 -0
- package/audit/04-standards-rubric.md +56 -0
- package/audit/05-competitor-rubric.md +77 -0
- package/audit/06-findings.md +160 -0
- package/audit/07-risk-register.csv +10 -0
- package/audit/08-remediation-roadmap.md +177 -0
- package/audit/09-signoff-check.md +23 -0
- package/audit/10-final-synthesis.md +29 -0
- package/audit/evidence/code/all-files-json-rest-api.txt +168 -0
- package/audit/evidence/code/all-files-json-rest-schema.txt +12 -0
- package/audit/evidence/code/boundary-leak-checks.txt +72 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__bulk-operations-plugin.js.txt +480 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__express-plugin.js.txt +532 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__busboy-detector.js.txt +175 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__formidable-detector.js.txt +138 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__request-helpers.js.txt +222 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__file-handling-plugin.js.txt +342 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__dbTablesOperations.js.txt +304 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__fractional-positioning.js.txt +188 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__compile-schemas.js.txt +336 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__connectors-query-parser.js.txt +165 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-capabilities.js.txt +265 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-value-normalizers.js.txt +369 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__default-data-helpers.js.txt +72 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__field-utils.js.txt +139 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-constants.js.txt +45 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-field-helpers.js.txt +396 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-json-api-transformers.js.txt +162 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__payload-validators.js.txt +1314 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__schema-helpers.js.txt +488 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__scope-validations.js.txt +484 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__simplified-helpers.js.txt +857 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-cross-table-search.js.txt +565 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-json-api-transformers-querying.js.txt +485 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-pagination-helpers.js.txt +560 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-process-includes.js.txt +167 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers-base.js.txt +109 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers.js.txt +1064 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-relationship-includes.js.txt +1983 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-window-queries.js.txt +328 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__url-helpers.js.txt +119 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__storage__storage-adapter.js.txt +160 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__many-to-many-manipulations.js.txt +300 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__relationship-processor.js.txt +245 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-access.js.txt +459 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-cors-plugin.js.txt +237 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-knex-plugin.js.txt +1135 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-multihome-plugin.js.txt +314 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__compile-resource-schemas.js.txt +7 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-relationship-routes.js.txt +71 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-scope-routes.js.txt +102 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__turn-scope-init-into-vars.js.txt +49 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__validate-include-configurations.js.txt +39 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__add-route.js.txt +23 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__check-permissions.js.txt +13 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__common.js.txt +639 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete-relationship.js.txt +117 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete.js.txt +104 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__enrich-attributes.js.txt +199 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-related.js.txt +353 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-relationship.js.txt +61 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get.js.txt +184 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch-relationship.js.txt +55 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch.js.txt +191 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post-relationship.js.txt +110 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post.js.txt +168 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__put.js.txt +253 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__query.js.txt +207 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__release.js.txt +12 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin.js.txt +165 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-positioning-plugin.js.txt +668 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__socketio-plugin.js.txt +621 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__local-storage.js.txt +218 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__s3-storage.js.txt +124 -0
- package/audit/evidence/code/critical-pass1/json-rest-schema__src__CorePlugin.js.txt +269 -0
- package/audit/evidence/code/critical-pass1/json-rest-schema__src__Schema.js.txt +242 -0
- package/audit/evidence/code/critical-pass1/json-rest-schema__src__index.js.txt +77 -0
- package/audit/evidence/code/critical-pass1-risk-keywords.txt +238 -0
- package/audit/evidence/code/critical-pass1-summary.csv +67 -0
- package/audit/evidence/code/critical-pass2-summary.csv +67 -0
- package/audit/evidence/code/early-silent-zones-no-test-token.txt +57 -0
- package/audit/evidence/code/error-model-signals.txt +98 -0
- package/audit/evidence/code/git-files-json-rest-api.txt +171 -0
- package/audit/evidence/code/git-files-json-rest-schema.txt +27 -0
- package/audit/evidence/code/hook-flow-signals.txt +282 -0
- package/audit/evidence/code/idproperty-signals.txt +157 -0
- package/audit/evidence/code/import-cycles-non-anyapi.txt +3 -0
- package/audit/evidence/code/import-hotspots-non-anyapi.txt +96 -0
- package/audit/evidence/code/imports-json-rest-api.txt +354 -0
- package/audit/evidence/code/imports-json-rest-schema.txt +15 -0
- package/audit/evidence/code/in-scope-files-json-rest-api.txt +160 -0
- package/audit/evidence/code/in-scope-files-json-rest-schema.txt +13 -0
- package/audit/evidence/code/non-anyapi-core-files.txt +101 -0
- package/audit/evidence/code/permission-signals.txt +193 -0
- package/audit/evidence/code/query-include-signals.txt +687 -0
- package/audit/evidence/code/transaction-signals.txt +28 -0
- package/audit/evidence/code/validation-signals.txt +650 -0
- package/audit/evidence/code/write-path-signals.txt +61 -0
- package/audit/evidence/docs/docs-claims-signals.txt +1075 -0
- package/audit/evidence/docs/schema-docs-claims-signals.txt +179 -0
- package/audit/evidence/external/corpus-freeze.md +21 -0
- package/audit/evidence/external/npm-competitor-metadata.txt +70 -0
- package/audit/evidence/external/npm-download-signals.txt +23 -0
- package/audit/evidence/external/npm-jsonapi-server.txt +8 -0
- package/audit/evidence/external/npm-modern-jsonapi-metadata.txt +38 -0
- package/audit/evidence/external/readme-fortune-signals.txt +26 -0
- package/audit/evidence/external/readme-fortune.md +165 -0
- package/audit/evidence/external/readme-json-api-serializer-signals.txt +80 -0
- package/audit/evidence/external/readme-json-api-serializer.md +690 -0
- package/audit/evidence/external/readme-jsonapi-serde-server-signals.txt +0 -0
- package/audit/evidence/external/readme-jsonapi-serde-server.md +22 -0
- package/audit/evidence/external/readme-jsonapi-serializer-signals.txt +63 -0
- package/audit/evidence/external/readme-jsonapi-serializer.md +315 -0
- package/audit/evidence/external/readme-jsonapi-server-signals.txt +19 -0
- package/audit/evidence/external/readme-jsonapi-server.md +104 -0
- package/audit/evidence/external/readme-nestjs-json-api-signals.txt +4 -0
- package/audit/evidence/external/readme-nestjs-json-api.md +94 -0
- package/audit/evidence/external/readme-yaser-json-api-nestjs-signals.txt +47 -0
- package/audit/evidence/external/readme-yaser-json-api-nestjs.md +447 -0
- package/audit/evidence/external/standard-jsonapi-snippets.txt +84 -0
- package/audit/evidence/external/standard-rfc-snippets.txt +168 -0
- package/audit/evidence/external/standards-sources.txt +6 -0
- package/audit/evidence/runtime/baseline-commits.txt +2 -0
- package/audit/evidence/runtime/db-adapter-assumptions.txt +3 -0
- package/audit/evidence/runtime/environment.txt +4 -0
- package/audit/evidence/runtime/invariant-runtime-mapping.md +38 -0
- package/audit/evidence/runtime/probe-patch-relationship-error.exitcode +1 -0
- package/audit/evidence/runtime/probe-patch-relationship-error.log +76 -0
- package/audit/evidence/runtime/probe-patch-relationship-error.mjs +67 -0
- package/audit/evidence/runtime/probe-patch-relationship-referenceerror.exitcode +1 -0
- package/audit/evidence/runtime/probe-patch-relationship-referenceerror.log +62 -0
- package/audit/evidence/runtime/probe-patch-relationship-referenceerror.mjs +47 -0
- package/audit/evidence/runtime/test-exitcodes.txt +4 -0
- package/audit/evidence/runtime/test-summary.md +107 -0
- package/audit/evidence/runtime/trace-anchors.txt +61 -0
- package/audit/evidence/tests/access-multitenancy-test-signals.txt +125 -0
- package/audit/evidence/tests/failing-tests-summary.txt +25 -0
- package/audit/evidence/tests/npm-test-json-rest-api.exitcode +1 -0
- package/audit/evidence/tests/npm-test-json-rest-api.log +5885 -0
- package/audit/evidence/tests/npm-test-json-rest-schema.exitcode +1 -0
- package/audit/evidence/tests/npm-test-json-rest-schema.log +356 -0
- package/audit/evidence/tests/query-include-test-signals.txt +948 -0
- package/audit/evidence/tests/relationship-test-signals.txt +223 -0
- package/audit/evidence/tests/skipped-tests.txt +10 -0
- package/audit/evidence/tests/test-suite-list-non-anyapi.txt +30 -0
- package/audit/evidence/tests/test-suite-list.txt +32 -0
- package/audit/subsystems/docs-drift.md +49 -0
- package/audit/subsystems/pass2-cross-review.md +131 -0
- package/audit/subsystems/plugins-access.md +51 -0
- package/audit/subsystems/plugins-bulk.md +50 -0
- package/audit/subsystems/plugins-cors.md +48 -0
- package/audit/subsystems/plugins-file.md +53 -0
- package/audit/subsystems/plugins-multihome.md +50 -0
- package/audit/subsystems/plugins-positioning.md +54 -0
- package/audit/subsystems/plugins-socketio.md +50 -0
- package/audit/subsystems/query-engine.md +61 -0
- package/audit/subsystems/relationships.md +55 -0
- package/audit/subsystems/rest-orchestration.md +66 -0
- package/audit/subsystems/schema-core.md +64 -0
- package/audit/subsystems/storage-knex.md +57 -0
- package/audit/subsystems/tests.md +48 -0
- package/audit/subsystems/transport-express.md +55 -0
- package/audit/traces/trace-read-get.md +22 -0
- package/audit/traces/trace-read-query.md +25 -0
- package/audit/traces/trace-relationship-endpoints.md +31 -0
- package/audit/traces/trace-write-delete.md +17 -0
- package/audit/traces/trace-write-patch.md +26 -0
- package/audit/traces/trace-write-post.md +22 -0
- package/audit/traces/trace-write-put.md +23 -0
- package/package.json +3 -3
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
1 import { RestApiResourceError } from '../../../lib/rest-api-errors.js'
|
|
2
|
+
2 import { validatePatchPayload } from '../lib/querying-writing/payload-validators.js'
|
|
3
|
+
3 import { processRelationships } from '../lib/writing/relationship-processor.js'
|
|
4
|
+
4 import { updateManyToManyRelationship } from '../lib/writing/many-to-many-manipulations.js'
|
|
5
|
+
5 import { ERROR_SUBTYPES } from '../lib/querying-writing/knex-constants.js'
|
|
6
|
+
6 import {
|
|
7
|
+
7 setupCommonRequest,
|
|
8
|
+
8 validateResourceAttributesBeforeWrite,
|
|
9
|
+
9 validateRelationshipAccess,
|
|
10
|
+
10 applyFieldSetters,
|
|
11
|
+
11 validatePivotResource,
|
|
12
|
+
12 handleRecordReturnAfterWrite,
|
|
13
|
+
13 handleWriteMethodError
|
|
14
|
+
14 } from './common.js'
|
|
15
|
+
15
|
|
16
|
+
16 /**
|
|
17
|
+
17 * PATCH
|
|
18
|
+
18 * Performs a partial update on an existing resource's attributes or relationships.
|
|
19
|
+
19 * Unlike PUT, PATCH only updates the fields provided, leaving other fields unchanged.
|
|
20
|
+
20 * This method supports updating both attributes and relationships (1:1 and n:n).
|
|
21
|
+
21 * For relationships, only the ones explicitly provided will be updated.
|
|
22
|
+
22 * Just like PUT, it CANNOT have the `included` array in data.
|
|
23
|
+
23 */
|
|
24
|
+
24 export default async function patchMethod ({
|
|
25
|
+
25 params,
|
|
26
|
+
26 context,
|
|
27
|
+
27 vars,
|
|
28
|
+
28 helpers,
|
|
29
|
+
29 scope,
|
|
30
|
+
30 scopes,
|
|
31
|
+
31 runHooks,
|
|
32
|
+
32 apiOptions,
|
|
33
|
+
33 pluginOptions,
|
|
34
|
+
34 scopeOptions,
|
|
35
|
+
35 scopeName,
|
|
36
|
+
36 api,
|
|
37
|
+
37 log
|
|
38
|
+
38 }) {
|
|
39
|
+
39 context.method = 'patch'
|
|
40
|
+
40
|
|
41
|
+
41 try {
|
|
42
|
+
42 const { schema, schemaStructure, schemaRelationships } = await setupCommonRequest({
|
|
43
|
+
43 params,
|
|
44
|
+
44 context,
|
|
45
|
+
45 vars,
|
|
46
|
+
46 scopes,
|
|
47
|
+
47 scopeOptions,
|
|
48
|
+
48 scopeName,
|
|
49
|
+
49 api,
|
|
50
|
+
50 helpers
|
|
51
|
+
51 })
|
|
52
|
+
52 context.id = context.inputRecord.data.id
|
|
53
|
+
53
|
|
54
|
+
54 // Run early hooks for pre-processing (e.g., file handling)
|
|
55
|
+
55 await runHooks('beforeProcessing')
|
|
56
|
+
56 await runHooks('beforeProcessingPatch')
|
|
57
|
+
57
|
|
58
|
+
58 // Validate PATCH payload to ensure the partial update actually contains changes.
|
|
59
|
+
59 // PATCH requests must include either attributes to update or relationships to modify -
|
|
60
|
+
60 // an empty PATCH is invalid. This prevents accidental no-op requests and ensures clients
|
|
61
|
+
61 // are explicit about what they want to change. Unlike PUT, PATCH preserves all fields
|
|
62
|
+
62 // not mentioned in the request.
|
|
63
|
+
63 // Example: data must have either attributes: {title: 'New'} or relationships: {author: {...}}
|
|
64
|
+
64 validatePatchPayload(context.inputRecord, scopes)
|
|
65
|
+
65
|
|
66
|
+
66 // Validate that user has read access to all related resources
|
|
67
|
+
67 // This ensures users can only create relationships to resources they can access
|
|
68
|
+
68 await validateRelationshipAccess(context, context.inputRecord, helpers, runHooks, api)
|
|
69
|
+
69
|
|
70
|
+
70 // Extract foreign keys from JSON:API relationships and prepare many-to-many operations
|
|
71
|
+
71 // Example: relationships.author -> author_id: '123' for storage
|
|
72
|
+
72 // Example: relationships.tags -> array of pivot records to create later (only for provided relationships in PATCH)
|
|
73
|
+
73 const { belongsToUpdates, manyToManyRelationships } = processRelationships(
|
|
74
|
+
74 scope,
|
|
75
|
+
75 { context }
|
|
76
|
+
76 )
|
|
77
|
+
77
|
|
78
|
+
78 await validateResourceAttributesBeforeWrite({
|
|
79
|
+
79 context,
|
|
80
|
+
80 schema,
|
|
81
|
+
81 belongsToUpdates,
|
|
82
|
+
82 runHooks,
|
|
83
|
+
83 isPartialValidation: true
|
|
84
|
+
84 })
|
|
85
|
+
85
|
|
86
|
+
86 // Fetch minimal record for authorization checks
|
|
87
|
+
87 const minimalRecord = await helpers.dataGetMinimal({
|
|
88
|
+
88 scopeName,
|
|
89
|
+
89 context,
|
|
90
|
+
90 runHooks
|
|
91
|
+
91 })
|
|
92
|
+
92
|
|
93
|
+
93 if (!minimalRecord) {
|
|
94
|
+
94 throw new RestApiResourceError(
|
|
95
|
+
95 `Resource not found: ${scopeName}/${context.id}`,
|
|
96
|
+
96 ERROR_SUBTYPES.NOT_FOUND
|
|
97
|
+
97 )
|
|
98
|
+
98 }
|
|
99
|
+
99
|
|
100
|
+
100 context.minimalRecord = minimalRecord
|
|
101
|
+
101
|
|
102
|
+
102 // Centralised checkPermissions function
|
|
103
|
+
103 await scope.checkPermissions({
|
|
104
|
+
104 method: 'patch',
|
|
105
|
+
105 originalContext: context,
|
|
106
|
+
106 })
|
|
107
|
+
107
|
|
108
|
+
108 // Merge belongsTo updates into attributes before patching the record
|
|
109
|
+
109 if (Object.keys(belongsToUpdates).length > 0) {
|
|
110
|
+
110 context.inputRecord.data.attributes = {
|
|
111
|
+
111 ...context.inputRecord.data.attributes,
|
|
112
|
+
112 ...belongsToUpdates
|
|
113
|
+
113 }
|
|
114
|
+
114 }
|
|
115
|
+
115
|
|
116
|
+
116 await runHooks('beforeDataCall')
|
|
117
|
+
117 await runHooks('beforeDataCallPatch')
|
|
118
|
+
118
|
|
119
|
+
119 // Apply field setters after validation and before storage
|
|
120
|
+
120 if (context.inputRecord?.data?.attributes) {
|
|
121
|
+
121 context.inputRecord.data.attributes = await applyFieldSetters(
|
|
122
|
+
122 context.inputRecord.data.attributes,
|
|
123
|
+
123 context.schemaInfo,
|
|
124
|
+
124 context,
|
|
125
|
+
125 api,
|
|
126
|
+
126 helpers
|
|
127
|
+
127 )
|
|
128
|
+
128 }
|
|
129
|
+
129
|
|
130
|
+
130 // Call the storage helper - should return the patched record
|
|
131
|
+
131 await helpers.dataPatch({
|
|
132
|
+
132 scopeName,
|
|
133
|
+
133 context
|
|
134
|
+
134 })
|
|
135
|
+
135
|
|
136
|
+
136 await runHooks('afterDataCallPatch')
|
|
137
|
+
137 await runHooks('afterDataCall')
|
|
138
|
+
138
|
|
139
|
+
139 // Process many-to-many relationships after main record update
|
|
140
|
+
140 // For PATCH, we only update the relationships that were explicitly provided
|
|
141
|
+
141 for (const { relName, relDef, relData } of manyToManyRelationships) {
|
|
142
|
+
142 if (relDef?.through && api.anyapi?.links?.syncMany) {
|
|
143
|
+
143 await api.anyapi.links.syncMany({
|
|
144
|
+
144 context,
|
|
145
|
+
145 scopeName,
|
|
146
|
+
146 relName,
|
|
147
|
+
147 relDef,
|
|
148
|
+
148 relData,
|
|
149
|
+
149 isUpdate: true,
|
|
150
|
+
150 })
|
|
151
|
+
151 continue
|
|
152
|
+
152 }
|
|
153
|
+
153
|
|
154
|
+
154 validatePivotResource(scopes, relDef, relName)
|
|
155
|
+
155
|
|
156
|
+
156 await updateManyToManyRelationship(null, {
|
|
157
|
+
157 api,
|
|
158
|
+
158 context: {
|
|
159
|
+
159 resourceId: context.id,
|
|
160
|
+
160 relDef,
|
|
161
|
+
161 relData,
|
|
162
|
+
162 transaction: context.transaction
|
|
163
|
+
163 }
|
|
164
|
+
164 })
|
|
165
|
+
165 }
|
|
166
|
+
166
|
|
167
|
+
167 const ret = await handleRecordReturnAfterWrite({
|
|
168
|
+
168 context,
|
|
169
|
+
169 scopeName,
|
|
170
|
+
170 api,
|
|
171
|
+
171 scopes,
|
|
172
|
+
172 schemaStructure,
|
|
173
|
+
173 schemaRelationships,
|
|
174
|
+
174 scopeOptions,
|
|
175
|
+
175 vars,
|
|
176
|
+
176 runHooks,
|
|
177
|
+
177 helpers,
|
|
178
|
+
178 log
|
|
179
|
+
179 })
|
|
180
|
+
180
|
|
181
|
+
181 // Commit transaction if we created it
|
|
182
|
+
182 if (context.shouldCommit) {
|
|
183
|
+
183 await context.transaction.commit()
|
|
184
|
+
184 await runHooks('afterCommit')
|
|
185
|
+
185 }
|
|
186
|
+
186
|
|
187
|
+
187 return ret
|
|
188
|
+
188 } catch (error) {
|
|
189
|
+
189 await handleWriteMethodError(error, context, 'PATCH', scopeName, log, runHooks)
|
|
190
|
+
190 }
|
|
191
|
+
191 }
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
1 import { RestApiResourceError, RestApiValidationError, RestApiPayloadError } from '../../../lib/rest-api-errors.js'
|
|
2
|
+
2 import { findRelationshipDefinition, handleWriteMethodError } from './common.js'
|
|
3
|
+
3 import { createPivotRecords } from '../lib/writing/many-to-many-manipulations.js'
|
|
4
|
+
4
|
|
5
|
+
5 /**
|
|
6
|
+
6 * POST RELATIONSHIP
|
|
7
|
+
7 * Adds members to a to-many relationship
|
|
8
|
+
8 * POST /api/articles/1/relationships/tags
|
|
9
|
+
9 *
|
|
10
|
+
10 * @param {string} id - The ID of the resource
|
|
11
|
+
11 * @param {string} relationshipName - The name of the relationship
|
|
12
|
+
12 * @param {array} relationshipData - Array of resource identifiers to add
|
|
13
|
+
13 * @returns {Promise<void>} 204 No Content
|
|
14
|
+
14 */
|
|
15
|
+
15 export default async function postRelationshipMethod ({ params, context, vars, helpers, scope, scopes, runHooks, scopeName, api, log }) {
|
|
16
|
+
16 context.method = 'postRelationship'
|
|
17
|
+
17 context.id = params.id
|
|
18
|
+
18 context.relationshipName = params.relationshipName
|
|
19
|
+
19 context.schemaInfo = scopes[scopeName].vars.schemaInfo
|
|
20
|
+
20
|
|
21
|
+
21 // Transaction handling
|
|
22
|
+
22 context.transaction = params.transaction ||
|
|
23
|
+
23 (helpers.newTransaction && !params.transaction ? await helpers.newTransaction() : null)
|
|
24
|
+
24 context.shouldCommit = !params.transaction && !!context.transaction
|
|
25
|
+
25
|
|
26
|
+
26 context.db = context.transaction || api.knex.instance
|
|
27
|
+
27
|
|
28
|
+
28 try {
|
|
29
|
+
29 // Validate
|
|
30
|
+
30 const relDef = findRelationshipDefinition(context.schemaInfo, context.relationshipName)
|
|
31
|
+
31 if (!relDef) {
|
|
32
|
+
32 throw new RestApiResourceError(
|
|
33
|
+
33 `Relationship '${context.relationshipName}' not found on resource '${scopeName}'`,
|
|
34
|
+
34 { subtype: 'relationship_not_found' }
|
|
35
|
+
35 )
|
|
36
|
+
36 }
|
|
37
|
+
37
|
|
38
|
+
38 if (relDef.type !== 'hasMany' && relDef.type !== 'manyToMany') {
|
|
39
|
+
39 throw new RestApiValidationError(
|
|
40
|
+
40 `Cannot POST to to-one relationship '${context.relationshipName}'. Use PATCH instead.`,
|
|
41
|
+
41 { fields: ['data'] }
|
|
42
|
+
42 )
|
|
43
|
+
43 }
|
|
44
|
+
44
|
|
45
|
+
45 if (!Array.isArray(params.relationshipData)) {
|
|
46
|
+
46 throw new RestApiPayloadError('POST to relationship requires array of resource identifiers')
|
|
47
|
+
47 }
|
|
48
|
+
48
|
|
49
|
+
49 // Check permissions
|
|
50
|
+
50 await runHooks('checkPermissions')
|
|
51
|
+
51 await runHooks('checkPermissionsPostRelationship')
|
|
52
|
+
52
|
|
53
|
+
53 // Verify parent exists
|
|
54
|
+
54 const exists = await helpers.dataExists({
|
|
55
|
+
55 scopeName,
|
|
56
|
+
56 context: { db: context.db, id: context.id, schemaInfo: context.schemaInfo },
|
|
57
|
+
57 })
|
|
58
|
+
58
|
|
59
|
+
59 if (!exists) {
|
|
60
|
+
60 throw new RestApiResourceError('Resource not found', { subtype: 'not_found' })
|
|
61
|
+
61 }
|
|
62
|
+
62
|
|
63
|
+
63 // Add relationships
|
|
64
|
+
64 if (relDef?.through) {
|
|
65
|
+
65 if (api.anyapi?.links?.attachMany) {
|
|
66
|
+
66 await api.anyapi.links.attachMany({
|
|
67
|
+
67 context,
|
|
68
|
+
68 scopeName,
|
|
69
|
+
69 relName: context.relationshipName,
|
|
70
|
+
70 relDef,
|
|
71
|
+
71 relData: params.relationshipData,
|
|
72
|
+
72 })
|
|
73
|
+
73 } else {
|
|
74
|
+
74 await createPivotRecords(api, context.id, {
|
|
75
|
+
75 through: relDef.through,
|
|
76
|
+
76 foreignKey: relDef.foreignKey,
|
|
77
|
+
77 otherKey: relDef.otherKey
|
|
78
|
+
78 }, params.relationshipData, context.transaction)
|
|
79
|
+
79 }
|
|
80
|
+
80 } else if (relDef.type === 'hasMany') {
|
|
81
|
+
81 // Update foreign keys for hasMany
|
|
82
|
+
82 const targetType = relDef.target
|
|
83
|
+
83 for (const identifier of params.relationshipData) {
|
|
84
|
+
84 await api.resources[targetType].patch({
|
|
85
|
+
85 id: identifier.id,
|
|
86
|
+
86 inputRecord: {
|
|
87
|
+
87 data: {
|
|
88
|
+
88 type: targetType,
|
|
89
|
+
89 id: identifier.id,
|
|
90
|
+
90 attributes: { [relDef.foreignKey]: context.id }
|
|
91
|
+
91 }
|
|
92
|
+
92 },
|
|
93
|
+
93 transaction: context.transaction,
|
|
94
|
+
94 simplified: false
|
|
95
|
+
95 })
|
|
96
|
+
96 }
|
|
97
|
+
97 }
|
|
98
|
+
98
|
|
99
|
+
99 await runHooks('finish')
|
|
100
|
+
100 await runHooks('finishPostRelationship')
|
|
101
|
+
101
|
|
102
|
+
102 if (context.shouldCommit) {
|
|
103
|
+
103 await context.transaction.commit()
|
|
104
|
+
104 }
|
|
105
|
+
105
|
|
106
|
+
106 // 204 No Content
|
|
107
|
+
107 } catch (error) {
|
|
108
|
+
108 await handleWriteMethodError(error, context, 'POST_RELATIONSHIP', scopeName, log, runHooks)
|
|
109
|
+
109 }
|
|
110
|
+
110 };
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
1 import { validatePostPayload } from '../lib/querying-writing/payload-validators.js'
|
|
2
|
+
2 import { processRelationships } from '../lib/writing/relationship-processor.js'
|
|
3
|
+
3 import { createPivotRecords } from '../lib/writing/many-to-many-manipulations.js'
|
|
4
|
+
4 import {
|
|
5
|
+
5 setupCommonRequest,
|
|
6
|
+
6 validateResourceAttributesBeforeWrite,
|
|
7
|
+
7 validateRelationshipAccess,
|
|
8
|
+
8 applyFieldSetters,
|
|
9
|
+
9 validatePivotResource,
|
|
10
|
+
10 handleRecordReturnAfterWrite,
|
|
11
|
+
11 handleWriteMethodError
|
|
12
|
+
12 } from './common.js'
|
|
13
|
+
13
|
|
14
|
+
14 /**
|
|
15
|
+
15 * POST
|
|
16
|
+
16 * Creates a new resource. The request must include a JSON:API document with a 'data' object
|
|
17
|
+
17 * containing 'type' and 'attributes'. It can also establish relationships to existing resources.
|
|
18
|
+
18 * The returned document contains the created resource with its server-assigned ID.
|
|
19
|
+
19 */
|
|
20
|
+
20 export default async function postMethod ({
|
|
21
|
+
21 params,
|
|
22
|
+
22 context,
|
|
23
|
+
23 vars,
|
|
24
|
+
24 helpers,
|
|
25
|
+
25 scope,
|
|
26
|
+
26 scopes,
|
|
27
|
+
27 runHooks,
|
|
28
|
+
28 apiOptions,
|
|
29
|
+
29 pluginOptions,
|
|
30
|
+
30 scopeOptions,
|
|
31
|
+
31 scopeName,
|
|
32
|
+
32 api,
|
|
33
|
+
33 log
|
|
34
|
+
34 }) {
|
|
35
|
+
35 context.method = 'post'
|
|
36
|
+
36
|
|
37
|
+
37 try {
|
|
38
|
+
38 const { schema, schemaStructure, schemaRelationships } = await setupCommonRequest({
|
|
39
|
+
39 params,
|
|
40
|
+
40 context,
|
|
41
|
+
41 vars,
|
|
42
|
+
42 scopes,
|
|
43
|
+
43 scopeOptions,
|
|
44
|
+
44 scopeName,
|
|
45
|
+
45 api,
|
|
46
|
+
46 helpers
|
|
47
|
+
47 })
|
|
48
|
+
48
|
|
49
|
+
49 // Run early hooks for pre-processing (e.g., file handling)
|
|
50
|
+
50 await runHooks('beforeProcessing')
|
|
51
|
+
51 await runHooks('beforeProcessingPost')
|
|
52
|
+
52
|
|
53
|
+
53 // Validate POST payload to ensure it follows JSON:API format and references valid resources.
|
|
54
|
+
54 // This checks the payload has required 'data' object with 'type' and 'attributes', validates
|
|
55
|
+
55 // that data.type matches a real resource type (preventing creation of non-existent resources),
|
|
56
|
+
56 // and ensures any relationships reference valid resource types with proper ID format.
|
|
57
|
+
57 // Example: data.type: 'articles' must be a registered scope, relationships.author must reference 'users'.
|
|
58
|
+
58 validatePostPayload(context.inputRecord, scopes)
|
|
59
|
+
59
|
|
60
|
+
60 // Validate that user has read access to all related resources
|
|
61
|
+
61 // This ensures users can only create relationships to resources they can access
|
|
62
|
+
62 await validateRelationshipAccess(context, context.inputRecord, helpers, runHooks, api)
|
|
63
|
+
63
|
|
64
|
+
64 // Extract foreign keys from JSON:API relationships and prepare many-to-many operations
|
|
65
|
+
65 // Example: relationships.author -> author_id: '123' for storage
|
|
66
|
+
66 // Example: relationships.tags -> array of pivot records to create later
|
|
67
|
+
67 const { belongsToUpdates, manyToManyRelationships } = await processRelationships(
|
|
68
|
+
68 scope,
|
|
69
|
+
69 { context }
|
|
70
|
+
70 )
|
|
71
|
+
71
|
|
72
|
+
72 // Merge belongsTo updates into attributes before validation (like PUT/PATCH do)
|
|
73
|
+
73 if (Object.keys(belongsToUpdates).length > 0) {
|
|
74
|
+
74 context.inputRecord.data.attributes = {
|
|
75
|
+
75 ...context.inputRecord.data.attributes,
|
|
76
|
+
76 ...belongsToUpdates
|
|
77
|
+
77 }
|
|
78
|
+
78 }
|
|
79
|
+
79
|
|
80
|
+
80 await validateResourceAttributesBeforeWrite({
|
|
81
|
+
81 context,
|
|
82
|
+
82 schema,
|
|
83
|
+
83 belongsToUpdates,
|
|
84
|
+
84 runHooks
|
|
85
|
+
85 })
|
|
86
|
+
86
|
|
87
|
+
87 if (context.inputRecord?.data) {
|
|
88
|
+
88 const inputData = context.inputRecord.data
|
|
89
|
+
89 context.minimalRecord = {
|
|
90
|
+
90 type: scopeName,
|
|
91
|
+
91 ...(inputData.id !== undefined && inputData.id !== null ? { id: String(inputData.id) } : {}),
|
|
92
|
+
92 attributes: structuredClone(inputData.attributes || {}),
|
|
93
|
+
93 relationships: structuredClone(inputData.relationships || {})
|
|
94
|
+
94 }
|
|
95
|
+
95 }
|
|
96
|
+
96
|
|
97
|
+
97 // Centralised checkPermissions function
|
|
98
|
+
98 await scope.checkPermissions({
|
|
99
|
+
99 method: 'post',
|
|
100
|
+
100 originalContext: context,
|
|
101
|
+
101 })
|
|
102
|
+
102
|
|
103
|
+
103 await runHooks('beforeDataCall')
|
|
104
|
+
104 await runHooks('beforeDataCallPost')
|
|
105
|
+
105
|
|
106
|
+
106 // Apply field setters after validation and before storage
|
|
107
|
+
107 if (context.inputRecord?.data?.attributes) {
|
|
108
|
+
108 context.inputRecord.data.attributes = await applyFieldSetters(
|
|
109
|
+
109 context.inputRecord.data.attributes,
|
|
110
|
+
110 context.schemaInfo,
|
|
111
|
+
111 context,
|
|
112
|
+
112 api,
|
|
113
|
+
113 helpers
|
|
114
|
+
114 )
|
|
115
|
+
115 }
|
|
116
|
+
116
|
|
117
|
+
117 // Create the main record - storage helper should return the created record with its ID
|
|
118
|
+
118 context.id = await helpers.dataPost({
|
|
119
|
+
119 scopeName,
|
|
120
|
+
120 context
|
|
121
|
+
121 })
|
|
122
|
+
122
|
|
123
|
+
123 await runHooks('afterDataCallPost')
|
|
124
|
+
124 await runHooks('afterDataCall')
|
|
125
|
+
125
|
|
126
|
+
126 // Process many-to-many relationships after main record creation
|
|
127
|
+
127 for (const { relName, relDef, relData } of manyToManyRelationships) {
|
|
128
|
+
128 if (api.anyapi?.links?.attachMany && relDef?.through) {
|
|
129
|
+
129 await api.anyapi.links.attachMany({
|
|
130
|
+
130 context,
|
|
131
|
+
131 scopeName,
|
|
132
|
+
132 relName,
|
|
133
|
+
133 relDef,
|
|
134
|
+
134 relData,
|
|
135
|
+
135 })
|
|
136
|
+
136 continue
|
|
137
|
+
137 }
|
|
138
|
+
138
|
|
139
|
+
139 // Validate pivot resource exists
|
|
140
|
+
140 validatePivotResource(scopes, relDef, relName)
|
|
141
|
+
141 await createPivotRecords(api, context.id, relDef, relData, context.transaction)
|
|
142
|
+
142 }
|
|
143
|
+
143
|
|
144
|
+
144 const ret = await handleRecordReturnAfterWrite({
|
|
145
|
+
145 context,
|
|
146
|
+
146 scopeName,
|
|
147
|
+
147 api,
|
|
148
|
+
148 scopes,
|
|
149
|
+
149 schemaStructure,
|
|
150
|
+
150 schemaRelationships,
|
|
151
|
+
151 scopeOptions,
|
|
152
|
+
152 vars,
|
|
153
|
+
153 runHooks,
|
|
154
|
+
154 helpers,
|
|
155
|
+
155 log
|
|
156
|
+
156 })
|
|
157
|
+
157
|
|
158
|
+
158 // Commit transaction if we created it
|
|
159
|
+
159 if (context.shouldCommit) {
|
|
160
|
+
160 await context.transaction.commit()
|
|
161
|
+
161 await runHooks('afterCommit')
|
|
162
|
+
162 }
|
|
163
|
+
163
|
|
164
|
+
164 return ret
|
|
165
|
+
165 } catch (error) {
|
|
166
|
+
166 await handleWriteMethodError(error, context, 'POST', scopeName, log, runHooks)
|
|
167
|
+
167 }
|
|
168
|
+
168 }
|