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,428 @@
|
|
|
1
|
+
// Collection loaders preserve per-parent filtering/order/limits; deps.loadNestedIncludes traverses child paths.
|
|
2
|
+
import { whereInIdentifiers } from './identifier-query.js'
|
|
3
|
+
import { toJsonApiRecord } from './knex-json-api-transformers-querying.js'
|
|
4
|
+
import {
|
|
5
|
+
RELATIONSHIPS_KEY,
|
|
6
|
+
RELATIONSHIP_METADATA_KEY,
|
|
7
|
+
COMPUTED_DEPENDENCIES_KEY,
|
|
8
|
+
} from '../querying-writing/knex-constants.js'
|
|
9
|
+
import { wrapUnexpectedError } from '../../../../lib/error-context.js'
|
|
10
|
+
import {
|
|
11
|
+
loadIncludedParentMap,
|
|
12
|
+
getIdColumnForScope,
|
|
13
|
+
getFieldValueForScope,
|
|
14
|
+
translateColumnForScope,
|
|
15
|
+
prepareCollectionInclude,
|
|
16
|
+
buildRelationshipLinks,
|
|
17
|
+
applyScopeFiltersToIncludeQuery,
|
|
18
|
+
loadRelationshipMetadata,
|
|
19
|
+
} from './include-query-helpers.js'
|
|
20
|
+
/**
|
|
21
|
+
* Loads hasMany relationships (one-to-many or many-to-many)
|
|
22
|
+
*
|
|
23
|
+
* This function loads the "child" side of a relationship. For example, if articles
|
|
24
|
+
* have many comments, this loads all comments for a set of articles. It also handles
|
|
25
|
+
* many-to-many relationships through a pivot table.
|
|
26
|
+
*
|
|
27
|
+
* @param {Object} scope - The resource object containing:
|
|
28
|
+
* - records: Array<Object> - Parent records to load relationships for
|
|
29
|
+
* - scopeName: string - The parent scope name
|
|
30
|
+
* - includeName: string - The relationship name to include
|
|
31
|
+
* - relDef: Object - The relationship definition
|
|
32
|
+
* - subIncludes: Object - Nested includes to process recursively
|
|
33
|
+
* - included: Map - Map of already included resources
|
|
34
|
+
* - processedPaths: Set - Set of already processed paths
|
|
35
|
+
* - currentPath: string - Current include path for tracking
|
|
36
|
+
* - fields: Object - Sparse fieldsets configuration
|
|
37
|
+
* @param {Object} deps - Dependencies object containing:
|
|
38
|
+
* - context.scopes: Object - The resources object
|
|
39
|
+
* - context.log: Object - Logger instance
|
|
40
|
+
* - context.knex: Object - Knex instance
|
|
41
|
+
* @returns {Promise<void>}
|
|
42
|
+
*/
|
|
43
|
+
export const loadHasMany = async (scope, deps) => {
|
|
44
|
+
const { records, scopeName, includeName, relDef, subIncludes, included, processedPaths, currentPath, fields } = scope
|
|
45
|
+
const { scopes, log, knex, capabilities, requestContext } = deps.context
|
|
46
|
+
try {
|
|
47
|
+
log.trace('[INCLUDE] Loading hasMany/manyToMany relationship:', {
|
|
48
|
+
scopeName,
|
|
49
|
+
includeName,
|
|
50
|
+
recordCount: records.length,
|
|
51
|
+
isManyToMany: relDef.type === 'manyToMany',
|
|
52
|
+
hasMany: relDef.type === 'hasMany' ? relDef.target : undefined
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// Collect all parent IDs
|
|
56
|
+
const mainIds = records.map(record => getFieldValueForScope(scopes, scopeName, record, 'id')).filter(id => id !== null && id !== undefined)
|
|
57
|
+
|
|
58
|
+
if (mainIds.length === 0) {
|
|
59
|
+
log.trace('[INCLUDE] No parent IDs found, skipping hasMany load')
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Check if this is a many-to-many relationship
|
|
64
|
+
if (relDef.type === 'manyToMany') {
|
|
65
|
+
// Handle many-to-many relationship
|
|
66
|
+
const pivotTable = scopes[relDef.through].vars.schemaInfo.tableName
|
|
67
|
+
const foreignKey = translateColumnForScope(scopes, relDef.through, relDef.foreignKey)
|
|
68
|
+
const otherKey = translateColumnForScope(scopes, relDef.through, relDef.otherKey)
|
|
69
|
+
|
|
70
|
+
if (!foreignKey || !otherKey) {
|
|
71
|
+
throw new Error(`Missing foreignKey or otherKey in many-to-many relationship '${includeName}' for scope '${scopeName}'`)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const targetScope = relDef.target || includeName
|
|
75
|
+
const targetTable = scopes[targetScope].vars.schemaInfo.tableName
|
|
76
|
+
|
|
77
|
+
log.debug(`[INCLUDE] Loading pivot records from ${pivotTable}:`, {
|
|
78
|
+
foreignKey,
|
|
79
|
+
parentCount: mainIds.length
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const pivotQuery = knex(pivotTable).modify(whereInIdentifiers, foreignKey, mainIds)
|
|
83
|
+
.select(foreignKey, otherKey).distinct()
|
|
84
|
+
const pivotState = await applyScopeFiltersToIncludeQuery({
|
|
85
|
+
query: pivotQuery, scopes, scopeName: relDef.through, requestContext, db: knex, tableName: pivotTable
|
|
86
|
+
})
|
|
87
|
+
const targetIdColumn = getIdColumnForScope(scopes, targetScope)
|
|
88
|
+
const perParent = relDef.include?.strategy === 'window'
|
|
89
|
+
const query = knex(targetTable)
|
|
90
|
+
if (perParent) query.join(pivotState.query.as('pivot'), `${targetTable}.${targetIdColumn}`, `pivot.${otherKey}`)
|
|
91
|
+
else query.whereIn(`${targetTable}.${targetIdColumn}`, knex.select(otherKey).from(pivotState.query.clone().as('include_candidates')))
|
|
92
|
+
const limited = await prepareCollectionInclude({
|
|
93
|
+
query, targetScope, parentColumn: perParent ? `pivot.${foreignKey}` : knex.raw('NULL'), includeConfig: relDef.include, fields
|
|
94
|
+
}, { scopes, knex, requestContext, capabilities })
|
|
95
|
+
const fieldSelectionInfo = limited.fieldSelectionInfo
|
|
96
|
+
const targetRecords = await limited.query
|
|
97
|
+
const parentsByChild = perParent
|
|
98
|
+
? null
|
|
99
|
+
: await loadIncludedParentMap({
|
|
100
|
+
query: knex(targetTable).join(pivotState.query.clone().as('pivot'), `${targetTable}.${targetIdColumn}`, `pivot.${otherKey}`)
|
|
101
|
+
.select({ parentId: `pivot.${foreignKey}`, childId: `${targetTable}.${targetIdColumn}` }),
|
|
102
|
+
parentIds: mainIds,
|
|
103
|
+
childIds: targetRecords.map(record => getFieldValueForScope(scopes, targetScope, record, 'id')),
|
|
104
|
+
knex
|
|
105
|
+
})
|
|
106
|
+
const childrenByParent = Object.create(null)
|
|
107
|
+
for (const record of targetRecords) {
|
|
108
|
+
const parents = perParent ? [record[limited.parentColumn]] : parentsByChild.get(String(getFieldValueForScope(scopes, targetScope, record, 'id'))) || []
|
|
109
|
+
for (const parentId of parents) (childrenByParent[parentId] ||= []).push(record)
|
|
110
|
+
for (const field of limited.temporaryFields) delete record[field]
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
log.trace('[INCLUDE] Loaded target records:', { count: targetRecords.length })
|
|
114
|
+
|
|
115
|
+
// Step 5: Load relationship metadata for all target records
|
|
116
|
+
await loadRelationshipMetadata(scopes, targetRecords, targetScope)
|
|
117
|
+
|
|
118
|
+
records.forEach(record => {
|
|
119
|
+
if (!record[RELATIONSHIPS_KEY]) record[RELATIONSHIPS_KEY] = {}
|
|
120
|
+
|
|
121
|
+
const recordId = getFieldValueForScope(scopes, scopeName, record, 'id')
|
|
122
|
+
const relData = (childrenByParent[recordId] || []).map(childRecord => {
|
|
123
|
+
// Add to included
|
|
124
|
+
const childRecordId = getFieldValueForScope(scopes, targetScope, childRecord, 'id')
|
|
125
|
+
const resourceKey = `${targetScope}:${childRecordId}`
|
|
126
|
+
if (!included.has(resourceKey)) {
|
|
127
|
+
const jsonApiRecord = toJsonApiRecord(
|
|
128
|
+
scopes[targetScope],
|
|
129
|
+
childRecord,
|
|
130
|
+
targetScope
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
// Add relationships from metadata
|
|
134
|
+
if (childRecord[RELATIONSHIP_METADATA_KEY]) {
|
|
135
|
+
jsonApiRecord.relationships = childRecord[RELATIONSHIP_METADATA_KEY]
|
|
136
|
+
// Clean up the temporary property
|
|
137
|
+
delete childRecord[RELATIONSHIP_METADATA_KEY]
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Attach computed dependencies info if sparse fieldsets were used
|
|
141
|
+
if (fieldSelectionInfo?.computedDependencies) {
|
|
142
|
+
jsonApiRecord[COMPUTED_DEPENDENCIES_KEY] = fieldSelectionInfo.computedDependencies
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
included.set(resourceKey, jsonApiRecord)
|
|
146
|
+
}
|
|
147
|
+
return { type: targetScope, id: String(childRecordId) }
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
const relationshipObject = { data: relData }
|
|
151
|
+
|
|
152
|
+
// Add links if urlPrefix is configured
|
|
153
|
+
if (scopeName) {
|
|
154
|
+
relationshipObject.links = buildRelationshipLinks(scopes, scopeName, record, includeName)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
record[RELATIONSHIPS_KEY][includeName] = relationshipObject
|
|
158
|
+
|
|
159
|
+
// Update the record in the included Map if it exists
|
|
160
|
+
const recordKey = `${scopeName}:${recordId}`
|
|
161
|
+
if (included.has(recordKey)) {
|
|
162
|
+
const existingRecord = included.get(recordKey)
|
|
163
|
+
if (!existingRecord.relationships) {
|
|
164
|
+
existingRecord.relationships = {}
|
|
165
|
+
}
|
|
166
|
+
existingRecord.relationships[includeName] = relationshipObject
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
// Step 8: Process nested includes if any
|
|
171
|
+
if (Object.keys(subIncludes).length > 0 && targetRecords.length > 0) {
|
|
172
|
+
const nextPath = `${currentPath}.${includeName}`
|
|
173
|
+
await deps.loadNestedIncludes(
|
|
174
|
+
{ records: targetRecords, scopeName: targetScope, includeTree: subIncludes, included, processedPaths, currentPath: nextPath, fields },
|
|
175
|
+
{ context: { scopes, log, knex, capabilities, requestContext } }
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
} else if (relDef.type === 'hasMany') {
|
|
179
|
+
// Handle one-to-many relationship
|
|
180
|
+
const targetScope = relDef.target
|
|
181
|
+
const targetTable = scopes[targetScope].vars.schemaInfo.tableName
|
|
182
|
+
const foreignKey = relDef.foreignKey
|
|
183
|
+
|
|
184
|
+
if (!foreignKey) {
|
|
185
|
+
throw new Error(`Missing foreignKey in hasMany relationship '${includeName}' for scope '${scopeName}'`)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
log.debug(`[INCLUDE] Loading ${targetScope} records with foreign key ${foreignKey}:`, {
|
|
189
|
+
parentCount: mainIds.length
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
const foreignKeyColumn = translateColumnForScope(scopes, targetScope, foreignKey)
|
|
193
|
+
const query = knex(targetTable).modify(whereInIdentifiers, foreignKeyColumn, mainIds)
|
|
194
|
+
const limited = await prepareCollectionInclude({
|
|
195
|
+
query, targetScope, parentColumn: `${targetTable}.${foreignKeyColumn}`, includeConfig: relDef.include, fields
|
|
196
|
+
}, { scopes, knex, requestContext, capabilities })
|
|
197
|
+
const fieldSelectionInfo = limited.fieldSelectionInfo
|
|
198
|
+
const targetRecords = await limited.query
|
|
199
|
+
for (const record of targetRecords) {
|
|
200
|
+
for (const field of limited.temporaryFields) delete record[field]
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Load relationship metadata for all target records
|
|
204
|
+
await loadRelationshipMetadata(scopes, targetRecords, targetScope)
|
|
205
|
+
|
|
206
|
+
// Group by parent ID
|
|
207
|
+
const childrenByParent = Object.create(null)
|
|
208
|
+
targetRecords.forEach(record => {
|
|
209
|
+
const parentId = getFieldValueForScope(scopes, targetScope, record, foreignKey)
|
|
210
|
+
if (!childrenByParent[parentId]) {
|
|
211
|
+
childrenByParent[parentId] = []
|
|
212
|
+
}
|
|
213
|
+
childrenByParent[parentId].push(record)
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
// Set relationships on parent records
|
|
217
|
+
records.forEach(record => {
|
|
218
|
+
if (!record[RELATIONSHIPS_KEY]) record[RELATIONSHIPS_KEY] = {}
|
|
219
|
+
|
|
220
|
+
const recordId = getFieldValueForScope(scopes, scopeName, record, 'id')
|
|
221
|
+
const children = childrenByParent[recordId] || []
|
|
222
|
+
const relData = children.map(childRecord => {
|
|
223
|
+
// Add to included
|
|
224
|
+
const childRecordId = getFieldValueForScope(scopes, targetScope, childRecord, 'id')
|
|
225
|
+
const resourceKey = `${targetScope}:${childRecordId}`
|
|
226
|
+
if (!included.has(resourceKey)) {
|
|
227
|
+
const jsonApiRecord = toJsonApiRecord(
|
|
228
|
+
scopes[targetScope],
|
|
229
|
+
childRecord,
|
|
230
|
+
targetScope
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
// Add relationships from metadata
|
|
234
|
+
if (childRecord[RELATIONSHIP_METADATA_KEY]) {
|
|
235
|
+
jsonApiRecord.relationships = childRecord[RELATIONSHIP_METADATA_KEY]
|
|
236
|
+
// Clean up the temporary property
|
|
237
|
+
delete childRecord[RELATIONSHIP_METADATA_KEY]
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Attach computed dependencies info if sparse fieldsets were used
|
|
241
|
+
if (fieldSelectionInfo?.computedDependencies) {
|
|
242
|
+
jsonApiRecord[COMPUTED_DEPENDENCIES_KEY] = fieldSelectionInfo.computedDependencies
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
included.set(resourceKey, jsonApiRecord)
|
|
246
|
+
}
|
|
247
|
+
return { type: targetScope, id: String(childRecordId) }
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
const relationshipObject = { data: relData }
|
|
251
|
+
|
|
252
|
+
// Add links if urlPrefix is configured
|
|
253
|
+
if (scopeName) {
|
|
254
|
+
relationshipObject.links = buildRelationshipLinks(scopes, scopeName, record, includeName)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
record[RELATIONSHIPS_KEY][includeName] = relationshipObject
|
|
258
|
+
|
|
259
|
+
// Update the record in the included Map if it exists
|
|
260
|
+
const recordKey = `${scopeName}:${recordId}`
|
|
261
|
+
if (included.has(recordKey)) {
|
|
262
|
+
const existingRecord = included.get(recordKey)
|
|
263
|
+
if (!existingRecord.relationships) {
|
|
264
|
+
existingRecord.relationships = {}
|
|
265
|
+
}
|
|
266
|
+
existingRecord.relationships[includeName] = relationshipObject
|
|
267
|
+
}
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
// Process nested includes
|
|
271
|
+
if (Object.keys(subIncludes).length > 0 && targetRecords.length > 0) {
|
|
272
|
+
const nextPath = `${currentPath}.${includeName}`
|
|
273
|
+
await deps.loadNestedIncludes(
|
|
274
|
+
{ records: targetRecords, scopeName: targetScope, includeTree: subIncludes, included, processedPaths, currentPath: nextPath, fields },
|
|
275
|
+
{ context: { scopes, log, knex, capabilities, requestContext } }
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
} catch (error) {
|
|
280
|
+
throw wrapUnexpectedError(error, {
|
|
281
|
+
message: `Failed to load hasMany relationship '${includeName}' for scope '${scopeName}'`,
|
|
282
|
+
context: { phase: 'include', scopeName, includeName, hasThrough: !!relDef.through }
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Loads reverse polymorphic relationships (via)
|
|
289
|
+
*
|
|
290
|
+
* Handles loading "child" records that have a polymorphic relationship back to the parent.
|
|
291
|
+
* For example, loading all comments (which can belong to articles or videos) for a specific article.
|
|
292
|
+
*
|
|
293
|
+
* @param {Object} scope - The resource object containing:
|
|
294
|
+
* - records: Array<Object> - Parent records
|
|
295
|
+
* - scopeName: string - The parent scope name
|
|
296
|
+
* - includeName: string - The relationship name
|
|
297
|
+
* - relDef: Object - The relationship definition with 'via' property
|
|
298
|
+
* - subIncludes: Object - Nested includes to process recursively
|
|
299
|
+
* - included: Map - Map of already included resources
|
|
300
|
+
* - processedPaths: Set - Set of already processed paths
|
|
301
|
+
* - currentPath: string - Current include path for tracking
|
|
302
|
+
* - fields: Object - Sparse fieldsets configuration
|
|
303
|
+
* @param {Object} deps - Dependencies object containing:
|
|
304
|
+
* - context.scopes: Object - The resources object
|
|
305
|
+
* - context.log: Object - Logger instance
|
|
306
|
+
* - context.knex: Object - Knex instance
|
|
307
|
+
* @returns {Promise<void>}
|
|
308
|
+
*/
|
|
309
|
+
export const loadReversePolymorphic = async (scope, deps) => {
|
|
310
|
+
const { records, scopeName, includeName, relDef, subIncludes, included, processedPaths, currentPath, fields } = scope
|
|
311
|
+
const { scopes, log, knex, capabilities, requestContext } = deps.context
|
|
312
|
+
try {
|
|
313
|
+
log.trace('[INCLUDE] Loading reverse polymorphic (via):', {
|
|
314
|
+
scopeName,
|
|
315
|
+
includeName,
|
|
316
|
+
via: relDef.via,
|
|
317
|
+
recordCount: records.length
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
const targetScope = relDef.target
|
|
321
|
+
const viaRelName = relDef.via
|
|
322
|
+
|
|
323
|
+
// Get the polymorphic field info from target scope
|
|
324
|
+
const targetRelationships = scopes[targetScope].vars.schemaInfo.schemaRelationships
|
|
325
|
+
const viaRel = targetRelationships?.[viaRelName]
|
|
326
|
+
|
|
327
|
+
if (!viaRel?.belongsToPolymorphic) {
|
|
328
|
+
throw new Error(`Reverse relationship '${scopeName}.${includeName}' requires polymorphic relationship '${targetScope}.${viaRelName}'`)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const { typeField, idField } = viaRel.belongsToPolymorphic
|
|
332
|
+
const targetTable = scopes[targetScope].vars.schemaInfo.tableName
|
|
333
|
+
|
|
334
|
+
// Collect parent IDs
|
|
335
|
+
const parentIds = records.map(record => getFieldValueForScope(scopes, scopeName, record, 'id')).filter(id => id !== null && id !== undefined)
|
|
336
|
+
if (parentIds.length === 0) return
|
|
337
|
+
|
|
338
|
+
log.debug(`[INCLUDE] Loading ${targetScope} records via ${viaRelName}:`, {
|
|
339
|
+
typeField,
|
|
340
|
+
idField,
|
|
341
|
+
scopeName,
|
|
342
|
+
parentCount: parentIds.length
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
const typeColumn = translateColumnForScope(scopes, targetScope, typeField)
|
|
346
|
+
const idColumn = translateColumnForScope(scopes, targetScope, idField)
|
|
347
|
+
const query = knex(targetTable).where(typeColumn, scopeName).modify(whereInIdentifiers, idColumn, parentIds)
|
|
348
|
+
const limited = await prepareCollectionInclude({
|
|
349
|
+
query, targetScope, parentColumn: `${targetTable}.${idColumn}`, includeConfig: relDef.include, fields
|
|
350
|
+
}, { scopes, knex, requestContext, capabilities })
|
|
351
|
+
const fieldSelectionInfo = limited.fieldSelectionInfo
|
|
352
|
+
const targetRecords = await limited.query
|
|
353
|
+
for (const record of targetRecords) {
|
|
354
|
+
for (const field of limited.temporaryFields) delete record[field]
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Load relationship metadata for all target records
|
|
358
|
+
await loadRelationshipMetadata(scopes, targetRecords, targetScope)
|
|
359
|
+
|
|
360
|
+
// Group by parent ID
|
|
361
|
+
const childrenByParent = Object.create(null)
|
|
362
|
+
targetRecords.forEach(record => {
|
|
363
|
+
const parentId = getFieldValueForScope(scopes, targetScope, record, idField)
|
|
364
|
+
if (!childrenByParent[parentId]) {
|
|
365
|
+
childrenByParent[parentId] = []
|
|
366
|
+
}
|
|
367
|
+
childrenByParent[parentId].push(record)
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
// Set relationships on parent records
|
|
371
|
+
records.forEach(record => {
|
|
372
|
+
if (!record[RELATIONSHIPS_KEY]) record[RELATIONSHIPS_KEY] = {}
|
|
373
|
+
|
|
374
|
+
const recordId = getFieldValueForScope(scopes, scopeName, record, 'id')
|
|
375
|
+
const children = childrenByParent[recordId] || []
|
|
376
|
+
const relData = children.map(childRecord => {
|
|
377
|
+
// Add to included
|
|
378
|
+
const childRecordId = getFieldValueForScope(scopes, targetScope, childRecord, 'id')
|
|
379
|
+
const resourceKey = `${targetScope}:${childRecordId}`
|
|
380
|
+
if (!included.has(resourceKey)) {
|
|
381
|
+
const jsonApiRecord = toJsonApiRecord(
|
|
382
|
+
scopes[targetScope],
|
|
383
|
+
childRecord,
|
|
384
|
+
targetScope
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
// Add relationships from metadata
|
|
388
|
+
if (childRecord[RELATIONSHIP_METADATA_KEY]) {
|
|
389
|
+
jsonApiRecord.relationships = childRecord[RELATIONSHIP_METADATA_KEY]
|
|
390
|
+
// Clean up the temporary property
|
|
391
|
+
delete childRecord[RELATIONSHIP_METADATA_KEY]
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Attach computed dependencies info if sparse fieldsets were used
|
|
395
|
+
if (fieldSelectionInfo?.computedDependencies) {
|
|
396
|
+
jsonApiRecord[COMPUTED_DEPENDENCIES_KEY] = fieldSelectionInfo.computedDependencies
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
included.set(resourceKey, jsonApiRecord)
|
|
400
|
+
}
|
|
401
|
+
return { type: targetScope, id: String(childRecordId) }
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
const relationshipObject = { data: relData }
|
|
405
|
+
|
|
406
|
+
// Add links if urlPrefix is configured
|
|
407
|
+
if (scopeName) {
|
|
408
|
+
relationshipObject.links = buildRelationshipLinks(scopes, scopeName, record, includeName)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
record[RELATIONSHIPS_KEY][includeName] = relationshipObject
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
// Process nested includes
|
|
415
|
+
if (Object.keys(subIncludes).length > 0 && targetRecords.length > 0) {
|
|
416
|
+
const nextPath = `${currentPath}.${includeName}`
|
|
417
|
+
await deps.loadNestedIncludes(
|
|
418
|
+
{ records: targetRecords, scopeName: targetScope, includeTree: subIncludes, included, processedPaths, currentPath: nextPath, fields },
|
|
419
|
+
{ context: { scopes, log, knex, capabilities, requestContext } }
|
|
420
|
+
)
|
|
421
|
+
}
|
|
422
|
+
} catch (error) {
|
|
423
|
+
throw wrapUnexpectedError(error, {
|
|
424
|
+
message: `Failed to load reverse polymorphic relationship '${includeName}' via '${relDef?.via}' for scope '${scopeName}'`,
|
|
425
|
+
context: { phase: 'include', scopeName, includeName, via: relDef?.via, targetScope: relDef?.target }
|
|
426
|
+
})
|
|
427
|
+
}
|
|
428
|
+
}
|