json-rest-api 1.0.22 → 1.0.23
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/CLAUDE.md +1 -2
- package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +180 -0
- package/SINGLE_VALIDATION_CONTRACT_TODO.txt +172 -0
- package/docs/API.md +51 -14
- package/docs/COMPARISON.md +2 -2
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +28 -1
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +5 -1
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +3 -1
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +12 -60
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +2 -2
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -1
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +2 -0
- package/docs/GUIDE/GUIDE_X_Autofiltering.md +355 -0
- package/docs/GUIDE/GUIDE_X_Fastify.md +78 -0
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +250 -0
- package/docs/GUIDE/GUIDE_X_Query_Projections.md +159 -0
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +199 -0
- package/docs/GUIDE/index.md +18 -9
- package/docs/QUICKSTART.md +5 -1
- package/docs/index.md +1 -1
- package/index.js +3 -2
- package/package.json +2 -2
- package/plugins/core/connectors/express-plugin.js +97 -184
- package/plugins/core/connectors/fastify-plugin.js +206 -0
- package/plugins/core/connectors/lib/connector-core.js +195 -0
- package/plugins/core/connectors/lib/transport-http-helpers.js +122 -0
- package/plugins/core/connectors/lib/transport-route-schemas.js +92 -0
- package/plugins/core/lib/dbIntrospection.js +1078 -0
- package/plugins/core/lib/dbTablesOperations.js +1104 -160
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +5 -2
- package/plugins/core/lib/querying/knex-process-includes.js +3 -4
- package/plugins/core/lib/querying/knex-query-helpers.js +17 -71
- package/plugins/core/lib/querying/knex-relationship-includes.js +466 -206
- package/plugins/core/lib/querying/query-builder-utils.js +19 -0
- package/plugins/core/lib/querying/query-field-sort-helpers.js +135 -0
- package/plugins/core/lib/querying/sort-helpers.js +33 -0
- package/plugins/core/lib/querying/storage-adapter-utils.js +51 -0
- package/plugins/core/lib/querying/url-helpers.js +8 -2
- package/plugins/core/lib/querying-writing/compile-schemas.js +12 -1
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +83 -13
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +13 -12
- package/plugins/core/lib/querying-writing/query-field-helpers.js +88 -0
- package/plugins/core/lib/querying-writing/request-contracts.js +590 -0
- package/plugins/core/lib/storage/canonical-storage-mapping.js +113 -0
- package/plugins/core/lib/storage/storage-adapter.js +79 -2
- package/plugins/core/lib/storage/storage-mapping.js +204 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +355 -228
- package/plugins/core/rest-api-autofilter-plugin.js +414 -0
- package/plugins/core/rest-api-knex-plugin.js +249 -211
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +10 -5
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +12 -6
- package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +41 -0
- package/plugins/core/rest-api-plugin-methods/common.js +121 -83
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +6 -1
- package/plugins/core/rest-api-plugin-methods/get.js +23 -14
- package/plugins/core/rest-api-plugin-methods/patch.js +28 -12
- package/plugins/core/rest-api-plugin-methods/post.js +13 -8
- package/plugins/core/rest-api-plugin-methods/put.js +45 -21
- package/plugins/core/rest-api-plugin-methods/query.js +15 -53
- package/plugins/core/rest-api-plugin.js +2 -0
- package/plugins/core/rest-api-positioning-plugin.js +1 -1
- package/plugins/core/rest-api-query-projections-plugin.js +130 -0
- package/plugins/core/socketio-plugin.js +1 -3
- package/tests/autofilter.test.js +463 -0
- package/tests/custom-id-simplified-post.test.js +75 -0
- package/tests/custom-idproperty-relationships.test.js +33 -0
- package/tests/db-introspection.test.js +518 -0
- package/tests/db-table-operations.test.js +781 -0
- package/tests/fastify-plugin.test.js +141 -0
- package/tests/field-getters.test.js +1 -2
- package/tests/field-setters.test.js +0 -4
- package/tests/fixtures/api-configs.js +188 -219
- package/tests/helpers/fake-fastify.js +99 -0
- package/tests/http-connectors-parity.test.js +235 -0
- package/tests/projected-fields.test.js +319 -0
- package/tests/relationships.test.js +3 -3
- package/tests/searchschema-knex-behavior.test.js +187 -0
- package/tests/searchschema-merge.test.js +21 -69
- package/tests/storage-mapping.test.js +210 -0
- package/audit/00-charter.md +0 -103
- package/audit/01-scope-map.md +0 -84
- package/audit/02-coverage-matrix.csv +0 -174
- package/audit/03-invariants.md +0 -123
- package/audit/04-standards-rubric.md +0 -56
- package/audit/05-competitor-rubric.md +0 -77
- package/audit/06-findings.md +0 -160
- package/audit/07-risk-register.csv +0 -10
- package/audit/08-remediation-roadmap.md +0 -177
- package/audit/09-signoff-check.md +0 -23
- package/audit/10-final-synthesis.md +0 -29
- package/audit/evidence/code/all-files-json-rest-api.txt +0 -168
- package/audit/evidence/code/all-files-json-rest-schema.txt +0 -12
- package/audit/evidence/code/boundary-leak-checks.txt +0 -72
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__bulk-operations-plugin.js.txt +0 -480
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__express-plugin.js.txt +0 -532
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__busboy-detector.js.txt +0 -175
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__formidable-detector.js.txt +0 -138
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__request-helpers.js.txt +0 -222
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__file-handling-plugin.js.txt +0 -342
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__dbTablesOperations.js.txt +0 -304
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__fractional-positioning.js.txt +0 -188
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__compile-schemas.js.txt +0 -336
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__connectors-query-parser.js.txt +0 -165
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-capabilities.js.txt +0 -265
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-value-normalizers.js.txt +0 -369
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__default-data-helpers.js.txt +0 -72
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__field-utils.js.txt +0 -139
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-constants.js.txt +0 -45
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-field-helpers.js.txt +0 -396
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-json-api-transformers.js.txt +0 -162
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__payload-validators.js.txt +0 -1314
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__schema-helpers.js.txt +0 -488
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__scope-validations.js.txt +0 -484
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__simplified-helpers.js.txt +0 -857
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-cross-table-search.js.txt +0 -565
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-json-api-transformers-querying.js.txt +0 -485
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-pagination-helpers.js.txt +0 -560
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-process-includes.js.txt +0 -167
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers-base.js.txt +0 -109
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers.js.txt +0 -1064
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-relationship-includes.js.txt +0 -1983
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-window-queries.js.txt +0 -328
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__url-helpers.js.txt +0 -119
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__storage__storage-adapter.js.txt +0 -160
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__many-to-many-manipulations.js.txt +0 -300
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__relationship-processor.js.txt +0 -245
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-access.js.txt +0 -459
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-cors-plugin.js.txt +0 -237
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-knex-plugin.js.txt +0 -1135
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-multihome-plugin.js.txt +0 -314
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__compile-resource-schemas.js.txt +0 -7
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-relationship-routes.js.txt +0 -71
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-scope-routes.js.txt +0 -102
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__turn-scope-init-into-vars.js.txt +0 -49
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__validate-include-configurations.js.txt +0 -39
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__add-route.js.txt +0 -23
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__check-permissions.js.txt +0 -13
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__common.js.txt +0 -639
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete-relationship.js.txt +0 -117
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete.js.txt +0 -104
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__enrich-attributes.js.txt +0 -199
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-related.js.txt +0 -353
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-relationship.js.txt +0 -61
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get.js.txt +0 -184
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch-relationship.js.txt +0 -55
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch.js.txt +0 -191
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post-relationship.js.txt +0 -110
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post.js.txt +0 -168
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__put.js.txt +0 -253
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__query.js.txt +0 -207
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__release.js.txt +0 -12
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin.js.txt +0 -165
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-positioning-plugin.js.txt +0 -668
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__socketio-plugin.js.txt +0 -621
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__local-storage.js.txt +0 -218
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__s3-storage.js.txt +0 -124
- package/audit/evidence/code/critical-pass1/json-rest-schema__src__CorePlugin.js.txt +0 -269
- package/audit/evidence/code/critical-pass1/json-rest-schema__src__Schema.js.txt +0 -242
- package/audit/evidence/code/critical-pass1/json-rest-schema__src__index.js.txt +0 -77
- package/audit/evidence/code/critical-pass1-risk-keywords.txt +0 -238
- package/audit/evidence/code/critical-pass1-summary.csv +0 -67
- package/audit/evidence/code/critical-pass2-summary.csv +0 -67
- package/audit/evidence/code/early-silent-zones-no-test-token.txt +0 -57
- package/audit/evidence/code/error-model-signals.txt +0 -98
- package/audit/evidence/code/git-files-json-rest-api.txt +0 -171
- package/audit/evidence/code/git-files-json-rest-schema.txt +0 -27
- package/audit/evidence/code/hook-flow-signals.txt +0 -282
- package/audit/evidence/code/idproperty-signals.txt +0 -157
- package/audit/evidence/code/import-cycles-non-anyapi.txt +0 -3
- package/audit/evidence/code/import-hotspots-non-anyapi.txt +0 -96
- package/audit/evidence/code/imports-json-rest-api.txt +0 -354
- package/audit/evidence/code/imports-json-rest-schema.txt +0 -15
- package/audit/evidence/code/in-scope-files-json-rest-api.txt +0 -160
- package/audit/evidence/code/in-scope-files-json-rest-schema.txt +0 -13
- package/audit/evidence/code/non-anyapi-core-files.txt +0 -101
- package/audit/evidence/code/permission-signals.txt +0 -193
- package/audit/evidence/code/query-include-signals.txt +0 -687
- package/audit/evidence/code/transaction-signals.txt +0 -28
- package/audit/evidence/code/validation-signals.txt +0 -650
- package/audit/evidence/code/write-path-signals.txt +0 -61
- package/audit/evidence/docs/docs-claims-signals.txt +0 -1075
- package/audit/evidence/docs/schema-docs-claims-signals.txt +0 -179
- package/audit/evidence/external/corpus-freeze.md +0 -21
- package/audit/evidence/external/npm-competitor-metadata.txt +0 -70
- package/audit/evidence/external/npm-download-signals.txt +0 -23
- package/audit/evidence/external/npm-jsonapi-server.txt +0 -8
- package/audit/evidence/external/npm-modern-jsonapi-metadata.txt +0 -38
- package/audit/evidence/external/readme-fortune-signals.txt +0 -26
- package/audit/evidence/external/readme-fortune.md +0 -165
- package/audit/evidence/external/readme-json-api-serializer-signals.txt +0 -80
- package/audit/evidence/external/readme-json-api-serializer.md +0 -690
- package/audit/evidence/external/readme-jsonapi-serde-server-signals.txt +0 -0
- package/audit/evidence/external/readme-jsonapi-serde-server.md +0 -22
- package/audit/evidence/external/readme-jsonapi-serializer-signals.txt +0 -63
- package/audit/evidence/external/readme-jsonapi-serializer.md +0 -315
- package/audit/evidence/external/readme-jsonapi-server-signals.txt +0 -19
- package/audit/evidence/external/readme-jsonapi-server.md +0 -104
- package/audit/evidence/external/readme-nestjs-json-api-signals.txt +0 -4
- package/audit/evidence/external/readme-nestjs-json-api.md +0 -94
- package/audit/evidence/external/readme-yaser-json-api-nestjs-signals.txt +0 -47
- package/audit/evidence/external/readme-yaser-json-api-nestjs.md +0 -447
- package/audit/evidence/external/standard-jsonapi-snippets.txt +0 -84
- package/audit/evidence/external/standard-rfc-snippets.txt +0 -168
- package/audit/evidence/external/standards-sources.txt +0 -6
- package/audit/evidence/runtime/baseline-commits.txt +0 -2
- package/audit/evidence/runtime/db-adapter-assumptions.txt +0 -3
- package/audit/evidence/runtime/environment.txt +0 -4
- package/audit/evidence/runtime/invariant-runtime-mapping.md +0 -38
- package/audit/evidence/runtime/probe-patch-relationship-error.exitcode +0 -1
- package/audit/evidence/runtime/probe-patch-relationship-error.log +0 -76
- package/audit/evidence/runtime/probe-patch-relationship-error.mjs +0 -67
- package/audit/evidence/runtime/probe-patch-relationship-referenceerror.exitcode +0 -1
- package/audit/evidence/runtime/probe-patch-relationship-referenceerror.log +0 -62
- package/audit/evidence/runtime/probe-patch-relationship-referenceerror.mjs +0 -47
- package/audit/evidence/runtime/test-exitcodes.txt +0 -4
- package/audit/evidence/runtime/test-summary.md +0 -107
- package/audit/evidence/runtime/trace-anchors.txt +0 -61
- package/audit/evidence/tests/access-multitenancy-test-signals.txt +0 -125
- package/audit/evidence/tests/failing-tests-summary.txt +0 -25
- package/audit/evidence/tests/npm-test-json-rest-api.exitcode +0 -1
- package/audit/evidence/tests/npm-test-json-rest-api.log +0 -5885
- package/audit/evidence/tests/npm-test-json-rest-schema.exitcode +0 -1
- package/audit/evidence/tests/npm-test-json-rest-schema.log +0 -356
- package/audit/evidence/tests/query-include-test-signals.txt +0 -948
- package/audit/evidence/tests/relationship-test-signals.txt +0 -223
- package/audit/evidence/tests/skipped-tests.txt +0 -10
- package/audit/evidence/tests/test-suite-list-non-anyapi.txt +0 -30
- package/audit/evidence/tests/test-suite-list.txt +0 -32
- package/audit/subsystems/docs-drift.md +0 -49
- package/audit/subsystems/pass2-cross-review.md +0 -131
- package/audit/subsystems/plugins-access.md +0 -51
- package/audit/subsystems/plugins-bulk.md +0 -50
- package/audit/subsystems/plugins-cors.md +0 -48
- package/audit/subsystems/plugins-file.md +0 -53
- package/audit/subsystems/plugins-multihome.md +0 -50
- package/audit/subsystems/plugins-positioning.md +0 -54
- package/audit/subsystems/plugins-socketio.md +0 -50
- package/audit/subsystems/query-engine.md +0 -61
- package/audit/subsystems/relationships.md +0 -55
- package/audit/subsystems/rest-orchestration.md +0 -66
- package/audit/subsystems/schema-core.md +0 -64
- package/audit/subsystems/storage-knex.md +0 -57
- package/audit/subsystems/tests.md +0 -48
- package/audit/subsystems/transport-express.md +0 -55
- package/audit/traces/trace-read-get.md +0 -22
- package/audit/traces/trace-read-query.md +0 -25
- package/audit/traces/trace-relationship-endpoints.md +0 -31
- package/audit/traces/trace-write-delete.md +0 -17
- package/audit/traces/trace-write-patch.md +0 -26
- package/audit/traces/trace-write-post.md +0 -22
- package/audit/traces/trace-write-put.md +0 -23
- package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +0 -707
- package/docs/GUIDE/GUIDE_X_Multihome.md +0 -666
- package/plugins/core/lib/querying-writing/payload-validators.js +0 -1314
- package/plugins/core/rest-api-access.js +0 -459
- package/plugins/core/rest-api-multihome-plugin.js +0 -314
- package/tests/multihome.test.js +0 -553
- package/tests/multihome.test.js.bak +0 -550
- package/tests/ownership-optout.test.js +0 -76
- package/tests/rest-api-access.test.js +0 -206
|
@@ -60,42 +60,48 @@ export default async function registerScopeRoutes ({ context, api, vars, log })
|
|
|
60
60
|
await api.addRoute({
|
|
61
61
|
method: 'GET',
|
|
62
62
|
path: scopePath,
|
|
63
|
-
handler: createRouteHandler(scopeName, 'query')
|
|
63
|
+
handler: createRouteHandler(scopeName, 'query'),
|
|
64
|
+
routeMeta: { kind: 'resource', scopeName, operation: 'query' }
|
|
64
65
|
})
|
|
65
66
|
|
|
66
67
|
// GET /api/{scope}/{id} - Get single resource
|
|
67
68
|
await api.addRoute({
|
|
68
69
|
method: 'GET',
|
|
69
70
|
path: `${scopePath}/:id`,
|
|
70
|
-
handler: createRouteHandler(scopeName, 'get')
|
|
71
|
+
handler: createRouteHandler(scopeName, 'get'),
|
|
72
|
+
routeMeta: { kind: 'resource', scopeName, operation: 'get' }
|
|
71
73
|
})
|
|
72
74
|
|
|
73
75
|
// POST /api/{scope} - Create resource
|
|
74
76
|
await api.addRoute({
|
|
75
77
|
method: 'POST',
|
|
76
78
|
path: scopePath,
|
|
77
|
-
handler: createRouteHandler(scopeName, 'post')
|
|
79
|
+
handler: createRouteHandler(scopeName, 'post'),
|
|
80
|
+
routeMeta: { kind: 'resource', scopeName, operation: 'post' }
|
|
78
81
|
})
|
|
79
82
|
|
|
80
83
|
// PUT /api/{scope}/{id} - Replace resource
|
|
81
84
|
await api.addRoute({
|
|
82
85
|
method: 'PUT',
|
|
83
86
|
path: `${scopePath}/:id`,
|
|
84
|
-
handler: createRouteHandler(scopeName, 'put')
|
|
87
|
+
handler: createRouteHandler(scopeName, 'put'),
|
|
88
|
+
routeMeta: { kind: 'resource', scopeName, operation: 'put' }
|
|
85
89
|
})
|
|
86
90
|
|
|
87
91
|
// PATCH /api/{scope}/{id} - Update resource
|
|
88
92
|
await api.addRoute({
|
|
89
93
|
method: 'PATCH',
|
|
90
94
|
path: `${scopePath}/:id`,
|
|
91
|
-
handler: createRouteHandler(scopeName, 'patch')
|
|
95
|
+
handler: createRouteHandler(scopeName, 'patch'),
|
|
96
|
+
routeMeta: { kind: 'resource', scopeName, operation: 'patch' }
|
|
92
97
|
})
|
|
93
98
|
|
|
94
99
|
// DELETE /api/{scope}/{id} - Delete resource
|
|
95
100
|
await api.addRoute({
|
|
96
101
|
method: 'DELETE',
|
|
97
102
|
path: `${scopePath}/:id`,
|
|
98
|
-
handler: createRouteHandler(scopeName, 'delete')
|
|
103
|
+
handler: createRouteHandler(scopeName, 'delete'),
|
|
104
|
+
routeMeta: { kind: 'resource', scopeName, operation: 'delete' }
|
|
99
105
|
})
|
|
100
106
|
|
|
101
107
|
log.info(`Routes registered for scope '${scopeName}'`)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export default async function applyQueryFiltersMethod ({ context, params = {}, runHooks }) {
|
|
2
|
+
const { query, filters, storageAdapter, ...knexQuery } = params
|
|
3
|
+
|
|
4
|
+
if (!query) {
|
|
5
|
+
return query
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const previousKnexQuery = context.knexQuery
|
|
9
|
+
const previousStorageAdapter = context.storageAdapter
|
|
10
|
+
|
|
11
|
+
context.knexQuery = {
|
|
12
|
+
query,
|
|
13
|
+
filters,
|
|
14
|
+
storageAdapter,
|
|
15
|
+
adapter: storageAdapter,
|
|
16
|
+
...knexQuery
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (storageAdapter) {
|
|
20
|
+
context.storageAdapter = storageAdapter
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
await runHooks('knexQueryFiltering')
|
|
25
|
+
return {
|
|
26
|
+
query: context.knexQuery?.query || query
|
|
27
|
+
}
|
|
28
|
+
} finally {
|
|
29
|
+
if (previousKnexQuery === undefined) {
|
|
30
|
+
delete context.knexQuery
|
|
31
|
+
} else {
|
|
32
|
+
context.knexQuery = previousKnexQuery
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (previousStorageAdapter === undefined) {
|
|
36
|
+
delete context.storageAdapter
|
|
37
|
+
} else {
|
|
38
|
+
context.storageAdapter = previousStorageAdapter
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
RestApiResourceError,
|
|
3
|
+
RestApiValidationError
|
|
4
4
|
} from '../../../lib/rest-api-errors.js'
|
|
5
5
|
import { transformSimplifiedToJsonApi } from '../lib/querying-writing/simplified-helpers.js'
|
|
6
6
|
import { createEnhancedLogger } from '../../../lib/enhanced-logger.js'
|
|
7
|
+
import { unwrapQueryBuilderState } from '../lib/querying/query-builder-utils.js'
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Gets an enhanced logger instance with full error details and stack traces
|
|
@@ -140,56 +141,6 @@ export async function setupCommonRequest ({ params, context, vars, scopes, scope
|
|
|
140
141
|
{ inputRecord: context.inputRecord },
|
|
141
142
|
{ context: { scopeName, schemaStructure, schemaRelationships } }
|
|
142
143
|
)
|
|
143
|
-
} else {
|
|
144
|
-
// Strict mode: validate no belongsTo fields in attributes
|
|
145
|
-
if (context.inputRecord?.data?.attributes) {
|
|
146
|
-
for (const [key, fieldDef] of Object.entries(schemaStructure)) {
|
|
147
|
-
if (fieldDef.belongsTo && key in context.inputRecord.data.attributes) {
|
|
148
|
-
throw new RestApiValidationError(
|
|
149
|
-
`Field '${key}' is a foreign key and must be set via relationships, not attributes`,
|
|
150
|
-
{ fields: [`data.attributes.${key}`] }
|
|
151
|
-
)
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (context.inputRecord.data.type !== scopeName) {
|
|
158
|
-
throw new RestApiValidationError(
|
|
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
|
-
}]
|
|
167
|
-
}
|
|
168
|
-
)
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Remove included validation since JSON:API doesn't support it
|
|
172
|
-
if (context.inputRecord.included) {
|
|
173
|
-
throw new RestApiPayloadError(
|
|
174
|
-
context.method + ' requests cannot include an "included" array. JSON:API does not support creating multiple resources in a single request.',
|
|
175
|
-
{ path: 'included', expected: 'undefined', received: 'array' }
|
|
176
|
-
)
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// If both URL path ID and request body ID are provided, they must match
|
|
180
|
-
// Convert both to strings for comparison since databases may return numeric IDs
|
|
181
|
-
if (context.id && context.inputRecord.data.id && String(context.id) !== String(context.inputRecord.data.id)) {
|
|
182
|
-
throw new RestApiValidationError(
|
|
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: 'Request body ID must match URL path ID when both are provided'
|
|
190
|
-
}]
|
|
191
|
-
}
|
|
192
|
-
)
|
|
193
144
|
}
|
|
194
145
|
|
|
195
146
|
// Return key schema-related objects for direct use in the main methods
|
|
@@ -261,6 +212,61 @@ export const getMethodHookSuffix = (method) => {
|
|
|
261
212
|
return method.charAt(0).toUpperCase() + method.slice(1)
|
|
262
213
|
}
|
|
263
214
|
|
|
215
|
+
export const validateCompleteReplacePayload = ({
|
|
216
|
+
context,
|
|
217
|
+
belongsToUpdates
|
|
218
|
+
}) => {
|
|
219
|
+
const schemaInfo = context.schemaInfo || {}
|
|
220
|
+
const schemaStructure = schemaInfo.schemaStructure || {}
|
|
221
|
+
const idProperty = schemaInfo.idProperty || 'id'
|
|
222
|
+
const originalInputAttributes = context.originalInputAttributes || context.inputRecord?.data?.attributes || {}
|
|
223
|
+
const missingFields = []
|
|
224
|
+
|
|
225
|
+
for (const [fieldName, fieldDef] of Object.entries(schemaStructure)) {
|
|
226
|
+
if (!fieldDef) continue
|
|
227
|
+
if (fieldName === idProperty) continue
|
|
228
|
+
if (fieldDef.computed === true || fieldDef.virtual === true) continue
|
|
229
|
+
if (fieldDef.type === undefined) continue
|
|
230
|
+
|
|
231
|
+
if (fieldDef.belongsTo && fieldDef.as) {
|
|
232
|
+
if (Object.hasOwn(belongsToUpdates, fieldName)) continue
|
|
233
|
+
|
|
234
|
+
const existingRelationshipData = context.minimalRecord?.relationships?.[fieldDef.as]?.data
|
|
235
|
+
if (existingRelationshipData?.id !== undefined && existingRelationshipData?.id !== null) {
|
|
236
|
+
missingFields.push({
|
|
237
|
+
field: `data.relationships.${fieldDef.as}.data.id`,
|
|
238
|
+
message: `PUT requests must explicitly include relationship '${fieldDef.as}' when it already has a value`
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
continue
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (Object.hasOwn(originalInputAttributes, fieldName)) continue
|
|
245
|
+
|
|
246
|
+
const existingValue = context.minimalRecord?.attributes?.[fieldName]
|
|
247
|
+
if (existingValue !== undefined && existingValue !== null) {
|
|
248
|
+
missingFields.push({
|
|
249
|
+
field: `data.attributes.${fieldName}`,
|
|
250
|
+
message: `PUT requests must explicitly include attribute '${fieldName}' when it already has a value`
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (missingFields.length > 0) {
|
|
256
|
+
throw new RestApiValidationError(
|
|
257
|
+
'PUT request omits persisted fields that already have stored values',
|
|
258
|
+
{
|
|
259
|
+
fields: missingFields.map(({ field }) => field),
|
|
260
|
+
violations: missingFields.map(({ field, message }) => ({
|
|
261
|
+
field,
|
|
262
|
+
rule: 'complete_replacement',
|
|
263
|
+
message
|
|
264
|
+
}))
|
|
265
|
+
}
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
264
270
|
/**
|
|
265
271
|
* Validates resource attributes before write operations
|
|
266
272
|
*
|
|
@@ -285,9 +291,8 @@ export const validateResourceAttributesBeforeWrite = async ({
|
|
|
285
291
|
await runHooks('beforeSchemaValidate')
|
|
286
292
|
await runHooks(`beforeSchemaValidate${methodSpecificHookSuffix}`)
|
|
287
293
|
|
|
288
|
-
// Store original input attributes before validation adds defaults
|
|
289
|
-
|
|
290
|
-
if (!context.originalInputAttributes && context.method === 'post') {
|
|
294
|
+
// Store original input attributes before validation adds defaults or casts.
|
|
295
|
+
if (!context.originalInputAttributes) {
|
|
291
296
|
context.originalInputAttributes = { ...(context.inputRecord.data.attributes || {}) }
|
|
292
297
|
}
|
|
293
298
|
|
|
@@ -361,9 +366,13 @@ export const validateResourceAttributesBeforeWrite = async ({
|
|
|
361
366
|
return acc
|
|
362
367
|
}, {})
|
|
363
368
|
|
|
364
|
-
const
|
|
369
|
+
const validationMethod = isPartialValidation
|
|
370
|
+
? 'patch'
|
|
371
|
+
: context.method === 'put'
|
|
372
|
+
? 'replace'
|
|
373
|
+
: 'create'
|
|
365
374
|
|
|
366
|
-
const { validatedObject, errors } = await schema
|
|
375
|
+
const { validatedObject, errors } = await schema[validationMethod](attributesForValidation)
|
|
367
376
|
|
|
368
377
|
if (Object.keys(errors).length > 0) {
|
|
369
378
|
// --- START OF MODIFICATION ---
|
|
@@ -416,11 +425,10 @@ export const validateResourceAttributesBeforeWrite = async ({
|
|
|
416
425
|
* @param {object} context - The context object containing authentication info
|
|
417
426
|
* @param {object} inputRecord - The input record containing relationships to validate
|
|
418
427
|
* @param {object} helpers - Data helpers including dataGetMinimal
|
|
419
|
-
* @param {function} runHooks - Function to run hooks
|
|
420
428
|
* @param {object} api - API instance to access resources
|
|
421
429
|
* @throws {Error} If user doesn't have access to any related resource
|
|
422
430
|
*/
|
|
423
|
-
export const validateRelationshipAccess = async (context, inputRecord, helpers,
|
|
431
|
+
export const validateRelationshipAccess = async (context, inputRecord, helpers, api) => {
|
|
424
432
|
if (!inputRecord?.data?.relationships) return
|
|
425
433
|
|
|
426
434
|
for (const [relName, relData] of Object.entries(inputRecord.data.relationships)) {
|
|
@@ -442,19 +450,47 @@ export const validateRelationshipAccess = async (context, inputRecord, helpers,
|
|
|
442
450
|
id: item.id,
|
|
443
451
|
schemaInfo: relatedScope.vars.schemaInfo,
|
|
444
452
|
scopeName: item.type,
|
|
453
|
+
queryParams: {},
|
|
445
454
|
method: 'get', // We're checking read permission
|
|
446
455
|
isUpdate: false
|
|
447
456
|
}
|
|
448
457
|
|
|
458
|
+
const targetScope = api.resources?.[item.type]
|
|
459
|
+
const applyQueryFilters = targetScope?.applyQueryFilters
|
|
460
|
+
? async ({ query, filters, tableName, db, scopeName, storageAdapter, isAnyApi }) => {
|
|
461
|
+
const queryState = await targetScope.applyQueryFilters({
|
|
462
|
+
query,
|
|
463
|
+
filters,
|
|
464
|
+
schemaInfo: getContext.schemaInfo,
|
|
465
|
+
scopeName,
|
|
466
|
+
tableName,
|
|
467
|
+
db,
|
|
468
|
+
isAnyApi,
|
|
469
|
+
storageAdapter
|
|
470
|
+
}, getContext)
|
|
471
|
+
|
|
472
|
+
return {
|
|
473
|
+
query: unwrapQueryBuilderState(queryState, query)
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
: undefined
|
|
477
|
+
|
|
449
478
|
// Get the minimal record
|
|
450
479
|
const record = await helpers.dataGetMinimal({
|
|
451
480
|
scopeName: item.type,
|
|
452
481
|
context: getContext,
|
|
453
|
-
|
|
482
|
+
applyQueryFilters
|
|
454
483
|
})
|
|
455
484
|
|
|
456
485
|
if (!record) {
|
|
457
|
-
throw new
|
|
486
|
+
throw new RestApiResourceError(
|
|
487
|
+
`Cannot create relationship to non-existent ${item.type} with id ${item.id}`,
|
|
488
|
+
{
|
|
489
|
+
subtype: 'not_found',
|
|
490
|
+
resourceType: item.type,
|
|
491
|
+
resourceId: item.id
|
|
492
|
+
}
|
|
493
|
+
)
|
|
458
494
|
}
|
|
459
495
|
|
|
460
496
|
// Check permissions using the related scope's checkPermissions
|
|
@@ -493,29 +529,31 @@ export const applyFieldSetters = async (attributes, schemaInfo, context, api, he
|
|
|
493
529
|
const transformedAttributes = { ...attributes }
|
|
494
530
|
|
|
495
531
|
for (const fieldName of sortedSetterFields) {
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
setterContext
|
|
514
|
-
)
|
|
515
|
-
} catch (error) {
|
|
516
|
-
console.warn(`Setter for field '${fieldName}' failed:`, error)
|
|
517
|
-
// Keep original value if setter fails
|
|
532
|
+
const setterInfo = fieldSetters[fieldName]
|
|
533
|
+
const fieldPresent = fieldName in transformedAttributes
|
|
534
|
+
const hasDependencies = Array.isArray(setterInfo.runSetterAfter) && setterInfo.runSetterAfter.length > 0
|
|
535
|
+
|
|
536
|
+
if (!fieldPresent && !hasDependencies) continue
|
|
537
|
+
|
|
538
|
+
try {
|
|
539
|
+
const setterContext = {
|
|
540
|
+
attributes: transformedAttributes, // Current state with previous setters applied
|
|
541
|
+
fieldName,
|
|
542
|
+
originalValue: attributes[fieldName],
|
|
543
|
+
originalAttributes: attributes,
|
|
544
|
+
scopeName: context.scopeName,
|
|
545
|
+
method: context.method,
|
|
546
|
+
api,
|
|
547
|
+
helpers,
|
|
548
|
+
auth: context.auth
|
|
518
549
|
}
|
|
550
|
+
transformedAttributes[fieldName] = await setterInfo.setter(
|
|
551
|
+
transformedAttributes[fieldName],
|
|
552
|
+
setterContext
|
|
553
|
+
)
|
|
554
|
+
} catch (error) {
|
|
555
|
+
console.warn(`Setter for field '${fieldName}' failed:`, error)
|
|
556
|
+
// Keep original value if setter fails
|
|
519
557
|
}
|
|
520
558
|
}
|
|
521
559
|
|
|
@@ -33,6 +33,7 @@ export default async function enrichAttributesMethod ({ context, params, runHook
|
|
|
33
33
|
const computedFields = scopes[scopeName]?.vars?.schemaInfo?.computed || {}
|
|
34
34
|
const fieldGetters = scopes[scopeName]?.vars?.schemaInfo?.fieldGetters || {}
|
|
35
35
|
const sortedGetterFields = scopes[scopeName]?.vars?.schemaInfo?.sortedGetterFields || []
|
|
36
|
+
const queryFields = scopes[scopeName]?.vars?.queryFields || {}
|
|
36
37
|
|
|
37
38
|
// Make a copy of attributes for transformation
|
|
38
39
|
const transformedAttributes = { ...attributes }
|
|
@@ -72,7 +73,11 @@ export default async function enrichAttributesMethod ({ context, params, runHook
|
|
|
72
73
|
// Filter hidden fields from attributes based on visibility rules
|
|
73
74
|
// This removes hidden:true fields and normallyHidden:true fields (unless requested)
|
|
74
75
|
const requestedFields = parentContext?.queryParams?.fields?.[scopeName]
|
|
75
|
-
const filteredAttributes = filterHiddenFields(
|
|
76
|
+
const filteredAttributes = filterHiddenFields(
|
|
77
|
+
transformedAttributes,
|
|
78
|
+
{ structure: { ...schemaStructure, ...queryFields } },
|
|
79
|
+
requestedFields
|
|
80
|
+
)
|
|
76
81
|
|
|
77
82
|
// Handle virtual fields - they need to be added from parent context if available
|
|
78
83
|
// Virtual fields come from input and need to be preserved in responses
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { RestApiResourceError } from '../../../lib/rest-api-errors.js'
|
|
2
|
-
import { validateGetPayload } from '../lib/querying-writing/payload-validators.js'
|
|
3
2
|
import { normalizeRecordAttributes } from '../lib/querying-writing/database-value-normalizers.js'
|
|
4
3
|
import { getRequestedComputedFields } from '../lib/querying-writing/knex-field-helpers.js'
|
|
5
4
|
import { transformJsonApiToSimplified } from '../lib/querying-writing/simplified-helpers.js'
|
|
5
|
+
import { getRequestContracts, validateRequestContractOrThrow } from '../lib/querying-writing/request-contracts.js'
|
|
6
6
|
import { cascadeConfig } from './common.js'
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -41,11 +41,11 @@ export default async function getMethod ({
|
|
|
41
41
|
|
|
42
42
|
// Assign common context properties
|
|
43
43
|
context.schemaInfo = scopes[scopeName].vars.schemaInfo
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
const rawQueryParams = params.queryParams || {}
|
|
45
|
+
context.queryParams = {
|
|
46
|
+
fields: cascadeConfig('fields', [rawQueryParams], {}),
|
|
47
|
+
include: cascadeConfig('include', [rawQueryParams], [])
|
|
48
|
+
}
|
|
49
49
|
|
|
50
50
|
context.transaction = params.transaction
|
|
51
51
|
context.db = context.transaction || api.knex.instance
|
|
@@ -56,14 +56,23 @@ export default async function getMethod ({
|
|
|
56
56
|
const schemaStructure = context.schemaInfo.schemaInstance.structure
|
|
57
57
|
const schemaRelationships = context.schemaInfo.schemaRelationships
|
|
58
58
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
59
|
+
const requestContracts = getRequestContracts({
|
|
60
|
+
scopeName,
|
|
61
|
+
schemaInfo: context.schemaInfo,
|
|
62
|
+
includeDepthLimit: vars.includeDepthLimit,
|
|
63
|
+
sortableFields: vars.sortableFields
|
|
64
|
+
})
|
|
65
|
+
const validatedRequest = validateRequestContractOrThrow(
|
|
66
|
+
requestContracts.get,
|
|
67
|
+
{
|
|
68
|
+
id: params.id,
|
|
69
|
+
queryParams: context.queryParams
|
|
70
|
+
},
|
|
71
|
+
'GET request parameters are invalid'
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
context.id = validatedRequest.id
|
|
75
|
+
context.queryParams = validatedRequest.queryParams || {}
|
|
67
76
|
|
|
68
77
|
// Fetch minimal record for authorization checks
|
|
69
78
|
const minimalRecord = await helpers.dataGetMinimal({
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { RestApiResourceError } from '../../../lib/rest-api-errors.js'
|
|
2
|
-
import { validatePatchPayload } from '../lib/querying-writing/payload-validators.js'
|
|
1
|
+
import { RestApiResourceError, RestApiValidationError } from '../../../lib/rest-api-errors.js'
|
|
3
2
|
import { processRelationships } from '../lib/writing/relationship-processor.js'
|
|
4
3
|
import { updateManyToManyRelationship } from '../lib/writing/many-to-many-manipulations.js'
|
|
5
4
|
import { ERROR_SUBTYPES } from '../lib/querying-writing/knex-constants.js'
|
|
5
|
+
import { getRequestContracts, validateRequestContractOrThrow } from '../lib/querying-writing/request-contracts.js'
|
|
6
6
|
import {
|
|
7
7
|
setupCommonRequest,
|
|
8
8
|
validateResourceAttributesBeforeWrite,
|
|
@@ -49,23 +49,39 @@ export default async function patchMethod ({
|
|
|
49
49
|
api,
|
|
50
50
|
helpers
|
|
51
51
|
})
|
|
52
|
-
context.id = context.inputRecord.data.id
|
|
53
|
-
|
|
54
52
|
// Run early hooks for pre-processing (e.g., file handling)
|
|
55
53
|
await runHooks('beforeProcessing')
|
|
56
54
|
await runHooks('beforeProcessingPatch')
|
|
57
55
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
56
|
+
const requestContracts = getRequestContracts({
|
|
57
|
+
scopeName,
|
|
58
|
+
schemaInfo: context.schemaInfo,
|
|
59
|
+
includeDepthLimit: vars.includeDepthLimit,
|
|
60
|
+
sortableFields: vars.sortableFields
|
|
61
|
+
})
|
|
62
|
+
context.inputRecord = validateRequestContractOrThrow(
|
|
63
|
+
requestContracts.patch,
|
|
64
|
+
context.inputRecord,
|
|
65
|
+
'PATCH request body is invalid'
|
|
66
|
+
)
|
|
67
|
+
if (params.id && String(params.id) !== String(context.inputRecord.data.id)) {
|
|
68
|
+
throw new RestApiValidationError(
|
|
69
|
+
`ID mismatch. URL path ID '${params.id}' does not match request body ID '${context.inputRecord.data.id}'`,
|
|
70
|
+
{
|
|
71
|
+
fields: ['data.id'],
|
|
72
|
+
violations: [{
|
|
73
|
+
field: 'data.id',
|
|
74
|
+
rule: 'id_consistency',
|
|
75
|
+
message: 'Request body ID must match URL path ID when both are provided'
|
|
76
|
+
}]
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
context.id = params.id || context.inputRecord.data.id
|
|
65
81
|
|
|
66
82
|
// Validate that user has read access to all related resources
|
|
67
83
|
// This ensures users can only create relationships to resources they can access
|
|
68
|
-
await validateRelationshipAccess(context, context.inputRecord, helpers,
|
|
84
|
+
await validateRelationshipAccess(context, context.inputRecord, helpers, api)
|
|
69
85
|
|
|
70
86
|
// Extract foreign keys from JSON:API relationships and prepare many-to-many operations
|
|
71
87
|
// Example: relationships.author -> author_id: '123' for storage
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { validatePostPayload } from '../lib/querying-writing/payload-validators.js'
|
|
2
1
|
import { processRelationships } from '../lib/writing/relationship-processor.js'
|
|
2
|
+
import { getRequestContracts, validateRequestContractOrThrow } from '../lib/querying-writing/request-contracts.js'
|
|
3
3
|
import { createPivotRecords } from '../lib/writing/many-to-many-manipulations.js'
|
|
4
4
|
import {
|
|
5
5
|
setupCommonRequest,
|
|
@@ -50,16 +50,21 @@ export default async function postMethod ({
|
|
|
50
50
|
await runHooks('beforeProcessing')
|
|
51
51
|
await runHooks('beforeProcessingPost')
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
const requestContracts = getRequestContracts({
|
|
54
|
+
scopeName,
|
|
55
|
+
schemaInfo: context.schemaInfo,
|
|
56
|
+
includeDepthLimit: vars.includeDepthLimit,
|
|
57
|
+
sortableFields: vars.sortableFields
|
|
58
|
+
})
|
|
59
|
+
context.inputRecord = validateRequestContractOrThrow(
|
|
60
|
+
requestContracts.post,
|
|
61
|
+
context.inputRecord,
|
|
62
|
+
'POST request body is invalid'
|
|
63
|
+
)
|
|
59
64
|
|
|
60
65
|
// Validate that user has read access to all related resources
|
|
61
66
|
// This ensures users can only create relationships to resources they can access
|
|
62
|
-
await validateRelationshipAccess(context, context.inputRecord, helpers,
|
|
67
|
+
await validateRelationshipAccess(context, context.inputRecord, helpers, api)
|
|
63
68
|
|
|
64
69
|
// Extract foreign keys from JSON:API relationships and prepare many-to-many operations
|
|
65
70
|
// Example: relationships.author -> author_id: '123' for storage
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { RestApiResourceError } from '../../../lib/rest-api-errors.js'
|
|
2
|
-
import { validatePutPayload } from '../lib/querying-writing/payload-validators.js'
|
|
1
|
+
import { RestApiResourceError, RestApiValidationError } from '../../../lib/rest-api-errors.js'
|
|
3
2
|
import { processRelationships } from '../lib/writing/relationship-processor.js'
|
|
4
3
|
import { updateManyToManyRelationship, createPivotRecords } from '../lib/writing/many-to-many-manipulations.js'
|
|
5
4
|
import { ERROR_SUBTYPES } from '../lib/querying-writing/knex-constants.js'
|
|
5
|
+
import { getRequestContracts, validateRequestContractOrThrow } from '../lib/querying-writing/request-contracts.js'
|
|
6
6
|
import {
|
|
7
7
|
setupCommonRequest,
|
|
8
|
+
validateCompleteReplacePayload,
|
|
8
9
|
validateResourceAttributesBeforeWrite,
|
|
9
10
|
validateRelationshipAccess,
|
|
10
11
|
applyFieldSetters,
|
|
@@ -17,7 +18,9 @@ import {
|
|
|
17
18
|
* PUT
|
|
18
19
|
* Updates an existing top-level resource by completely replacing it.
|
|
19
20
|
* This method supports updating both attributes and relationships (1:1 and n:n).
|
|
20
|
-
*
|
|
21
|
+
* Existing persisted values cannot be silently dropped: if a stored attribute or belongsTo
|
|
22
|
+
* relationship already has a value, PUT must include it explicitly. Relationship collections
|
|
23
|
+
* still follow replacement semantics when a relationships object is provided.
|
|
21
24
|
* This method does NOT support creating new related resources via an `included` array.
|
|
22
25
|
*/
|
|
23
26
|
export default async function putMethod ({
|
|
@@ -48,23 +51,39 @@ export default async function putMethod ({
|
|
|
48
51
|
api,
|
|
49
52
|
helpers
|
|
50
53
|
})
|
|
51
|
-
context.id = context.inputRecord.data.id
|
|
52
|
-
|
|
53
54
|
// Run early hooks for pre-processing (e.g., file handling)
|
|
54
55
|
await runHooks('beforeProcessing')
|
|
55
56
|
await runHooks('beforeProcessingPut')
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
const requestContracts = getRequestContracts({
|
|
59
|
+
scopeName,
|
|
60
|
+
schemaInfo: context.schemaInfo,
|
|
61
|
+
includeDepthLimit: vars.includeDepthLimit,
|
|
62
|
+
sortableFields: vars.sortableFields
|
|
63
|
+
})
|
|
64
|
+
context.inputRecord = validateRequestContractOrThrow(
|
|
65
|
+
requestContracts.put,
|
|
66
|
+
context.inputRecord,
|
|
67
|
+
'PUT request body is invalid'
|
|
68
|
+
)
|
|
69
|
+
if (params.id && String(params.id) !== String(context.inputRecord.data.id)) {
|
|
70
|
+
throw new RestApiValidationError(
|
|
71
|
+
`ID mismatch. URL path ID '${params.id}' does not match request body ID '${context.inputRecord.data.id}'`,
|
|
72
|
+
{
|
|
73
|
+
fields: ['data.id'],
|
|
74
|
+
violations: [{
|
|
75
|
+
field: 'data.id',
|
|
76
|
+
rule: 'id_consistency',
|
|
77
|
+
message: 'Request body ID must match URL path ID when both are provided'
|
|
78
|
+
}]
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
context.id = params.id || context.inputRecord.data.id
|
|
64
83
|
|
|
65
84
|
// Validate that user has read access to all related resources
|
|
66
85
|
// This ensures users can only create relationships to resources they can access
|
|
67
|
-
await validateRelationshipAccess(context, context.inputRecord, helpers,
|
|
86
|
+
await validateRelationshipAccess(context, context.inputRecord, helpers, api)
|
|
68
87
|
|
|
69
88
|
// Extract foreign keys from JSON:API relationships and prepare many-to-many operations
|
|
70
89
|
// Example: relationships.author -> author_id: '123' for storage
|
|
@@ -74,13 +93,6 @@ export default async function putMethod ({
|
|
|
74
93
|
{ context }
|
|
75
94
|
)
|
|
76
95
|
|
|
77
|
-
await validateResourceAttributesBeforeWrite({
|
|
78
|
-
context,
|
|
79
|
-
schema,
|
|
80
|
-
belongsToUpdates,
|
|
81
|
-
runHooks
|
|
82
|
-
})
|
|
83
|
-
|
|
84
96
|
// Check existence first
|
|
85
97
|
context.exists = await helpers.dataExists({
|
|
86
98
|
scopeName,
|
|
@@ -108,8 +120,20 @@ export default async function putMethod ({
|
|
|
108
120
|
context.minimalRecord = minimalRecord
|
|
109
121
|
}
|
|
110
122
|
|
|
123
|
+
validateCompleteReplacePayload({
|
|
124
|
+
context,
|
|
125
|
+
belongsToUpdates
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
await validateResourceAttributesBeforeWrite({
|
|
129
|
+
context,
|
|
130
|
+
schema,
|
|
131
|
+
belongsToUpdates,
|
|
132
|
+
runHooks
|
|
133
|
+
})
|
|
134
|
+
|
|
111
135
|
// For PUT, we also need to handle relationships that are NOT provided
|
|
112
|
-
// (they should be set to null/empty
|
|
136
|
+
// (they should be set to null/empty when a relationships object is provided)
|
|
113
137
|
const allRelationships = {}
|
|
114
138
|
|
|
115
139
|
// Collect all defined relationships for this resource
|