json-rest-api 1.0.29 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +10 -670
- package/LICENSE-GPL-3.0 +674 -0
- package/LICENSE-MIT +21 -0
- package/README.md +39 -30
- package/docs/API.md +744 -1273
- package/docs/GUIDE/01-getting-started.md +338 -0
- package/docs/GUIDE/02-resources-and-relationships.md +33 -0
- package/docs/GUIDE/03-running-example.md +87 -0
- package/docs/GUIDE/04-creating-and-querying.md +181 -0
- package/docs/GUIDE/05-belongs-to.md +177 -0
- package/docs/GUIDE/06-has-many.md +184 -0
- package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
- package/docs/GUIDE/08-many-to-many.md +184 -0
- package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
- package/docs/GUIDE/10-put-and-patch.md +174 -0
- package/docs/GUIDE/11-relationship-endpoints.md +174 -0
- package/docs/GUIDE/12-field-transformations.md +287 -0
- package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
- package/docs/GUIDE/14-custom-resource-methods.md +125 -0
- package/docs/GUIDE/15-query-projections.md +266 -0
- package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
- package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
- package/docs/GUIDE/18-labels.md +86 -0
- package/docs/GUIDE/19-managed-transactions.md +179 -0
- package/docs/GUIDE/20-transaction-outcomes.md +207 -0
- package/docs/GUIDE/21-schema-and-migrations.md +634 -0
- package/docs/GUIDE/22-version-field-migrations.md +208 -0
- package/docs/GUIDE/23-fastify.md +179 -0
- package/docs/GUIDE/24-cors.md +172 -0
- package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
- package/docs/GUIDE/26-file-uploads.md +278 -0
- package/docs/GUIDE/27-bulk-operations.md +277 -0
- package/docs/GUIDE/28-socketio.md +325 -0
- package/docs/GUIDE/29-writing-plugins.md +278 -0
- package/docs/GUIDE/30-backend-capabilities.md +100 -0
- package/docs/GUIDE/31-positioning.md +181 -0
- package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +74 -30
- package/docs/GUIDE/33-migrating-to-v2.md +3827 -0
- package/docs/GUIDE/index.md +51 -59
- package/docs/QUICKSTART.md +120 -287
- package/docs/architecture.md +75 -0
- package/docs/contributing.md +178 -0
- package/index.d.ts +36 -0
- package/index.js +1 -22
- package/lib/enhanced-logger.js +91 -85
- package/lib/error-context.js +445 -5
- package/lib/error-formatter.js +219 -59
- package/lib/knex-transaction.js +64 -0
- package/lib/logger-types.d.ts +22 -0
- package/lib/missing-package.js +4 -0
- package/lib/rest-api-errors.js +103 -8
- package/lib/runtime/json-rest-api.js +172 -0
- package/lib/transaction-types.d.ts +59 -0
- package/package.json +97 -27
- package/plugins/core/bulk-operations-plugin.js +191 -184
- package/plugins/core/connectors/express-plugin.js +148 -219
- package/plugins/core/connectors/fastify-plugin.js +125 -82
- package/plugins/core/connectors/lib/busboy-detector.js +74 -156
- package/plugins/core/connectors/lib/connector-core.js +144 -20
- package/plugins/core/connectors/lib/formidable-detector.js +63 -120
- package/plugins/core/connectors/lib/http-validators.js +77 -0
- package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
- package/plugins/core/connectors/lib/request-helpers.js +10 -1
- package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
- package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
- package/plugins/core/file-handling-plugin.js +87 -177
- package/plugins/core/lib/anyapi/anyapi-registry.js +213 -164
- package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
- package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
- package/plugins/core/lib/anyapi/schema-utils.js +25 -16
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
- package/plugins/core/lib/dbIntrospection.js +228 -21
- package/plugins/core/lib/dbTablesOperations.js +409 -92
- package/plugins/core/lib/fractional-positioning.js +12 -48
- package/plugins/core/lib/querying/identifier-query.js +19 -0
- package/plugins/core/lib/querying/include-query-helpers.js +364 -0
- package/plugins/core/lib/querying/include-to-many.js +428 -0
- package/plugins/core/lib/querying/include-to-one.js +579 -0
- package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
- package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
- package/plugins/core/lib/querying/knex-process-includes.js +5 -110
- package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
- package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
- package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
- package/plugins/core/lib/querying/knex-window-queries.js +62 -253
- package/plugins/core/lib/querying/query-builder-utils.js +44 -4
- package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
- package/plugins/core/lib/querying/query-constraint.js +27 -0
- package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
- package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
- package/plugins/core/lib/querying/sort-helpers.js +13 -1
- package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
- package/plugins/core/lib/querying/url-helpers.js +11 -4
- package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
- package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
- package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
- package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
- package/plugins/core/lib/querying-writing/field-utils.js +113 -92
- package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
- package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
- package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
- package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
- package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
- package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
- package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
- package/plugins/core/lib/querying-writing/response-options.js +35 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
- package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
- package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
- package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
- package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
- package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
- package/plugins/core/lib/storage/storage-adapter.js +128 -62
- package/plugins/core/lib/storage/storage-mapping.js +78 -28
- package/plugins/core/lib/storage/storage-types.d.ts +502 -0
- package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
- package/plugins/core/lib/writing/relationship-processor.js +94 -158
- package/plugins/core/lib/writing/resource-version.js +172 -0
- package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +116 -2485
- package/plugins/core/rest-api-autofilter-plugin.js +17 -19
- package/plugins/core/rest-api-cors-plugin.js +52 -205
- package/plugins/core/rest-api-knex-plugin.js +45 -1077
- package/plugins/core/rest-api-label-plugin.js +25 -38
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
- package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
- package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
- package/plugins/core/rest-api-plugin-methods/common.js +291 -395
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
- package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
- package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
- package/plugins/core/rest-api-plugin-methods/get.js +36 -68
- package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
- package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
- package/plugins/core/rest-api-plugin-methods/post.js +51 -51
- package/plugins/core/rest-api-plugin-methods/put.js +69 -148
- package/plugins/core/rest-api-plugin-methods/query.js +37 -81
- package/plugins/core/rest-api-plugin-methods/release.js +2 -1
- package/plugins/core/rest-api-plugin.js +32 -46
- package/plugins/core/rest-api-positioning-plugin.js +183 -628
- package/plugins/core/rest-api-query-projections-plugin.js +19 -102
- package/plugins/core/rest-api-row-policy-plugin.js +3 -3
- package/plugins/core/socketio-plugin.js +364 -344
- package/plugins/storage/local-storage.js +40 -36
- package/types/autofilter.d.ts +57 -0
- package/types/bulk-methods.d.ts +63 -0
- package/types/errors.d.ts +118 -0
- package/types/fastify-options.d.ts +19 -0
- package/types/file-detectors.d.ts +33 -0
- package/types/file-storage.d.ts +48 -0
- package/types/hook-context.d.ts +180 -0
- package/types/plugin-options.d.ts +53 -0
- package/types/relationship-methods.d.ts +74 -0
- package/types/representations.d.ts +50 -0
- package/types/resource-methods.d.ts +95 -0
- package/types/resource-schema.d.ts +157 -0
- package/types/row-policy.d.ts +31 -0
- package/types/runtime.d.ts +132 -0
- package/types/transactions.d.ts +21 -0
- package/.claude/agents/library-guide-expert.md +0 -30
- package/.claude/agents/rest-api-internals-expert.md +0 -54
- package/.claude/settings.local.json +0 -70
- package/AGENTS.md +0 -34
- package/CLAUDE.md +0 -160
- package/CRUSH.md +0 -55
- package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
- package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
- package/TODO.todo +0 -13
- package/connectors_ideas/fastify.js +0 -217
- package/connectors_ideas/koa-plugin.js +0 -112
- package/docs/.bundle/config +0 -2
- package/docs/COMPARISON.md +0 -433
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
- package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
- package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
- package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
- package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
- package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
- package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
- package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
- package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
- package/docs/Gemfile +0 -6
- package/docs/Gemfile.lock +0 -308
- package/docs/ONBOARDING.md +0 -315
- package/docs/_config.yml +0 -36
- package/docs/_layouts/default.html +0 -123
- package/docs/apple-touch-icon.svg +0 -4
- package/docs/assets/css/style.scss +0 -392
- package/docs/favicon.svg +0 -4
- package/docs/index.md +0 -164
- package/docs/mini-guides/file-uploads-complete.md +0 -161
- package/eslint.config.js +0 -13
- package/examples/url-override-example.js +0 -137
- package/fixing_plan.md +0 -112
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
- package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
- package/plugins/storage/s3-storage.js +0 -93
- package/quickTest.js +0 -136
- package/scripts/debug-anyapi-review-filter.mjs +0 -63
- package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
- package/scripts/inspect-anyapi-descriptor.mjs +0 -19
- package/scripts/run-docs.js +0 -133
- package/test-debug.js +0 -59
- package/tests/README.md +0 -202
- package/tests/TEST_TEMPLATE.test.js +0 -178
- package/tests/anyapi-basic.test.js +0 -105
- package/tests/anyapi-cursor-pagination.test.js +0 -148
- package/tests/anyapi-custom-idproperty.test.js +0 -163
- package/tests/autofilter.test.js +0 -463
- package/tests/bulk-operations.test.js +0 -657
- package/tests/computed-fields.test.js +0 -586
- package/tests/cors.test.js +0 -834
- package/tests/curl.test.js +0 -467
- package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
- package/tests/custom-id-simplified-post.test.js +0 -75
- package/tests/custom-idproperty-relationships.test.js +0 -932
- package/tests/db-introspection.test.js +0 -521
- package/tests/db-table-operations.test.js +0 -798
- package/tests/fastify-plugin.test.js +0 -184
- package/tests/field-getters.test.js +0 -370
- package/tests/field-setters.test.js +0 -306
- package/tests/file-handling.test.js +0 -155
- package/tests/fixtures/api-configs.js +0 -2566
- package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
- package/tests/helpers/fake-fastify.js +0 -99
- package/tests/helpers/storage-mode.js +0 -65
- package/tests/helpers/test-utils.js +0 -203
- package/tests/http-connectors-parity.test.js +0 -395
- package/tests/id-normalization.test.js +0 -444
- package/tests/include-depth-validation.test.js +0 -331
- package/tests/includes.test.js +0 -427
- package/tests/local-storage.test.js +0 -96
- package/tests/logical-id-write-validation.test.js +0 -151
- package/tests/nested-includes.test.js +0 -617
- package/tests/pagination-boundaries.test.js +0 -154
- package/tests/pagination-contract.test.js +0 -122
- package/tests/pagination-cursor-multifield.test.js +0 -641
- package/tests/pagination-enhanced.test.js +0 -563
- package/tests/polymorphic-relationships.test.js +0 -347
- package/tests/polymorphic-simple.test.js +0 -148
- package/tests/positioning-advanced.test.js +0 -692
- package/tests/positioning-extended.test.js +0 -915
- package/tests/positioning-multifilter.test.js +0 -699
- package/tests/positioning.test.js +0 -560
- package/tests/post-commit-hooks.test.js +0 -120
- package/tests/projected-fields.test.js +0 -319
- package/tests/public-exports.test.js +0 -15
- package/tests/put-create-parity.test.js +0 -216
- package/tests/queries.test.js +0 -521
- package/tests/query-limits.test.js +0 -584
- package/tests/relationship-endpoints.test.js +0 -620
- package/tests/relationship-include-storage-adapter.test.js +0 -56
- package/tests/relationships.test.js +0 -681
- package/tests/rest-api.test.js +0 -337
- package/tests/return-record-settings.test.js +0 -184
- package/tests/row-policy.test.js +0 -489
- package/tests/s3-storage.test.js +0 -38
- package/tests/searchschema-knex-behavior.test.js +0 -187
- package/tests/searchschema-merge.test.js +0 -151
- package/tests/socketio.test.js +0 -860
- package/tests/storage-mapping.test.js +0 -362
- package/tests/temporal-boundaries.test.js +0 -208
- package/tests/test-development-guide.md +0 -370
- package/tests/virtual-fields.test.js +0 -404
|
@@ -1,30 +1,36 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/** @import {
|
|
3
|
+
* Document, PivotField, RelationshipArguments, RelationshipContext
|
|
4
|
+
* } from './lifecycle-types.js' */
|
|
5
|
+
import { rejectRemovedOptions, resolveFormat } from '../lib/querying-writing/response-options.js'
|
|
6
|
+
import { transformJsonApiToSimplified } from '../lib/querying-writing/simplified-helpers.js'
|
|
1
7
|
import { RestApiResourceError } from '../../../lib/rest-api-errors.js'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
8
|
+
import { getVisibleRelationshipParent, validateRequestedIncludes } from './common.js'
|
|
9
|
+
import { findRelationshipDefinition } from '../lib/querying-writing/relationship-contracts.js'
|
|
10
|
+
import { buildRelationshipUrl, buildJsonApiLink } from '../lib/querying/url-helpers.js'
|
|
4
11
|
import { requireExistingResourceId } from '../lib/querying-writing/resource-id-normalization.js'
|
|
12
|
+
import { queryConstraint } from '../lib/querying/query-constraint.js'
|
|
5
13
|
|
|
6
14
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* @param {string} id - The ID of the parent resource
|
|
12
|
-
* @param {string} relationshipName - The name of the relationship
|
|
13
|
-
* @param {object} queryParams - Standard query parameters
|
|
14
|
-
* @returns {Promise<object>} Related resources with full data
|
|
15
|
+
* Build a JSON:API document for the authorized related resource or collection.
|
|
16
|
+
* The outer getRelated method selects the caller-facing representation.
|
|
17
|
+
* Parent identity, relationship name and query controls come from params.
|
|
15
18
|
*/
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
/** @param {RelationshipArguments} args @returns {Promise<Document & { links: import('../../../types/representations.js').JsonApiLinks }>} */
|
|
20
|
+
async function getRelatedDocument ({ params, context: callerContext, vars, helpers, scope, scopes, runHooks, scopeOptions, scopeName, api }) {
|
|
21
|
+
callerContext.method = 'getRelated'
|
|
22
|
+
callerContext.scopeName = scopeName
|
|
23
|
+
callerContext.id = requireExistingResourceId(params.id, {
|
|
19
24
|
scopeOptions,
|
|
20
25
|
vars,
|
|
21
26
|
scopeName
|
|
22
27
|
})
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
callerContext.relationshipName = params.relationshipName
|
|
29
|
+
callerContext.queryParams = params.queryParams || {}
|
|
30
|
+
callerContext.schemaInfo = scope.vars.schemaInfo
|
|
31
|
+
callerContext.transaction = params.transaction
|
|
32
|
+
callerContext.db = callerContext.transaction || api.knex.instance
|
|
33
|
+
const context = /** @type {RelationshipContext} */ (callerContext)
|
|
28
34
|
|
|
29
35
|
// Validate the relationship exists
|
|
30
36
|
const relDef = findRelationshipDefinition(context.schemaInfo, context.relationshipName)
|
|
@@ -36,325 +42,127 @@ export default async function getRelatedMethod ({ params, context, vars, helpers
|
|
|
36
42
|
)
|
|
37
43
|
}
|
|
38
44
|
|
|
39
|
-
|
|
45
|
+
await runHooks('checkPermissions')
|
|
46
|
+
await runHooks('checkPermissionsGetRelated')
|
|
47
|
+
const parentRecord = await getVisibleRelationshipParent({ context, helpers, scopeName, runHooks })
|
|
48
|
+
if (!parentRecord) {
|
|
49
|
+
throw new RestApiResourceError('Resource not found', { subtype: 'not_found' })
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (relDef.belongsTo || relDef.belongsToPolymorphic || relDef.type === 'hasOne') {
|
|
53
|
+
const types = relDef.belongsToPolymorphic?.types || [relDef.belongsTo || relDef.target].filter(type => type !== undefined)
|
|
54
|
+
validateRequestedIncludes({ ...context, scopeName: types.length === 1 ? types[0] || scopeName : scopeName }, scopes, types)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// A polymorphic target is determined by the visible parent record.
|
|
40
58
|
let targetType
|
|
41
59
|
if (relDef.type === 'hasMany' || relDef.type === 'hasOne') {
|
|
42
60
|
targetType = relDef.target
|
|
43
61
|
} else if (relDef.type === 'manyToMany') {
|
|
44
|
-
targetType = context.relationshipName
|
|
62
|
+
targetType = relDef.target || context.relationshipName
|
|
45
63
|
} else if (relDef.belongsTo) {
|
|
46
64
|
targetType = relDef.belongsTo // belongsTo still in schema
|
|
65
|
+
} else if (relDef.belongsToPolymorphic) {
|
|
66
|
+
const linkage = parentRecord.relationships?.[context.relationshipName]?.data
|
|
67
|
+
targetType = Array.isArray(linkage) ? undefined : linkage?.type
|
|
68
|
+
if (!targetType) {
|
|
69
|
+
return {
|
|
70
|
+
links: { self: buildRelationshipUrl(context, scope, scopeName, context.id, context.relationshipName, false) },
|
|
71
|
+
data: null
|
|
72
|
+
}
|
|
73
|
+
}
|
|
47
74
|
}
|
|
48
75
|
|
|
49
|
-
|
|
76
|
+
const targetScope = targetType ? scopes[targetType] : undefined
|
|
77
|
+
if (!targetType || !targetScope) {
|
|
50
78
|
throw new RestApiResourceError(
|
|
51
79
|
`Related resource type '${targetType}' not found`,
|
|
52
80
|
{ subtype: 'related_type_not_found' }
|
|
53
81
|
)
|
|
54
82
|
}
|
|
55
83
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (!parentRecord) {
|
|
69
|
-
throw new RestApiResourceError('Resource not found', { subtype: 'not_found' })
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Handle to-one relationships (belongsTo and hasOne)
|
|
73
|
-
// For example: GET /api/books/1/country or GET /api/books/1/publisher
|
|
74
|
-
if (relDef.belongsTo || relDef.type === 'hasOne') {
|
|
75
|
-
// OPTIMIZATION: Detect if we actually need to make two API calls
|
|
76
|
-
//
|
|
77
|
-
// The naive approach always makes 2 calls:
|
|
78
|
-
// 1. Get parent with relationship included (fetches FULL related record)
|
|
79
|
-
// 2. Extract just the ID and fetch the same record again with queryParams
|
|
80
|
-
//
|
|
81
|
-
// This optimization checks if there are queryParams that would affect
|
|
82
|
-
// the related resource. If not, we can use the data from the first call.
|
|
83
|
-
const hasRelevantQueryParams = context.queryParams && (
|
|
84
|
-
// Check for includes on the related resource (e.g., ?include=some.nested.relation)
|
|
85
|
-
context.queryParams.include?.length > 0 ||
|
|
86
|
-
// Check for field selection on the related resource (e.g., ?fields[countries]=name,code)
|
|
87
|
-
context.queryParams.fields?.[targetType] ||
|
|
88
|
-
// Note: Filters and sorting don't make sense for a single to-one relationship
|
|
89
|
-
// so we don't check for them
|
|
90
|
-
false
|
|
91
|
-
)
|
|
92
|
-
|
|
93
|
-
if (hasRelevantQueryParams) {
|
|
94
|
-
// CASE 1: Has queryParams that affect the related resource
|
|
95
|
-
// We need to make two calls to properly apply the queryParams
|
|
96
|
-
|
|
97
|
-
// First call: Get parent with minimal data (just need the related ID)
|
|
98
|
-
const parent = await scope.get({
|
|
99
|
-
id: context.id,
|
|
100
|
-
queryParams: {
|
|
101
|
-
include: [context.relationshipName],
|
|
102
|
-
fields: { [scopeName]: vars.idProperty || 'id' } // Only fetch parent ID to minimize data
|
|
103
|
-
},
|
|
104
|
-
transaction: context.transaction,
|
|
105
|
-
simplified: false,
|
|
106
|
-
isTransport: params.isTransport
|
|
107
|
-
}, { ...context })
|
|
108
|
-
|
|
109
|
-
const relatedId = parent.data.relationships?.[context.relationshipName]?.data?.id
|
|
110
|
-
if (!relatedId) {
|
|
111
|
-
return {
|
|
112
|
-
links: { self: buildRelationshipUrl(context, scope, scopeName, context.id, context.relationshipName, false) },
|
|
113
|
-
data: null
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Second call: Get the related resource with all queryParams applied
|
|
118
|
-
const related = await api.resources[targetType].get({
|
|
119
|
-
id: relatedId,
|
|
120
|
-
queryParams: context.queryParams,
|
|
121
|
-
transaction: context.transaction,
|
|
122
|
-
simplified: false,
|
|
123
|
-
isTransport: params.isTransport
|
|
124
|
-
}, { ...context })
|
|
125
|
-
|
|
126
|
-
return {
|
|
127
|
-
links: { self: buildRelationshipUrl(context, scope, scopeName, context.id, context.relationshipName, false) },
|
|
128
|
-
data: related.data,
|
|
129
|
-
included: related.included
|
|
130
|
-
}
|
|
131
|
-
} else {
|
|
132
|
-
// CASE 2: No queryParams that affect the related resource
|
|
133
|
-
// We can get everything in one call and extract from included
|
|
134
|
-
|
|
135
|
-
// Single call: Get parent with full related resource included
|
|
136
|
-
const parent = await scope.get({
|
|
137
|
-
id: context.id,
|
|
138
|
-
queryParams: {
|
|
139
|
-
include: [context.relationshipName],
|
|
140
|
-
fields: context.queryParams.fields // Respect any field selections for the parent
|
|
141
|
-
},
|
|
142
|
-
transaction: context.transaction,
|
|
143
|
-
simplified: false,
|
|
144
|
-
isTransport: params.isTransport
|
|
145
|
-
}, { ...context })
|
|
84
|
+
if (relDef.belongsTo || relDef.belongsToPolymorphic || relDef.type === 'hasOne') {
|
|
85
|
+
// Resolve visible linkage, then use the target GET lifecycle for every selection.
|
|
86
|
+
const parent = await scope.get({
|
|
87
|
+
id: context.id,
|
|
88
|
+
queryParams: {
|
|
89
|
+
include: [context.relationshipName],
|
|
90
|
+
fields: targetType === scopeName ? {} : { [targetType]: 'id' }
|
|
91
|
+
},
|
|
92
|
+
transaction: context.transaction,
|
|
93
|
+
format: 'jsonapi'
|
|
94
|
+
}, { ...context })
|
|
146
95
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
links: { self: buildRelationshipUrl(context, scope, scopeName, context.id, context.relationshipName, false) },
|
|
152
|
-
data: null
|
|
153
|
-
}
|
|
154
|
-
}
|
|
96
|
+
const linkage = parent.data.relationships?.[context.relationshipName]?.data
|
|
97
|
+
const relatedId = Array.isArray(linkage) ? undefined : linkage?.id
|
|
98
|
+
const links = { self: buildRelationshipUrl(context, scope, scopeName, context.id, context.relationshipName, false) }
|
|
99
|
+
if (relatedId == null) return { links, data: null }
|
|
155
100
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
101
|
+
const related = await targetScope.get({
|
|
102
|
+
id: relatedId,
|
|
103
|
+
queryParams: context.queryParams,
|
|
104
|
+
transaction: context.transaction,
|
|
105
|
+
format: 'jsonapi'
|
|
106
|
+
}, { ...context })
|
|
161
107
|
|
|
162
|
-
|
|
163
|
-
links: { self: buildRelationshipUrl(context, scope, scopeName, context.id, context.relationshipName, false) },
|
|
164
|
-
data: relatedResource || null
|
|
165
|
-
}
|
|
166
|
-
}
|
|
108
|
+
return { links, data: related.data, ...(related.included ? { included: related.included } : {}) }
|
|
167
109
|
}
|
|
168
110
|
|
|
169
|
-
|
|
111
|
+
let constraint
|
|
170
112
|
if (relDef.type === 'hasMany') {
|
|
171
|
-
|
|
113
|
+
let values
|
|
172
114
|
if (relDef.via) {
|
|
173
|
-
|
|
174
|
-
// Example: publishers hasMany reviews via reviewable
|
|
175
|
-
const targetRelationships = scopes[targetType].vars.schemaInfo.schemaRelationships
|
|
115
|
+
const targetRelationships = targetScope.vars.schemaInfo.schemaRelationships
|
|
176
116
|
const viaRel = targetRelationships?.[relDef.via]
|
|
177
|
-
|
|
178
117
|
if (!viaRel?.belongsToPolymorphic) {
|
|
179
118
|
throw new RestApiResourceError(
|
|
180
119
|
`Via relationship '${relDef.via}' not found or not polymorphic in '${targetType}'`,
|
|
181
120
|
{ subtype: 'invalid_via_relationship' }
|
|
182
121
|
)
|
|
183
122
|
}
|
|
184
|
-
|
|
185
123
|
const { typeField, idField } = viaRel.belongsToPolymorphic
|
|
186
|
-
|
|
187
|
-
// Add polymorphic filters
|
|
188
|
-
const filters = {
|
|
189
|
-
...context.queryParams.filters,
|
|
190
|
-
[typeField]: scopeName,
|
|
191
|
-
[idField]: context.id
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const result = await api.resources[targetType].query({
|
|
195
|
-
queryParams: { ...context.queryParams, filters },
|
|
196
|
-
transaction: context.transaction,
|
|
197
|
-
simplified: false,
|
|
198
|
-
isTransport: params.isTransport
|
|
199
|
-
}, { ...context })
|
|
200
|
-
|
|
201
|
-
if (!result.links) {
|
|
202
|
-
result.links = {}
|
|
203
|
-
}
|
|
204
|
-
result.links.self = buildRelationshipUrl(context, scope, scopeName, context.id, context.relationshipName, false)
|
|
205
|
-
return result
|
|
124
|
+
values = { [typeField]: scopeName, [idField]: context.id }
|
|
206
125
|
} else {
|
|
207
|
-
|
|
208
|
-
// Need to find the relationship name in the target resource that points back to this resource
|
|
209
|
-
const targetSchema = scopes[targetType].vars.schemaInfo.schemaStructure
|
|
210
|
-
let relationshipFilterName = null
|
|
211
|
-
|
|
212
|
-
// Find the field in target schema that has the foreign key and get its relationship name
|
|
213
|
-
for (const [fieldName, fieldDef] of Object.entries(targetSchema)) {
|
|
214
|
-
if (fieldName === relDef.foreignKey && fieldDef.belongsTo === scopeName && fieldDef.as) {
|
|
215
|
-
relationshipFilterName = fieldDef.as
|
|
216
|
-
break
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Fall back to foreign key if no relationship name found (shouldn't happen with proper schema)
|
|
221
|
-
const filterKey = relationshipFilterName || relDef.foreignKey
|
|
222
|
-
|
|
223
|
-
const filters = {
|
|
224
|
-
...context.queryParams.filters,
|
|
225
|
-
[filterKey]: context.id
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const result = await api.resources[targetType].query({
|
|
229
|
-
queryParams: { ...context.queryParams, filters },
|
|
230
|
-
transaction: context.transaction,
|
|
231
|
-
simplified: false,
|
|
232
|
-
isTransport: params.isTransport
|
|
233
|
-
}, { ...context })
|
|
234
|
-
|
|
235
|
-
if (!result.links) {
|
|
236
|
-
result.links = {}
|
|
237
|
-
}
|
|
238
|
-
result.links.self = buildRelationshipUrl(context, scope, scopeName, context.id, context.relationshipName, false)
|
|
239
|
-
return result
|
|
126
|
+
values = { [/** @type {string} */ (relDef.foreignKey)]: context.id }
|
|
240
127
|
}
|
|
128
|
+
constraint = { scopeName: targetType, values }
|
|
129
|
+
} else if (relDef.type === 'manyToMany') {
|
|
130
|
+
const pivot = /** @type {PivotField} */ (relDef)
|
|
131
|
+
const { query } = await helpers.dataRelatedIdsQuery({ context, scopeName, relDef: pivot })
|
|
132
|
+
constraint = { scopeName: targetType, idsQuery: query }
|
|
241
133
|
}
|
|
242
134
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
queryParams: context.queryParams,
|
|
258
|
-
transaction: context.transaction,
|
|
259
|
-
simplified: false,
|
|
260
|
-
isTransport: params.isTransport,
|
|
261
|
-
}, { ...context })
|
|
262
|
-
if (related?.data) {
|
|
263
|
-
results.push(related.data)
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
return {
|
|
268
|
-
links: {
|
|
269
|
-
self: buildRelationshipUrl(context, scope, scopeName, context.id, context.relationshipName, false)
|
|
270
|
-
},
|
|
271
|
-
data: results,
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// Legacy fallback
|
|
276
|
-
const pivotResource = relDef.through
|
|
277
|
-
const foreignKey = relDef.foreignKey
|
|
278
|
-
const otherKey = relDef.otherKey
|
|
279
|
-
if (!foreignKey || !otherKey) {
|
|
280
|
-
throw new Error('Missing foreignKey or otherKey in many-to-many relationship')
|
|
281
|
-
}
|
|
282
|
-
const pivotScope = scopes[pivotResource]
|
|
283
|
-
if (!pivotScope) {
|
|
284
|
-
throw new RestApiResourceError(
|
|
285
|
-
`Pivot table resource '${pivotResource}' not found`,
|
|
286
|
-
{ subtype: 'pivot_table_not_found' }
|
|
287
|
-
)
|
|
288
|
-
}
|
|
289
|
-
const pivotSchema = pivotScope.vars.schemaInfo.schemaStructure
|
|
290
|
-
let parentRelationshipName = null
|
|
291
|
-
for (const [fieldName, fieldDef] of Object.entries(pivotSchema)) {
|
|
292
|
-
if (fieldName === foreignKey && fieldDef.belongsTo === scopeName && fieldDef.as) {
|
|
293
|
-
parentRelationshipName = fieldDef.as
|
|
294
|
-
break
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
const filterKey = parentRelationshipName || foreignKey
|
|
298
|
-
const pivotFilters = {
|
|
299
|
-
[filterKey]: context.id
|
|
300
|
-
}
|
|
301
|
-
const pivotResult = await api.resources[pivotResource].query({
|
|
302
|
-
queryParams: {
|
|
303
|
-
filters: pivotFilters,
|
|
304
|
-
include: [context.relationshipName],
|
|
305
|
-
fields: context.queryParams.fields,
|
|
306
|
-
sort: context.queryParams.sort,
|
|
307
|
-
page: context.queryParams.page
|
|
308
|
-
},
|
|
309
|
-
transaction: context.transaction,
|
|
310
|
-
simplified: false,
|
|
311
|
-
isTransport: params.isTransport
|
|
312
|
-
}, { ...context })
|
|
313
|
-
let includedResources = pivotResult.included?.filter((r) => r.type === targetType) || []
|
|
314
|
-
|
|
315
|
-
if (includedResources.length === 0) {
|
|
316
|
-
const pivotData = pivotResult.data || []
|
|
317
|
-
const relatedIds = [...new Set(pivotData
|
|
318
|
-
.map((item) => {
|
|
319
|
-
const attrId = item?.attributes?.[otherKey]
|
|
320
|
-
if (attrId !== null && attrId !== undefined) {
|
|
321
|
-
return String(attrId)
|
|
322
|
-
}
|
|
323
|
-
const relationships = item?.relationships || {}
|
|
324
|
-
for (const rel of Object.values(relationships)) {
|
|
325
|
-
const data = rel?.data
|
|
326
|
-
if (data?.type === targetType && data?.id != null) {
|
|
327
|
-
return String(data.id)
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
return null
|
|
331
|
-
})
|
|
332
|
-
.filter((id) => id !== null)
|
|
333
|
-
)]
|
|
334
|
-
|
|
335
|
-
if (relatedIds.length > 0) {
|
|
336
|
-
includedResources = []
|
|
337
|
-
for (const relatedId of relatedIds) {
|
|
338
|
-
const related = await api.resources[targetType].get({
|
|
339
|
-
id: relatedId,
|
|
340
|
-
queryParams: context.queryParams,
|
|
341
|
-
transaction: context.transaction,
|
|
342
|
-
simplified: false,
|
|
343
|
-
isTransport: params.isTransport,
|
|
344
|
-
}, { ...context })
|
|
345
|
-
if (related?.data) {
|
|
346
|
-
includedResources.push(related.data)
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
return {
|
|
353
|
-
links: {
|
|
354
|
-
self: buildRelationshipUrl(context, scope, scopeName, context.id, context.relationshipName, false)
|
|
355
|
-
},
|
|
356
|
-
data: includedResources,
|
|
357
|
-
meta: pivotResult.meta
|
|
358
|
-
}
|
|
135
|
+
const result = await targetScope.query({
|
|
136
|
+
queryParams: { ...context.queryParams },
|
|
137
|
+
transaction: context.transaction,
|
|
138
|
+
format: 'jsonapi',
|
|
139
|
+
[queryConstraint]: constraint
|
|
140
|
+
}, { ...context })
|
|
141
|
+
|
|
142
|
+
const relatedUrl = buildRelationshipUrl(context, scope, scopeName, context.id, context.relationshipName, false)
|
|
143
|
+
result.links ||= { self: relatedUrl }
|
|
144
|
+
for (const name of ['self', 'first', 'last', 'prev', 'next']) {
|
|
145
|
+
const link = result.links[name]
|
|
146
|
+
if (typeof link !== 'string') continue
|
|
147
|
+
const queryIndex = link.indexOf('?')
|
|
148
|
+
result.links[name] = relatedUrl + (queryIndex === -1 ? '' : link.slice(queryIndex))
|
|
359
149
|
}
|
|
150
|
+
return /** @type {typeof result & { links: import('../../../types/representations.js').JsonApiLinks }} */ (result)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** @param {RelationshipArguments} args */
|
|
154
|
+
export default async function getRelatedMethod (args) {
|
|
155
|
+
const { params, vars, scopes } = args
|
|
156
|
+
rejectRemovedOptions(params)
|
|
157
|
+
const format = resolveFormat(params.format, vars.format)
|
|
158
|
+
const record = await getRelatedDocument(args)
|
|
159
|
+
if (params.queryParams?.include != null) record.included ||= []
|
|
160
|
+
if (!Array.isArray(record.data)) record.links.self = buildJsonApiLink(record.links.self, args.context.queryParams)
|
|
161
|
+
if (format === 'jsonapi') return record
|
|
162
|
+
if (record.data === null) return null
|
|
163
|
+
const type = Array.isArray(record.data) ? record.data[0]?.type : record.data.type
|
|
164
|
+
const schemaInfo = type === undefined ? undefined : scopes[type]?.vars.schemaInfo
|
|
165
|
+
return transformJsonApiToSimplified({ record }, {
|
|
166
|
+
context: { schemaStructure: schemaInfo?.schemaStructure, schemaRelationships: schemaInfo?.schemaRelationships, scopes }
|
|
167
|
+
})
|
|
360
168
|
}
|
|
@@ -1,26 +1,34 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/** @import {
|
|
3
|
+
* RelationshipArguments, RelationshipContext
|
|
4
|
+
* } from './lifecycle-types.js' */
|
|
5
|
+
import { rejectRemovedOptions, resolveFormat } from '../lib/querying-writing/response-options.js'
|
|
1
6
|
import { RestApiResourceError } from '../../../lib/rest-api-errors.js'
|
|
2
|
-
import { findRelationshipDefinition } from '
|
|
7
|
+
import { findRelationshipDefinition } from '../lib/querying-writing/relationship-contracts.js'
|
|
3
8
|
import { buildRelationshipUrl } from '../lib/querying/url-helpers.js'
|
|
4
9
|
import { requireExistingResourceId } from '../lib/querying-writing/resource-id-normalization.js'
|
|
5
10
|
|
|
6
11
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* @param {string} id - The ID of the resource
|
|
12
|
-
* @param {string} relationshipName - The name of the relationship
|
|
13
|
-
* @returns {Promise<object>} Relationship linkage with links
|
|
12
|
+
* Read authorized relationship linkage for params.id/relationshipName.
|
|
13
|
+
* This returns a JSON:API linkage document regardless of resource format;
|
|
14
|
+
* to-many linkage must not be truncated by ordinary include limits.
|
|
15
|
+
* @param {RelationshipArguments} args
|
|
14
16
|
*/
|
|
15
|
-
export default async function getRelationshipMethod ({ params, context, vars,
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
export default async function getRelationshipMethod ({ params, context: callerContext, vars, scope, scopes, runHooks, scopeOptions, scopeName, api }) {
|
|
18
|
+
rejectRemovedOptions(params)
|
|
19
|
+
if (params.format !== undefined) resolveFormat(params.format)
|
|
20
|
+
callerContext.method = 'getRelationship'
|
|
21
|
+
callerContext.scopeName = scopeName
|
|
22
|
+
callerContext.id = requireExistingResourceId(params.id, {
|
|
18
23
|
scopeOptions,
|
|
19
24
|
vars,
|
|
20
25
|
scopeName
|
|
21
26
|
})
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
callerContext.relationshipName = params.relationshipName
|
|
28
|
+
callerContext.schemaInfo = scope.vars.schemaInfo
|
|
29
|
+
callerContext.transaction = params.transaction
|
|
30
|
+
callerContext.db = callerContext.transaction || api.knex.instance
|
|
31
|
+
const context = /** @type {RelationshipContext} */ (callerContext)
|
|
24
32
|
|
|
25
33
|
// Validate the relationship exists
|
|
26
34
|
const relDef = findRelationshipDefinition(context.schemaInfo, context.relationshipName)
|
|
@@ -36,16 +44,15 @@ export default async function getRelationshipMethod ({ params, context, vars, he
|
|
|
36
44
|
await runHooks('checkPermissions')
|
|
37
45
|
await runHooks('checkPermissionsGetRelationship')
|
|
38
46
|
|
|
39
|
-
//
|
|
47
|
+
// Keep parent attributes available to its GET permission hooks.
|
|
40
48
|
const fullRecord = await scope.get({
|
|
41
49
|
id: context.id,
|
|
42
50
|
queryParams: {
|
|
43
|
-
include
|
|
44
|
-
|
|
51
|
+
// To-many linkage is loaded independently; include limits must not truncate it.
|
|
52
|
+
include: relDef.type === 'hasMany' || relDef.type === 'manyToMany' ? [] : [context.relationshipName]
|
|
45
53
|
},
|
|
46
54
|
transaction: context.transaction,
|
|
47
|
-
|
|
48
|
-
isTransport: params.isTransport
|
|
55
|
+
format: 'jsonapi',
|
|
49
56
|
}, { ...context })
|
|
50
57
|
|
|
51
58
|
if (!fullRecord || !fullRecord.data) {
|