json-rest-api 1.0.28 → 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 +746 -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} +90 -31
- 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 -174
- 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 +67 -330
- 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 +205 -30
- package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
- package/plugins/core/lib/querying/sort-helpers.js +20 -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 +238 -187
- 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 +111 -287
- 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 +109 -44
- 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 +142 -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 -2509
- 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 -1069
- 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 -385
- 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 -145
- 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 +38 -69
- 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 +39 -82
- 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 -1238
- 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 -150
- 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 -118
- package/tests/fixtures/api-configs.js +0 -2509
- 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-cursor-multifield.test.js +0 -643
- 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/test-development-guide.md +0 -370
- package/tests/virtual-fields.test.js +0 -404
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
import { whereInIdentifiers } from '../querying/identifier-query.js'
|
|
2
|
+
import { loadIncludedParentMap, parseIncludeTree } from '../querying/include-query-helpers.js'
|
|
3
|
+
import { getResourceFieldset, parseFieldset } from '../querying-writing/field-utils.js'
|
|
4
|
+
import { COMPUTED_DEPENDENCIES_KEY, RELATIONSHIP_READ_BATCH_SIZE } from '../querying-writing/knex-constants.js'
|
|
5
|
+
import { buildResourceUrl, buildRelationshipUrl } from '../querying/url-helpers.js'
|
|
6
|
+
import { normalizeId } from './utils/descriptor-helpers.js'
|
|
7
|
+
import { applyFieldSelectionToQuery, buildFieldSelection } from '../querying-writing/knex-field-helpers.js'
|
|
8
|
+
import { applyDatabaseReadOptions } from '../querying-writing/database-value-normalizers.js'
|
|
9
|
+
import {
|
|
10
|
+
getCanonicalResourceId,
|
|
11
|
+
getCanonicalResourceIdColumn,
|
|
12
|
+
translateCanonicalRecordFromStorage,
|
|
13
|
+
} from '../storage/canonical-storage-mapping.js'
|
|
14
|
+
import { buildEffectiveSortList } from '../querying/query-field-sort-helpers.js'
|
|
15
|
+
import { unwrapQueryBuilderState } from '../querying/query-builder-utils.js'
|
|
16
|
+
import { applyIncludeQueryConfig } from '../querying/knex-window-queries.js'
|
|
17
|
+
|
|
18
|
+
export const decorateResourceLinks = ({ resource, scope, scopeName, context }) => {
|
|
19
|
+
if (!resource || !scope || !scopeName || !context) return
|
|
20
|
+
|
|
21
|
+
resource.links = resource.links || {}
|
|
22
|
+
resource.links.self = buildResourceUrl(context, scope, scopeName, resource.id)
|
|
23
|
+
|
|
24
|
+
if (!resource.relationships || typeof resource.relationships !== 'object') {
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
for (const [relationshipName, relationshipObject] of Object.entries(resource.relationships)) {
|
|
29
|
+
if (!relationshipObject || typeof relationshipObject !== 'object') {
|
|
30
|
+
continue
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
relationshipObject.links = {
|
|
34
|
+
self: buildRelationshipUrl(context, scope, scopeName, resource.id, relationshipName, true),
|
|
35
|
+
related: buildRelationshipUrl(context, scope, scopeName, resource.id, relationshipName, false),
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const getAllowedQueryFieldNames = (api, scopeName) => (
|
|
41
|
+
Object.keys(api.resources?.[scopeName]?.vars?.schemaInfo?.queryFields || {})
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
// Relationship reads share the installed API and link store, never request state.
|
|
45
|
+
export function createCanonicalRelationshipReader ({ api, knex, getDescriptor, getScopeStorageAdapter, linkStore }) {
|
|
46
|
+
const { getManyToManyInfo, buildRelatedLinkQuery, buildVisibleLinkTargets, fetchLinksForParents } = linkStore
|
|
47
|
+
const applyTargetScopeFilters = async ({
|
|
48
|
+
query,
|
|
49
|
+
resourceName,
|
|
50
|
+
tableName,
|
|
51
|
+
context,
|
|
52
|
+
filters,
|
|
53
|
+
queryPurpose = 'include'
|
|
54
|
+
}) => {
|
|
55
|
+
const targetScope = api.resources?.[resourceName]
|
|
56
|
+
if (!targetScope?.applyQueryFilters) {
|
|
57
|
+
return { query }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const storageAdapter = getScopeStorageAdapter(resourceName)
|
|
61
|
+
const queryState = await targetScope.applyQueryFilters({
|
|
62
|
+
query,
|
|
63
|
+
filters,
|
|
64
|
+
schemaInfo: targetScope.vars?.schemaInfo,
|
|
65
|
+
scopeName: resourceName,
|
|
66
|
+
tableName,
|
|
67
|
+
db: context.db || context.transaction || api.knex.instance,
|
|
68
|
+
isAnyApi: true,
|
|
69
|
+
queryPurpose,
|
|
70
|
+
storageAdapter
|
|
71
|
+
}, {
|
|
72
|
+
...context,
|
|
73
|
+
scopeName: resourceName,
|
|
74
|
+
schemaInfo: targetScope.vars?.schemaInfo,
|
|
75
|
+
storageAdapter
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
query: unwrapQueryBuilderState(queryState, query)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const applyIncludeFieldSelection = async ({ query, resourceName, tableName, context }) => {
|
|
84
|
+
const targetScope = api.resources?.[resourceName]
|
|
85
|
+
if (!targetScope) {
|
|
86
|
+
return { query, fieldSelectionInfo: null }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const storageAdapter = getScopeStorageAdapter(resourceName)
|
|
90
|
+
const fieldSelectionInfo = await buildFieldSelection(targetScope, {
|
|
91
|
+
context: {
|
|
92
|
+
...context,
|
|
93
|
+
scopeName: resourceName,
|
|
94
|
+
schemaInfo: targetScope.vars.schemaInfo,
|
|
95
|
+
storageAdapter,
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
const selectionState = await applyFieldSelectionToQuery({
|
|
100
|
+
query,
|
|
101
|
+
scope: targetScope,
|
|
102
|
+
fieldSelectionInfo,
|
|
103
|
+
tableName,
|
|
104
|
+
useTablePrefix: false,
|
|
105
|
+
storageAdapter,
|
|
106
|
+
db: context.db || context.transaction || api.knex.instance,
|
|
107
|
+
context,
|
|
108
|
+
scopeName: resourceName
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
...selectionState,
|
|
113
|
+
fieldSelectionInfo
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const buildIncludedResource = ({ row, descriptor, context, fieldSelectionInfo = null }) => {
|
|
118
|
+
const translated = translateCanonicalRecordFromStorage(
|
|
119
|
+
row,
|
|
120
|
+
descriptor,
|
|
121
|
+
{ allowedExtraFields: getAllowedQueryFieldNames(api, descriptor.resource) }
|
|
122
|
+
)
|
|
123
|
+
const resourceId = getCanonicalResourceId(row, descriptor)
|
|
124
|
+
const includeResource = {
|
|
125
|
+
type: descriptor.resource,
|
|
126
|
+
id: resourceId,
|
|
127
|
+
attributes: translated.attributes,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (translated.relationships && Object.keys(translated.relationships).length > 0) {
|
|
131
|
+
includeResource.relationships = translated.relationships
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (fieldSelectionInfo?.computedDependencies?.length) {
|
|
135
|
+
includeResource[COMPUTED_DEPENDENCIES_KEY] = fieldSelectionInfo.computedDependencies
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const targetScope = api.resources?.[descriptor.resource]
|
|
139
|
+
if (targetScope) {
|
|
140
|
+
decorateResourceLinks({
|
|
141
|
+
resource: includeResource,
|
|
142
|
+
scope: targetScope,
|
|
143
|
+
scopeName: descriptor.resource,
|
|
144
|
+
context,
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return includeResource
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const includeRow = (options, includes) => {
|
|
152
|
+
const id = getCanonicalResourceId(options.row, options.descriptor)
|
|
153
|
+
const key = `${options.descriptor.resource}:${id}`
|
|
154
|
+
if (!includes.has(key)) {
|
|
155
|
+
includes.set(key, buildIncludedResource(options))
|
|
156
|
+
}
|
|
157
|
+
return includes.get(key)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const collectIncludes = async ({ descriptor, resources, includeTree, context, includes }) => {
|
|
161
|
+
const entries = Object.entries(includeTree || {})
|
|
162
|
+
if (entries.length === 0 || !resources || resources.length === 0) return
|
|
163
|
+
|
|
164
|
+
const db = context.db || context.transaction || api.knex.instance
|
|
165
|
+
|
|
166
|
+
for (const [relName, childTree] of entries) {
|
|
167
|
+
const childKeys = Object.keys(childTree || {})
|
|
168
|
+
|
|
169
|
+
const belongsToInfo = Object.hasOwn(descriptor.belongsTo || {}, relName) ? descriptor.belongsTo[relName] : undefined
|
|
170
|
+
if (belongsToInfo) {
|
|
171
|
+
const ids = [...new Set(resources
|
|
172
|
+
.map((resource) => resource.relationships?.[relName]?.data?.id)
|
|
173
|
+
.filter((id) => id !== undefined && id !== null))]
|
|
174
|
+
|
|
175
|
+
if (ids.length === 0) continue
|
|
176
|
+
|
|
177
|
+
const targetDescriptor = getDescriptor(belongsToInfo.target)
|
|
178
|
+
|
|
179
|
+
const nestedResources = []
|
|
180
|
+
for (let offset = 0; offset < ids.length; offset += RELATIONSHIP_READ_BATCH_SIZE) {
|
|
181
|
+
let query = db(targetDescriptor.canonical.tableName)
|
|
182
|
+
.where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
|
|
183
|
+
.where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
|
|
184
|
+
.whereIn(getCanonicalResourceIdColumn(targetDescriptor), ids.slice(offset, offset + RELATIONSHIP_READ_BATCH_SIZE))
|
|
185
|
+
const selectionState = await applyIncludeFieldSelection({
|
|
186
|
+
query,
|
|
187
|
+
resourceName: targetDescriptor.resource,
|
|
188
|
+
tableName: targetDescriptor.canonical.tableName,
|
|
189
|
+
context
|
|
190
|
+
})
|
|
191
|
+
query = selectionState.query
|
|
192
|
+
const scopedQueryState = await applyTargetScopeFilters({
|
|
193
|
+
query,
|
|
194
|
+
resourceName: targetDescriptor.resource,
|
|
195
|
+
tableName: targetDescriptor.canonical.tableName,
|
|
196
|
+
context
|
|
197
|
+
})
|
|
198
|
+
query = scopedQueryState.query
|
|
199
|
+
const rows = await applyDatabaseReadOptions(query)
|
|
200
|
+
|
|
201
|
+
for (const row of rows) {
|
|
202
|
+
const includeResource = includeRow({
|
|
203
|
+
row,
|
|
204
|
+
descriptor: targetDescriptor,
|
|
205
|
+
context,
|
|
206
|
+
fieldSelectionInfo: selectionState.fieldSelectionInfo
|
|
207
|
+
}, includes)
|
|
208
|
+
nestedResources.push(includeResource)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (childKeys.length > 0 && nestedResources.length > 0) {
|
|
213
|
+
await attachReverseRelationships({ resources: nestedResources, descriptor: targetDescriptor, context, includeTree: childTree })
|
|
214
|
+
await attachManyToManyRelationships({ resources: nestedResources, descriptor: targetDescriptor, context, includeTree: childTree })
|
|
215
|
+
await collectIncludes({
|
|
216
|
+
descriptor: targetDescriptor,
|
|
217
|
+
resources: nestedResources,
|
|
218
|
+
includeTree: childTree,
|
|
219
|
+
context,
|
|
220
|
+
includes,
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
continue
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const polymorphicInfo = Object.hasOwn(descriptor.polymorphicBelongsTo || {}, relName) ? descriptor.polymorphicBelongsTo[relName] : undefined
|
|
227
|
+
if (polymorphicInfo) {
|
|
228
|
+
const typeToIds = new Map()
|
|
229
|
+
for (const resource of resources) {
|
|
230
|
+
const relData = resource.relationships?.[relName]?.data
|
|
231
|
+
if (!relData?.type || relData?.id == null) continue
|
|
232
|
+
const targetType = String(relData.type)
|
|
233
|
+
const targetId = normalizeId(relData.id)
|
|
234
|
+
if (targetId == null) continue
|
|
235
|
+
if (!typeToIds.has(targetType)) {
|
|
236
|
+
typeToIds.set(targetType, new Set())
|
|
237
|
+
}
|
|
238
|
+
typeToIds.get(targetType).add(targetId)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const nestedResources = []
|
|
242
|
+
for (const [targetType, idSet] of typeToIds.entries()) {
|
|
243
|
+
if (idSet.size === 0) continue
|
|
244
|
+
const targetDescriptor = getDescriptor(targetType)
|
|
245
|
+
|
|
246
|
+
const ids = [...idSet]
|
|
247
|
+
for (let offset = 0; offset < ids.length; offset += RELATIONSHIP_READ_BATCH_SIZE) {
|
|
248
|
+
let query = db(targetDescriptor.canonical.tableName)
|
|
249
|
+
.where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
|
|
250
|
+
.where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
|
|
251
|
+
.whereIn(getCanonicalResourceIdColumn(targetDescriptor), ids.slice(offset, offset + RELATIONSHIP_READ_BATCH_SIZE))
|
|
252
|
+
const selectionState = await applyIncludeFieldSelection({
|
|
253
|
+
query,
|
|
254
|
+
resourceName: targetDescriptor.resource,
|
|
255
|
+
tableName: targetDescriptor.canonical.tableName,
|
|
256
|
+
context
|
|
257
|
+
})
|
|
258
|
+
query = selectionState.query
|
|
259
|
+
const scopedQueryState = await applyTargetScopeFilters({
|
|
260
|
+
query,
|
|
261
|
+
resourceName: targetDescriptor.resource,
|
|
262
|
+
tableName: targetDescriptor.canonical.tableName,
|
|
263
|
+
context
|
|
264
|
+
})
|
|
265
|
+
query = scopedQueryState.query
|
|
266
|
+
const rows = await applyDatabaseReadOptions(query)
|
|
267
|
+
|
|
268
|
+
for (const row of rows) {
|
|
269
|
+
const includeResource = includeRow({
|
|
270
|
+
row,
|
|
271
|
+
descriptor: targetDescriptor,
|
|
272
|
+
context,
|
|
273
|
+
fieldSelectionInfo: selectionState.fieldSelectionInfo
|
|
274
|
+
}, includes)
|
|
275
|
+
nestedResources.push(includeResource)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (childKeys.length > 0 && nestedResources.length > 0) {
|
|
281
|
+
const resourcesByType = new Map()
|
|
282
|
+
for (const includeResource of nestedResources) {
|
|
283
|
+
if (!resourcesByType.has(includeResource.type)) {
|
|
284
|
+
resourcesByType.set(includeResource.type, [])
|
|
285
|
+
}
|
|
286
|
+
resourcesByType.get(includeResource.type).push(includeResource)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
for (const [targetType, groupedResources] of resourcesByType.entries()) {
|
|
290
|
+
const targetDescriptor = getDescriptor(targetType)
|
|
291
|
+
|
|
292
|
+
await attachReverseRelationships({ resources: groupedResources, descriptor: targetDescriptor, context, includeTree: childTree })
|
|
293
|
+
await attachManyToManyRelationships({ resources: groupedResources, descriptor: targetDescriptor, context, includeTree: childTree })
|
|
294
|
+
await collectIncludes({
|
|
295
|
+
descriptor: targetDescriptor,
|
|
296
|
+
resources: groupedResources,
|
|
297
|
+
includeTree: childTree,
|
|
298
|
+
context,
|
|
299
|
+
includes,
|
|
300
|
+
})
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
continue
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const reverseInfo = Object.hasOwn(descriptor.relationships || {}, relName) ? descriptor.relationships[relName] : undefined
|
|
308
|
+
if (['hasMany', 'hasOne'].includes(reverseInfo?.type) && reverseInfo.target && (reverseInfo.foreignKey || reverseInfo.via)) {
|
|
309
|
+
const targetDescriptor = getDescriptor(reverseInfo.target)
|
|
310
|
+
const parentIds = resources.map(resource => normalizeId(resource.id)).filter(id => id !== null)
|
|
311
|
+
if (parentIds.length === 0) continue
|
|
312
|
+
const groupingColumn = reverseInfo.foreignKey
|
|
313
|
+
? getScopeStorageAdapter(targetDescriptor.resource).translateColumn(reverseInfo.foreignKey)
|
|
314
|
+
: targetDescriptor.polymorphicBelongsTo?.[reverseInfo.via]?.idColumn
|
|
315
|
+
if (!groupingColumn) throw new Error(`Missing grouping column for reverse relationship '${descriptor.resource}.${relName}'`)
|
|
316
|
+
const tableName = targetDescriptor.canonical.tableName
|
|
317
|
+
const query = getScopeStorageAdapter(targetDescriptor.resource).buildBaseQuery({ transaction: db })
|
|
318
|
+
.modify(whereInIdentifiers, groupingColumn, parentIds)
|
|
319
|
+
if (reverseInfo.via) query.where(targetDescriptor.polymorphicBelongsTo[reverseInfo.via].typeColumn, descriptor.resource)
|
|
320
|
+
const includeConfig = api.resources[descriptor.resource].vars.schemaInfo.schemaRelationships[relName]?.include || {}
|
|
321
|
+
const includeContext = { ...context, queryParams: { fields: context.queryParams?.fields, sort: buildEffectiveSortList(includeConfig.orderBy, { defaultSort: api.resources[targetDescriptor.resource].vars.defaultSort }) } }
|
|
322
|
+
const selection = await applyIncludeFieldSelection({ query, resourceName: targetDescriptor.resource, tableName, context: includeContext })
|
|
323
|
+
const filtered = await applyTargetScopeFilters({ query: selection.query, resourceName: targetDescriptor.resource, tableName, context })
|
|
324
|
+
const limited = reverseInfo.type === 'hasOne'
|
|
325
|
+
? { query: filtered.query, parentColumn: groupingColumn, temporaryFields: [] }
|
|
326
|
+
: await applyIncludeQueryConfig({
|
|
327
|
+
query: filtered.query,
|
|
328
|
+
scopeName: targetDescriptor.resource,
|
|
329
|
+
tableName,
|
|
330
|
+
parentColumn: `${tableName}.${groupingColumn}`,
|
|
331
|
+
includeConfig,
|
|
332
|
+
context: includeContext,
|
|
333
|
+
capabilities: api.knex.capabilities,
|
|
334
|
+
queryFieldRuntimeByField: selection.queryFieldRuntimeByField
|
|
335
|
+
}, { scopes: api.resources, knex, getStorageAdapter: getScopeStorageAdapter })
|
|
336
|
+
let rows = await limited.query
|
|
337
|
+
if (reverseInfo.type === 'hasOne') rows = [...new Map(rows.map(row => [String(row[groupingColumn]), row])).values()]
|
|
338
|
+
const linkage = new Map()
|
|
339
|
+
const nestedResources = []
|
|
340
|
+
for (const row of rows) {
|
|
341
|
+
const parentId = String(row[limited.parentColumn])
|
|
342
|
+
if (!linkage.has(parentId)) linkage.set(parentId, [])
|
|
343
|
+
linkage.get(parentId).push({ type: targetDescriptor.resource, id: getCanonicalResourceId(row, targetDescriptor) })
|
|
344
|
+
for (const field of limited.temporaryFields) delete row[field]
|
|
345
|
+
nestedResources.push(includeRow({ row, descriptor: targetDescriptor, context, fieldSelectionInfo: selection.fieldSelectionInfo }, includes))
|
|
346
|
+
}
|
|
347
|
+
for (const resource of resources) {
|
|
348
|
+
resource.relationships ||= {}
|
|
349
|
+
const children = linkage.get(String(resource.id)) || []
|
|
350
|
+
resource.relationships[relName] = { ...resource.relationships[relName], data: reverseInfo.type === 'hasOne' ? (children[0] || null) : children }
|
|
351
|
+
}
|
|
352
|
+
if (childKeys.length > 0 && nestedResources.length > 0) {
|
|
353
|
+
await attachReverseRelationships({ resources: nestedResources, descriptor: targetDescriptor, context, includeTree: childTree })
|
|
354
|
+
await attachManyToManyRelationships({ resources: nestedResources, descriptor: targetDescriptor, context, includeTree: childTree })
|
|
355
|
+
await collectIncludes({ descriptor: targetDescriptor, resources: nestedResources, includeTree: childTree, context, includes })
|
|
356
|
+
}
|
|
357
|
+
continue
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const manyInfo = Object.hasOwn(descriptor.manyToMany || {}, relName) ? descriptor.manyToMany[relName] : undefined
|
|
361
|
+
if (manyInfo) {
|
|
362
|
+
const info = getManyToManyInfo(descriptor.resource, relName)
|
|
363
|
+
if (!info) continue
|
|
364
|
+
const links = buildRelatedLinkQuery({ ...info, parentIds: resources.map(resource => resource.id), db })
|
|
365
|
+
const targetDescriptor = getDescriptor(info.relInfo.target)
|
|
366
|
+
const tableName = targetDescriptor.canonical.tableName
|
|
367
|
+
const storageAdapter = getScopeStorageAdapter(targetDescriptor.resource)
|
|
368
|
+
const includeConfig = api.resources[descriptor.resource].vars.schemaInfo.schemaRelationships[relName]?.include || {}
|
|
369
|
+
const perParent = includeConfig.strategy === 'window'
|
|
370
|
+
const query = storageAdapter.buildBaseQuery({ transaction: db })
|
|
371
|
+
if (perParent) query.join(links.as('pivot'), `${tableName}.${storageAdapter.getIdColumn()}`, 'pivot.childId')
|
|
372
|
+
else query.whereIn(`${tableName}.${storageAdapter.getIdColumn()}`, db.select('childId').from(links.clone().as('include_candidates')))
|
|
373
|
+
const includeContext = { ...context, queryParams: { fields: context.queryParams?.fields, sort: buildEffectiveSortList(includeConfig.orderBy, { defaultSort: api.resources[targetDescriptor.resource].vars.defaultSort }) } }
|
|
374
|
+
const selection = await applyIncludeFieldSelection({ query, resourceName: targetDescriptor.resource, tableName, context: includeContext })
|
|
375
|
+
const filtered = await applyTargetScopeFilters({ query: selection.query, resourceName: targetDescriptor.resource, tableName, context })
|
|
376
|
+
const limited = await applyIncludeQueryConfig({
|
|
377
|
+
query: filtered.query,
|
|
378
|
+
scopeName: targetDescriptor.resource,
|
|
379
|
+
tableName,
|
|
380
|
+
parentColumn: perParent ? 'pivot.parentId' : db.raw('NULL'),
|
|
381
|
+
includeConfig,
|
|
382
|
+
context: includeContext,
|
|
383
|
+
capabilities: api.knex.capabilities,
|
|
384
|
+
queryFieldRuntimeByField: selection.queryFieldRuntimeByField
|
|
385
|
+
}, { scopes: api.resources, knex, getStorageAdapter: getScopeStorageAdapter })
|
|
386
|
+
const rows = await limited.query
|
|
387
|
+
const parentsByChild = perParent
|
|
388
|
+
? null
|
|
389
|
+
: await loadIncludedParentMap({
|
|
390
|
+
query: storageAdapter.buildBaseQuery({ transaction: db })
|
|
391
|
+
.join(links.clone().as('pivot'), `${tableName}.${storageAdapter.getIdColumn()}`, 'pivot.childId')
|
|
392
|
+
.select({ parentId: 'pivot.parentId', childId: `${tableName}.${storageAdapter.getIdColumn()}` }),
|
|
393
|
+
parentIds: resources.map(resource => resource.id),
|
|
394
|
+
childIds: rows.map(row => getCanonicalResourceId(row, targetDescriptor)),
|
|
395
|
+
knex: db
|
|
396
|
+
})
|
|
397
|
+
const linkage = new Map()
|
|
398
|
+
const nestedResources = []
|
|
399
|
+
for (const row of rows) {
|
|
400
|
+
const id = getCanonicalResourceId(row, targetDescriptor)
|
|
401
|
+
const parents = perParent ? [String(row[limited.parentColumn])] : parentsByChild.get(String(id))
|
|
402
|
+
if (!parents) continue
|
|
403
|
+
for (const parentId of parents) {
|
|
404
|
+
if (!linkage.has(parentId)) linkage.set(parentId, [])
|
|
405
|
+
linkage.get(parentId).push({ type: targetDescriptor.resource, id })
|
|
406
|
+
}
|
|
407
|
+
for (const field of limited.temporaryFields) delete row[field]
|
|
408
|
+
nestedResources.push(includeRow({ row, descriptor: targetDescriptor, context, fieldSelectionInfo: selection.fieldSelectionInfo }, includes))
|
|
409
|
+
}
|
|
410
|
+
for (const resource of resources) {
|
|
411
|
+
resource.relationships ||= {}
|
|
412
|
+
resource.relationships[relName] = { ...resource.relationships[relName], data: linkage.get(String(resource.id)) || [] }
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (childKeys.length > 0 && nestedResources.length > 0) {
|
|
416
|
+
await attachReverseRelationships({ resources: nestedResources, descriptor: targetDescriptor, context, includeTree: childTree })
|
|
417
|
+
await attachManyToManyRelationships({ resources: nestedResources, descriptor: targetDescriptor, context, includeTree: childTree })
|
|
418
|
+
await collectIncludes({
|
|
419
|
+
descriptor: targetDescriptor,
|
|
420
|
+
resources: nestedResources,
|
|
421
|
+
includeTree: childTree,
|
|
422
|
+
context,
|
|
423
|
+
includes,
|
|
424
|
+
})
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const buildIncludes = async ({ parentResources, descriptor, context }) => {
|
|
431
|
+
const includeTree = parseIncludeTree(context.queryParams?.include)
|
|
432
|
+
if (Object.keys(includeTree).length === 0) return []
|
|
433
|
+
|
|
434
|
+
// A cyclic path enriches primary linkage on the same resource object.
|
|
435
|
+
const primary = new Map(parentResources.map(resource => [`${resource.type}:${resource.id}`, resource]))
|
|
436
|
+
const includes = new Map(primary)
|
|
437
|
+
|
|
438
|
+
await collectIncludes({
|
|
439
|
+
descriptor,
|
|
440
|
+
resources: parentResources,
|
|
441
|
+
includeTree,
|
|
442
|
+
context,
|
|
443
|
+
includes,
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
return [...includes].filter(([key]) => !primary.has(key)).map(([, resource]) => resource)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const attachReverseRelationships = async ({ resources, descriptor, context, includeTree = parseIncludeTree(context.queryParams?.include) }) => {
|
|
450
|
+
if (!resources || resources.length === 0) return
|
|
451
|
+
const requestedFields = parseFieldset(getResourceFieldset(context.queryParams?.fields, descriptor.resource))
|
|
452
|
+
const reverseEntries = Object.entries(descriptor.relationships || {})
|
|
453
|
+
.filter(([, relDef]) => ['hasMany', 'hasOne'].includes(relDef?.type) && relDef.target && (relDef.foreignKey || relDef.via))
|
|
454
|
+
.filter(([name]) => requestedFields === null || requestedFields.includes(name))
|
|
455
|
+
.filter(([name]) => !Object.hasOwn(includeTree, name))
|
|
456
|
+
|
|
457
|
+
if (reverseEntries.length === 0) return
|
|
458
|
+
|
|
459
|
+
const db = context.db || context.transaction || api.knex.instance
|
|
460
|
+
|
|
461
|
+
for (const [relName, relDef] of reverseEntries) {
|
|
462
|
+
// Default linkage must not replace an explicitly loaded include limit.
|
|
463
|
+
const pendingResources = resources.filter(resource => !Object.hasOwn(resource.relationships || {}, relName))
|
|
464
|
+
const normalizedParentIds = pendingResources.map(resource => normalizeId(resource.id)).filter(id => id !== null)
|
|
465
|
+
if (normalizedParentIds.length === 0) continue
|
|
466
|
+
const targetDescriptor = getDescriptor(relDef.target)
|
|
467
|
+
|
|
468
|
+
if (relDef.foreignKey) {
|
|
469
|
+
const foreignField = targetDescriptor.fields?.[relDef.foreignKey]
|
|
470
|
+
if (!foreignField?.slot) throw new Error(`Reverse relationship '${descriptor.resource}.${relName}' requires field '${relDef.target}.${relDef.foreignKey}'`)
|
|
471
|
+
const column = foreignField.slot
|
|
472
|
+
|
|
473
|
+
let query = db(targetDescriptor.canonical.tableName)
|
|
474
|
+
.where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
|
|
475
|
+
.where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
|
|
476
|
+
.modify(whereInIdentifiers, column, normalizedParentIds)
|
|
477
|
+
.select(['id', getCanonicalResourceIdColumn(targetDescriptor), column].map(name => `${targetDescriptor.canonical.tableName}.${name}`))
|
|
478
|
+
const scopedQueryState = await applyTargetScopeFilters({
|
|
479
|
+
query,
|
|
480
|
+
resourceName: targetDescriptor.resource,
|
|
481
|
+
tableName: targetDescriptor.canonical.tableName,
|
|
482
|
+
context,
|
|
483
|
+
queryPurpose: 'relationship-identifiers'
|
|
484
|
+
})
|
|
485
|
+
query = scopedQueryState.query
|
|
486
|
+
const rows = await applyDatabaseReadOptions(query)
|
|
487
|
+
|
|
488
|
+
const grouped = rows.reduce((acc, row) => {
|
|
489
|
+
const parentId = row[column]
|
|
490
|
+
if (parentId == null) return acc
|
|
491
|
+
const key = String(parentId)
|
|
492
|
+
acc[key] = acc[key] || []
|
|
493
|
+
acc[key].push({ type: targetDescriptor.resource, id: getCanonicalResourceId(row, targetDescriptor) })
|
|
494
|
+
return acc
|
|
495
|
+
}, Object.create(null))
|
|
496
|
+
|
|
497
|
+
for (const resource of pendingResources) {
|
|
498
|
+
resource.relationships = resource.relationships || {}
|
|
499
|
+
const related = grouped[String(resource.id)] || []
|
|
500
|
+
resource.relationships[relName] = { data: relDef.type === 'hasOne' ? (related.at(-1) || null) : related }
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
continue
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (relDef.via) {
|
|
507
|
+
const polyInfo = targetDescriptor.polymorphicBelongsTo?.[relDef.via]
|
|
508
|
+
if (!polyInfo?.idColumn || !polyInfo?.typeColumn) {
|
|
509
|
+
throw new Error(`Reverse relationship '${descriptor.resource}.${relName}' requires polymorphic relationship '${relDef.target}.${relDef.via}'`)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const queryIds = Array.from(new Set([
|
|
513
|
+
...normalizedParentIds,
|
|
514
|
+
...normalizedParentIds
|
|
515
|
+
.map((id) => {
|
|
516
|
+
const numeric = Number(id)
|
|
517
|
+
return Number.isFinite(numeric) ? numeric : null
|
|
518
|
+
})
|
|
519
|
+
.filter((value) => value !== null),
|
|
520
|
+
]))
|
|
521
|
+
|
|
522
|
+
let query = db(targetDescriptor.canonical.tableName)
|
|
523
|
+
.where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
|
|
524
|
+
.where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
|
|
525
|
+
.where(polyInfo.typeColumn, descriptor.resource)
|
|
526
|
+
.modify(whereInIdentifiers, polyInfo.idColumn, queryIds)
|
|
527
|
+
.select(['id', getCanonicalResourceIdColumn(targetDescriptor), polyInfo.idColumn].map(name => `${targetDescriptor.canonical.tableName}.${name}`))
|
|
528
|
+
const scopedQueryState = await applyTargetScopeFilters({
|
|
529
|
+
query,
|
|
530
|
+
resourceName: targetDescriptor.resource,
|
|
531
|
+
tableName: targetDescriptor.canonical.tableName,
|
|
532
|
+
context,
|
|
533
|
+
queryPurpose: 'relationship-identifiers'
|
|
534
|
+
})
|
|
535
|
+
query = scopedQueryState.query
|
|
536
|
+
const rows = await applyDatabaseReadOptions(query)
|
|
537
|
+
|
|
538
|
+
const grouped = rows.reduce((acc, row) => {
|
|
539
|
+
const parentId = row[polyInfo.idColumn]
|
|
540
|
+
if (parentId == null) return acc
|
|
541
|
+
const key = String(parentId)
|
|
542
|
+
acc[key] = acc[key] || []
|
|
543
|
+
acc[key].push({ type: targetDescriptor.resource, id: getCanonicalResourceId(row, targetDescriptor) })
|
|
544
|
+
return acc
|
|
545
|
+
}, Object.create(null))
|
|
546
|
+
|
|
547
|
+
for (const resource of pendingResources) {
|
|
548
|
+
resource.relationships = resource.relationships || {}
|
|
549
|
+
const related = grouped[String(resource.id)] || []
|
|
550
|
+
resource.relationships[relName] = { data: relDef.type === 'hasOne' ? (related.at(-1) || null) : related }
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const attachManyToManyRelationships = async ({ resources, descriptor, context, includeTree = parseIncludeTree(context.queryParams?.include) }) => {
|
|
557
|
+
if (!resources || resources.length === 0) return
|
|
558
|
+
const requestedFields = parseFieldset(getResourceFieldset(context.queryParams?.fields, descriptor.resource))
|
|
559
|
+
const manyEntries = Object.entries(descriptor.manyToMany || {})
|
|
560
|
+
.filter(([name]) => requestedFields === null || requestedFields.includes(name))
|
|
561
|
+
.filter(([name]) => !Object.hasOwn(includeTree, name))
|
|
562
|
+
if (manyEntries.length === 0) return
|
|
563
|
+
|
|
564
|
+
const db = context.db || context.transaction || api.knex.instance
|
|
565
|
+
|
|
566
|
+
for (const [relName] of manyEntries) {
|
|
567
|
+
const pendingResources = resources.filter(resource => !Object.hasOwn(resource.relationships || {}, relName))
|
|
568
|
+
if (pendingResources.length === 0) continue
|
|
569
|
+
const parentIds = pendingResources.map(resource => resource.id)
|
|
570
|
+
const info = getManyToManyInfo(descriptor.resource, relName)
|
|
571
|
+
if (!info) continue
|
|
572
|
+
const { query: visibleTargets } = await buildVisibleLinkTargets({ ...info, parentIds, db, context })
|
|
573
|
+
const rows = await fetchLinksForParents({
|
|
574
|
+
descriptor: info.descriptor,
|
|
575
|
+
relationshipKey: info.relationshipKey,
|
|
576
|
+
targetResource: info.relInfo.target,
|
|
577
|
+
parentIds,
|
|
578
|
+
db,
|
|
579
|
+
visibleTargets,
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
const grouped = rows.reduce((acc, row) => {
|
|
583
|
+
const key = String(row.parentId)
|
|
584
|
+
acc[key] = acc[key] || []
|
|
585
|
+
acc[key].push({ type: row.childType, id: String(row.childId) })
|
|
586
|
+
return acc
|
|
587
|
+
}, Object.create(null))
|
|
588
|
+
|
|
589
|
+
for (const resource of pendingResources) {
|
|
590
|
+
const related = grouped[resource.id] || []
|
|
591
|
+
resource.relationships = resource.relationships || {}
|
|
592
|
+
resource.relationships[relName] = { data: related }
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
return { buildIncludes, attachReverseRelationships, attachManyToManyRelationships }
|
|
598
|
+
}
|