json-rest-api 1.0.29 → 2.0.0
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/LICENSE +10 -670
- package/LICENSE-GPL-3.0 +674 -0
- package/LICENSE-MIT +21 -0
- package/README.md +39 -30
- package/docs/API.md +744 -1273
- package/docs/GUIDE/01-getting-started.md +338 -0
- package/docs/GUIDE/02-resources-and-relationships.md +33 -0
- package/docs/GUIDE/03-running-example.md +87 -0
- package/docs/GUIDE/04-creating-and-querying.md +181 -0
- package/docs/GUIDE/05-belongs-to.md +177 -0
- package/docs/GUIDE/06-has-many.md +184 -0
- package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
- package/docs/GUIDE/08-many-to-many.md +184 -0
- package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
- package/docs/GUIDE/10-put-and-patch.md +174 -0
- package/docs/GUIDE/11-relationship-endpoints.md +174 -0
- package/docs/GUIDE/12-field-transformations.md +287 -0
- package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
- package/docs/GUIDE/14-custom-resource-methods.md +125 -0
- package/docs/GUIDE/15-query-projections.md +266 -0
- package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
- package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
- package/docs/GUIDE/18-labels.md +86 -0
- package/docs/GUIDE/19-managed-transactions.md +179 -0
- package/docs/GUIDE/20-transaction-outcomes.md +207 -0
- package/docs/GUIDE/21-schema-and-migrations.md +634 -0
- package/docs/GUIDE/22-version-field-migrations.md +208 -0
- package/docs/GUIDE/23-fastify.md +179 -0
- package/docs/GUIDE/24-cors.md +172 -0
- package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
- package/docs/GUIDE/26-file-uploads.md +278 -0
- package/docs/GUIDE/27-bulk-operations.md +277 -0
- package/docs/GUIDE/28-socketio.md +325 -0
- package/docs/GUIDE/29-writing-plugins.md +278 -0
- package/docs/GUIDE/30-backend-capabilities.md +100 -0
- package/docs/GUIDE/31-positioning.md +181 -0
- package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +74 -30
- package/docs/GUIDE/33-migrating-to-v2.md +3827 -0
- package/docs/GUIDE/index.md +51 -59
- package/docs/QUICKSTART.md +120 -287
- package/docs/architecture.md +75 -0
- package/docs/contributing.md +178 -0
- package/index.d.ts +36 -0
- package/index.js +1 -22
- package/lib/enhanced-logger.js +91 -85
- package/lib/error-context.js +445 -5
- package/lib/error-formatter.js +219 -59
- package/lib/knex-transaction.js +64 -0
- package/lib/logger-types.d.ts +22 -0
- package/lib/missing-package.js +4 -0
- package/lib/rest-api-errors.js +103 -8
- package/lib/runtime/json-rest-api.js +172 -0
- package/lib/transaction-types.d.ts +59 -0
- package/package.json +97 -27
- package/plugins/core/bulk-operations-plugin.js +191 -184
- package/plugins/core/connectors/express-plugin.js +148 -219
- package/plugins/core/connectors/fastify-plugin.js +125 -82
- package/plugins/core/connectors/lib/busboy-detector.js +74 -156
- package/plugins/core/connectors/lib/connector-core.js +144 -20
- package/plugins/core/connectors/lib/formidable-detector.js +63 -120
- package/plugins/core/connectors/lib/http-validators.js +77 -0
- package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
- package/plugins/core/connectors/lib/request-helpers.js +10 -1
- package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
- package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
- package/plugins/core/file-handling-plugin.js +87 -177
- package/plugins/core/lib/anyapi/anyapi-registry.js +213 -164
- package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
- package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
- package/plugins/core/lib/anyapi/schema-utils.js +25 -16
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
- package/plugins/core/lib/dbIntrospection.js +228 -21
- package/plugins/core/lib/dbTablesOperations.js +409 -92
- package/plugins/core/lib/fractional-positioning.js +12 -48
- package/plugins/core/lib/querying/identifier-query.js +19 -0
- package/plugins/core/lib/querying/include-query-helpers.js +364 -0
- package/plugins/core/lib/querying/include-to-many.js +428 -0
- package/plugins/core/lib/querying/include-to-one.js +579 -0
- package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
- package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
- package/plugins/core/lib/querying/knex-process-includes.js +5 -110
- package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
- package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
- package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
- package/plugins/core/lib/querying/knex-window-queries.js +62 -253
- package/plugins/core/lib/querying/query-builder-utils.js +44 -4
- package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
- package/plugins/core/lib/querying/query-constraint.js +27 -0
- package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
- package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
- package/plugins/core/lib/querying/sort-helpers.js +13 -1
- package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
- package/plugins/core/lib/querying/url-helpers.js +11 -4
- package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
- package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
- package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
- package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
- package/plugins/core/lib/querying-writing/field-utils.js +113 -92
- package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
- package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
- package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
- package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
- package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
- package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
- package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
- package/plugins/core/lib/querying-writing/response-options.js +35 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
- package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
- package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
- package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
- package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
- package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
- package/plugins/core/lib/storage/storage-adapter.js +128 -62
- package/plugins/core/lib/storage/storage-mapping.js +78 -28
- package/plugins/core/lib/storage/storage-types.d.ts +502 -0
- package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
- package/plugins/core/lib/writing/relationship-processor.js +94 -158
- package/plugins/core/lib/writing/resource-version.js +172 -0
- package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +116 -2485
- package/plugins/core/rest-api-autofilter-plugin.js +17 -19
- package/plugins/core/rest-api-cors-plugin.js +52 -205
- package/plugins/core/rest-api-knex-plugin.js +45 -1077
- package/plugins/core/rest-api-label-plugin.js +25 -38
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
- package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
- package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
- package/plugins/core/rest-api-plugin-methods/common.js +291 -395
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
- package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
- package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
- package/plugins/core/rest-api-plugin-methods/get.js +36 -68
- package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
- package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
- package/plugins/core/rest-api-plugin-methods/post.js +51 -51
- package/plugins/core/rest-api-plugin-methods/put.js +69 -148
- package/plugins/core/rest-api-plugin-methods/query.js +37 -81
- package/plugins/core/rest-api-plugin-methods/release.js +2 -1
- package/plugins/core/rest-api-plugin.js +32 -46
- package/plugins/core/rest-api-positioning-plugin.js +183 -628
- package/plugins/core/rest-api-query-projections-plugin.js +19 -102
- package/plugins/core/rest-api-row-policy-plugin.js +3 -3
- package/plugins/core/socketio-plugin.js +364 -344
- package/plugins/storage/local-storage.js +40 -36
- package/types/autofilter.d.ts +57 -0
- package/types/bulk-methods.d.ts +63 -0
- package/types/errors.d.ts +118 -0
- package/types/fastify-options.d.ts +19 -0
- package/types/file-detectors.d.ts +33 -0
- package/types/file-storage.d.ts +48 -0
- package/types/hook-context.d.ts +180 -0
- package/types/plugin-options.d.ts +53 -0
- package/types/relationship-methods.d.ts +74 -0
- package/types/representations.d.ts +50 -0
- package/types/resource-methods.d.ts +95 -0
- package/types/resource-schema.d.ts +157 -0
- package/types/row-policy.d.ts +31 -0
- package/types/runtime.d.ts +132 -0
- package/types/transactions.d.ts +21 -0
- package/.claude/agents/library-guide-expert.md +0 -30
- package/.claude/agents/rest-api-internals-expert.md +0 -54
- package/.claude/settings.local.json +0 -70
- package/AGENTS.md +0 -34
- package/CLAUDE.md +0 -160
- package/CRUSH.md +0 -55
- package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
- package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
- package/TODO.todo +0 -13
- package/connectors_ideas/fastify.js +0 -217
- package/connectors_ideas/koa-plugin.js +0 -112
- package/docs/.bundle/config +0 -2
- package/docs/COMPARISON.md +0 -433
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
- package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
- package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
- package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
- package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
- package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
- package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
- package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
- package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
- package/docs/Gemfile +0 -6
- package/docs/Gemfile.lock +0 -308
- package/docs/ONBOARDING.md +0 -315
- package/docs/_config.yml +0 -36
- package/docs/_layouts/default.html +0 -123
- package/docs/apple-touch-icon.svg +0 -4
- package/docs/assets/css/style.scss +0 -392
- package/docs/favicon.svg +0 -4
- package/docs/index.md +0 -164
- package/docs/mini-guides/file-uploads-complete.md +0 -161
- package/eslint.config.js +0 -13
- package/examples/url-override-example.js +0 -137
- package/fixing_plan.md +0 -112
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
- package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
- package/plugins/storage/s3-storage.js +0 -93
- package/quickTest.js +0 -136
- package/scripts/debug-anyapi-review-filter.mjs +0 -63
- package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
- package/scripts/inspect-anyapi-descriptor.mjs +0 -19
- package/scripts/run-docs.js +0 -133
- package/test-debug.js +0 -59
- package/tests/README.md +0 -202
- package/tests/TEST_TEMPLATE.test.js +0 -178
- package/tests/anyapi-basic.test.js +0 -105
- package/tests/anyapi-cursor-pagination.test.js +0 -148
- package/tests/anyapi-custom-idproperty.test.js +0 -163
- package/tests/autofilter.test.js +0 -463
- package/tests/bulk-operations.test.js +0 -657
- package/tests/computed-fields.test.js +0 -586
- package/tests/cors.test.js +0 -834
- package/tests/curl.test.js +0 -467
- package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
- package/tests/custom-id-simplified-post.test.js +0 -75
- package/tests/custom-idproperty-relationships.test.js +0 -932
- package/tests/db-introspection.test.js +0 -521
- package/tests/db-table-operations.test.js +0 -798
- package/tests/fastify-plugin.test.js +0 -184
- package/tests/field-getters.test.js +0 -370
- package/tests/field-setters.test.js +0 -306
- package/tests/file-handling.test.js +0 -155
- package/tests/fixtures/api-configs.js +0 -2566
- package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
- package/tests/helpers/fake-fastify.js +0 -99
- package/tests/helpers/storage-mode.js +0 -65
- package/tests/helpers/test-utils.js +0 -203
- package/tests/http-connectors-parity.test.js +0 -395
- package/tests/id-normalization.test.js +0 -444
- package/tests/include-depth-validation.test.js +0 -331
- package/tests/includes.test.js +0 -427
- package/tests/local-storage.test.js +0 -96
- package/tests/logical-id-write-validation.test.js +0 -151
- package/tests/nested-includes.test.js +0 -617
- package/tests/pagination-boundaries.test.js +0 -154
- package/tests/pagination-contract.test.js +0 -122
- package/tests/pagination-cursor-multifield.test.js +0 -641
- package/tests/pagination-enhanced.test.js +0 -563
- package/tests/polymorphic-relationships.test.js +0 -347
- package/tests/polymorphic-simple.test.js +0 -148
- package/tests/positioning-advanced.test.js +0 -692
- package/tests/positioning-extended.test.js +0 -915
- package/tests/positioning-multifilter.test.js +0 -699
- package/tests/positioning.test.js +0 -560
- package/tests/post-commit-hooks.test.js +0 -120
- package/tests/projected-fields.test.js +0 -319
- package/tests/public-exports.test.js +0 -15
- package/tests/put-create-parity.test.js +0 -216
- package/tests/queries.test.js +0 -521
- package/tests/query-limits.test.js +0 -584
- package/tests/relationship-endpoints.test.js +0 -620
- package/tests/relationship-include-storage-adapter.test.js +0 -56
- package/tests/relationships.test.js +0 -681
- package/tests/rest-api.test.js +0 -337
- package/tests/return-record-settings.test.js +0 -184
- package/tests/row-policy.test.js +0 -489
- package/tests/s3-storage.test.js +0 -38
- package/tests/searchschema-knex-behavior.test.js +0 -187
- package/tests/searchschema-merge.test.js +0 -151
- package/tests/socketio.test.js +0 -860
- package/tests/storage-mapping.test.js +0 -362
- package/tests/temporal-boundaries.test.js +0 -208
- package/tests/test-development-guide.md +0 -370
- package/tests/virtual-fields.test.js +0 -404
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
1
|
+
// @ts-check
|
|
2
|
+
/** @import {
|
|
3
|
+
* CompletedWriteContext, LifecycleArguments, PivotField
|
|
4
|
+
* } from './lifecycle-types.js' */
|
|
5
|
+
import { applyResourceVersion, captureInverseVersions, invalidateInverseVersions } from '../lib/writing/resource-version.js'
|
|
6
|
+
import { RestApiResourceError } from '../../../lib/rest-api-errors.js'
|
|
7
|
+
import { lockRelationshipParent, lockRelationshipTargets, processRelationships } from '../lib/writing/relationship-processor.js'
|
|
8
|
+
import { updateReverseRelationship } from '../lib/writing/reverse-relationship-manipulations.js'
|
|
3
9
|
import { updateManyToManyRelationship, createPivotRecords } from '../lib/writing/many-to-many-manipulations.js'
|
|
4
10
|
import { ERROR_SUBTYPES } from '../lib/querying-writing/knex-constants.js'
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
requireDocumentResourceId,
|
|
8
|
-
requireExistingResourceId
|
|
9
|
-
} from '../lib/querying-writing/resource-id-normalization.js'
|
|
11
|
+
import { validateUpdateRequest } from '../lib/querying-writing/request-contracts.js'
|
|
10
12
|
import {
|
|
11
13
|
setupCommonRequest,
|
|
14
|
+
writePrecondition,
|
|
12
15
|
validateCompleteReplacePayload,
|
|
13
16
|
validateResourceAttributesBeforeWrite,
|
|
14
17
|
validateRelationshipAccess,
|
|
@@ -20,109 +23,51 @@ import {
|
|
|
20
23
|
} from './common.js'
|
|
21
24
|
|
|
22
25
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* still follow replacement semantics when a relationships object is provided.
|
|
29
|
-
* This method does NOT support creating new related resources via an `included` array.
|
|
26
|
+
* Create or replace a resource at the selected ID. Replacement requires existing
|
|
27
|
+
* stored values to be explicitly represented; relationship collections follow
|
|
28
|
+
* replacement semantics when a relationships object is supplied. This operation
|
|
29
|
+
* links existing related resources rather than creating an included graph.
|
|
30
|
+
* @param {LifecycleArguments} args
|
|
30
31
|
*/
|
|
31
32
|
export default async function putMethod ({
|
|
32
33
|
params,
|
|
33
|
-
context,
|
|
34
|
+
context: callerContext,
|
|
34
35
|
vars,
|
|
35
36
|
helpers,
|
|
36
37
|
scope,
|
|
37
38
|
scopes,
|
|
38
39
|
runHooks,
|
|
39
|
-
apiOptions,
|
|
40
|
-
pluginOptions,
|
|
41
40
|
scopeOptions,
|
|
42
41
|
scopeName,
|
|
43
42
|
api,
|
|
44
43
|
log
|
|
45
44
|
}) {
|
|
46
|
-
|
|
45
|
+
callerContext.method = 'put'
|
|
47
46
|
|
|
48
47
|
try {
|
|
49
|
-
const { schema, schemaStructure, schemaRelationships } = await setupCommonRequest({
|
|
48
|
+
const { schema, schemaStructure, schemaRelationships, versionState, context: preparedContext } = await setupCommonRequest({
|
|
50
49
|
params,
|
|
51
|
-
context,
|
|
50
|
+
context: callerContext,
|
|
52
51
|
vars,
|
|
53
52
|
scopes,
|
|
54
|
-
scopeOptions,
|
|
55
53
|
scopeName,
|
|
56
54
|
api,
|
|
57
|
-
helpers
|
|
55
|
+
helpers,
|
|
56
|
+
runHooks
|
|
58
57
|
})
|
|
59
|
-
// Run early hooks for pre-processing (e.g., file handling)
|
|
60
58
|
await runHooks('beforeProcessing')
|
|
61
59
|
await runHooks('beforeProcessingPut')
|
|
62
60
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
schemaInfo: context.schemaInfo,
|
|
66
|
-
includeDepthLimit: vars.includeDepthLimit,
|
|
67
|
-
sortableFields: vars.sortableFields
|
|
68
|
-
})
|
|
69
|
-
const normalizedPathId = params.id === undefined
|
|
70
|
-
? null
|
|
71
|
-
: requireExistingResourceId(params.id, {
|
|
72
|
-
scopeOptions,
|
|
73
|
-
vars,
|
|
74
|
-
scopeName
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
if (normalizedPathId && !context.inputRecord?.data?.id) {
|
|
78
|
-
context.inputRecord = {
|
|
79
|
-
...context.inputRecord,
|
|
80
|
-
data: {
|
|
81
|
-
...(context.inputRecord?.data || {}),
|
|
82
|
-
id: normalizedPathId
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
context.inputRecord = validateRequestContractOrThrow(
|
|
88
|
-
requestContracts.put,
|
|
89
|
-
context.inputRecord,
|
|
90
|
-
'PUT request body is invalid'
|
|
91
|
-
)
|
|
92
|
-
const normalizedBodyId = requireDocumentResourceId(context.inputRecord.data.id, {
|
|
93
|
-
scopeOptions,
|
|
94
|
-
vars
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
if (normalizedPathId && normalizedPathId !== normalizedBodyId) {
|
|
98
|
-
throw new RestApiValidationError(
|
|
99
|
-
`ID mismatch. URL path ID '${normalizedPathId}' does not match request body ID '${normalizedBodyId}'`,
|
|
100
|
-
{
|
|
101
|
-
fields: ['data.id'],
|
|
102
|
-
violations: [{
|
|
103
|
-
field: 'data.id',
|
|
104
|
-
rule: 'id_consistency',
|
|
105
|
-
message: 'Request body ID must match URL path ID when both are provided'
|
|
106
|
-
}]
|
|
107
|
-
}
|
|
108
|
-
)
|
|
109
|
-
}
|
|
110
|
-
context.inputRecord.data.id = normalizedBodyId
|
|
111
|
-
context.id = normalizedPathId || normalizedBodyId
|
|
61
|
+
validateUpdateRequest({ method: 'put', params, context: preparedContext, vars, scopeOptions, scopeName })
|
|
62
|
+
const context = /** @type {CompletedWriteContext} */ (preparedContext)
|
|
112
63
|
|
|
113
|
-
// Validate that user has read access to all related resources
|
|
114
|
-
// This ensures users can only create relationships to resources they can access
|
|
115
64
|
await validateRelationshipAccess(context, context.inputRecord, helpers, api)
|
|
116
65
|
|
|
117
|
-
|
|
118
|
-
// Example: relationships.author -> author_id: '123' for storage
|
|
119
|
-
// Example: relationships.tags -> array of pivot records to create later
|
|
120
|
-
const { belongsToUpdates, manyToManyRelationships } = processRelationships(
|
|
66
|
+
const { belongsToUpdates, belongsToTargets, manyToManyRelationships, reverseRelationships } = processRelationships(
|
|
121
67
|
scope,
|
|
122
68
|
{ context }
|
|
123
69
|
)
|
|
124
70
|
|
|
125
|
-
// Check existence first
|
|
126
71
|
context.exists = await helpers.dataExists({
|
|
127
72
|
scopeName,
|
|
128
73
|
context
|
|
@@ -142,7 +87,7 @@ export default async function putMethod ({
|
|
|
142
87
|
if (!minimalRecord) {
|
|
143
88
|
throw new RestApiResourceError(
|
|
144
89
|
`Resource not found: ${scopeName}/${context.id}`,
|
|
145
|
-
ERROR_SUBTYPES.NOT_FOUND
|
|
90
|
+
{ subtype: ERROR_SUBTYPES.NOT_FOUND }
|
|
146
91
|
)
|
|
147
92
|
}
|
|
148
93
|
|
|
@@ -161,53 +106,26 @@ export default async function putMethod ({
|
|
|
161
106
|
runHooks
|
|
162
107
|
})
|
|
163
108
|
|
|
164
|
-
//
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// Also check schema fields for belongsTo relationships
|
|
183
|
-
for (const [fieldName, fieldDef] of Object.entries(schemaStructure)) {
|
|
184
|
-
if (fieldDef.as && fieldDef.belongsTo) {
|
|
185
|
-
allRelationships[fieldDef.as] = {
|
|
186
|
-
type: 'belongsTo',
|
|
187
|
-
fieldName,
|
|
188
|
-
fieldDef
|
|
109
|
+
// Supplying relationships makes PUT clear omitted linkage; omitting the object preserves it.
|
|
110
|
+
if (context.inputRecord.data.relationships !== undefined) {
|
|
111
|
+
const provided = new Set(Object.keys(context.inputRecord.data.relationships))
|
|
112
|
+
for (const [relName, relDef] of Object.entries(schemaRelationships)) {
|
|
113
|
+
if (provided.has(relName)) continue
|
|
114
|
+
if (relDef.type === 'hasOne' || relDef.type === 'hasMany') {
|
|
115
|
+
reverseRelationships.push({ relName, relDef, relData: relDef.type === 'hasOne' ? null : [] })
|
|
116
|
+
} else if (relDef.type === 'manyToMany') {
|
|
117
|
+
manyToManyRelationships.push({
|
|
118
|
+
relName,
|
|
119
|
+
relDef: /** @type {PivotField} */ ({
|
|
120
|
+
through: relDef.through, foreignKey: relDef.foreignKey, otherKey: relDef.otherKey
|
|
121
|
+
}),
|
|
122
|
+
relData: []
|
|
123
|
+
})
|
|
189
124
|
}
|
|
190
125
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const hasRelationshipsObject = context.inputRecord.data.relationships !== undefined
|
|
195
|
-
const providedRelationships = new Set(Object.keys(context.inputRecord.data.relationships || {}))
|
|
196
|
-
|
|
197
|
-
// Only null out missing relationships if a relationships object was provided
|
|
198
|
-
if (hasRelationshipsObject) {
|
|
199
|
-
for (const [relName, relInfo] of Object.entries(allRelationships)) {
|
|
200
|
-
if (!providedRelationships.has(relName)) {
|
|
201
|
-
if (relInfo.type === 'belongsTo') {
|
|
202
|
-
belongsToUpdates[relInfo.fieldName] = null
|
|
203
|
-
} else if (relInfo.type === 'manyToMany') {
|
|
204
|
-
// Add to manyToManyRelationships with empty array
|
|
205
|
-
manyToManyRelationships.push({
|
|
206
|
-
relName,
|
|
207
|
-
relDef: relInfo.relDef,
|
|
208
|
-
relData: [] // Empty array means delete all
|
|
209
|
-
})
|
|
210
|
-
}
|
|
126
|
+
for (const [field, definition] of Object.entries(schemaStructure)) {
|
|
127
|
+
if (definition.belongsTo && definition.as && !provided.has(definition.as)) {
|
|
128
|
+
belongsToUpdates[field] = null
|
|
211
129
|
}
|
|
212
130
|
}
|
|
213
131
|
}
|
|
@@ -220,31 +138,34 @@ export default async function putMethod ({
|
|
|
220
138
|
}
|
|
221
139
|
}
|
|
222
140
|
|
|
223
|
-
// Centralised checkPermissions function
|
|
224
141
|
await scope.checkPermissions({
|
|
225
142
|
method: 'put',
|
|
226
143
|
originalContext: context,
|
|
227
144
|
})
|
|
228
145
|
|
|
146
|
+
await params[writePrecondition]?.()
|
|
147
|
+
|
|
229
148
|
await runHooks('beforeDataCall')
|
|
230
149
|
await runHooks('beforeDataCallPut')
|
|
231
150
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
context.schemaInfo,
|
|
237
|
-
context,
|
|
238
|
-
api,
|
|
239
|
-
helpers
|
|
240
|
-
)
|
|
151
|
+
await lockRelationshipTargets(api, context.transaction, belongsToTargets)
|
|
152
|
+
|
|
153
|
+
if (!context.isCreate && (manyToManyRelationships.length || reverseRelationships.length)) {
|
|
154
|
+
await lockRelationshipParent({ context, helpers, scopeName })
|
|
241
155
|
}
|
|
242
156
|
|
|
243
|
-
|
|
157
|
+
await applyFieldSetters(context, api, helpers)
|
|
158
|
+
const inverseVersions = await captureInverseVersions({ api, helpers, context, scopeName, isCreate: context.isCreate })
|
|
159
|
+
await applyResourceVersion({ state: versionState, context, helpers, scopeName, isCreate: context.isCreate })
|
|
160
|
+
|
|
161
|
+
const putContext = /** @type {CompletedWriteContext & { isCreate: boolean }} */ (context)
|
|
162
|
+
|
|
244
163
|
await helpers.dataPut({
|
|
245
164
|
scopeName,
|
|
246
|
-
context
|
|
165
|
+
context: putContext
|
|
247
166
|
})
|
|
167
|
+
await invalidateInverseVersions({ state: inverseVersions, context, helpers, api })
|
|
168
|
+
|
|
248
169
|
await runHooks('afterDataCallPut')
|
|
249
170
|
await runHooks('afterDataCall')
|
|
250
171
|
|
|
@@ -268,6 +189,7 @@ export default async function putMethod ({
|
|
|
268
189
|
await updateManyToManyRelationship(null, {
|
|
269
190
|
api,
|
|
270
191
|
context: {
|
|
192
|
+
...context,
|
|
271
193
|
resourceId: context.id,
|
|
272
194
|
relDef,
|
|
273
195
|
relData,
|
|
@@ -275,28 +197,27 @@ export default async function putMethod ({
|
|
|
275
197
|
}
|
|
276
198
|
})
|
|
277
199
|
} else if (relData.length > 0) {
|
|
278
|
-
await createPivotRecords(api, context.id, relDef, relData, context.transaction
|
|
200
|
+
await createPivotRecords(api, context.id, relDef, relData, context.transaction)
|
|
279
201
|
}
|
|
280
202
|
}
|
|
281
203
|
|
|
282
|
-
const
|
|
204
|
+
for (const { relDef, relData } of reverseRelationships) {
|
|
205
|
+
await updateReverseRelationship({ api, helpers, context, scopeName, relDef, relData })
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const response = await handleRecordReturnAfterWrite({
|
|
283
209
|
context,
|
|
284
210
|
scopeName,
|
|
285
211
|
api,
|
|
286
212
|
scopes,
|
|
287
|
-
schemaStructure,
|
|
288
|
-
schemaRelationships,
|
|
289
|
-
scopeOptions,
|
|
290
|
-
vars,
|
|
291
213
|
runHooks,
|
|
292
|
-
helpers
|
|
293
|
-
log
|
|
214
|
+
helpers
|
|
294
215
|
})
|
|
295
216
|
|
|
296
|
-
await commitOwnedTransaction(context
|
|
217
|
+
await commitOwnedTransaction(context)
|
|
297
218
|
|
|
298
|
-
return
|
|
219
|
+
return response
|
|
299
220
|
} catch (error) {
|
|
300
|
-
await handleWriteMethodError(error,
|
|
221
|
+
await handleWriteMethodError(error, callerContext, 'PUT', scopeName, log)
|
|
301
222
|
}
|
|
302
223
|
}
|
|
@@ -1,72 +1,61 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/** @import {
|
|
3
|
+
* LifecycleArguments, QueryContext, Resource
|
|
4
|
+
* } from './lifecycle-types.js' */
|
|
5
|
+
import { enrichIncludedAttributes } from './enrich-attributes.js'
|
|
6
|
+
import { rejectRemovedOptions, resolveFormat } from '../lib/querying-writing/response-options.js'
|
|
1
7
|
import { normalizeRecordAttributes } from '../lib/querying-writing/database-value-normalizers.js'
|
|
8
|
+
import { filterResponseFields, getResourceFieldset } from '../lib/querying-writing/field-utils.js'
|
|
2
9
|
import { getRequestedComputedFields, validateRequestedFieldsets } from '../lib/querying-writing/knex-field-helpers.js'
|
|
3
10
|
import { getEffectiveSortableFields } from '../lib/querying/query-field-sort-helpers.js'
|
|
4
11
|
import { transformJsonApiToSimplified } from '../lib/querying-writing/simplified-helpers.js'
|
|
5
12
|
import { getRequestContracts, validateRequestContractOrThrow } from '../lib/querying-writing/request-contracts.js'
|
|
6
|
-
import {
|
|
13
|
+
import { validateRequestedIncludes, filterToOneResponseLinkage } from './common.js'
|
|
14
|
+
import { queryConstraint } from '../lib/querying/query-constraint.js'
|
|
7
15
|
|
|
8
16
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* @param {string} resourceType - The type of resource collection to fetch (e.g., "articles").
|
|
14
|
-
* @param {object} [queryParams={}] - Optional. An object to customize the query for the collection.
|
|
15
|
-
* @param {string[]} [queryParams.include=[]] - An optional array of relationship paths to sideload for each resource in the collection. These paths will be converted to a comma-separated string for the URL (e.g., `['author', 'comments.user']` becomes `author,comments.user`). Supports deep relationships (e.g., "publisher.country").
|
|
16
|
-
* @param {object} [queryParams.fields] - An object to request only specific fields (sparse fieldsets) for each resource in the collection and its included relationships. Keys are resource types, values are comma-separated field names.
|
|
17
|
-
* @param {object} [queryParams.filters] - An object to filter the collection. Keys are filter parameters (specific to your API's implementation, e.g., 'status', 'title'), values are the filter criteria.
|
|
18
|
-
* @param {string[]} [queryParams.sort=[]] - An optional array of fields to sort the collection by. Each string represents a field; prefix with '-' for descending order (e.g., `['title', '-published-date']` becomes `title,-published-date`).
|
|
19
|
-
* @param {object} [queryParams.page] - An object for pagination. Typically includes `number` (page number) and `size` (items per page). E.g., `{ number: 1, size: 10 }`.
|
|
20
|
-
* @returns {Promise<object>} A Promise that resolves to the JSON:API response document containing the resource collection.
|
|
17
|
+
* Read a filtered collection using params.queryParams and the selected format.
|
|
18
|
+
* Preserve authorization and field selection through collection execution.
|
|
19
|
+
* Pagination metadata belongs to the result; this method sends no HTTP request.
|
|
20
|
+
* @param {LifecycleArguments<Resource[]>} args
|
|
21
21
|
*/
|
|
22
22
|
export default async function queryMethod ({
|
|
23
23
|
params,
|
|
24
|
-
context,
|
|
24
|
+
context: callerContext,
|
|
25
25
|
vars,
|
|
26
26
|
helpers,
|
|
27
27
|
scope,
|
|
28
28
|
scopes,
|
|
29
29
|
runHooks,
|
|
30
|
-
apiOptions,
|
|
31
|
-
pluginOptions,
|
|
32
|
-
scopeOptions,
|
|
33
30
|
scopeName,
|
|
34
|
-
log,
|
|
35
31
|
api
|
|
36
32
|
}) {
|
|
37
|
-
|
|
33
|
+
callerContext.method = 'query'
|
|
34
|
+
// Nested queries only inherit membership when it is explicitly supplied again.
|
|
35
|
+
callerContext[queryConstraint] = params[queryConstraint]
|
|
38
36
|
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
rejectRemovedOptions(params)
|
|
38
|
+
callerContext.format = resolveFormat(params.format, vars.format)
|
|
39
|
+
callerContext.simplified = callerContext.format === 'plain'
|
|
41
40
|
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
callerContext.schemaInfo = scope.vars.schemaInfo // This is the object variable created by compileSchemas
|
|
42
|
+
callerContext.queryParams = params.queryParams || {}
|
|
44
43
|
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
callerContext.queryParams.fields = params.queryParams?.fields ?? {}
|
|
45
|
+
if (callerContext.queryParams.include == null) delete callerContext.queryParams.include
|
|
46
|
+
callerContext.queryParams.sort = params.queryParams?.sort ?? []
|
|
47
|
+
callerContext.queryParams.page = params.queryParams?.page ?? {}
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
context.queryParams = params.queryParams || {}
|
|
49
|
+
callerContext.transaction = params.transaction
|
|
50
|
+
callerContext.db = callerContext.transaction || api.knex.instance
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
context
|
|
54
|
-
context.queryParams.include = cascadeConfig('include', [params.queryParams], [])
|
|
55
|
-
context.queryParams.sort = cascadeConfig('sort', [params.queryParams], [])
|
|
56
|
-
context.queryParams.page = cascadeConfig('page', [params.queryParams], {})
|
|
52
|
+
callerContext.scopeName = scopeName
|
|
53
|
+
const context = /** @type {QueryContext} */ (callerContext)
|
|
57
54
|
|
|
58
|
-
context.transaction = params.transaction
|
|
59
|
-
context.db = context.transaction || api.knex.instance
|
|
60
|
-
|
|
61
|
-
context.scopeName = scopeName
|
|
62
|
-
|
|
63
|
-
// These are just shortcuts used in this function and will be returned
|
|
64
55
|
const schemaStructure = context.schemaInfo.schemaInstance.structure
|
|
65
56
|
const schemaRelationships = context.schemaInfo.schemaRelationships
|
|
66
57
|
|
|
67
|
-
// Sortable fields and sort (mab)
|
|
68
58
|
context.sortableFields = getEffectiveSortableFields(vars)
|
|
69
|
-
// Apply default sort if no sort specified
|
|
70
59
|
if (context.queryParams.sort.length === 0 && vars.defaultSort) {
|
|
71
60
|
context.queryParams.sort = Array.isArray(vars.defaultSort) ? vars.defaultSort : [vars.defaultSort]
|
|
72
61
|
}
|
|
@@ -84,13 +73,13 @@ export default async function queryMethod ({
|
|
|
84
73
|
)
|
|
85
74
|
context.queryParams = validatedRequest.queryParams || {}
|
|
86
75
|
|
|
87
|
-
// Centralised checkPermissions function
|
|
88
76
|
await scope.checkPermissions({
|
|
89
77
|
method: 'query',
|
|
90
78
|
originalContext: context,
|
|
91
79
|
})
|
|
92
80
|
|
|
93
81
|
await validateRequestedFieldsets(context, scopes)
|
|
82
|
+
validateRequestedIncludes(context, scopes)
|
|
94
83
|
await runHooks('beforeData')
|
|
95
84
|
await runHooks('beforeDataQuery')
|
|
96
85
|
context.record = await helpers.dataQuery({
|
|
@@ -99,32 +88,19 @@ export default async function queryMethod ({
|
|
|
99
88
|
runHooks
|
|
100
89
|
})
|
|
101
90
|
|
|
102
|
-
// Normalize database values (e.g., convert 1/0 to true/false for booleans)
|
|
103
91
|
context.record = normalizeRecordAttributes(context.record, scopes)
|
|
104
92
|
|
|
105
|
-
|
|
106
|
-
try {
|
|
107
|
-
context.originalRecord = structuredClone(context.record)
|
|
108
|
-
} catch (e) {
|
|
109
|
-
log.error('Failed to clone record:', {
|
|
110
|
-
error: e.message,
|
|
111
|
-
recordKeys: Object.keys(context.record || {}),
|
|
112
|
-
hasHttpRequest: !!context.raw?.req
|
|
113
|
-
})
|
|
114
|
-
throw e
|
|
115
|
-
}
|
|
93
|
+
context.originalRecord = structuredClone(context.record)
|
|
116
94
|
|
|
117
|
-
// This will enhance record, which is the WHOLE JSON:API record
|
|
118
95
|
await runHooks('enrichRecord')
|
|
119
96
|
|
|
120
|
-
// Get computed field information for main resource
|
|
121
97
|
const computedFields = scope.vars.schemaInfo?.computed || {}
|
|
122
|
-
const requestedFields = context.queryParams.fields
|
|
98
|
+
const requestedFields = getResourceFieldset(context.queryParams.fields, scopeName)
|
|
123
99
|
const requestedComputedFields = getRequestedComputedFields(scopeName, requestedFields, computedFields)
|
|
124
100
|
|
|
125
|
-
// Run enrichAttributes for every single set of attribute, calling it from the right scope
|
|
126
101
|
for (const entry of context.record.data) {
|
|
127
102
|
entry.attributes = await scope.enrichAttributes({
|
|
103
|
+
id: entry.id,
|
|
128
104
|
attributes: entry.attributes,
|
|
129
105
|
parentContext: context,
|
|
130
106
|
requestedComputedFields,
|
|
@@ -132,40 +108,20 @@ export default async function queryMethod ({
|
|
|
132
108
|
computedDependencies: context.computedDependencies
|
|
133
109
|
})
|
|
134
110
|
}
|
|
135
|
-
|
|
136
|
-
const entryScope = scopes[entry.type]
|
|
137
|
-
const entryComputed = entryScope.vars.schemaInfo?.computed || {}
|
|
138
|
-
const entryRequestedFields = context.queryParams.fields?.[entry.type]
|
|
139
|
-
const entryRequestedComputed = getRequestedComputedFields(
|
|
140
|
-
entry.type,
|
|
141
|
-
entryRequestedFields,
|
|
142
|
-
entryComputed
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
entry.attributes = await entryScope.enrichAttributes({
|
|
146
|
-
attributes: entry.attributes,
|
|
147
|
-
parentContext: context,
|
|
148
|
-
requestedComputedFields: entryRequestedComputed,
|
|
149
|
-
isMainResource: false,
|
|
150
|
-
computedDependencies: entry.__$jsonrestapi_computed_deps$__
|
|
151
|
-
})
|
|
152
|
-
}
|
|
111
|
+
await enrichIncludedAttributes(context, scopes)
|
|
153
112
|
|
|
154
|
-
// The called hooks should NOT change context.record
|
|
155
113
|
await runHooks('finish')
|
|
156
114
|
await runHooks('finishQuery')
|
|
115
|
+
await filterToOneResponseLinkage(context, scopes)
|
|
157
116
|
|
|
158
117
|
// Enrichers and finish hooks run after the database pass and may introduce
|
|
159
118
|
// native values. Reassert the public temporal contract at the return boundary.
|
|
160
119
|
context.record = normalizeRecordAttributes(context.record, scopes, {
|
|
161
120
|
source: 'response'
|
|
162
121
|
})
|
|
122
|
+
filterResponseFields(context.record, context.queryParams.fields)
|
|
163
123
|
|
|
164
|
-
// Transform output if in simplified mode
|
|
165
124
|
if (context.simplified) {
|
|
166
|
-
// Convert JSON:API response back to simplified format
|
|
167
|
-
// Example: {data: {type: 'posts', id: '1', attributes: {title: 'My Post'}, relationships: {author: {data: {type: 'users', id: '123'}}}}}
|
|
168
|
-
// becomes: {id: '1', title: 'My Post', author_id: '123'} - flattens structure and restores foreign keys
|
|
169
125
|
return transformJsonApiToSimplified(
|
|
170
126
|
{ record: context.record },
|
|
171
127
|
{ context: { schemaStructure, schemaRelationships, scopes } }
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
// @ts-check
|
|
1
2
|
/**
|
|
2
3
|
* release
|
|
3
4
|
* Runs the hook that tells plugins to release resources
|
|
4
5
|
*/
|
|
6
|
+
/** @param {import('../../../types/runtime.js').RuntimeArguments} args */
|
|
5
7
|
export default async ({ runHooks }) => {
|
|
6
|
-
// Run the addRoute hook to notify transport plugins
|
|
7
8
|
await runHooks('release')
|
|
8
9
|
|
|
9
10
|
return { }
|