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
|
@@ -1,300 +0,0 @@
|
|
|
1
|
-
1 import { RestApiResourceError } from '../../../../lib/rest-api-errors.js'
|
|
2
|
-
2 import { transformSimplifiedToJsonApi } from '../querying-writing/simplified-helpers.js'
|
|
3
|
-
3
|
|
4
|
-
4 /**
|
|
5
|
-
5 * Updates many-to-many relationships intelligently by synchronizing pivot table records
|
|
6
|
-
6 *
|
|
7
|
-
7 * @param {Object} scope - The scope object (not used directly, for consistency)
|
|
8
|
-
8 * @param {Object} deps - Dependencies object
|
|
9
|
-
9 * @returns {Promise<void>}
|
|
10
|
-
10 *
|
|
11
|
-
11 * @example
|
|
12
|
-
12 * // Input: Article currently has tags [1, 2], want to change to [1, 3]
|
|
13
|
-
13 * const deps = {
|
|
14
|
-
14 * api,
|
|
15
|
-
15 * context: {
|
|
16
|
-
16 * resourceId: '100',
|
|
17
|
-
17 * relDef: {
|
|
18
|
-
18 * through: 'article_tags', // Pivot table
|
|
19
|
-
19 * foreignKey: 'article_id', // Points to article
|
|
20
|
-
20 * otherKey: 'tag_id' // Points to tag
|
|
21
|
-
21 * },
|
|
22
|
-
22 * relData: [
|
|
23
|
-
23 * { type: 'tags', id: '1' }, // Keep this
|
|
24
|
-
24 * { type: 'tags', id: '3' } // Add this
|
|
25
|
-
25 * ],
|
|
26
|
-
26 * transaction: trx
|
|
27
|
-
27 * }
|
|
28
|
-
28 * };
|
|
29
|
-
29 *
|
|
30
|
-
30 * // Before: article_tags table
|
|
31
|
-
31 * // article_id | tag_id | created_at
|
|
32
|
-
32 * // 100 | 1 | 2024-01-01
|
|
33
|
-
33 * // 100 | 2 | 2024-01-02
|
|
34
|
-
34 *
|
|
35
|
-
35 * await updateManyToManyRelationship(null, deps);
|
|
36
|
-
36 *
|
|
37
|
-
37 * // After: article_tags table
|
|
38
|
-
38 * // article_id | tag_id | created_at
|
|
39
|
-
39 * // 100 | 1 | 2024-01-01 (preserved!)
|
|
40
|
-
40 * // 100 | 3 | 2024-12-01 (new)
|
|
41
|
-
41 * // Tag 2 was deleted, Tag 1 kept its metadata
|
|
42
|
-
42 *
|
|
43
|
-
43 * @example
|
|
44
|
-
44 * // Input: Pivot table has extra fields to preserve
|
|
45
|
-
45 * // article_tags has: article_id, tag_id, display_order, featured
|
|
46
|
-
46 *
|
|
47
|
-
47 * // Current data:
|
|
48
|
-
48 * // article_id | tag_id | display_order | featured
|
|
49
|
-
49 * // 100 | 1 | 1 | true
|
|
50
|
-
50 * // 100 | 2 | 2 | false
|
|
51
|
-
51 *
|
|
52
|
-
52 * const deps = {
|
|
53
|
-
53 * context: {
|
|
54
|
-
54 * resourceId: '100',
|
|
55
|
-
55 * relData: [
|
|
56
|
-
56 * { type: 'tags', id: '1' }, // Keep tag 1
|
|
57
|
-
57 * { type: 'tags', id: '5' } // Add tag 5
|
|
58
|
-
58 * ]
|
|
59
|
-
59 * }
|
|
60
|
-
60 * };
|
|
61
|
-
61 *
|
|
62
|
-
62 * await updateManyToManyRelationship(null, deps);
|
|
63
|
-
63 *
|
|
64
|
-
64 * // Result:
|
|
65
|
-
65 * // article_id | tag_id | display_order | featured
|
|
66
|
-
66 * // 100 | 1 | 1 | true (preserved!)
|
|
67
|
-
67 * // 100 | 5 | NULL | NULL (new with defaults)
|
|
68
|
-
68 *
|
|
69
|
-
69 * @example
|
|
70
|
-
70 * // Input: Clear all relationships
|
|
71
|
-
71 * const deps = {
|
|
72
|
-
72 * context: {
|
|
73
|
-
73 * resourceId: '100',
|
|
74
|
-
74 * relData: [] // Empty array means remove all
|
|
75
|
-
75 * }
|
|
76
|
-
76 * };
|
|
77
|
-
77 *
|
|
78
|
-
78 * await updateManyToManyRelationship(null, deps);
|
|
79
|
-
79 * // All article_tags records for article 100 are deleted
|
|
80
|
-
80 *
|
|
81
|
-
81 * @description
|
|
82
|
-
82 * Used by:
|
|
83
|
-
83 * - relationship-processor.js calls this for many-to-many updates
|
|
84
|
-
84 * - Used in PATCH operations to sync relationships
|
|
85
|
-
85 * - Also used in PUT operations (replaces deleteExistingPivotRecords pattern)
|
|
86
|
-
86 *
|
|
87
|
-
87 * Purpose:
|
|
88
|
-
88 * - Intelligently syncs pivot table to match desired state
|
|
89
|
-
89 * - Preserves existing pivot records that should remain (with their metadata)
|
|
90
|
-
90 * - Only deletes records that should be removed
|
|
91
|
-
91 * - Only creates records that are new
|
|
92
|
-
92 * - Much better than delete-all-then-recreate pattern
|
|
93
|
-
93 *
|
|
94
|
-
94 * Data flow:
|
|
95
|
-
95 * 1. Queries existing pivot records for the resource
|
|
96
|
-
96 * 2. Compares existing IDs with desired IDs
|
|
97
|
-
97 * 3. Calculates which to delete and which to add
|
|
98
|
-
98 * 4. Validates new related resources exist (optional)
|
|
99
|
-
99 * 5. Performs bulk delete for removed relationships
|
|
100
|
-
100 * 6. Performs bulk insert for new relationships
|
|
101
|
-
101 * 7. Records that exist in both are untouched (metadata preserved)
|
|
102
|
-
102 */
|
|
103
|
-
103 export const updateManyToManyRelationship = async (scope, deps) => {
|
|
104
|
-
104 // Extract values from deps
|
|
105
|
-
105 const { api, context } = deps
|
|
106
|
-
106 const { resourceId, relDef, relData, transaction: trx } = context
|
|
107
|
-
107
|
|
108
|
-
108 // Get the knex instance from the pivot scope
|
|
109
|
-
109 const pivotScope = api.resources[relDef.through]
|
|
110
|
-
110 if (!pivotScope) {
|
|
111
|
-
111 throw new Error(`Pivot table resource '${relDef.through}' not found`)
|
|
112
|
-
112 }
|
|
113
|
-
113
|
|
114
|
-
114 // Get the actual database table name (might be different from scope name)
|
|
115
|
-
115 const tableName = pivotScope.vars.schemaInfo.tableName || relDef.through
|
|
116
|
-
116
|
|
117
|
-
117 // Get existing pivot records directly from database
|
|
118
|
-
118 const existingRecords = await trx(tableName)
|
|
119
|
-
119 .where(relDef.foreignKey, resourceId)
|
|
120
|
-
120 .select(relDef.otherKey)
|
|
121
|
-
121
|
|
122
|
-
122 // Create sets for efficient comparison
|
|
123
|
-
123 const existingIds = new Set(existingRecords.map(r => String(r[relDef.otherKey])))
|
|
124
|
-
124 const newIds = new Set(relData.map(r => String(r.id)))
|
|
125
|
-
125
|
|
126
|
-
126 // Determine what to delete and add
|
|
127
|
-
127 const toDelete = [...existingIds].filter(id => !newIds.has(id))
|
|
128
|
-
128 const toAdd = [...newIds].filter(id => !existingIds.has(id))
|
|
129
|
-
129
|
|
130
|
-
130 // Validate related resources exist if needed (do this before any changes)
|
|
131
|
-
131 if (relDef.validateExists !== false && toAdd.length > 0) {
|
|
132
|
-
132 for (const relatedId of toAdd) {
|
|
133
|
-
133 const related = relData.find(r => String(r.id) === relatedId)
|
|
134
|
-
134 try {
|
|
135
|
-
135 await api.resources[related.type].get({
|
|
136
|
-
136 id: related.id,
|
|
137
|
-
137 transaction: trx
|
|
138
|
-
138 })
|
|
139
|
-
139 } catch (error) {
|
|
140
|
-
140 throw new RestApiResourceError(
|
|
141
|
-
141 `Related ${related.type} with id ${related.id} not found`,
|
|
142
|
-
142 {
|
|
143
|
-
143 subtype: 'not_found',
|
|
144
|
-
144 resourceType: related.type,
|
|
145
|
-
145 resourceId: related.id
|
|
146
|
-
146 }
|
|
147
|
-
147 )
|
|
148
|
-
148 }
|
|
149
|
-
149 }
|
|
150
|
-
150 }
|
|
151
|
-
151
|
|
152
|
-
152 // Bulk delete records that should be removed
|
|
153
|
-
153 if (toDelete.length > 0) {
|
|
154
|
-
154 await trx(tableName)
|
|
155
|
-
155 .where(relDef.foreignKey, resourceId)
|
|
156
|
-
156 .whereIn(relDef.otherKey, toDelete)
|
|
157
|
-
157 .delete()
|
|
158
|
-
158 }
|
|
159
|
-
159
|
|
160
|
-
160 // Bulk insert new records
|
|
161
|
-
161 if (toAdd.length > 0) {
|
|
162
|
-
162 const recordsToInsert = toAdd.map(relatedId => ({
|
|
163
|
-
163 [relDef.foreignKey]: resourceId,
|
|
164
|
-
164 [relDef.otherKey]: relatedId
|
|
165
|
-
165 }))
|
|
166
|
-
166
|
|
167
|
-
167 await trx(tableName).insert(recordsToInsert)
|
|
168
|
-
168 }
|
|
169
|
-
169
|
|
170
|
-
170 // Records that exist in both are automatically preserved with their pivot data
|
|
171
|
-
171 }
|
|
172
|
-
172
|
|
173
|
-
173 // Note: deleteExistingPivotRecords has been removed in favor of using
|
|
174
|
-
174 // updateManyToManyRelationship for all sync operations (including PUT).
|
|
175
|
-
175 // This aligns with industry standards where ORMs use intelligent sync
|
|
176
|
-
176 // rather than delete-all-then-recreate patterns.
|
|
177
|
-
177
|
|
178
|
-
178 /**
|
|
179
|
-
179 * Creates new pivot table records for many-to-many relationships
|
|
180
|
-
180 *
|
|
181
|
-
181 * @param {Object} api - The API instance with access to resources
|
|
182
|
-
182 * @param {string|number} resourceId - The ID of the primary resource
|
|
183
|
-
183 * @param {Object} relDef - The relationship definition
|
|
184
|
-
184 * @param {Array} relData - Array of related resources to link
|
|
185
|
-
185 * @param {Object} trx - Database transaction object
|
|
186
|
-
186 * @returns {Promise<void>}
|
|
187
|
-
187 *
|
|
188
|
-
188 * @example
|
|
189
|
-
189 * // Input: Create article-tag relationships
|
|
190
|
-
190 * const relDef = {
|
|
191
|
-
191 * through: 'article_tags',
|
|
192
|
-
192 * foreignKey: 'article_id',
|
|
193
|
-
193 * otherKey: 'tag_id'
|
|
194
|
-
194 * };
|
|
195
|
-
195 * const relData = [
|
|
196
|
-
196 * { type: 'tags', id: '10' },
|
|
197
|
-
197 * { type: 'tags', id: '20' },
|
|
198
|
-
198 * { type: 'tags', id: '30' }
|
|
199
|
-
199 * ];
|
|
200
|
-
200 *
|
|
201
|
-
201 * await createPivotRecords(api, '100', relDef, relData, trx);
|
|
202
|
-
202 *
|
|
203
|
-
203 * // Result: 3 new records in article_tags table
|
|
204
|
-
204 * // article_id | tag_id
|
|
205
|
-
205 * // 100 | 10
|
|
206
|
-
206 * // 100 | 20
|
|
207
|
-
207 * // 100 | 30
|
|
208
|
-
208 *
|
|
209
|
-
209 * @example
|
|
210
|
-
210 * // Input: Validation ensures related resources exist
|
|
211
|
-
211 * const relData = [
|
|
212
|
-
212 * { type: 'tags', id: '999' } // Non-existent tag
|
|
213
|
-
213 * ];
|
|
214
|
-
214 *
|
|
215
|
-
215 * try {
|
|
216
|
-
216 * await createPivotRecords(api, '100', relDef, relData, trx);
|
|
217
|
-
217 * } catch (error) {
|
|
218
|
-
218 * console.log(error.message);
|
|
219
|
-
219 * // "Related tags with id 999 not found"
|
|
220
|
-
220 * // Transaction rolled back, no records created
|
|
221
|
-
221 * }
|
|
222
|
-
222 *
|
|
223
|
-
223 * @example
|
|
224
|
-
224 * // Input: Skip validation for performance
|
|
225
|
-
225 * const relDef = {
|
|
226
|
-
226 * through: 'user_permissions',
|
|
227
|
-
227 * foreignKey: 'user_id',
|
|
228
|
-
228 * otherKey: 'permission_id',
|
|
229
|
-
229 * validateExists: false // Skip GET requests
|
|
230
|
-
230 * };
|
|
231
|
-
231 *
|
|
232
|
-
232 * // With 100 permissions, saves 100 GET requests
|
|
233
|
-
233 * await createPivotRecords(api, userId, relDef, permissions, trx);
|
|
234
|
-
234 *
|
|
235
|
-
235 * // Risk: Could create orphaned relationships if permissions don't exist
|
|
236
|
-
236 * // Benefit: Much faster for bulk operations when you trust the data
|
|
237
|
-
237 *
|
|
238
|
-
238 * @description
|
|
239
|
-
239 * Used by:
|
|
240
|
-
240 * - relationship-processor.js for POST operations
|
|
241
|
-
241 * - updateManyToManyRelationship internally for new relationships
|
|
242
|
-
242 * - Any code that needs to create pivot records
|
|
243
|
-
243 *
|
|
244
|
-
244 * Purpose:
|
|
245
|
-
245 * - Creates pivot table records to link resources
|
|
246
|
-
246 * - Validates related resources exist by default (referential integrity)
|
|
247
|
-
247 * - Supports bulk insert for efficiency
|
|
248
|
-
248 * - Works within transactions for atomicity
|
|
249
|
-
249 * - Allows skipping validation when performance matters
|
|
250
|
-
250 *
|
|
251
|
-
251 * Data flow:
|
|
252
|
-
252 * 1. Validates pivot table resource exists
|
|
253
|
-
253 * 2. Gets actual database table name
|
|
254
|
-
254 * 3. Optionally validates each related resource exists (GET requests)
|
|
255
|
-
255 * 4. Prepares bulk insert data with foreign keys
|
|
256
|
-
256 * 5. Performs single INSERT with all records
|
|
257
|
-
257 * 6. Returns (no data returned, throws on error)
|
|
258
|
-
258 */
|
|
259
|
-
259 export const createPivotRecords = async (api, resourceId, relDef, relData, trx) => {
|
|
260
|
-
260 if (relData.length === 0) return // Early exit if nothing to create
|
|
261
|
-
261
|
|
262
|
-
262 // Get pivot table info
|
|
263
|
-
263 const pivotScope = api.resources[relDef.through]
|
|
264
|
-
264 if (!pivotScope) {
|
|
265
|
-
265 throw new Error(`Pivot table resource '${relDef.through}' not found`)
|
|
266
|
-
266 }
|
|
267
|
-
267
|
|
268
|
-
268 // Get the actual database table name (might be different from scope name)
|
|
269
|
-
269 const tableName = pivotScope.vars.schemaInfo.tableName || relDef.through
|
|
270
|
-
270
|
|
271
|
-
271 // Validate all related resources exist if needed (do this before any inserts)
|
|
272
|
-
272 if (relDef.validateExists !== false) {
|
|
273
|
-
273 for (const related of relData) {
|
|
274
|
-
274 try {
|
|
275
|
-
275 await api.resources[related.type].get({
|
|
276
|
-
276 id: related.id,
|
|
277
|
-
277 transaction: trx
|
|
278
|
-
278 })
|
|
279
|
-
279 } catch (error) {
|
|
280
|
-
280 throw new RestApiResourceError(
|
|
281
|
-
281 `Related ${related.type} with id ${related.id} not found`,
|
|
282
|
-
282 {
|
|
283
|
-
283 subtype: 'not_found',
|
|
284
|
-
284 resourceType: related.type,
|
|
285
|
-
285 resourceId: related.id
|
|
286
|
-
286 }
|
|
287
|
-
287 )
|
|
288
|
-
288 }
|
|
289
|
-
289 }
|
|
290
|
-
290 }
|
|
291
|
-
291
|
|
292
|
-
292 // Prepare records for bulk insert
|
|
293
|
-
293 const recordsToInsert = relData.map(related => ({
|
|
294
|
-
294 [relDef.foreignKey]: resourceId,
|
|
295
|
-
295 [relDef.otherKey]: related.id
|
|
296
|
-
296 }))
|
|
297
|
-
297
|
|
298
|
-
298 // Bulk insert all pivot records in a single query
|
|
299
|
-
299 await trx(tableName).insert(recordsToInsert)
|
|
300
|
-
300 }
|
|
@@ -1,245 +0,0 @@
|
|
|
1
|
-
1 import { RestApiValidationError } from '../../../../lib/rest-api-errors.js'
|
|
2
|
-
2
|
|
3
|
-
3 /**
|
|
4
|
-
4 * Processes JSON:API relationship data and converts to database operations
|
|
5
|
-
5 *
|
|
6
|
-
6 * @param {Object} scope - The scope object containing schema info
|
|
7
|
-
7 * @param {Object} deps - Dependencies object
|
|
8
|
-
8 * @returns {Object} Object with belongsToUpdates and manyToManyRelationships arrays
|
|
9
|
-
9 *
|
|
10
|
-
10 * @example
|
|
11
|
-
11 * // Input: Simple belongsTo relationship
|
|
12
|
-
12 * const inputRecord = {
|
|
13
|
-
13 * data: {
|
|
14
|
-
14 * type: 'articles',
|
|
15
|
-
15 * attributes: { title: 'My Article' },
|
|
16
|
-
16 * relationships: {
|
|
17
|
-
17 * author: {
|
|
18
|
-
18 * data: { type: 'users', id: '123' }
|
|
19
|
-
19 * },
|
|
20
|
-
20 * category: {
|
|
21
|
-
21 * data: { type: 'categories', id: '5' }
|
|
22
|
-
22 * }
|
|
23
|
-
23 * }
|
|
24
|
-
24 * }
|
|
25
|
-
25 * };
|
|
26
|
-
26 *
|
|
27
|
-
27 * // Schema has:
|
|
28
|
-
28 * // author_id: { belongsTo: 'users', as: 'author' }
|
|
29
|
-
29 * // category_id: { belongsTo: 'categories', as: 'category' }
|
|
30
|
-
30 *
|
|
31
|
-
31 * const result = processRelationships(scope, { context: { inputRecord } });
|
|
32
|
-
32 *
|
|
33
|
-
33 * // Output: Foreign keys extracted
|
|
34
|
-
34 * // {
|
|
35
|
-
35 * // belongsToUpdates: {
|
|
36
|
-
36 * // author_id: '123',
|
|
37
|
-
37 * // category_id: '5'
|
|
38
|
-
38 * // },
|
|
39
|
-
39 * // manyToManyRelationships: []
|
|
40
|
-
40 * // }
|
|
41
|
-
41 *
|
|
42
|
-
42 * @example
|
|
43
|
-
43 * // Input: Polymorphic relationship
|
|
44
|
-
44 * const inputRecord = {
|
|
45
|
-
45 * data: {
|
|
46
|
-
46 * type: 'comments',
|
|
47
|
-
47 * relationships: {
|
|
48
|
-
48 * commentable: {
|
|
49
|
-
49 * data: { type: 'posts', id: '456' }
|
|
50
|
-
50 * }
|
|
51
|
-
51 * }
|
|
52
|
-
52 * }
|
|
53
|
-
53 * };
|
|
54
|
-
54 *
|
|
55
|
-
55 * // Schema relationships has:
|
|
56
|
-
56 * // commentable: {
|
|
57
|
-
57 * // belongsToPolymorphic: {
|
|
58
|
-
58 * // types: ['posts', 'videos'],
|
|
59
|
-
59 * // typeField: 'commentable_type',
|
|
60
|
-
60 * // idField: 'commentable_id'
|
|
61
|
-
61 * // }
|
|
62
|
-
62 * // }
|
|
63
|
-
63 *
|
|
64
|
-
64 * const result = processRelationships(scope, { context: { inputRecord } });
|
|
65
|
-
65 *
|
|
66
|
-
66 * // Output: Both type and id fields set
|
|
67
|
-
67 * // {
|
|
68
|
-
68 * // belongsToUpdates: {
|
|
69
|
-
69 * // commentable_type: 'posts',
|
|
70
|
-
70 * // commentable_id: '456'
|
|
71
|
-
71 * // },
|
|
72
|
-
72 * // manyToManyRelationships: []
|
|
73
|
-
73 * // }
|
|
74
|
-
74 *
|
|
75
|
-
75 * @example
|
|
76
|
-
76 * // Input: Many-to-many relationship
|
|
77
|
-
77 * const inputRecord = {
|
|
78
|
-
78 * data: {
|
|
79
|
-
79 * type: 'articles',
|
|
80
|
-
80 * relationships: {
|
|
81
|
-
81 * tags: {
|
|
82
|
-
82 * data: [
|
|
83
|
-
83 * { type: 'tags', id: '10' },
|
|
84
|
-
84 * { type: 'tags', id: '20' }
|
|
85
|
-
85 * ]
|
|
86
|
-
86 * }
|
|
87
|
-
87 * }
|
|
88
|
-
88 * }
|
|
89
|
-
89 * };
|
|
90
|
-
90 *
|
|
91
|
-
91 * // Schema relationships has:
|
|
92
|
-
92 * // tags: {
|
|
93
|
-
93 * // type: 'manyToMany',
|
|
94
|
-
94 * // through: 'article_tags',
|
|
95
|
-
95 * // foreignKey: 'article_id',
|
|
96
|
-
96 * // otherKey: 'tag_id'
|
|
97
|
-
97 * // }
|
|
98
|
-
98 *
|
|
99
|
-
99 * const result = processRelationships(scope, { context: { inputRecord } });
|
|
100
|
-
100 *
|
|
101
|
-
101 * // Output: Many-to-many data collected
|
|
102
|
-
102 * // {
|
|
103
|
-
103 * // belongsToUpdates: {},
|
|
104
|
-
104 * // manyToManyRelationships: [{
|
|
105
|
-
105 * // relName: 'tags',
|
|
106
|
-
106 * // relDef: {
|
|
107
|
-
107 * // through: 'article_tags',
|
|
108
|
-
108 * // foreignKey: 'article_id',
|
|
109
|
-
109 * // otherKey: 'tag_id'
|
|
110
|
-
110 * // },
|
|
111
|
-
111 * // relData: [
|
|
112
|
-
112 * // { type: 'tags', id: '10' },
|
|
113
|
-
113 * // { type: 'tags', id: '20' }
|
|
114
|
-
114 * // ]
|
|
115
|
-
115 * // }]
|
|
116
|
-
116 * // }
|
|
117
|
-
117 *
|
|
118
|
-
118 * @example
|
|
119
|
-
119 * // Input: Clearing relationships
|
|
120
|
-
120 * const inputRecord = {
|
|
121
|
-
121 * data: {
|
|
122
|
-
122 * relationships: {
|
|
123
|
-
123 * author: { data: null }, // Clear belongsTo
|
|
124
|
-
124 * tags: { data: [] } // Clear many-to-many
|
|
125
|
-
125 * }
|
|
126
|
-
126 * }
|
|
127
|
-
127 * };
|
|
128
|
-
128 *
|
|
129
|
-
129 * const result = processRelationships(scope, { context: { inputRecord } });
|
|
130
|
-
130 *
|
|
131
|
-
131 * // Output: Null for belongsTo, empty array for many-to-many
|
|
132
|
-
132 * // {
|
|
133
|
-
133 * // belongsToUpdates: { author_id: null },
|
|
134
|
-
134 * // manyToManyRelationships: [{
|
|
135
|
-
135 * // relName: 'tags',
|
|
136
|
-
136 * // relDef: { ... },
|
|
137
|
-
137 * // relData: [] // Will delete all pivot records
|
|
138
|
-
138 * // }]
|
|
139
|
-
139 * // }
|
|
140
|
-
140 *
|
|
141
|
-
141 * @description
|
|
142
|
-
142 * Used by:
|
|
143
|
-
143 * - rest-api-knex-plugin's dataPut and dataPatch methods
|
|
144
|
-
144 * - Called before database writes to prepare relationship updates
|
|
145
|
-
145 *
|
|
146
|
-
146 * Purpose:
|
|
147
|
-
147 * - Converts JSON:API relationship format to database operations
|
|
148
|
-
148 * - Handles all relationship types: belongsTo, polymorphic, many-to-many
|
|
149
|
-
149 * - Validates polymorphic types against allowed values
|
|
150
|
-
150 * - Separates concerns: foreign keys vs pivot table operations
|
|
151
|
-
151 *
|
|
152
|
-
152 * Data flow:
|
|
153
|
-
153 * 1. Receives JSON:API document with relationships section
|
|
154
|
-
154 * 2. Analyzes schema to determine relationship types
|
|
155
|
-
155 * 3. For belongsTo: extracts foreign key values
|
|
156
|
-
156 * 4. For polymorphic: extracts both type and id fields
|
|
157
|
-
157 * 5. For many-to-many: collects data for pivot operations
|
|
158
|
-
158 * 6. Returns structured data for database updates
|
|
159
|
-
159 */
|
|
160
|
-
160 export const processRelationships = (scope, deps) => {
|
|
161
|
-
161 // Extract values from scope
|
|
162
|
-
162 const {
|
|
163
|
-
163 vars: {
|
|
164
|
-
164 schemaInfo: { schemaStructure: schemaFields, schemaRelationships: relationships }
|
|
165
|
-
165 }
|
|
166
|
-
166 } = scope
|
|
167
|
-
167
|
|
168
|
-
168 // Extract values from deps
|
|
169
|
-
169 const { context } = deps
|
|
170
|
-
170 const { inputRecord } = context
|
|
171
|
-
171 const belongsToUpdates = {}
|
|
172
|
-
172 const manyToManyRelationships = []
|
|
173
|
-
173
|
|
174
|
-
174 if (!inputRecord.data.relationships) {
|
|
175
|
-
175 return { belongsToUpdates, manyToManyRelationships }
|
|
176
|
-
176 }
|
|
177
|
-
177
|
|
178
|
-
178 for (const [relName, relData] of Object.entries(inputRecord.data.relationships)) {
|
|
179
|
-
179 const relDef = relationships?.[relName]
|
|
180
|
-
180
|
|
181
|
-
181 // Find the schema field that defines this relationship
|
|
182
|
-
182 const schemaField = Object.entries(schemaFields).find(([fieldName, fieldDef]) =>
|
|
183
|
-
183 fieldDef.as === relName
|
|
184
|
-
184 )
|
|
185
|
-
185
|
|
186
|
-
186 if (schemaField) {
|
|
187
|
-
187 const [fieldName, fieldDef] = schemaField
|
|
188
|
-
188
|
|
189
|
-
189 // Handle regular belongsTo (1:1)
|
|
190
|
-
190 if (fieldDef.belongsTo) {
|
|
191
|
-
191 if (relData.data === null) {
|
|
192
|
-
192 belongsToUpdates[fieldName] = null
|
|
193
|
-
193 } else if (relData.data?.id) {
|
|
194
|
-
194 belongsToUpdates[fieldName] = relData.data.id
|
|
195
|
-
195 }
|
|
196
|
-
196 }
|
|
197
|
-
197 }
|
|
198
|
-
198
|
|
199
|
-
199 // Check for polymorphic belongsTo defined in relationships object (not as schema field)
|
|
200
|
-
200 if (!schemaField && relDef?.belongsToPolymorphic && relData.data !== undefined) {
|
|
201
|
-
201 if (relData.data === null) {
|
|
202
|
-
202 const { typeField, idField } = relDef.belongsToPolymorphic
|
|
203
|
-
203 belongsToUpdates[typeField] = null
|
|
204
|
-
204 belongsToUpdates[idField] = null
|
|
205
|
-
205 } else if (relData.data) {
|
|
206
|
-
206 const { type, id } = relData.data
|
|
207
|
-
207 const { types, typeField, idField } = relDef.belongsToPolymorphic
|
|
208
|
-
208
|
|
209
|
-
209 // Validate type is allowed
|
|
210
|
-
210 if (!types.includes(type)) {
|
|
211
|
-
211 throw new RestApiValidationError(
|
|
212
|
-
212 `Invalid type '${type}' for polymorphic relationship '${relName}'. Allowed types: ${types.join(', ')}`,
|
|
213
|
-
213 {
|
|
214
|
-
214 fields: [`data.relationships.${relName}.data.type`],
|
|
215
|
-
215 violations: [{
|
|
216
|
-
216 field: `data.relationships.${relName}.data.type`,
|
|
217
|
-
217 rule: 'polymorphic_type',
|
|
218
|
-
218 message: `Type must be one of: ${types.join(', ')}`
|
|
219
|
-
219 }]
|
|
220
|
-
220 }
|
|
221
|
-
221 )
|
|
222
|
-
222 }
|
|
223
|
-
223
|
|
224
|
-
224 belongsToUpdates[typeField] = type
|
|
225
|
-
225 belongsToUpdates[idField] = id
|
|
226
|
-
226 }
|
|
227
|
-
227 }
|
|
228
|
-
228
|
|
229
|
-
229 // Check relationship type and process accordingly
|
|
230
|
-
230 if (relDef?.type === 'manyToMany' && relData.data !== undefined) {
|
|
231
|
-
231 manyToManyRelationships.push({
|
|
232
|
-
232 relName,
|
|
233
|
-
233 relDef: {
|
|
234
|
-
234 through: relDef.through,
|
|
235
|
-
235 foreignKey: relDef.foreignKey,
|
|
236
|
-
236 otherKey: relDef.otherKey
|
|
237
|
-
237 },
|
|
238
|
-
238 relData: relData.data || [] // null means empty array for many-to-many
|
|
239
|
-
239 })
|
|
240
|
-
240 }
|
|
241
|
-
241 // Note: hasOne and hasMany don't need processing here as they don't update the current record
|
|
242
|
-
242 }
|
|
243
|
-
243
|
|
244
|
-
244 return { belongsToUpdates, manyToManyRelationships }
|
|
245
|
-
245 }
|