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
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { whereInIdentifiers } from './identifier-query.js'
|
|
2
|
+
import { RELATIONSHIPS_KEY } from '../querying-writing/knex-constants.js'
|
|
3
|
+
import { databaseIdentityExpression } from '../querying-writing/database-value-normalizers.js'
|
|
4
|
+
import { getResourceFieldset, parseFieldset } from '../querying-writing/field-utils.js'
|
|
5
|
+
import {
|
|
6
|
+
getIdColumnForScope,
|
|
7
|
+
getFieldValueForScope,
|
|
8
|
+
translateColumnForScope,
|
|
9
|
+
applyScopeFiltersToIncludeQuery,
|
|
10
|
+
parseIncludeTree,
|
|
11
|
+
} from './include-query-helpers.js'
|
|
12
|
+
/**
|
|
13
|
+
* Loads relationship identifiers for all hasMany relationships without fetching full related records.
|
|
14
|
+
*
|
|
15
|
+
* ## Purpose
|
|
16
|
+
* This function ensures that all hasMany relationships (one-to-many, many-to-many, and polymorphic)
|
|
17
|
+
* always return resource identifiers in the JSON:API response, even when the related resources
|
|
18
|
+
* are not included via the ?include parameter.
|
|
19
|
+
*
|
|
20
|
+
* ## Why this is needed
|
|
21
|
+
* 1. **JSON:API Consistency**: The JSON:API spec allows servers to include relationship identifiers
|
|
22
|
+
* without including the full related resources. This provides a consistent API surface where
|
|
23
|
+
* clients always know what relationships exist and their IDs.
|
|
24
|
+
*
|
|
25
|
+
* 2. **Simplified Mode Support**: In simplified mode, relationship IDs are transformed into
|
|
26
|
+
* minimal objects (e.g., `reviews: [{id: '1'}, {id: '2'}, {id: '3'}]`). Without this function,
|
|
27
|
+
* these objects only appear when using ?include, creating an inconsistent API where fields appear/disappear.
|
|
28
|
+
*
|
|
29
|
+
* 3. **Performance Balance**: Loading just IDs is much cheaper than loading full records. This gives
|
|
30
|
+
* clients the ability to know what relationships exist without the cost of fetching all data.
|
|
31
|
+
*
|
|
32
|
+
* ## What it does
|
|
33
|
+
* - Runs ONE query per relationship type (not per record) to fetch all related IDs
|
|
34
|
+
* - Populates the relationship data with resource identifiers: `{ type: 'resource', id: '123' }`
|
|
35
|
+
* - Handles all relationship types: one-to-many, many-to-many, and polymorphic
|
|
36
|
+
* - Works for both JSON:API and simplified modes (transformation happens elsewhere)
|
|
37
|
+
*
|
|
38
|
+
* ## When it runs
|
|
39
|
+
* This runs after the main records are fetched but before includes are processed.
|
|
40
|
+
* If includes ARE specified, they will overwrite these IDs with full data.
|
|
41
|
+
*
|
|
42
|
+
* @param {Array<Object>} records - The parent records to load relationships for
|
|
43
|
+
* @param {string} scopeName - The parent scope name (e.g., 'authors')
|
|
44
|
+
* @param {Object} scopes - All available scopes with their schemas
|
|
45
|
+
* @param {Object} knex - Knex instance for database queries
|
|
46
|
+
* @param {import('../storage/storage-types.js').DataReadContext | null} [requestContext]
|
|
47
|
+
* @returns {Promise<void>} Modifies records in place by adding relationship data
|
|
48
|
+
*/
|
|
49
|
+
export const loadRelationshipIdentifiers = async (records, scopeName, scopes, knex, requestContext = null) => {
|
|
50
|
+
if (!records.length) return
|
|
51
|
+
|
|
52
|
+
const schemaInfo = scopes[scopeName]?.vars?.schemaInfo
|
|
53
|
+
if (!schemaInfo) throw new Error(`Missing relationship schema for scope '${scopeName}'`)
|
|
54
|
+
|
|
55
|
+
const relationships = schemaInfo.schemaRelationships || {}
|
|
56
|
+
const recordIds = records.map(record => String(getFieldValueForScope(scopes, scopeName, record, 'id')))
|
|
57
|
+
const requestedFields = parseFieldset(getResourceFieldset(requestContext?.queryParams?.fields, scopeName))
|
|
58
|
+
const includeTree = parseIncludeTree(requestContext?.queryParams?.include)
|
|
59
|
+
|
|
60
|
+
// Process each hasMany relationship
|
|
61
|
+
for (const [relName, relDef] of Object.entries(relationships)) {
|
|
62
|
+
if (requestedFields !== null && !requestedFields.includes(relName)) continue
|
|
63
|
+
if (Object.hasOwn(includeTree, relName)) continue
|
|
64
|
+
const idsMap = Object.create(null)
|
|
65
|
+
|
|
66
|
+
if (relDef.type === 'hasMany' && !relDef.via) {
|
|
67
|
+
// Regular one-to-many
|
|
68
|
+
// Example: publisher hasMany authors
|
|
69
|
+
const foreignKey = relDef.foreignKey
|
|
70
|
+
|
|
71
|
+
if (!foreignKey) {
|
|
72
|
+
throw new Error(`Missing foreignKey in hasMany relationship '${relName}' for scope '${scopeName}'`)
|
|
73
|
+
}
|
|
74
|
+
const targetScope = scopes[relDef.target]
|
|
75
|
+
const targetTable = targetScope?.vars?.schemaInfo?.tableName || relDef.target
|
|
76
|
+
const targetIdColumn = getIdColumnForScope(scopes, relDef.target)
|
|
77
|
+
const foreignKeyColumn = translateColumnForScope(scopes, relDef.target, foreignKey)
|
|
78
|
+
|
|
79
|
+
let query = knex(targetTable)
|
|
80
|
+
.modify(whereInIdentifiers, foreignKeyColumn, recordIds)
|
|
81
|
+
.select({ id: databaseIdentityExpression(knex, `${targetTable}.${targetIdColumn}`), [foreignKeyColumn]: databaseIdentityExpression(knex, `${targetTable}.${foreignKeyColumn}`) })
|
|
82
|
+
|
|
83
|
+
const scopedQueryState = await applyScopeFiltersToIncludeQuery({
|
|
84
|
+
query,
|
|
85
|
+
scopes,
|
|
86
|
+
scopeName: relDef.target,
|
|
87
|
+
requestContext,
|
|
88
|
+
db: knex,
|
|
89
|
+
tableName: targetTable,
|
|
90
|
+
queryPurpose: 'relationship-identifiers'
|
|
91
|
+
})
|
|
92
|
+
query = scopedQueryState.query
|
|
93
|
+
|
|
94
|
+
const results = await query
|
|
95
|
+
|
|
96
|
+
results.forEach(row => {
|
|
97
|
+
const parentId = String(row[foreignKeyColumn])
|
|
98
|
+
if (!idsMap[parentId]) idsMap[parentId] = []
|
|
99
|
+
idsMap[parentId].push(String(row.id))
|
|
100
|
+
})
|
|
101
|
+
} else if (relDef.type === 'manyToMany') {
|
|
102
|
+
// Many-to-many relationship
|
|
103
|
+
// Example: { type: 'manyToMany', through: 'article_tags', foreignKey: 'article_id', otherKey: 'tag_id' }
|
|
104
|
+
const { through, foreignKey, otherKey } = relDef
|
|
105
|
+
const fk = translateColumnForScope(scopes, through, foreignKey)
|
|
106
|
+
const ok = translateColumnForScope(scopes, through, otherKey)
|
|
107
|
+
|
|
108
|
+
if (!fk || !ok) {
|
|
109
|
+
throw new Error(`Missing foreignKey or otherKey in manyToMany relationship '${relName}' for scope '${scopeName}'`)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Get the actual table name from the pivot scope
|
|
113
|
+
const pivotScope = scopes[through]
|
|
114
|
+
const pivotTable = pivotScope?.vars?.schemaInfo?.tableName || through
|
|
115
|
+
|
|
116
|
+
const targetScopeName = relDef.target || relName
|
|
117
|
+
const targetTable = scopes[targetScopeName]?.vars?.schemaInfo?.tableName || targetScopeName
|
|
118
|
+
const targetIdColumn = getIdColumnForScope(scopes, targetScopeName)
|
|
119
|
+
let query = knex(targetTable)
|
|
120
|
+
.join(`${pivotTable} as pivot`, `${targetTable}.${targetIdColumn}`, `pivot.${ok}`)
|
|
121
|
+
.modify(whereInIdentifiers, `pivot.${fk}`, recordIds)
|
|
122
|
+
.select({ __parent_id: databaseIdentityExpression(knex, `pivot.${fk}`), id: databaseIdentityExpression(knex, `${targetTable}.${targetIdColumn}`) })
|
|
123
|
+
|
|
124
|
+
const scopedQueryState = await applyScopeFiltersToIncludeQuery({
|
|
125
|
+
query,
|
|
126
|
+
scopes,
|
|
127
|
+
scopeName: targetScopeName,
|
|
128
|
+
requestContext,
|
|
129
|
+
db: knex,
|
|
130
|
+
tableName: targetTable,
|
|
131
|
+
queryPurpose: 'relationship-identifiers'
|
|
132
|
+
})
|
|
133
|
+
const pivotState = await applyScopeFiltersToIncludeQuery({
|
|
134
|
+
query: scopedQueryState.query, scopes, scopeName: through, requestContext, db: knex, tableName: 'pivot', queryPurpose: 'relationship-identifiers'
|
|
135
|
+
})
|
|
136
|
+
query = pivotState.query
|
|
137
|
+
|
|
138
|
+
const results = await query
|
|
139
|
+
|
|
140
|
+
results.forEach(row => {
|
|
141
|
+
const parentId = String(row.__parent_id)
|
|
142
|
+
const childId = String(row.id)
|
|
143
|
+
if (!idsMap[parentId]) idsMap[parentId] = []
|
|
144
|
+
idsMap[parentId].push(childId)
|
|
145
|
+
})
|
|
146
|
+
} else if (relDef.type === 'hasMany' && relDef.via) {
|
|
147
|
+
// Polymorphic reverse (via)
|
|
148
|
+
// Example: publishers hasMany reviews via reviewable (where reviews.reviewable_type = 'publishers')
|
|
149
|
+
const targetScope = relDef.target
|
|
150
|
+
const targetRelationships = scopes[targetScope]?.vars?.schemaInfo?.schemaRelationships
|
|
151
|
+
const viaRel = targetRelationships?.[relDef.via]
|
|
152
|
+
|
|
153
|
+
if (!viaRel?.belongsToPolymorphic) {
|
|
154
|
+
throw new Error(`Reverse relationship '${scopeName}.${relName}' requires polymorphic relationship '${targetScope}.${relDef.via}'`)
|
|
155
|
+
}
|
|
156
|
+
const { typeField, idField } = viaRel.belongsToPolymorphic
|
|
157
|
+
|
|
158
|
+
// Get the actual table name
|
|
159
|
+
const targetTable = scopes[targetScope]?.vars?.schemaInfo?.tableName || targetScope
|
|
160
|
+
const targetIdColumn = getIdColumnForScope(scopes, targetScope)
|
|
161
|
+
const typeColumn = translateColumnForScope(scopes, targetScope, typeField)
|
|
162
|
+
const idColumn = translateColumnForScope(scopes, targetScope, idField)
|
|
163
|
+
|
|
164
|
+
let query = knex(targetTable)
|
|
165
|
+
.where(typeColumn, scopeName)
|
|
166
|
+
.modify(whereInIdentifiers, idColumn, recordIds)
|
|
167
|
+
.select({ id: databaseIdentityExpression(knex, `${targetTable}.${targetIdColumn}`), [idColumn]: databaseIdentityExpression(knex, `${targetTable}.${idColumn}`) })
|
|
168
|
+
|
|
169
|
+
const scopedQueryState = await applyScopeFiltersToIncludeQuery({
|
|
170
|
+
query,
|
|
171
|
+
scopes,
|
|
172
|
+
scopeName: targetScope,
|
|
173
|
+
requestContext,
|
|
174
|
+
db: knex,
|
|
175
|
+
tableName: targetTable,
|
|
176
|
+
queryPurpose: 'relationship-identifiers'
|
|
177
|
+
})
|
|
178
|
+
query = scopedQueryState.query
|
|
179
|
+
|
|
180
|
+
const results = await query
|
|
181
|
+
|
|
182
|
+
results.forEach(row => {
|
|
183
|
+
const parentId = String(row[idColumn])
|
|
184
|
+
if (!idsMap[parentId]) idsMap[parentId] = []
|
|
185
|
+
idsMap[parentId].push(String(row.id))
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Apply the collected IDs to all records
|
|
190
|
+
if (Object.keys(idsMap).length > 0 || relDef.type === 'hasMany' || relDef.type === 'manyToMany') {
|
|
191
|
+
records.forEach(record => {
|
|
192
|
+
if (!record[RELATIONSHIPS_KEY]) {
|
|
193
|
+
record[RELATIONSHIPS_KEY] = {}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const ids = idsMap[String(getFieldValueForScope(scopes, scopeName, record, 'id'))] || []
|
|
197
|
+
const targetType = relDef.target || relName
|
|
198
|
+
|
|
199
|
+
record[RELATIONSHIPS_KEY][relName] = {
|
|
200
|
+
data: ids.map(id => ({
|
|
201
|
+
type: targetType,
|
|
202
|
+
id
|
|
203
|
+
}))
|
|
204
|
+
}
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -1,7 +1,15 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {unknown} [sort]
|
|
5
|
+
* @param {{ idField?: string }} [options]
|
|
6
|
+
* @returns {string[]}
|
|
7
|
+
*/
|
|
1
8
|
export function normalizeStableSort (sort = [], { idField = 'id' } = {}) {
|
|
9
|
+
/** @type {Set<string>} */
|
|
2
10
|
const seenFields = new Set()
|
|
3
11
|
const sortList = Array.isArray(sort)
|
|
4
|
-
? sort.filter((entry) => {
|
|
12
|
+
? sort.filter(/** @param {unknown} entry @returns {entry is string} */ (entry) => {
|
|
5
13
|
if (typeof entry !== 'string' || !entry.trim()) return false
|
|
6
14
|
const field = entry.startsWith('-') ? entry.slice(1) : entry
|
|
7
15
|
if (seenFields.has(field)) return false
|
|
@@ -26,6 +34,10 @@ export function normalizeStableSort (sort = [], { idField = 'id' } = {}) {
|
|
|
26
34
|
return sortList
|
|
27
35
|
}
|
|
28
36
|
|
|
37
|
+
/**
|
|
38
|
+
* @param {string} sortEntry
|
|
39
|
+
* @returns {{ raw: string, field: string, desc: boolean, direction: 'asc' | 'desc', sqlDirection: 'ASC' | 'DESC' }}
|
|
40
|
+
*/
|
|
29
41
|
export function parseSortEntry (sortEntry) {
|
|
30
42
|
const desc = typeof sortEntry === 'string' && sortEntry.startsWith('-')
|
|
31
43
|
const field = desc ? sortEntry.slice(1) : sortEntry
|
|
@@ -1,27 +1,33 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/** @import { StorageAdapter, StorageHookParams } from '../storage/storage-types.js' */
|
|
3
|
+
|
|
4
|
+
/** @param {StorageHookParams} hookParams @param {{ getStorageAdapter?: (scopeName: string) => StorageAdapter | null | undefined }} [options] */
|
|
1
5
|
export const createStorageAdapterUtilities = (hookParams, { getStorageAdapter } = {}) => {
|
|
2
6
|
const context = hookParams?.context || {}
|
|
3
7
|
const activeQuery = context.knexQuery || {}
|
|
4
8
|
const activeScopeName = activeQuery.scopeName
|
|
5
9
|
const activeTableName = activeQuery.tableName
|
|
6
10
|
const activeStorageAdapter = activeQuery.storageAdapter || context.storageAdapter
|
|
11
|
+
/** @type {Map<string, StorageAdapter | null | undefined>} */
|
|
7
12
|
const storageCache = new Map()
|
|
8
13
|
|
|
14
|
+
/** @param {string} scopeName @returns {StorageAdapter | null | undefined} */
|
|
9
15
|
const fetchStorageAdapter = (scopeName) => {
|
|
10
16
|
if (!scopeName) return null
|
|
11
17
|
if (storageCache.has(scopeName)) return storageCache.get(scopeName)
|
|
12
18
|
|
|
13
19
|
let adapter = null
|
|
14
|
-
if (scopeName === activeScopeName) {
|
|
15
|
-
adapter = activeStorageAdapter
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
adapter = typeof getStorageAdapter === 'function' ? getStorageAdapter(scopeName) : null
|
|
20
|
+
if (scopeName === activeScopeName && activeStorageAdapter) {
|
|
21
|
+
adapter = activeStorageAdapter
|
|
22
|
+
} else if (typeof getStorageAdapter === 'function') {
|
|
23
|
+
adapter = getStorageAdapter(scopeName)
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
storageCache.set(scopeName, adapter)
|
|
22
27
|
return adapter
|
|
23
28
|
}
|
|
24
29
|
|
|
30
|
+
/** @param {string} scopeName @returns {string} */
|
|
25
31
|
const defaultAliasForScope = (scopeName) => {
|
|
26
32
|
if (scopeName === activeScopeName) {
|
|
27
33
|
return activeTableName || scopeName
|
|
@@ -29,6 +35,7 @@ export const createStorageAdapterUtilities = (hookParams, { getStorageAdapter }
|
|
|
29
35
|
return scopeName
|
|
30
36
|
}
|
|
31
37
|
|
|
38
|
+
/** @param {string} scopeName @param {string} field @param {string | null} [alias] @returns {string} */
|
|
32
39
|
const translateColumn = (scopeName, field, alias = defaultAliasForScope(scopeName)) => {
|
|
33
40
|
const adapter = fetchStorageAdapter(scopeName)
|
|
34
41
|
const translated = adapter?.translateColumn?.(field) ?? field
|
|
@@ -36,6 +43,7 @@ export const createStorageAdapterUtilities = (hookParams, { getStorageAdapter }
|
|
|
36
43
|
return `${alias}.${translated}`
|
|
37
44
|
}
|
|
38
45
|
|
|
46
|
+
/** @param {string} scopeName @param {string} field @param {unknown} value @returns {unknown} */
|
|
39
47
|
const translateFilterValue = (scopeName, field, value) => {
|
|
40
48
|
const adapter = fetchStorageAdapter(scopeName)
|
|
41
49
|
if (!adapter?.translateFilterValue) return value
|
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
* and support flexible URL prefix override capabilities.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { serializeJsonApiQuery } from '../querying-writing/connectors-query-parser.js'
|
|
9
|
+
|
|
10
|
+
export const buildJsonApiLink = (baseUrl, queryParams, page) => {
|
|
11
|
+
const queryString = serializeJsonApiQuery(queryParams, { page })
|
|
12
|
+
return queryString ? `${baseUrl}?${queryString}` : baseUrl
|
|
13
|
+
}
|
|
14
|
+
|
|
8
15
|
/**
|
|
9
16
|
* Get the complete URL prefix, handling all calculation and override logic
|
|
10
17
|
*
|
|
@@ -63,7 +70,7 @@ export function getUrlPrefix (context, scope) {
|
|
|
63
70
|
*/
|
|
64
71
|
export function buildResourceUrl (context, scope, scopeName, id) {
|
|
65
72
|
const urlPrefix = getUrlPrefix(context, scope)
|
|
66
|
-
return `${urlPrefix}/${scopeName}/${id}`
|
|
73
|
+
return `${urlPrefix}/${scopeName}/${encodeURIComponent(id)}`
|
|
67
74
|
}
|
|
68
75
|
|
|
69
76
|
/**
|
|
@@ -87,11 +94,11 @@ export function buildResourceUrl (context, scope, scopeName, id) {
|
|
|
87
94
|
* // Returns: "https://api.example.com/api/books/1/author"
|
|
88
95
|
*/
|
|
89
96
|
export function buildRelationshipUrl (context, scope, scopeName, id, relationshipName, isRelationshipEndpoint = false) {
|
|
90
|
-
const
|
|
97
|
+
const resourceUrl = buildResourceUrl(context, scope, scopeName, id)
|
|
91
98
|
if (isRelationshipEndpoint) {
|
|
92
|
-
return `${
|
|
99
|
+
return `${resourceUrl}/relationships/${relationshipName}`
|
|
93
100
|
}
|
|
94
|
-
return `${
|
|
101
|
+
return `${resourceUrl}/${relationshipName}`
|
|
95
102
|
}
|
|
96
103
|
|
|
97
104
|
/**
|