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,261 +1,70 @@
|
|
|
1
1
|
import { RestApiResourceError } from '../../../../lib/rest-api-errors.js'
|
|
2
2
|
import { ROW_NUMBER_KEY, DEFAULT_QUERY_LIMIT, DEFAULT_MAX_QUERY_LIMIT, DEFAULT_MAX_INCLUDE_LIMIT } from '../querying-writing/knex-constants.js'
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
// Apply defaults for limit
|
|
32
|
-
const effectiveLimit = includeConfig.limit ?? scopeVars.queryDefaultLimit ?? DEFAULT_QUERY_LIMIT
|
|
33
|
-
|
|
34
|
-
// Validate against max
|
|
35
|
-
if (scopeVars.queryMaxLimit && effectiveLimit > scopeVars.queryMaxLimit) {
|
|
36
|
-
throw new RestApiResourceError({
|
|
37
|
-
title: 'Include Limit Exceeds Maximum',
|
|
38
|
-
detail: `Requested include limit (${effectiveLimit}) exceeds queryMaxLimit (${scopeVars.queryMaxLimit})`,
|
|
39
|
-
status: 400
|
|
40
|
-
})
|
|
3
|
+
import { prepareReferenceSortColumns } from './knex-query-helpers.js'
|
|
4
|
+
import { buildEffectiveSortList, parseSortEntry, resolveSortField } from './query-field-sort-helpers.js'
|
|
5
|
+
import { applyDatabaseReadOptions, databaseIdentityExpression } from '../querying-writing/database-value-normalizers.js'
|
|
6
|
+
|
|
7
|
+
// Apply ordering and limits after target/pivot visibility has constrained the query.
|
|
8
|
+
export async function applyIncludeQueryConfig ({ query, scopeName, tableName, parentColumn, includeConfig = {}, context = {}, capabilities, queryFieldRuntimeByField = new Map() }, dependencies) {
|
|
9
|
+
const { scopes, knex, getStorageAdapter } = dependencies
|
|
10
|
+
const db = context.db || context.transaction || knex
|
|
11
|
+
const vars = scopes[scopeName].vars
|
|
12
|
+
const storageAdapter = getStorageAdapter(scopeName)
|
|
13
|
+
const fields = buildEffectiveSortList(includeConfig.orderBy, { defaultSort: vars.defaultSort, schemaInfo: vars.schemaInfo }).map(parseSortEntry)
|
|
14
|
+
const references = await prepareReferenceSortColumns({
|
|
15
|
+
query,
|
|
16
|
+
fields: fields.map(({ field }) => field).filter(field => !queryFieldRuntimeByField.has(field)),
|
|
17
|
+
scopeName,
|
|
18
|
+
tableAlias: tableName,
|
|
19
|
+
context
|
|
20
|
+
}, dependencies)
|
|
21
|
+
const names = new Set([
|
|
22
|
+
...Object.keys(vars.schemaInfo?.queryFields || {}),
|
|
23
|
+
...Object.keys(vars.schemaInfo.schemaStructure).flatMap(field => [field, storageAdapter.translateColumn(field)]),
|
|
24
|
+
...[...references.values()].map(reference => reference.resultColumn)
|
|
25
|
+
])
|
|
26
|
+
const uniqueName = base => {
|
|
27
|
+
let name = base
|
|
28
|
+
while (names.has(name)) name += '_'
|
|
29
|
+
names.add(name)
|
|
30
|
+
return name
|
|
41
31
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
database: dbInfo.client,
|
|
52
|
-
version: dbInfo.version,
|
|
53
|
-
requiredFeature: 'window_functions'
|
|
32
|
+
const parentResultColumn = uniqueName('__jra_include_parent')
|
|
33
|
+
const rowNumberColumn = uniqueName(ROW_NUMBER_KEY)
|
|
34
|
+
query.select({ [parentResultColumn]: typeof parentColumn === 'string' ? databaseIdentityExpression(db, parentColumn) : parentColumn })
|
|
35
|
+
const ordering = fields.map(({ field, sqlDirection }) => {
|
|
36
|
+
const runtime = queryFieldRuntimeByField.get(field)
|
|
37
|
+
if (runtime) {
|
|
38
|
+
return {
|
|
39
|
+
sql: `(${runtime.sql}) IS NULL ASC, (${runtime.sql}) ${sqlDirection}`,
|
|
40
|
+
bindings: [...runtime.bindings, ...runtime.bindings]
|
|
54
41
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
// This creates a subquery that partitions by the foreign key and numbers rows
|
|
60
|
-
const subquery = knex(tableName)
|
|
61
|
-
.select('*')
|
|
62
|
-
.select(
|
|
63
|
-
knex.raw(
|
|
64
|
-
'ROW_NUMBER() OVER (PARTITION BY ?? ORDER BY ' +
|
|
65
|
-
buildOrderByClause(orderBy) +
|
|
66
|
-
') as ' + ROW_NUMBER_KEY,
|
|
67
|
-
[foreignKey]
|
|
68
|
-
)
|
|
69
|
-
)
|
|
70
|
-
.whereIn(foreignKey, parentIds)
|
|
71
|
-
|
|
72
|
-
// Apply field selection if specified
|
|
73
|
-
if (fieldsToSelect !== '*' && Array.isArray(fieldsToSelect)) {
|
|
74
|
-
// Include the foreign key and row number in selection
|
|
75
|
-
const fieldsWithFK = [...new Set([...fieldsToSelect, foreignKey, ROW_NUMBER_KEY])]
|
|
76
|
-
subquery.select(fieldsWithFK)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return { subquery, effectiveLimit }
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Builds ORDER BY clause from array of sort fields
|
|
84
|
-
*
|
|
85
|
-
* @param {Array<string>} orderBy - Array of field names, prefix with '-' for DESC
|
|
86
|
-
* @param {string} tablePrefix - Optional table name to prefix fields
|
|
87
|
-
* @returns {string} SQL ORDER BY clause
|
|
88
|
-
*
|
|
89
|
-
* @example
|
|
90
|
-
* // Input: Simple ascending sort
|
|
91
|
-
* const clause = buildOrderByClause(['name', 'created_at']);
|
|
92
|
-
* // Output: "name ASC, created_at ASC"
|
|
93
|
-
*
|
|
94
|
-
* @example
|
|
95
|
-
* // Input: Mixed ascending/descending with '-' prefix
|
|
96
|
-
* const clause = buildOrderByClause(['name', '-created_at', 'status']);
|
|
97
|
-
* // Output: "name ASC, created_at DESC, status ASC"
|
|
98
|
-
*
|
|
99
|
-
* @example
|
|
100
|
-
* // Input: With table prefix for joins
|
|
101
|
-
* const clause = buildOrderByClause(['-updated_at', 'title'], 'articles');
|
|
102
|
-
* // Output: "articles.updated_at DESC, articles.title ASC"
|
|
103
|
-
*
|
|
104
|
-
* @example
|
|
105
|
-
* // Input: Empty array (default to id)
|
|
106
|
-
* const clause = buildOrderByClause([]);
|
|
107
|
-
* // Output: "id ASC"
|
|
108
|
-
*
|
|
109
|
-
* @description
|
|
110
|
-
* Used by:
|
|
111
|
-
* - buildWindowedIncludeSubquery for PARTITION BY ordering
|
|
112
|
-
* - applyStandardIncludeConfig for regular ORDER BY
|
|
113
|
-
* - Query builders that need consistent sort syntax
|
|
114
|
-
*
|
|
115
|
-
* Purpose:
|
|
116
|
-
* - Converts API sort syntax (with '-' prefix) to SQL ORDER BY
|
|
117
|
-
* - Handles table prefixing for queries with joins
|
|
118
|
-
* - Provides default ordering by id when none specified
|
|
119
|
-
* - Ensures consistent sort behavior across the API
|
|
120
|
-
*
|
|
121
|
-
* Data flow:
|
|
122
|
-
* 1. Receives array of sort fields from API parameters
|
|
123
|
-
* 2. Detects DESC sorts by '-' prefix
|
|
124
|
-
* 3. Optionally prefixes with table name
|
|
125
|
-
* 4. Joins into SQL-compatible ORDER BY clause
|
|
126
|
-
*/
|
|
127
|
-
export const buildOrderByClause = (orderBy, tablePrefix) => {
|
|
128
|
-
if (!orderBy || orderBy.length === 0) {
|
|
129
|
-
const defaultField = tablePrefix ? `${tablePrefix}.id` : 'id'
|
|
130
|
-
return `${defaultField} ASC` // Default ordering
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return orderBy.map(field => {
|
|
134
|
-
const isDesc = field.startsWith('-')
|
|
135
|
-
const fieldName = isDesc ? field.substring(1) : field
|
|
136
|
-
const qualifiedField = tablePrefix ? `${tablePrefix}.${fieldName}` : fieldName
|
|
137
|
-
return `${qualifiedField} ${isDesc ? 'DESC' : 'ASC'}`
|
|
138
|
-
}).join(', ')
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Applies standard (non-windowed) include configuration to a query
|
|
143
|
-
*
|
|
144
|
-
* @param {Object} query - Knex query builder instance
|
|
145
|
-
* @param {Object} includeConfig - Include configuration object
|
|
146
|
-
* @param {Object} scopeVars - Scope variables with defaults and limits
|
|
147
|
-
* @param {Object} log - Logger instance
|
|
148
|
-
* @returns {Object} Modified query builder
|
|
149
|
-
*
|
|
150
|
-
* @example
|
|
151
|
-
* // Input: Basic include with limit and ordering
|
|
152
|
-
* let query = knex('comments').whereIn('article_id', [1, 2, 3]);
|
|
153
|
-
* query = applyStandardIncludeConfig(
|
|
154
|
-
* query,
|
|
155
|
-
* { limit: 20, orderBy: ['-created_at', 'id'] },
|
|
156
|
-
* { queryDefaultLimit: 100, queryMaxLimit: 1000 },
|
|
157
|
-
* logger
|
|
158
|
-
* );
|
|
159
|
-
*
|
|
160
|
-
* // Result: Query modified with:
|
|
161
|
-
* // ORDER BY created_at DESC, id ASC
|
|
162
|
-
* // LIMIT 20
|
|
163
|
-
*
|
|
164
|
-
* @example
|
|
165
|
-
* // Input: No explicit limit, uses defaults
|
|
166
|
-
* let query = knex('tags');
|
|
167
|
-
* query = applyStandardIncludeConfig(
|
|
168
|
-
* query,
|
|
169
|
-
* { orderBy: ['name'] }, // No limit specified
|
|
170
|
-
* { queryDefaultLimit: 50, queryMaxLimit: 500 },
|
|
171
|
-
* logger
|
|
172
|
-
* );
|
|
173
|
-
*
|
|
174
|
-
* // Result: Uses default limit
|
|
175
|
-
* // ORDER BY name ASC
|
|
176
|
-
* // LIMIT 50
|
|
177
|
-
* // Log: "Using default limit"
|
|
178
|
-
*
|
|
179
|
-
* @example
|
|
180
|
-
* // Input: Limit exceeds maximum
|
|
181
|
-
* let query = knex('reviews');
|
|
182
|
-
* query = applyStandardIncludeConfig(
|
|
183
|
-
* query,
|
|
184
|
-
* { limit: 5000 }, // Exceeds max
|
|
185
|
-
* { queryMaxLimit: 1000, maxIncludeLimit: 500 },
|
|
186
|
-
* logger
|
|
187
|
-
* );
|
|
188
|
-
*
|
|
189
|
-
* // Result: Clamped to effective maximum
|
|
190
|
-
* // LIMIT 500 (min of maxIncludeLimit and queryMaxLimit)
|
|
191
|
-
*
|
|
192
|
-
* @example
|
|
193
|
-
* // Input: Explicitly disable limit
|
|
194
|
-
* let query = knex('categories');
|
|
195
|
-
* query = applyStandardIncludeConfig(
|
|
196
|
-
* query,
|
|
197
|
-
* { limit: null }, // Explicitly no limit
|
|
198
|
-
* { queryDefaultLimit: 100 },
|
|
199
|
-
* logger
|
|
200
|
-
* );
|
|
201
|
-
*
|
|
202
|
-
* // Result: No LIMIT clause added
|
|
203
|
-
* // Log: "No limit applied to include query (explicitly disabled)"
|
|
204
|
-
*
|
|
205
|
-
* @description
|
|
206
|
-
* Used by:
|
|
207
|
-
* - loadHasMany when window functions not available or not requested
|
|
208
|
-
* - loadReversePolymorphic for standard relationship queries
|
|
209
|
-
* - Any include loader that needs consistent limit/order handling
|
|
210
|
-
*
|
|
211
|
-
* Purpose:
|
|
212
|
-
* - Provides fallback when window functions unavailable
|
|
213
|
-
* - Applies consistent ordering based on API sort syntax
|
|
214
|
-
* - Enforces limit hierarchy: explicit > default > max
|
|
215
|
-
* - Respects both queryMaxLimit and maxIncludeLimit
|
|
216
|
-
* - Logs decisions for debugging
|
|
217
|
-
*
|
|
218
|
-
* Data flow:
|
|
219
|
-
* 1. Applies ORDER BY for each field in orderBy array
|
|
220
|
-
* 2. Determines effective limit from explicit/default/max values
|
|
221
|
-
* 3. Clamps limit to maximum allowed values
|
|
222
|
-
* 4. Adds LIMIT clause unless explicitly disabled
|
|
223
|
-
* 5. Logs the reasoning for the applied limit
|
|
224
|
-
*/
|
|
225
|
-
export const applyStandardIncludeConfig = (query, includeConfig, scopeVars, log) => {
|
|
226
|
-
const { orderBy = [] } = includeConfig
|
|
227
|
-
|
|
228
|
-
// Apply ordering
|
|
229
|
-
orderBy.forEach(field => {
|
|
230
|
-
const desc = field.startsWith('-')
|
|
231
|
-
const column = desc ? field.substring(1) : field
|
|
232
|
-
query = query.orderBy(column, desc ? 'desc' : 'asc')
|
|
42
|
+
}
|
|
43
|
+
const actualField = resolveSortField(field, vars.schemaInfo)
|
|
44
|
+
const column = references.get(field)?.column || `${tableName}.${storageAdapter.translateColumn(actualField)}`
|
|
45
|
+
return { sql: `?? IS NULL ASC, ?? ${sqlDirection}`, bindings: [column, column] }
|
|
233
46
|
})
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
if (limit !== null && limit
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
log.debug('Applied include limit:', {
|
|
250
|
-
requested: requestedLimit,
|
|
251
|
-
default: defaultLimit,
|
|
252
|
-
effective: effectiveLimit,
|
|
253
|
-
maxAllowed: effectiveMax,
|
|
254
|
-
note: requestedLimit !== undefined ? 'Using explicit limit' : 'Using default limit'
|
|
255
|
-
})
|
|
47
|
+
const orderSql = ordering.map(order => order.sql).join(', ')
|
|
48
|
+
const orderBindings = ordering.flatMap(order => order.bindings)
|
|
49
|
+
const requested = includeConfig.limit
|
|
50
|
+
const limit = requested === null || requested === false
|
|
51
|
+
? null
|
|
52
|
+
: Math.min(requested ?? vars.queryDefaultLimit ?? DEFAULT_QUERY_LIMIT,
|
|
53
|
+
vars.queryMaxLimit ?? DEFAULT_MAX_QUERY_LIMIT, vars.maxIncludeLimit ?? DEFAULT_MAX_INCLUDE_LIMIT)
|
|
54
|
+
if (limit !== null && (!Number.isInteger(limit) || limit < 0)) throw new Error(`Invalid include limit for ${scopeName}: expected a non-negative integer or null`)
|
|
55
|
+
|
|
56
|
+
if (includeConfig.strategy === 'window' && limit !== null) {
|
|
57
|
+
if (capabilities?.windowFunctions === false) throw new RestApiResourceError('Per-parent include limits require window function support', { subtype: 'unsupported_operation' })
|
|
58
|
+
query.select(db.raw(`ROW_NUMBER() OVER (PARTITION BY ?? ORDER BY ${orderSql}) as ??`, [parentColumn, ...orderBindings, rowNumberColumn]))
|
|
59
|
+
query = db.select('*').from(query.as('_windowed')).where(rowNumberColumn, '<=', limit)
|
|
60
|
+
.orderBy(parentResultColumn).orderBy(rowNumberColumn)
|
|
256
61
|
} else {
|
|
257
|
-
|
|
62
|
+
query.orderByRaw(orderSql, orderBindings)
|
|
63
|
+
if (limit !== null) query.limit(limit)
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
query: applyDatabaseReadOptions(query),
|
|
67
|
+
parentColumn: parentResultColumn,
|
|
68
|
+
temporaryFields: [parentResultColumn, rowNumberColumn, ...[...references.values()].map(reference => reference.resultColumn)]
|
|
258
69
|
}
|
|
259
|
-
|
|
260
|
-
return query
|
|
261
70
|
}
|
|
@@ -1,4 +1,24 @@
|
|
|
1
|
-
|
|
1
|
+
// @ts-check
|
|
2
|
+
/** @import { StorageQuery, QueryFilteringState } from '../storage/storage-types.js' */
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @overload
|
|
6
|
+
* @param {import('../storage/storage-types.js').QueryBuilderResult | null | undefined} value
|
|
7
|
+
* @param {StorageQuery} fallback
|
|
8
|
+
* @returns {StorageQuery}
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* @overload
|
|
12
|
+
* @param {unknown} value
|
|
13
|
+
* @param {unknown} [fallback]
|
|
14
|
+
* @returns {unknown}
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* @param {unknown} value
|
|
18
|
+
* @param {unknown} [fallback]
|
|
19
|
+
* @returns {unknown} Hook values are untrusted until the caller validates them.
|
|
20
|
+
*/
|
|
21
|
+
export function unwrapQueryBuilderState (value, fallback = null) {
|
|
2
22
|
let current = value
|
|
3
23
|
const seen = new Set()
|
|
4
24
|
|
|
@@ -7,13 +27,33 @@ export const unwrapQueryBuilderState = (value, fallback = null) => {
|
|
|
7
27
|
typeof current === 'object' &&
|
|
8
28
|
!Array.isArray(current) &&
|
|
9
29
|
Object.hasOwn(current, 'query') &&
|
|
10
|
-
current.query &&
|
|
11
|
-
current.query !== current &&
|
|
30
|
+
/** @type {{ query?: unknown }} */ (current).query &&
|
|
31
|
+
/** @type {{ query?: unknown }} */ (current).query !== current &&
|
|
12
32
|
!seen.has(current)
|
|
13
33
|
) {
|
|
14
34
|
seen.add(current)
|
|
15
|
-
current = current.query
|
|
35
|
+
current = /** @type {{ query?: unknown }} */ (current).query
|
|
16
36
|
}
|
|
17
37
|
|
|
18
38
|
return current || fallback
|
|
19
39
|
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {{ knexQuery?: QueryFilteringState | null }} context
|
|
43
|
+
* @param {QueryFilteringState & { query: StorageQuery }} state
|
|
44
|
+
* @param {() => unknown | Promise<unknown>} applyFilters
|
|
45
|
+
* @returns {Promise<{ query: StorageQuery }>}
|
|
46
|
+
*/
|
|
47
|
+
// Return a wrapper: resolving an async function with a Knex builder executes it.
|
|
48
|
+
export async function withQueryFilteringContext (context, state, applyFilters) {
|
|
49
|
+
const query = state.query
|
|
50
|
+
const previousKnexQuery = context.knexQuery
|
|
51
|
+
context.knexQuery = state
|
|
52
|
+
try {
|
|
53
|
+
await applyFilters()
|
|
54
|
+
return { query: context.knexQuery?.query || query }
|
|
55
|
+
} finally {
|
|
56
|
+
if (previousKnexQuery === undefined) delete context.knexQuery
|
|
57
|
+
else context.knexQuery = previousKnexQuery
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { MandatoryQueryConstraint, StorageAdapter, StorageQuery } from '../storage/storage-types.js'
|
|
2
|
+
|
|
3
|
+
export type { MandatoryQueryConstraint } from '../storage/storage-types.js'
|
|
4
|
+
export declare const queryConstraint: unique symbol
|
|
5
|
+
|
|
6
|
+
export declare function applyQueryConstraint(options: {
|
|
7
|
+
query: StorageQuery
|
|
8
|
+
context: { [queryConstraint]?: MandatoryQueryConstraint }
|
|
9
|
+
scopeName: string
|
|
10
|
+
storageAdapter: StorageAdapter
|
|
11
|
+
tableName: string
|
|
12
|
+
}): void
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/** @import { MandatoryQueryConstraint, StorageAdapter, StorageQuery } from '../storage/storage-types.js' */
|
|
3
|
+
|
|
4
|
+
// Mandatory selection is independent of public search fields and caller filters.
|
|
5
|
+
export const queryConstraint = Symbol('queryConstraint')
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {object} options
|
|
9
|
+
* @param {StorageQuery} options.query
|
|
10
|
+
* @param {{ [queryConstraint]?: MandatoryQueryConstraint }} options.context
|
|
11
|
+
* @param {string} options.scopeName
|
|
12
|
+
* @param {StorageAdapter} options.storageAdapter
|
|
13
|
+
* @param {string} options.tableName
|
|
14
|
+
*/
|
|
15
|
+
export function applyQueryConstraint ({ query, context, scopeName, storageAdapter, tableName }) {
|
|
16
|
+
const constraint = context[queryConstraint]
|
|
17
|
+
if (!constraint || constraint.scopeName !== scopeName) return
|
|
18
|
+
|
|
19
|
+
if (constraint.idsQuery) {
|
|
20
|
+
query.whereIn(`${tableName}.${storageAdapter.getIdColumn()}`, constraint.idsQuery)
|
|
21
|
+
}
|
|
22
|
+
for (const [field, value] of Object.entries(constraint.values || {})) {
|
|
23
|
+
query.where({
|
|
24
|
+
[`${tableName}.${storageAdapter.translateColumn(field)}`]: storageAdapter.translateFilterValue(field, value)
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
}
|