json-rest-api 1.0.29 → 2.1.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 +733 -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 +103 -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 +3838 -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 +37 -0
- package/index.js +3 -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 +70 -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 +586 -418
- 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 +120 -56
- package/plugins/core/lib/anyapi/stored-definition.js +109 -0
- 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 +290 -2475
- 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/anyapi.d.ts +30 -0
- 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 +65 -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,303 +1,133 @@
|
|
|
1
|
-
import { RestApiResourceError } from '../../../../lib/rest-api-errors.js'
|
|
2
1
|
import { normalizeRelationshipIdentifiers } from '../querying-writing/resource-id-normalization.js'
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
* through: 'article_tags', // Pivot table
|
|
19
|
-
* foreignKey: 'article_id', // Points to article
|
|
20
|
-
* otherKey: 'tag_id' // Points to tag
|
|
21
|
-
* },
|
|
22
|
-
* relData: [
|
|
23
|
-
* { type: 'tags', id: '1' }, // Keep this
|
|
24
|
-
* { type: 'tags', id: '3' } // Add this
|
|
25
|
-
* ],
|
|
26
|
-
* transaction: trx
|
|
27
|
-
* }
|
|
28
|
-
* };
|
|
29
|
-
*
|
|
30
|
-
* // Before: article_tags table
|
|
31
|
-
* // article_id | tag_id | created_at
|
|
32
|
-
* // 100 | 1 | 2024-01-01
|
|
33
|
-
* // 100 | 2 | 2024-01-02
|
|
34
|
-
*
|
|
35
|
-
* await updateManyToManyRelationship(null, deps);
|
|
36
|
-
*
|
|
37
|
-
* // After: article_tags table
|
|
38
|
-
* // article_id | tag_id | created_at
|
|
39
|
-
* // 100 | 1 | 2024-01-01 (preserved!)
|
|
40
|
-
* // 100 | 3 | 2024-12-01 (new)
|
|
41
|
-
* // Tag 2 was deleted, Tag 1 kept its metadata
|
|
42
|
-
*
|
|
43
|
-
* @example
|
|
44
|
-
* // Input: Pivot table has extra fields to preserve
|
|
45
|
-
* // article_tags has: article_id, tag_id, display_order, featured
|
|
46
|
-
*
|
|
47
|
-
* // Current data:
|
|
48
|
-
* // article_id | tag_id | display_order | featured
|
|
49
|
-
* // 100 | 1 | 1 | true
|
|
50
|
-
* // 100 | 2 | 2 | false
|
|
51
|
-
*
|
|
52
|
-
* const deps = {
|
|
53
|
-
* context: {
|
|
54
|
-
* resourceId: '100',
|
|
55
|
-
* relData: [
|
|
56
|
-
* { type: 'tags', id: '1' }, // Keep tag 1
|
|
57
|
-
* { type: 'tags', id: '5' } // Add tag 5
|
|
58
|
-
* ]
|
|
59
|
-
* }
|
|
60
|
-
* };
|
|
61
|
-
*
|
|
62
|
-
* await updateManyToManyRelationship(null, deps);
|
|
63
|
-
*
|
|
64
|
-
* // Result:
|
|
65
|
-
* // article_id | tag_id | display_order | featured
|
|
66
|
-
* // 100 | 1 | 1 | true (preserved!)
|
|
67
|
-
* // 100 | 5 | NULL | NULL (new with defaults)
|
|
68
|
-
*
|
|
69
|
-
* @example
|
|
70
|
-
* // Input: Clear all relationships
|
|
71
|
-
* const deps = {
|
|
72
|
-
* context: {
|
|
73
|
-
* resourceId: '100',
|
|
74
|
-
* relData: [] // Empty array means remove all
|
|
75
|
-
* }
|
|
76
|
-
* };
|
|
77
|
-
*
|
|
78
|
-
* await updateManyToManyRelationship(null, deps);
|
|
79
|
-
* // All article_tags records for article 100 are deleted
|
|
80
|
-
*
|
|
81
|
-
* @description
|
|
82
|
-
* Used by:
|
|
83
|
-
* - relationship-processor.js calls this for many-to-many updates
|
|
84
|
-
* - Used in PATCH operations to sync relationships
|
|
85
|
-
* - Also used in PUT operations (replaces deleteExistingPivotRecords pattern)
|
|
86
|
-
*
|
|
87
|
-
* Purpose:
|
|
88
|
-
* - Intelligently syncs pivot table to match desired state
|
|
89
|
-
* - Preserves existing pivot records that should remain (with their metadata)
|
|
90
|
-
* - Only deletes records that should be removed
|
|
91
|
-
* - Only creates records that are new
|
|
92
|
-
* - Much better than delete-all-then-recreate pattern
|
|
93
|
-
*
|
|
94
|
-
* Data flow:
|
|
95
|
-
* 1. Queries existing pivot records for the resource
|
|
96
|
-
* 2. Compares existing IDs with desired IDs
|
|
97
|
-
* 3. Calculates which to delete and which to add
|
|
98
|
-
* 4. Validates new related resources exist (optional)
|
|
99
|
-
* 5. Performs bulk delete for removed relationships
|
|
100
|
-
* 6. Performs bulk insert for new relationships
|
|
101
|
-
* 7. Records that exist in both are untouched (metadata preserved)
|
|
102
|
-
*/
|
|
103
|
-
export const updateManyToManyRelationship = async (scope, deps) => {
|
|
104
|
-
// Extract values from deps
|
|
105
|
-
const { api, context } = deps
|
|
106
|
-
const { resourceId, relDef, transaction: trx } = context
|
|
107
|
-
const relData = normalizeRelationshipIdentifiers(context.relData || [], { api })
|
|
108
|
-
|
|
109
|
-
// Get the knex instance from the pivot scope
|
|
110
|
-
const pivotScope = api.resources[relDef.through]
|
|
111
|
-
if (!pivotScope) {
|
|
112
|
-
throw new Error(`Pivot table resource '${relDef.through}' not found`)
|
|
2
|
+
import { getIdColumn, getStorageColumn } from '../storage/storage-mapping.js'
|
|
3
|
+
import { lockRelationshipTargets } from './relationship-processor.js'
|
|
4
|
+
import { RELATIONSHIP_WRITE_BATCH_SIZE } from '../querying-writing/knex-constants.js'
|
|
5
|
+
import { whereInIdentifiers } from '../querying/identifier-query.js'
|
|
6
|
+
import { applyDatabaseReadOptions, databaseIdentityExpression } from '../querying-writing/database-value-normalizers.js'
|
|
7
|
+
import { assertVersionedRelationshipTransaction, hasVersionedInverse, invalidateManyToManyVersions } from './resource-version.js'
|
|
8
|
+
|
|
9
|
+
export async function invalidateDeletedPivotTargets (api, scopeName, context) {
|
|
10
|
+
const relationships = []
|
|
11
|
+
for (const [parentType, parent] of Object.entries(api.resources)) {
|
|
12
|
+
if (!parent.vars.schemaInfo?.versionField) continue
|
|
13
|
+
for (const relationship of Object.values(parent.vars.schemaInfo.schemaRelationships)) {
|
|
14
|
+
if (relationship.type !== 'manyToMany' || relationship.target !== scopeName) continue
|
|
15
|
+
relationships.push({ target: parentType, through: relationship.through, foreignKey: relationship.otherKey, otherKey: relationship.foreignKey })
|
|
16
|
+
}
|
|
113
17
|
}
|
|
18
|
+
if (!relationships.length) return
|
|
19
|
+
assertVersionedRelationshipTransaction(api, relationships[0], context.transaction)
|
|
20
|
+
await lockRelationshipTargets(api, context.transaction, [{ type: scopeName, id: context.id }])
|
|
21
|
+
const id = api.knex.helpers.getStorageAdapter(scopeName).translateFilterValue('id', context.id)
|
|
22
|
+
for (const inverse of relationships) {
|
|
23
|
+
const pivot = getPivotStorage(api, inverse)
|
|
24
|
+
const query = context.transaction(pivot.tableName).where(pivot.foreignKey, id)
|
|
25
|
+
await invalidateRemovedPivotVersions(api, inverse, query, context.transaction)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
114
28
|
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
// Create sets for efficient comparison
|
|
124
|
-
const existingIds = new Set(existingRecords.map(r => String(r[relDef.otherKey])))
|
|
125
|
-
const newIds = new Set(relData.map(r => String(r.id)))
|
|
126
|
-
|
|
127
|
-
// Determine what to delete and add
|
|
128
|
-
const toDelete = [...existingIds].filter(id => !newIds.has(id))
|
|
129
|
-
const toAdd = [...newIds].filter(id => !existingIds.has(id))
|
|
130
|
-
|
|
131
|
-
// Validate related resources exist if needed (do this before any changes)
|
|
132
|
-
if (relDef.validateExists !== false && toAdd.length > 0) {
|
|
133
|
-
for (const relatedId of toAdd) {
|
|
134
|
-
const related = relData.find(r => String(r.id) === relatedId)
|
|
135
|
-
try {
|
|
136
|
-
await api.resources[related.type].get({
|
|
137
|
-
id: related.id,
|
|
138
|
-
transaction: trx
|
|
139
|
-
}, { ...context })
|
|
140
|
-
} catch (error) {
|
|
141
|
-
throw new RestApiResourceError(
|
|
142
|
-
`Related ${related.type} with id ${related.id} not found`,
|
|
143
|
-
{
|
|
144
|
-
subtype: 'not_found',
|
|
145
|
-
resourceType: related.type,
|
|
146
|
-
resourceId: related.id
|
|
147
|
-
}
|
|
148
|
-
)
|
|
29
|
+
export async function deletePivotReferences (api, scopeName, context) {
|
|
30
|
+
const targets = new Map()
|
|
31
|
+
for (const [ownerType, owner] of Object.entries(api.resources)) {
|
|
32
|
+
for (const relationship of Object.values(owner.vars.schemaInfo?.schemaRelationships || {})) {
|
|
33
|
+
if (relationship.type !== 'manyToMany' || (ownerType !== scopeName && relationship.target !== scopeName)) continue
|
|
34
|
+
const pivot = getPivotStorage(api, relationship)
|
|
35
|
+
for (const column of [ownerType === scopeName ? pivot.foreignKey : null, relationship.target === scopeName ? pivot.otherKey : null]) {
|
|
36
|
+
if (column) targets.set(JSON.stringify([pivot.tableName, column]), { tableName: pivot.tableName, column })
|
|
149
37
|
}
|
|
150
38
|
}
|
|
151
39
|
}
|
|
40
|
+
const id = api.knex.helpers.getStorageAdapter(scopeName).translateFilterValue('id', context.id)
|
|
41
|
+
const db = context.transaction || context.db || api.knex.instance
|
|
42
|
+
for (const { tableName, column } of targets.values()) await db(tableName).where(column, id).delete()
|
|
43
|
+
}
|
|
152
44
|
|
|
153
|
-
|
|
154
|
-
if (
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}))
|
|
167
|
-
|
|
168
|
-
await trx(tableName).insert(recordsToInsert)
|
|
45
|
+
export async function invalidateRemovedPivotVersions (api, relDef, query, transaction) {
|
|
46
|
+
if (!hasVersionedInverse(api, relDef)) return
|
|
47
|
+
assertVersionedRelationshipTransaction(api, relDef, transaction)
|
|
48
|
+
const pivot = getPivotStorage(api, relDef)
|
|
49
|
+
let afterId
|
|
50
|
+
while (true) {
|
|
51
|
+
const page = query.clone().orderBy(pivot.idColumn).limit(RELATIONSHIP_WRITE_BATCH_SIZE).forUpdate()
|
|
52
|
+
.select({ [pivot.idColumn]: databaseIdentityExpression(transaction, pivot.idColumn), [pivot.otherKey]: databaseIdentityExpression(transaction, pivot.otherKey) })
|
|
53
|
+
if (afterId !== undefined) page.where(pivot.idColumn, '>', afterId)
|
|
54
|
+
const rows = await applyDatabaseReadOptions(page)
|
|
55
|
+
if (!rows.length) break
|
|
56
|
+
await invalidateManyToManyVersions(api, relDef, rows.map(row => row[pivot.otherKey]), transaction)
|
|
57
|
+
afterId = rows.at(-1)[pivot.idColumn]
|
|
169
58
|
}
|
|
170
|
-
|
|
171
|
-
// Records that exist in both are automatically preserved with their pivot data
|
|
172
59
|
}
|
|
173
60
|
|
|
174
|
-
|
|
175
|
-
// updateManyToManyRelationship for all sync operations (including PUT).
|
|
176
|
-
// This aligns with industry standards where ORMs use intelligent sync
|
|
177
|
-
// rather than delete-all-then-recreate patterns.
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Creates new pivot table records for many-to-many relationships
|
|
181
|
-
*
|
|
182
|
-
* @param {Object} api - The API instance with access to resources
|
|
183
|
-
* @param {string|number} resourceId - The ID of the primary resource
|
|
184
|
-
* @param {Object} relDef - The relationship definition
|
|
185
|
-
* @param {Array} relData - Array of related resources to link
|
|
186
|
-
* @param {Object} trx - Database transaction object
|
|
187
|
-
* @param {Object} [context] - Caller context forwarded to relationship validation
|
|
188
|
-
* @returns {Promise<void>}
|
|
189
|
-
*
|
|
190
|
-
* @example
|
|
191
|
-
* // Input: Create article-tag relationships
|
|
192
|
-
* const relDef = {
|
|
193
|
-
* through: 'article_tags',
|
|
194
|
-
* foreignKey: 'article_id',
|
|
195
|
-
* otherKey: 'tag_id'
|
|
196
|
-
* };
|
|
197
|
-
* const relData = [
|
|
198
|
-
* { type: 'tags', id: '10' },
|
|
199
|
-
* { type: 'tags', id: '20' },
|
|
200
|
-
* { type: 'tags', id: '30' }
|
|
201
|
-
* ];
|
|
202
|
-
*
|
|
203
|
-
* await createPivotRecords(api, '100', relDef, relData, trx);
|
|
204
|
-
*
|
|
205
|
-
* // Result: 3 new records in article_tags table
|
|
206
|
-
* // article_id | tag_id
|
|
207
|
-
* // 100 | 10
|
|
208
|
-
* // 100 | 20
|
|
209
|
-
* // 100 | 30
|
|
210
|
-
*
|
|
211
|
-
* @example
|
|
212
|
-
* // Input: Validation ensures related resources exist
|
|
213
|
-
* const relData = [
|
|
214
|
-
* { type: 'tags', id: '999' } // Non-existent tag
|
|
215
|
-
* ];
|
|
216
|
-
*
|
|
217
|
-
* try {
|
|
218
|
-
* await createPivotRecords(api, '100', relDef, relData, trx);
|
|
219
|
-
* } catch (error) {
|
|
220
|
-
* console.log(error.message);
|
|
221
|
-
* // "Related tags with id 999 not found"
|
|
222
|
-
* // Transaction rolled back, no records created
|
|
223
|
-
* }
|
|
224
|
-
*
|
|
225
|
-
* @example
|
|
226
|
-
* // Input: Skip validation for performance
|
|
227
|
-
* const relDef = {
|
|
228
|
-
* through: 'user_permissions',
|
|
229
|
-
* foreignKey: 'user_id',
|
|
230
|
-
* otherKey: 'permission_id',
|
|
231
|
-
* validateExists: false // Skip GET requests
|
|
232
|
-
* };
|
|
233
|
-
*
|
|
234
|
-
* // With 100 permissions, saves 100 GET requests
|
|
235
|
-
* await createPivotRecords(api, userId, relDef, permissions, trx);
|
|
236
|
-
*
|
|
237
|
-
* // Risk: Could create orphaned relationships if permissions don't exist
|
|
238
|
-
* // Benefit: Much faster for bulk operations when you trust the data
|
|
239
|
-
*
|
|
240
|
-
* @description
|
|
241
|
-
* Used by:
|
|
242
|
-
* - relationship-processor.js for POST operations
|
|
243
|
-
* - updateManyToManyRelationship internally for new relationships
|
|
244
|
-
* - Any code that needs to create pivot records
|
|
245
|
-
*
|
|
246
|
-
* Purpose:
|
|
247
|
-
* - Creates pivot table records to link resources
|
|
248
|
-
* - Validates related resources exist by default (referential integrity)
|
|
249
|
-
* - Supports bulk insert for efficiency
|
|
250
|
-
* - Works within transactions for atomicity
|
|
251
|
-
* - Allows skipping validation when performance matters
|
|
252
|
-
*
|
|
253
|
-
* Data flow:
|
|
254
|
-
* 1. Validates pivot table resource exists
|
|
255
|
-
* 2. Gets actual database table name
|
|
256
|
-
* 3. Optionally validates each related resource exists (GET requests)
|
|
257
|
-
* 4. Prepares bulk insert data with foreign keys
|
|
258
|
-
* 5. Performs single INSERT with all records
|
|
259
|
-
* 6. Returns (no data returned, throws on error)
|
|
260
|
-
*/
|
|
261
|
-
export const createPivotRecords = async (api, resourceId, relDef, relData, trx, context = {}) => {
|
|
262
|
-
relData = normalizeRelationshipIdentifiers(relData, { api })
|
|
263
|
-
if (relData.length === 0) return // Early exit if nothing to create
|
|
264
|
-
|
|
265
|
-
// Get pivot table info
|
|
61
|
+
const getPivotStorage = (api, relDef) => {
|
|
266
62
|
const pivotScope = api.resources[relDef.through]
|
|
267
|
-
if (!pivotScope) {
|
|
268
|
-
|
|
63
|
+
if (!pivotScope) throw new Error(`Pivot table resource '${relDef.through}' not found`)
|
|
64
|
+
const { schemaInfo } = pivotScope.vars
|
|
65
|
+
return {
|
|
66
|
+
tableName: schemaInfo.tableName || relDef.through,
|
|
67
|
+
idColumn: getIdColumn(schemaInfo),
|
|
68
|
+
foreignKey: getStorageColumn(schemaInfo, relDef.foreignKey),
|
|
69
|
+
otherKey: getStorageColumn(schemaInfo, relDef.otherKey)
|
|
269
70
|
}
|
|
71
|
+
}
|
|
270
72
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
)
|
|
73
|
+
// Targets are already authorized and locked. Keep existing pivot rows intact.
|
|
74
|
+
const insertMissingPivotRecords = async (trx, { tableName, idColumn, foreignKey, otherKey }, resourceId, ids) => {
|
|
75
|
+
for (let offset = 0; offset < ids.length; offset += RELATIONSHIP_WRITE_BATCH_SIZE) {
|
|
76
|
+
const batch = ids.slice(offset, offset + RELATIONSHIP_WRITE_BATCH_SIZE)
|
|
77
|
+
const requestedIds = new Set(batch)
|
|
78
|
+
const existingIds = new Set()
|
|
79
|
+
let hasExisting = false
|
|
80
|
+
let afterId
|
|
81
|
+
// A pivot resource can contain duplicate edges; bound returned rows as well as IDs.
|
|
82
|
+
while (true) {
|
|
83
|
+
const query = trx(tableName).where(foreignKey, resourceId).whereIn(otherKey, batch)
|
|
84
|
+
.orderBy(`${tableName}.${idColumn}`).limit(RELATIONSHIP_WRITE_BATCH_SIZE + 1).forUpdate()
|
|
85
|
+
.select({ [idColumn]: databaseIdentityExpression(trx, idColumn), [otherKey]: databaseIdentityExpression(trx, otherKey) })
|
|
86
|
+
if (afterId !== undefined) query.where(idColumn, '>', afterId)
|
|
87
|
+
const existing = await applyDatabaseReadOptions(query)
|
|
88
|
+
hasExisting ||= existing.length > 0
|
|
89
|
+
for (const record of existing) {
|
|
90
|
+
const id = String(record[otherKey])
|
|
91
|
+
if (requestedIds.has(id)) existingIds.add(id)
|
|
291
92
|
}
|
|
93
|
+
if (existing.length <= RELATIONSHIP_WRITE_BATCH_SIZE) break
|
|
94
|
+
afterId = existing.at(-1)[idColumn]
|
|
95
|
+
}
|
|
96
|
+
const toAdd = []
|
|
97
|
+
for (const id of batch) {
|
|
98
|
+
if (existingIds.has(id)) continue
|
|
99
|
+
// A returned spelling can match another requested ID under the column's collation.
|
|
100
|
+
if (hasExisting && await trx(tableName).where(foreignKey, resourceId).where(otherKey, id).forUpdate().first(otherKey)) continue
|
|
101
|
+
toAdd.push({ [foreignKey]: resourceId, [otherKey]: id })
|
|
292
102
|
}
|
|
103
|
+
if (toAdd.length) await trx(tableName).insert(toAdd)
|
|
293
104
|
}
|
|
105
|
+
}
|
|
294
106
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
})
|
|
107
|
+
// The resource operation authorizes targets before calling these storage writers.
|
|
108
|
+
export const updateManyToManyRelationship = async (scope, { api, context }) => {
|
|
109
|
+
const { resourceId, relDef, transaction: trx } = context
|
|
110
|
+
assertVersionedRelationshipTransaction(api, relDef, trx)
|
|
111
|
+
const relData = normalizeRelationshipIdentifiers(context.relData || [], { api })
|
|
112
|
+
const pivot = getPivotStorage(api, relDef)
|
|
113
|
+
const targets = await lockRelationshipTargets(api, trx, relData)
|
|
114
|
+
const ids = [...new Set(targets.map(record => String(record.id)))]
|
|
115
|
+
// Negate the complete keep-list once; independent batch deletions lose wanted links.
|
|
116
|
+
const removals = trx(pivot.tableName).where(pivot.foreignKey, resourceId)
|
|
117
|
+
.whereNot(query => whereInIdentifiers(query, pivot.otherKey, ids))
|
|
118
|
+
await invalidateRemovedPivotVersions(api, relDef, removals, trx)
|
|
119
|
+
await removals.delete()
|
|
120
|
+
await insertMissingPivotRecords(trx, pivot, resourceId, ids)
|
|
121
|
+
await invalidateManyToManyVersions(api, relDef, ids, trx)
|
|
122
|
+
}
|
|
300
123
|
|
|
301
|
-
|
|
302
|
-
|
|
124
|
+
export const createPivotRecords = async (api, resourceId, relDef, relData, trx) => {
|
|
125
|
+
resourceId = String(resourceId)
|
|
126
|
+
relData = [...new Map(normalizeRelationshipIdentifiers(relData, { api }).map(record => [record.id, record])).values()]
|
|
127
|
+
if (relData.length === 0) return
|
|
128
|
+
assertVersionedRelationshipTransaction(api, relDef, trx)
|
|
129
|
+
const pivot = getPivotStorage(api, relDef)
|
|
130
|
+
const targets = await lockRelationshipTargets(api, trx, relData)
|
|
131
|
+
await insertMissingPivotRecords(trx, pivot, resourceId, targets.map(record => record.id))
|
|
132
|
+
await invalidateManyToManyVersions(api, relDef, targets.map(record => record.id), trx)
|
|
303
133
|
}
|