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
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { throwMissingPackage } from '../../../lib/missing-package.js'
|
|
2
2
|
|
|
3
3
|
let generateKeyBetween
|
|
4
4
|
try {
|
|
5
5
|
({ generateKeyBetween } = await import('fractional-indexing'))
|
|
6
6
|
} catch (e) {
|
|
7
|
-
|
|
7
|
+
throwMissingPackage('fractional-indexing', 'positioning',
|
|
8
8
|
'Fractional indexing is required for the positioning plugin to generate sortable position keys. This is a peer dependency.')
|
|
9
9
|
}
|
|
10
10
|
|
|
@@ -14,52 +14,13 @@ try {
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
18
|
-
* @param {
|
|
19
|
-
* @param {string}
|
|
20
|
-
* @param {string} idField - Name of the ID field
|
|
21
|
-
* @param {string} positionField - Name of the position field
|
|
17
|
+
* Generate a key between already-selected neighboring position strings.
|
|
18
|
+
* @param {string | null | undefined} previousPosition - Lower bound, or the beginning
|
|
19
|
+
* @param {string | null | undefined} nextPosition - Upper bound, or the end
|
|
22
20
|
* @returns {string} New position key
|
|
23
21
|
*/
|
|
24
|
-
export function calculatePosition (
|
|
25
|
-
|
|
26
|
-
// Important: fractional-indexing expects ASCII/Unicode ordering, not locale-specific
|
|
27
|
-
const sorted = [...items].sort((a, b) => {
|
|
28
|
-
const posA = a[positionField] || ''
|
|
29
|
-
const posB = b[positionField] || ''
|
|
30
|
-
return posA < posB ? -1 : posA > posB ? 1 : 0
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
// If no items, start in the middle
|
|
34
|
-
if (sorted.length === 0) {
|
|
35
|
-
return generateKeyBetween(null, null) // Returns 'a0'
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Position at end
|
|
39
|
-
if (beforeId === null || beforeId === undefined) {
|
|
40
|
-
const lastItem = sorted[sorted.length - 1]
|
|
41
|
-
return generateKeyBetween(lastItem[positionField], null)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Find the item to position before
|
|
45
|
-
const beforeIndex = sorted.findIndex(item => String(item[idField]) === String(beforeId))
|
|
46
|
-
|
|
47
|
-
// If beforeId not found, position at end
|
|
48
|
-
if (beforeIndex === -1) {
|
|
49
|
-
const lastItem = sorted[sorted.length - 1]
|
|
50
|
-
return generateKeyBetween(lastItem[positionField], null)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Position before the found item
|
|
54
|
-
const beforeItem = sorted[beforeIndex]
|
|
55
|
-
const prevItem = beforeIndex > 0 ? sorted[beforeIndex - 1] : null
|
|
56
|
-
|
|
57
|
-
const result = generateKeyBetween(
|
|
58
|
-
prevItem ? prevItem[positionField] : null,
|
|
59
|
-
beforeItem[positionField]
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
return result
|
|
22
|
+
export function calculatePosition (previousPosition, nextPosition) {
|
|
23
|
+
return generateKeyBetween(previousPosition ?? null, nextPosition ?? null)
|
|
63
24
|
}
|
|
64
25
|
|
|
65
26
|
/**
|
|
@@ -123,8 +84,11 @@ export function isValidPosition (position) {
|
|
|
123
84
|
return false
|
|
124
85
|
}
|
|
125
86
|
|
|
126
|
-
|
|
127
|
-
|
|
87
|
+
if (!/^[a-z0-9]+$/i.test(position)) return false
|
|
88
|
+
try {
|
|
89
|
+
generateKeyBetween(position, null)
|
|
90
|
+
return true
|
|
91
|
+
} catch { return false }
|
|
128
92
|
}
|
|
129
93
|
|
|
130
94
|
/**
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { RELATIONSHIP_READ_BATCH_SIZE } from '../querying-writing/knex-constants.js'
|
|
2
|
+
|
|
3
|
+
// Keep collection sorting and limits in one query even for large parent sets.
|
|
4
|
+
export function whereInIdentifiers (query, column, identifiers) {
|
|
5
|
+
const ids = [...new Set(identifiers)]
|
|
6
|
+
if (ids.length <= RELATIONSHIP_READ_BATCH_SIZE) return query.whereIn(column, ids)
|
|
7
|
+
|
|
8
|
+
const client = query.client.config.client
|
|
9
|
+
if (client === 'pg' || client === 'postgresql') {
|
|
10
|
+
return query.whereRaw('?? = ANY(?)', [column, ids])
|
|
11
|
+
}
|
|
12
|
+
if (client === 'sqlite3' || client === 'better-sqlite3') {
|
|
13
|
+
const values = JSON.stringify(ids, (_key, value) => typeof value === 'bigint' ? String(value) : value)
|
|
14
|
+
return query.whereIn(column, query.client.raw('select value from json_each(?)', [values]))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Knex's MySQL text protocol does not impose a prepared-parameter count limit.
|
|
18
|
+
return query.whereIn(column, ids)
|
|
19
|
+
}
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import { whereInIdentifiers } from './identifier-query.js'
|
|
2
|
+
import { applyFieldSelectionToQuery, buildFieldSelection } from '../querying-writing/knex-field-helpers.js'
|
|
3
|
+
import { applyIncludeQueryConfig } from './knex-window-queries.js'
|
|
4
|
+
import { buildEffectiveSortList } from './query-field-sort-helpers.js'
|
|
5
|
+
import { unwrapQueryBuilderState } from './query-builder-utils.js'
|
|
6
|
+
import { RELATIONSHIP_METADATA_KEY, RELATIONSHIP_READ_BATCH_SIZE } from '../querying-writing/knex-constants.js'
|
|
7
|
+
import { RestApiResourceError } from '../../../../lib/rest-api-errors.js'
|
|
8
|
+
import { wrapUnexpectedError } from '../../../../lib/error-context.js'
|
|
9
|
+
import { getPolymorphicLinkage } from '../querying-writing/relationship-contracts.js'
|
|
10
|
+
import {
|
|
11
|
+
getFieldValue as getStorageFieldValue,
|
|
12
|
+
getIdColumn as getStorageIdColumn,
|
|
13
|
+
getStorageColumn as getMappedStorageColumn,
|
|
14
|
+
} from '../storage/storage-mapping.js'
|
|
15
|
+
import { createStorageAdapter } from '../storage/storage-adapter.js'
|
|
16
|
+
import { applyDatabaseReadOptions, databaseIdentityExpression } from '../querying-writing/database-value-normalizers.js'
|
|
17
|
+
|
|
18
|
+
const getScopeStorageAdapter = (scopes, scopeName) => scopes[scopeName]?.vars?.storageAdapter || null
|
|
19
|
+
|
|
20
|
+
export const loadIncludedParentMap = async ({ query, parentIds, childIds, knex }) => {
|
|
21
|
+
const parents = [...new Set(parentIds)]
|
|
22
|
+
const children = [...new Set(childIds)]
|
|
23
|
+
const result = new Map()
|
|
24
|
+
if (!parents.length || !children.length) return result
|
|
25
|
+
const identity = name => databaseIdentityExpression(knex, `included_pairs.${name}`)
|
|
26
|
+
for (let childOffset = 0; childOffset < children.length; childOffset += RELATIONSHIP_READ_BATCH_SIZE) {
|
|
27
|
+
const pairs = knex.from(query.clone().as('included_links')).distinct('parentId', 'childId')
|
|
28
|
+
.modify(whereInIdentifiers, 'parentId', parents)
|
|
29
|
+
.whereIn('childId', children.slice(childOffset, childOffset + RELATIONSHIP_READ_BATCH_SIZE))
|
|
30
|
+
let after
|
|
31
|
+
while (true) {
|
|
32
|
+
const read = knex.from(pairs.clone().as('included_pairs')).select({ parentId: identity('parentId'), childId: identity('childId') })
|
|
33
|
+
.orderBy('included_pairs.parentId').orderBy('included_pairs.childId').limit(RELATIONSHIP_READ_BATCH_SIZE + 1)
|
|
34
|
+
if (after) read.where(next => next.where('included_pairs.parentId', '>', after.parentId).orWhere(tie => tie.where('included_pairs.parentId', after.parentId).where('included_pairs.childId', '>', after.childId)))
|
|
35
|
+
const rows = await applyDatabaseReadOptions(read)
|
|
36
|
+
for (const { parentId, childId } of rows) {
|
|
37
|
+
const parent = String(parentId)
|
|
38
|
+
const child = String(childId)
|
|
39
|
+
if (!result.has(child)) result.set(child, new Set())
|
|
40
|
+
result.get(child).add(parent)
|
|
41
|
+
}
|
|
42
|
+
if (rows.length <= RELATIONSHIP_READ_BATCH_SIZE) break
|
|
43
|
+
after = rows.at(-1)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return result
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const resolveScopeStorageAdapter = (scopes, scopeName, knex) => {
|
|
50
|
+
const configuredAdapter = getScopeStorageAdapter(scopes, scopeName)
|
|
51
|
+
if (configuredAdapter) return configuredAdapter
|
|
52
|
+
|
|
53
|
+
const schemaInfo = scopes[scopeName]?.vars?.schemaInfo
|
|
54
|
+
if (!schemaInfo) return null
|
|
55
|
+
|
|
56
|
+
return createStorageAdapter({ knex, schemaInfo })
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const getIdColumnForScope = (scopes, scopeName) => {
|
|
60
|
+
const adapter = getScopeStorageAdapter(scopes, scopeName)
|
|
61
|
+
if (adapter?.getIdColumn) {
|
|
62
|
+
return adapter.getIdColumn()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return getStorageIdColumn(scopes[scopeName]?.vars?.schemaInfo || {})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const getFieldValueForScope = (scopes, scopeName, record, fieldName) => {
|
|
69
|
+
const adapter = getScopeStorageAdapter(scopes, scopeName)
|
|
70
|
+
if (adapter?.getFieldValue) {
|
|
71
|
+
return adapter.getFieldValue(record, fieldName)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return getStorageFieldValue(record, scopes[scopeName]?.vars?.schemaInfo || {}, fieldName)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const translateColumnForScope = (scopes, scopeName, fieldName) => {
|
|
78
|
+
const adapter = getScopeStorageAdapter(scopes, scopeName)
|
|
79
|
+
if (adapter?.translateColumn) {
|
|
80
|
+
return adapter.translateColumn(fieldName)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return getMappedStorageColumn(scopes[scopeName]?.vars?.schemaInfo || {}, fieldName)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export const prepareCollectionInclude = async ({ query, targetScope, parentColumn, includeConfig, fields }, { scopes, knex, requestContext, capabilities }) => {
|
|
87
|
+
const target = scopes[targetScope]
|
|
88
|
+
const tableName = target.vars.schemaInfo.tableName
|
|
89
|
+
const storageAdapter = resolveScopeStorageAdapter(scopes, targetScope, knex)
|
|
90
|
+
const context = {
|
|
91
|
+
...(requestContext || {}),
|
|
92
|
+
scopeName: targetScope,
|
|
93
|
+
schemaInfo: target.vars.schemaInfo,
|
|
94
|
+
storageAdapter,
|
|
95
|
+
queryParams: { fields, sort: buildEffectiveSortList(includeConfig?.orderBy, { defaultSort: target.vars.defaultSort }) }
|
|
96
|
+
}
|
|
97
|
+
const fieldSelectionInfo = await buildFieldSelection(target, { context })
|
|
98
|
+
const selection = await applyFieldSelectionToQuery({
|
|
99
|
+
query,
|
|
100
|
+
scope: target,
|
|
101
|
+
fieldSelectionInfo,
|
|
102
|
+
tableName,
|
|
103
|
+
useTablePrefix: true,
|
|
104
|
+
storageAdapter,
|
|
105
|
+
db: knex,
|
|
106
|
+
context,
|
|
107
|
+
scopeName: targetScope
|
|
108
|
+
})
|
|
109
|
+
const filtered = await applyScopeFiltersToIncludeQuery({
|
|
110
|
+
query: selection.query, scopes, scopeName: targetScope, requestContext, db: knex, tableName
|
|
111
|
+
})
|
|
112
|
+
const limited = await applyIncludeQueryConfig({
|
|
113
|
+
query: filtered.query,
|
|
114
|
+
scopeName: targetScope,
|
|
115
|
+
tableName,
|
|
116
|
+
parentColumn,
|
|
117
|
+
includeConfig,
|
|
118
|
+
context,
|
|
119
|
+
capabilities,
|
|
120
|
+
queryFieldRuntimeByField: selection.queryFieldRuntimeByField
|
|
121
|
+
}, { scopes, knex, getStorageAdapter: name => resolveScopeStorageAdapter(scopes, name, knex) })
|
|
122
|
+
return { ...limited, fieldSelectionInfo }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export const buildRelationshipLinks = (scopes, scopeName, record, relationshipName) => {
|
|
126
|
+
const urlPrefix = scopes[scopeName]?.vars?.returnBasePath || scopes[scopeName]?.vars?.mountPath || ''
|
|
127
|
+
const recordId = getFieldValueForScope(scopes, scopeName, record, 'id')
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
self: `${urlPrefix}/${scopeName}/${recordId}/relationships/${relationshipName}`,
|
|
131
|
+
related: `${urlPrefix}/${scopeName}/${recordId}/${relationshipName}`
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const buildScopedIncludeContext = ({ requestContext, scopeName, scopeObject, storageAdapter }) => {
|
|
136
|
+
const queryParams = requestContext?.queryParams
|
|
137
|
+
? { ...requestContext.queryParams }
|
|
138
|
+
: {}
|
|
139
|
+
|
|
140
|
+
if ('filters' in queryParams) {
|
|
141
|
+
delete queryParams.filters
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
...(requestContext || {}),
|
|
146
|
+
scopeName,
|
|
147
|
+
schemaInfo: scopeObject.vars.schemaInfo,
|
|
148
|
+
storageAdapter,
|
|
149
|
+
queryParams
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export const applyScopeFiltersToIncludeQuery = async ({
|
|
154
|
+
query,
|
|
155
|
+
scopes,
|
|
156
|
+
scopeName,
|
|
157
|
+
requestContext,
|
|
158
|
+
db,
|
|
159
|
+
tableName,
|
|
160
|
+
queryPurpose = 'include'
|
|
161
|
+
}) => {
|
|
162
|
+
const scopeObject = scopes[scopeName]
|
|
163
|
+
if (!scopeObject?.applyQueryFilters) {
|
|
164
|
+
return { query }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const storageAdapter = resolveScopeStorageAdapter(scopes, scopeName, db)
|
|
168
|
+
|
|
169
|
+
const queryState = await scopeObject.applyQueryFilters({
|
|
170
|
+
query,
|
|
171
|
+
filters: undefined,
|
|
172
|
+
scopeName,
|
|
173
|
+
tableName: tableName || scopeObject.vars.schemaInfo.tableName,
|
|
174
|
+
db,
|
|
175
|
+
schemaInfo: scopeObject.vars.schemaInfo,
|
|
176
|
+
queryPurpose,
|
|
177
|
+
isAnyApi: storageAdapter?.isCanonical() || false,
|
|
178
|
+
storageAdapter
|
|
179
|
+
}, buildScopedIncludeContext({
|
|
180
|
+
requestContext,
|
|
181
|
+
scopeName,
|
|
182
|
+
scopeObject,
|
|
183
|
+
storageAdapter
|
|
184
|
+
}))
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
query: applyDatabaseReadOptions(unwrapQueryBuilderState(queryState, query))
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export const filterVisibleIdentifiers = async ({ identifiers, scopes, knex, context }) => {
|
|
192
|
+
const idsByType = new Map()
|
|
193
|
+
for (const { type, id } of identifiers) {
|
|
194
|
+
if (!idsByType.has(type)) idsByType.set(type, new Set())
|
|
195
|
+
idsByType.get(type).add(String(id))
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const visible = new Map()
|
|
199
|
+
for (const [type, ids] of idsByType) {
|
|
200
|
+
const adapter = Object.hasOwn(scopes, type) && resolveScopeStorageAdapter(scopes, type, knex)
|
|
201
|
+
if (!adapter) throw new RestApiResourceError(`Related resource '${type}' not found`, { subtype: 'related_type_not_found' })
|
|
202
|
+
const tableName = adapter.getTableName()
|
|
203
|
+
const idColumn = adapter.getIdColumn()
|
|
204
|
+
const column = `${tableName}.${idColumn}`
|
|
205
|
+
const visibleIds = new Set()
|
|
206
|
+
visible.set(type, visibleIds)
|
|
207
|
+
const loadBatch = async values => {
|
|
208
|
+
const query = adapter.buildBaseQuery({ transaction: context.transaction })
|
|
209
|
+
.whereIn(column, values)
|
|
210
|
+
.distinct({ [idColumn]: adapter.isCanonical() ? column : databaseIdentityExpression(knex, column) })
|
|
211
|
+
const state = await applyScopeFiltersToIncludeQuery({
|
|
212
|
+
query,
|
|
213
|
+
scopes,
|
|
214
|
+
scopeName: type,
|
|
215
|
+
requestContext: context,
|
|
216
|
+
db: context.db || knex,
|
|
217
|
+
tableName,
|
|
218
|
+
queryPurpose: 'relationship-identifiers'
|
|
219
|
+
})
|
|
220
|
+
for (const row of await state.query) visibleIds.add(String(row[idColumn]))
|
|
221
|
+
}
|
|
222
|
+
let values = []
|
|
223
|
+
for (const id of ids) {
|
|
224
|
+
values.push(adapter.translateFilterValue('id', id))
|
|
225
|
+
if (values.length === RELATIONSHIP_READ_BATCH_SIZE) {
|
|
226
|
+
await loadBatch(values)
|
|
227
|
+
values = []
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (values.length) await loadBatch(values)
|
|
231
|
+
}
|
|
232
|
+
return identifiers.filter(({ type, id }) => visible.get(type)?.has(String(id)))
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Groups records by their polymorphic type for efficient batch loading
|
|
237
|
+
*
|
|
238
|
+
* @param {Array<Object>} records - Records containing polymorphic fields
|
|
239
|
+
* @param {string} typeField - Name of the type field (e.g., 'commentable_type')
|
|
240
|
+
* @param {string} idField - Name of the ID field (e.g., 'commentable_id')
|
|
241
|
+
* @returns {Object<string, Array<number|string>>} Map of type to array of unique IDs
|
|
242
|
+
*/
|
|
243
|
+
export const groupByPolymorphicType = (records, typeField, idField, getFieldValue = (record, fieldName) => record[fieldName]) => {
|
|
244
|
+
const grouped = {}
|
|
245
|
+
const seenByType = Object.create(null)
|
|
246
|
+
|
|
247
|
+
records.forEach(record => {
|
|
248
|
+
const type = getFieldValue(record, typeField)
|
|
249
|
+
const id = getFieldValue(record, idField)
|
|
250
|
+
|
|
251
|
+
// Skip if either type or id is missing
|
|
252
|
+
if (!type || id === null || id === undefined) return
|
|
253
|
+
|
|
254
|
+
if (!Object.hasOwn(grouped, type)) {
|
|
255
|
+
Object.defineProperty(grouped, type, { value: [], enumerable: true, writable: true, configurable: true })
|
|
256
|
+
seenByType[type] = new Set()
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Only add unique IDs
|
|
260
|
+
if (!seenByType[type].has(id)) {
|
|
261
|
+
seenByType[type].add(id)
|
|
262
|
+
grouped[type].push(id)
|
|
263
|
+
}
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
return grouped
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Parses the include parameter string into a tree structure
|
|
271
|
+
*
|
|
272
|
+
* @param {string|Array<string>} includeParam - The include parameter value
|
|
273
|
+
* @returns {Object} Nested object representing the include tree
|
|
274
|
+
*/
|
|
275
|
+
export const parseIncludeTree = (includeParam) => {
|
|
276
|
+
if (!includeParam) return {}
|
|
277
|
+
|
|
278
|
+
// Handle array input (already split)
|
|
279
|
+
const includes = Array.isArray(includeParam)
|
|
280
|
+
? includeParam
|
|
281
|
+
: includeParam.split(',').map(s => s.trim()).filter(Boolean)
|
|
282
|
+
|
|
283
|
+
const tree = {}
|
|
284
|
+
|
|
285
|
+
includes.forEach(include => {
|
|
286
|
+
const parts = include.split('.')
|
|
287
|
+
let current = tree
|
|
288
|
+
|
|
289
|
+
parts.forEach(part => {
|
|
290
|
+
if (!Object.hasOwn(current, part)) {
|
|
291
|
+
Object.defineProperty(current, part, { value: {}, enumerable: true, writable: true, configurable: true })
|
|
292
|
+
}
|
|
293
|
+
current = current[part]
|
|
294
|
+
})
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
return tree
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Loads relationship metadata for included resources
|
|
302
|
+
* This ensures included resources have complete JSON:API representation
|
|
303
|
+
*
|
|
304
|
+
* @param {Object} scopes - The resources object
|
|
305
|
+
* @param {Array<Object>} records - Records to add relationships to
|
|
306
|
+
* @param {string} scopeName - The scope/resource type name
|
|
307
|
+
*/
|
|
308
|
+
export const loadRelationshipMetadata = async (scopes, records, scopeName) => {
|
|
309
|
+
try {
|
|
310
|
+
// Get schema information
|
|
311
|
+
const schemaInfo = scopes[scopeName]?.vars?.schemaInfo
|
|
312
|
+
if (!schemaInfo) throw new Error(`Missing relationship schema for scope '${scopeName}'`)
|
|
313
|
+
|
|
314
|
+
const schema = schemaInfo.schemaStructure
|
|
315
|
+
const relationships = schemaInfo.schemaRelationships || {}
|
|
316
|
+
|
|
317
|
+
// Process each record
|
|
318
|
+
records.forEach(record => {
|
|
319
|
+
record[RELATIONSHIP_METADATA_KEY] = {}
|
|
320
|
+
|
|
321
|
+
// Process belongsTo relationships from schema
|
|
322
|
+
for (const [fieldName, fieldDef] of Object.entries(schema)) {
|
|
323
|
+
if (fieldDef.belongsTo && fieldDef.as) {
|
|
324
|
+
const foreignKeyValue = getFieldValueForScope(scopes, scopeName, record, fieldName)
|
|
325
|
+
if (foreignKeyValue != null) {
|
|
326
|
+
record[RELATIONSHIP_METADATA_KEY][fieldDef.as] = {
|
|
327
|
+
data: {
|
|
328
|
+
type: fieldDef.belongsTo,
|
|
329
|
+
id: String(foreignKeyValue)
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
} else {
|
|
333
|
+
record[RELATIONSHIP_METADATA_KEY][fieldDef.as] = { data: null }
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Process hasMany, hasOne and manyToMany relationships
|
|
339
|
+
for (const [relName, relDef] of Object.entries(relationships)) {
|
|
340
|
+
if (relDef.type === 'hasMany' || relDef.type === 'manyToMany') {
|
|
341
|
+
// Add empty relationship data - will be populated if explicitly included
|
|
342
|
+
record[RELATIONSHIP_METADATA_KEY][relName] = { data: [] }
|
|
343
|
+
} else if (relDef.type === 'hasOne') {
|
|
344
|
+
// hasOne expects a single object or null
|
|
345
|
+
record[RELATIONSHIP_METADATA_KEY][relName] = { data: null }
|
|
346
|
+
} else if (relDef.belongsToPolymorphic) {
|
|
347
|
+
// Process polymorphic relationships defined in relationships section
|
|
348
|
+
const { typeField, idField, types } = relDef.belongsToPolymorphic
|
|
349
|
+
const type = getFieldValueForScope(scopes, scopeName, record, typeField)
|
|
350
|
+
const id = getFieldValueForScope(scopes, scopeName, record, idField)
|
|
351
|
+
|
|
352
|
+
record[RELATIONSHIP_METADATA_KEY][relName] = {
|
|
353
|
+
data: getPolymorphicLinkage({ type, id, types, scopeName, relationshipName: relName })
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
})
|
|
358
|
+
} catch (error) {
|
|
359
|
+
throw wrapUnexpectedError(error, {
|
|
360
|
+
message: `Failed to load relationship metadata for scope '${scopeName}'`,
|
|
361
|
+
context: { phase: 'relationshipMetadata', scopeName, recordCount: records?.length || 0 }
|
|
362
|
+
})
|
|
363
|
+
}
|
|
364
|
+
}
|