json-rest-api 1.0.29 → 2.1.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 +733 -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 +103 -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 +3838 -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 +37 -0
- package/index.js +3 -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 +70 -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 +586 -418
- 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 +120 -56
- package/plugins/core/lib/anyapi/stored-definition.js +109 -0
- 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 +290 -2475
- 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/anyapi.d.ts +30 -0
- 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 +65 -0
- package/types/relationship-methods.d.ts +74 -0
- package/types/representations.d.ts +50 -0
- package/types/resource-methods.d.ts +95 -0
- package/types/resource-schema.d.ts +157 -0
- package/types/row-policy.d.ts +31 -0
- package/types/runtime.d.ts +132 -0
- package/types/transactions.d.ts +21 -0
- package/.claude/agents/library-guide-expert.md +0 -30
- package/.claude/agents/rest-api-internals-expert.md +0 -54
- package/.claude/settings.local.json +0 -70
- package/AGENTS.md +0 -34
- package/CLAUDE.md +0 -160
- package/CRUSH.md +0 -55
- package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
- package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
- package/TODO.todo +0 -13
- package/connectors_ideas/fastify.js +0 -217
- package/connectors_ideas/koa-plugin.js +0 -112
- package/docs/.bundle/config +0 -2
- package/docs/COMPARISON.md +0 -433
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
- package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
- package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
- package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
- package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
- package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
- package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
- package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
- package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
- package/docs/Gemfile +0 -6
- package/docs/Gemfile.lock +0 -308
- package/docs/ONBOARDING.md +0 -315
- package/docs/_config.yml +0 -36
- package/docs/_layouts/default.html +0 -123
- package/docs/apple-touch-icon.svg +0 -4
- package/docs/assets/css/style.scss +0 -392
- package/docs/favicon.svg +0 -4
- package/docs/index.md +0 -164
- package/docs/mini-guides/file-uploads-complete.md +0 -161
- package/eslint.config.js +0 -13
- package/examples/url-override-example.js +0 -137
- package/fixing_plan.md +0 -112
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
- package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
- package/plugins/storage/s3-storage.js +0 -93
- package/quickTest.js +0 -136
- package/scripts/debug-anyapi-review-filter.mjs +0 -63
- package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
- package/scripts/inspect-anyapi-descriptor.mjs +0 -19
- package/scripts/run-docs.js +0 -133
- package/test-debug.js +0 -59
- package/tests/README.md +0 -202
- package/tests/TEST_TEMPLATE.test.js +0 -178
- package/tests/anyapi-basic.test.js +0 -105
- package/tests/anyapi-cursor-pagination.test.js +0 -148
- package/tests/anyapi-custom-idproperty.test.js +0 -163
- package/tests/autofilter.test.js +0 -463
- package/tests/bulk-operations.test.js +0 -657
- package/tests/computed-fields.test.js +0 -586
- package/tests/cors.test.js +0 -834
- package/tests/curl.test.js +0 -467
- package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
- package/tests/custom-id-simplified-post.test.js +0 -75
- package/tests/custom-idproperty-relationships.test.js +0 -932
- package/tests/db-introspection.test.js +0 -521
- package/tests/db-table-operations.test.js +0 -798
- package/tests/fastify-plugin.test.js +0 -184
- package/tests/field-getters.test.js +0 -370
- package/tests/field-setters.test.js +0 -306
- package/tests/file-handling.test.js +0 -155
- package/tests/fixtures/api-configs.js +0 -2566
- package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
- package/tests/helpers/fake-fastify.js +0 -99
- package/tests/helpers/storage-mode.js +0 -65
- package/tests/helpers/test-utils.js +0 -203
- package/tests/http-connectors-parity.test.js +0 -395
- package/tests/id-normalization.test.js +0 -444
- package/tests/include-depth-validation.test.js +0 -331
- package/tests/includes.test.js +0 -427
- package/tests/local-storage.test.js +0 -96
- package/tests/logical-id-write-validation.test.js +0 -151
- package/tests/nested-includes.test.js +0 -617
- package/tests/pagination-boundaries.test.js +0 -154
- package/tests/pagination-contract.test.js +0 -122
- package/tests/pagination-cursor-multifield.test.js +0 -641
- package/tests/pagination-enhanced.test.js +0 -563
- package/tests/polymorphic-relationships.test.js +0 -347
- package/tests/polymorphic-simple.test.js +0 -148
- package/tests/positioning-advanced.test.js +0 -692
- package/tests/positioning-extended.test.js +0 -915
- package/tests/positioning-multifilter.test.js +0 -699
- package/tests/positioning.test.js +0 -560
- package/tests/post-commit-hooks.test.js +0 -120
- package/tests/projected-fields.test.js +0 -319
- package/tests/public-exports.test.js +0 -15
- package/tests/put-create-parity.test.js +0 -216
- package/tests/queries.test.js +0 -521
- package/tests/query-limits.test.js +0 -584
- package/tests/relationship-endpoints.test.js +0 -620
- package/tests/relationship-include-storage-adapter.test.js +0 -56
- package/tests/relationships.test.js +0 -681
- package/tests/rest-api.test.js +0 -337
- package/tests/return-record-settings.test.js +0 -184
- package/tests/row-policy.test.js +0 -489
- package/tests/s3-storage.test.js +0 -38
- package/tests/searchschema-knex-behavior.test.js +0 -187
- package/tests/searchschema-merge.test.js +0 -151
- package/tests/socketio.test.js +0 -860
- package/tests/storage-mapping.test.js +0 -362
- package/tests/temporal-boundaries.test.js +0 -208
- package/tests/test-development-guide.md +0 -370
- package/tests/virtual-fields.test.js +0 -404
|
@@ -1,10 +1,48 @@
|
|
|
1
1
|
import { getFieldValue } from '../storage/storage-mapping.js'
|
|
2
|
-
import {
|
|
2
|
+
import { buildJsonApiLink } from './url-helpers.js'
|
|
3
3
|
import { normalizeDateValue } from '../querying-writing/database-value-normalizers.js'
|
|
4
|
+
import { DEFAULT_QUERY_LIMIT, DEFAULT_MAX_QUERY_LIMIT } from '../querying-writing/knex-constants.js'
|
|
5
|
+
import { applyCursorPredicate, validateCursorValues } from './query-field-sort-helpers.js'
|
|
6
|
+
import { RestApiValidationError } from '../../../../lib/rest-api-errors.js'
|
|
4
7
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
+
// Page parameters have passed the shared request contract. Physical sort
|
|
9
|
+
// columns and built-in value conversion remain with descriptors and adapters.
|
|
10
|
+
export const applyPaginationToQuery = ({ query, page = {}, vars, sortDescriptors, storageAdapter }) => {
|
|
11
|
+
const pageSize = Math.min(page.size ?? (vars.queryDefaultLimit || DEFAULT_QUERY_LIMIT), vars.queryMaxLimit || DEFAULT_MAX_QUERY_LIMIT)
|
|
12
|
+
if (!Number.isInteger(pageSize) || pageSize <= 0) {
|
|
13
|
+
throw new RestApiValidationError('Page size must be a positive integer', {
|
|
14
|
+
fields: ['page.size'],
|
|
15
|
+
violations: [{ field: 'page.size', rule: 'min_value', message: 'Page size must be a positive integer' }]
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
const parameter = page.before !== undefined ? 'before' : page.after !== undefined ? 'after' : null
|
|
19
|
+
const mode = page.number !== undefined ? 'offset' : parameter || page.size !== undefined ? 'cursor' : 'default'
|
|
20
|
+
const before = parameter === 'before'
|
|
21
|
+
const pageNumber = page.number ?? 1
|
|
22
|
+
|
|
23
|
+
if (parameter) {
|
|
24
|
+
let values
|
|
25
|
+
try {
|
|
26
|
+
values = parseCursor(page[parameter])
|
|
27
|
+
} catch (error) {
|
|
28
|
+
const field = `page.${parameter}`
|
|
29
|
+
throw new RestApiValidationError(`Invalid cursor format in page[${parameter}] parameter`, {
|
|
30
|
+
fields: [field],
|
|
31
|
+
violations: [{ field, rule: 'invalid_cursor', message: error.message }]
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
values = validateCursorValues(sortDescriptors, values, parameter, vars.schemaInfo.schemaInstance)
|
|
35
|
+
applyCursorPredicate(query, sortDescriptors, values, direction => {
|
|
36
|
+
const descending = direction.toUpperCase() === 'DESC'
|
|
37
|
+
return before ? (descending ? '>' : '<') : (descending ? '<' : '>')
|
|
38
|
+
}, (builder, descriptor, operator, value) => builder.where(
|
|
39
|
+
descriptor.column, operator, storageAdapter.translateCursorValue(descriptor.actualField, value)
|
|
40
|
+
))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
query.limit(pageSize + (mode === 'cursor' ? 1 : 0))
|
|
44
|
+
if (mode === 'offset') query.offset((pageNumber - 1) * pageSize)
|
|
45
|
+
return { mode, page: pageNumber, pageSize, before }
|
|
8
46
|
}
|
|
9
47
|
|
|
10
48
|
/**
|
|
@@ -15,47 +53,6 @@ const buildJsonApiLink = (baseUrl, queryParams, page) => {
|
|
|
15
53
|
* @param {number} pageSize - Number of records per page
|
|
16
54
|
* @returns {Object} Pagination metadata including page count and hasMore flag
|
|
17
55
|
* @throws {Error} If pageSize <= 0 or page < 1
|
|
18
|
-
*
|
|
19
|
-
* @example
|
|
20
|
-
* // Input: 100 total records, viewing page 2 of 20 records each
|
|
21
|
-
* const meta = calculatePaginationMeta(100, 2, 20);
|
|
22
|
-
* // Output:
|
|
23
|
-
* // {
|
|
24
|
-
* // page: 2, // current page
|
|
25
|
-
* // pageSize: 20, // records per page
|
|
26
|
-
* // pageCount: 5, // total pages (100/20)
|
|
27
|
-
* // total: 100, // total records
|
|
28
|
-
* // hasMore: true // page 2 < 5, so more pages exist
|
|
29
|
-
* // }
|
|
30
|
-
*
|
|
31
|
-
* @example
|
|
32
|
-
* // Input: Last page scenario
|
|
33
|
-
* const meta = calculatePaginationMeta(45, 5, 10);
|
|
34
|
-
* // Output:
|
|
35
|
-
* // {
|
|
36
|
-
* // page: 5,
|
|
37
|
-
* // pageSize: 10,
|
|
38
|
-
* // pageCount: 5, // 45/10 = 4.5, rounds up to 5
|
|
39
|
-
* // total: 45,
|
|
40
|
-
* // hasMore: false // on last page
|
|
41
|
-
* // }
|
|
42
|
-
*
|
|
43
|
-
* @description
|
|
44
|
-
* Used by:
|
|
45
|
-
* - rest-api-knex-plugin's dataQuery method after counting total records
|
|
46
|
-
* - generatePaginationLinks to build navigation links
|
|
47
|
-
* - Applied when offset-based pagination is enabled
|
|
48
|
-
*
|
|
49
|
-
* Purpose:
|
|
50
|
-
* - Provides consistent pagination metadata across all collection responses
|
|
51
|
-
* - Calculates derived values like pageCount and hasMore flag
|
|
52
|
-
* - Validates pagination parameters to prevent invalid states
|
|
53
|
-
*
|
|
54
|
-
* Data flow:
|
|
55
|
-
* 1. Query method counts total records when pagination.counts is enabled
|
|
56
|
-
* 2. calculatePaginationMeta processes the count with current page/size
|
|
57
|
-
* 3. Returns metadata that goes into response.meta.pagination
|
|
58
|
-
* 4. Used by generatePaginationLinks to determine which links to include
|
|
59
56
|
*/
|
|
60
57
|
export const calculatePaginationMeta = (total, page, pageSize) => {
|
|
61
58
|
if (pageSize <= 0) {
|
|
@@ -85,65 +82,7 @@ export const calculatePaginationMeta = (total, page, pageSize) => {
|
|
|
85
82
|
* @param {string} scopeName - Resource type name
|
|
86
83
|
* @param {Object} queryParams - Current query parameters
|
|
87
84
|
* @param {Object} paginationMeta - Pagination metadata from calculatePaginationMeta
|
|
88
|
-
* @returns {
|
|
89
|
-
*
|
|
90
|
-
* @example
|
|
91
|
-
* // Input data:
|
|
92
|
-
* const urlPrefix = '/api/v1';
|
|
93
|
-
* const scopeName = 'articles';
|
|
94
|
-
* const queryParams = {
|
|
95
|
-
* filter: { status: 'published' },
|
|
96
|
-
* sort: ['-created_at'],
|
|
97
|
-
* page: { number: 2, size: 20 }
|
|
98
|
-
* };
|
|
99
|
-
* const paginationMeta = {
|
|
100
|
-
* page: 2,
|
|
101
|
-
* pageSize: 20,
|
|
102
|
-
* pageCount: 5,
|
|
103
|
-
* total: 100,
|
|
104
|
-
* hasMore: true
|
|
105
|
-
* };
|
|
106
|
-
*
|
|
107
|
-
* const links = generatePaginationLinks(urlPrefix, scopeName, queryParams, paginationMeta);
|
|
108
|
-
*
|
|
109
|
-
* // Output - complete URLs with all parameters preserved:
|
|
110
|
-
* // {
|
|
111
|
-
* // self: '/api/articles?filter[status]=published&sort=-created_at&page[number]=2&page[size]=20',
|
|
112
|
-
* // first: '/api/articles?filter[status]=published&sort=-created_at&page[number]=1&page[size]=20',
|
|
113
|
-
* // last: '/api/articles?filter[status]=published&sort=-created_at&page[number]=5&page[size]=20',
|
|
114
|
-
* // prev: '/api/articles?filter[status]=published&sort=-created_at&page[number]=1&page[size]=20',
|
|
115
|
-
* // next: '/api/articles?filter[status]=published&sort=-created_at&page[number]=3&page[size]=20'
|
|
116
|
-
* // }
|
|
117
|
-
*
|
|
118
|
-
* @example
|
|
119
|
-
* // Input: First page (no prev link)
|
|
120
|
-
* const paginationMeta = { page: 1, pageSize: 10, pageCount: 3 };
|
|
121
|
-
* const links = generatePaginationLinks('/api', 'users', {}, paginationMeta);
|
|
122
|
-
*
|
|
123
|
-
* // Output - notice no 'prev' property:
|
|
124
|
-
* // {
|
|
125
|
-
* // self: '/api/users?page[number]=1&page[size]=10',
|
|
126
|
-
* // first: '/api/users?page[number]=1&page[size]=10',
|
|
127
|
-
* // last: '/api/users?page[number]=3&page[size]=10',
|
|
128
|
-
* // next: '/api/users?page[number]=2&page[size]=10'
|
|
129
|
-
* // }
|
|
130
|
-
*
|
|
131
|
-
* @description
|
|
132
|
-
* Used by:
|
|
133
|
-
* - rest-api-knex-plugin's dataQuery adds these links to response.links
|
|
134
|
-
* - Called when offset-based pagination is used (not cursor-based)
|
|
135
|
-
*
|
|
136
|
-
* Purpose:
|
|
137
|
-
* - JSON:API spec requires pagination links for easy navigation
|
|
138
|
-
* - Preserves all other query parameters (filters, sorts, includes)
|
|
139
|
-
* - Uses the shared JSON:API query serializer for transport-supported params
|
|
140
|
-
* - Only includes prev/next when applicable
|
|
141
|
-
*
|
|
142
|
-
* Data flow:
|
|
143
|
-
* 1. After records are fetched and pagination calculated
|
|
144
|
-
* 2. Builds complete URLs preserving all query parameters
|
|
145
|
-
* 3. Conditionally includes prev (not on page 1) and next (not on last page)
|
|
146
|
-
* 4. Added to response.links for client navigation
|
|
85
|
+
* @returns {import('../../../../types/representations.js').JsonApiLinks | null} Links object with self, first, last, prev, next
|
|
147
86
|
*/
|
|
148
87
|
export const generatePaginationLinks = (urlPrefix, scopeName, queryParams, paginationMeta) => {
|
|
149
88
|
// Allow empty urlPrefix to generate relative links
|
|
@@ -177,62 +116,8 @@ export const generatePaginationLinks = (urlPrefix, scopeName, queryParams, pagin
|
|
|
177
116
|
*
|
|
178
117
|
* @param {Object} record - Database record to create cursor from
|
|
179
118
|
* @param {Array<string>} sortFields - Fields to include in cursor (default: ['id'])
|
|
119
|
+
* @param {import('../storage/storage-types.js').DataCursorOptions} [options]
|
|
180
120
|
* @returns {string} URL-safe cursor string
|
|
181
|
-
*
|
|
182
|
-
* @example
|
|
183
|
-
* // Input: Simple ID-based cursor
|
|
184
|
-
* const record = {
|
|
185
|
-
* id: 123,
|
|
186
|
-
* title: 'My Article',
|
|
187
|
-
* created_at: '2024-01-01T10:00:00Z'
|
|
188
|
-
* };
|
|
189
|
-
* const cursor = createCursor(record, ['id']);
|
|
190
|
-
*
|
|
191
|
-
* // Output: "id:123"
|
|
192
|
-
* // This cursor marks position at record with id=123
|
|
193
|
-
*
|
|
194
|
-
* @example
|
|
195
|
-
* // Input: Multi-field cursor for complex sorting
|
|
196
|
-
* const record = {
|
|
197
|
-
* id: 5,
|
|
198
|
-
* created_at: new Date('2024-01-15T08:30:00Z'),
|
|
199
|
-
* title: 'Article: Part 2'
|
|
200
|
-
* };
|
|
201
|
-
* const cursor = createCursor(record, ['created_at', 'id']);
|
|
202
|
-
*
|
|
203
|
-
* // Output: "created_at:2024-01-15T08%3A30%3A00.000Z,id:5"
|
|
204
|
-
* // URL-encoded to handle the colons in timestamp
|
|
205
|
-
* // Used for queries like: WHERE (created_at, id) > ('2024-01-15T08:30:00.000Z', 5)
|
|
206
|
-
*
|
|
207
|
-
* @example
|
|
208
|
-
* // Input: Handling special characters
|
|
209
|
-
* const record = {
|
|
210
|
-
* category: 'Tech & Science',
|
|
211
|
-
* title: 'AI: The Future?',
|
|
212
|
-
* id: 42
|
|
213
|
-
* };
|
|
214
|
-
* const cursor = createCursor(record, ['category', 'title', 'id']);
|
|
215
|
-
*
|
|
216
|
-
* // Output: "category:Tech%20%26%20Science,title:AI%3A%20The%20Future%3F,id:42"
|
|
217
|
-
* // Spaces encoded as %20, & as %26, : as %3A, ? as %3F
|
|
218
|
-
*
|
|
219
|
-
* @description
|
|
220
|
-
* Used by:
|
|
221
|
-
* - generateCursorPaginationLinks to create next/prev cursors
|
|
222
|
-
* - buildCursorMeta to provide cursor in response metadata
|
|
223
|
-
*
|
|
224
|
-
* Purpose:
|
|
225
|
-
* - Cursor-based pagination is more stable than offset for changing data
|
|
226
|
-
* - Encodes multiple sort fields to maintain stable ordering
|
|
227
|
-
* - URL-encodes values to handle special characters safely
|
|
228
|
-
* - Simple format that's easy to parse back
|
|
229
|
-
*
|
|
230
|
-
* Data flow:
|
|
231
|
-
* 1. After fetching records, takes the last record
|
|
232
|
-
* 2. Extracts values for all sort fields
|
|
233
|
-
* 3. Creates cursor encoding those values
|
|
234
|
-
* 4. Cursor used in 'next' link for fetching subsequent pages
|
|
235
|
-
* 5. Enables efficient "WHERE (field1, field2) > (val1, val2)" queries
|
|
236
121
|
*/
|
|
237
122
|
export const createCursor = (record, sortFields = ['id'], { schemaInfo = null, definitions = {} } = {}) => {
|
|
238
123
|
const parts = []
|
|
@@ -245,7 +130,7 @@ export const createCursor = (record, sortFields = ['id'], { schemaInfo = null, d
|
|
|
245
130
|
parts.push(`${field}:~null`)
|
|
246
131
|
return
|
|
247
132
|
}
|
|
248
|
-
if (['date', 'dateTime', 'time'].includes(definition?.type) && value != null) {
|
|
133
|
+
if (!definition?.storage?.serialize && !definition?.select && ['date', 'dateTime', 'time'].includes(definition?.type) && value != null) {
|
|
249
134
|
value = normalizeDateValue(value, definition.type, {
|
|
250
135
|
temporalPrecision: definition.temporalPrecision,
|
|
251
136
|
fieldName: field,
|
|
@@ -267,51 +152,6 @@ export const createCursor = (record, sortFields = ['id'], { schemaInfo = null, d
|
|
|
267
152
|
* @param {string} cursor - Cursor string to parse
|
|
268
153
|
* @returns {Object} Object with field names as keys and decoded values
|
|
269
154
|
* @throws {Error} If cursor format is invalid
|
|
270
|
-
*
|
|
271
|
-
* @example
|
|
272
|
-
* // Input: Simple cursor
|
|
273
|
-
* const cursor = "id:123";
|
|
274
|
-
* const data = parseCursor(cursor);
|
|
275
|
-
*
|
|
276
|
-
* // Output: { id: "123" }
|
|
277
|
-
* // Ready to use in SQL: WHERE id > '123'
|
|
278
|
-
*
|
|
279
|
-
* @example
|
|
280
|
-
* // Input: Multi-field cursor with URL-encoded values
|
|
281
|
-
* const cursor = "created_at:2024-01-15T08%3A30%3A00.000Z,id:5";
|
|
282
|
-
* const data = parseCursor(cursor);
|
|
283
|
-
*
|
|
284
|
-
* // Output:
|
|
285
|
-
* // {
|
|
286
|
-
* // created_at: "2024-01-15T08:30:00.000Z", // Decoded
|
|
287
|
-
* // id: "5"
|
|
288
|
-
* // }
|
|
289
|
-
* // Used for: WHERE (created_at, id) > ('2024-01-15T08:30:00.000Z', '5')
|
|
290
|
-
*
|
|
291
|
-
* @example
|
|
292
|
-
* // Input: Invalid cursor (throws error)
|
|
293
|
-
* try {
|
|
294
|
-
* const data = parseCursor("invalid-no-colon");
|
|
295
|
-
* } catch (e) {
|
|
296
|
-
* console.log(e.message);
|
|
297
|
-
* // "Invalid cursor format: Invalid cursor format: missing colon separator"
|
|
298
|
-
* }
|
|
299
|
-
*
|
|
300
|
-
* @description
|
|
301
|
-
* Used by:
|
|
302
|
-
* - rest-api-knex-plugin's dataQuery when processing page[after] parameter
|
|
303
|
-
* - Used to build WHERE clause for cursor-based queries
|
|
304
|
-
*
|
|
305
|
-
* Purpose:
|
|
306
|
-
* - Decodes cursor back to usable values for SQL queries
|
|
307
|
-
* - Handles URL-encoded special characters
|
|
308
|
-
* - Validates cursor format to prevent injection attacks
|
|
309
|
-
*
|
|
310
|
-
* Data flow:
|
|
311
|
-
* 1. Client sends page[after]=cursor parameter
|
|
312
|
-
* 2. parseCursor extracts field values from cursor
|
|
313
|
-
* 3. Values used to build WHERE clause like "WHERE id > 123"
|
|
314
|
-
* 4. Ensures pagination continues from exact position
|
|
315
155
|
*/
|
|
316
156
|
export const parseCursor = (cursor) => {
|
|
317
157
|
try {
|
|
@@ -355,70 +195,8 @@ export const parseCursor = (cursor) => {
|
|
|
355
195
|
* @param {number} pageSize - Records per page
|
|
356
196
|
* @param {boolean} hasMore - Whether more records exist
|
|
357
197
|
* @param {Array<string>} sortFields - Fields used for cursor
|
|
358
|
-
* @
|
|
359
|
-
*
|
|
360
|
-
* @example
|
|
361
|
-
* // Input: First page of results
|
|
362
|
-
* const urlPrefix = '/api/v1';
|
|
363
|
-
* const scopeName = 'articles';
|
|
364
|
-
* const queryParams = {
|
|
365
|
-
* filter: { status: 'published' },
|
|
366
|
-
* page: { size: 20 }
|
|
367
|
-
* };
|
|
368
|
-
* const records = [
|
|
369
|
-
* { id: 1, created_at: '2024-01-01', title: 'First' },
|
|
370
|
-
* { id: 2, created_at: '2024-01-02', title: 'Second' },
|
|
371
|
-
* { id: 3, created_at: '2024-01-03', title: 'Third' }
|
|
372
|
-
* ];
|
|
373
|
-
* const hasMore = true; // More records exist
|
|
374
|
-
*
|
|
375
|
-
* const links = generateCursorPaginationLinks(
|
|
376
|
-
* urlPrefix, scopeName, queryParams, records, 20, hasMore, ['created_at', 'id']
|
|
377
|
-
* );
|
|
378
|
-
*
|
|
379
|
-
* // Output:
|
|
380
|
-
* // {
|
|
381
|
-
* // self: '/api/v1/articles?filter[status]=published&page[size]=20',
|
|
382
|
-
* // first: '/api/v1/articles?filter[status]=published&page[size]=20',
|
|
383
|
-
* // next: '/api/v1/articles?filter[status]=published&page[size]=20&page[after]=created_at:2024-01-03,id:3'
|
|
384
|
-
* // }
|
|
385
|
-
* // The 'next' cursor points after the last record (id:3)
|
|
386
|
-
*
|
|
387
|
-
* @example
|
|
388
|
-
* // Input: Last page (no more records)
|
|
389
|
-
* const records = [
|
|
390
|
-
* { id: 98, created_at: '2024-03-01' },
|
|
391
|
-
* { id: 99, created_at: '2024-03-02' }
|
|
392
|
-
* ];
|
|
393
|
-
* const hasMore = false; // No more records
|
|
394
|
-
*
|
|
395
|
-
* const links = generateCursorPaginationLinks(
|
|
396
|
-
* '/api', 'articles', {}, records, 20, hasMore, ['id']
|
|
397
|
-
* );
|
|
398
|
-
*
|
|
399
|
-
* // Output - no 'next' link:
|
|
400
|
-
* // {
|
|
401
|
-
* // self: '/api/articles?page[size]=20',
|
|
402
|
-
* // first: '/api/articles?page[size]=20'
|
|
403
|
-
* // }
|
|
404
|
-
*
|
|
405
|
-
* @description
|
|
406
|
-
* Used by:
|
|
407
|
-
* - rest-api-knex-plugin when cursor pagination is enabled
|
|
408
|
-
* - Called after fetching records with cursor-based query
|
|
409
|
-
*
|
|
410
|
-
* Purpose:
|
|
411
|
-
* - Cursor pagination is more efficient for large datasets
|
|
412
|
-
* - Stable pagination when data is being added/removed
|
|
413
|
-
* - Only includes 'next' link when more data exists
|
|
414
|
-
* - Preserves all other query parameters
|
|
415
|
-
*
|
|
416
|
-
* Data flow:
|
|
417
|
-
* 1. After cursor-based query fetches records
|
|
418
|
-
* 2. If hasMore is true, creates cursor from last record
|
|
419
|
-
* 3. Builds next link with page[after] parameter
|
|
420
|
-
* 4. Added to response.links for navigation
|
|
421
|
-
* 5. Client uses next link to fetch subsequent pages
|
|
198
|
+
* @param {import('../storage/storage-types.js').DataCursorOptions} [options]
|
|
199
|
+
* @returns {import('../../../../types/representations.js').JsonApiLinks | null} Links object with self, first, next
|
|
422
200
|
*/
|
|
423
201
|
export const generateCursorPaginationLinks = (
|
|
424
202
|
urlPrefix,
|
|
@@ -463,55 +241,8 @@ export const generateCursorPaginationLinks = (
|
|
|
463
241
|
* @param {number} pageSize - Records per page
|
|
464
242
|
* @param {boolean} hasMore - Whether more records exist
|
|
465
243
|
* @param {Array<string>} sortFields - Fields used for cursor
|
|
244
|
+
* @param {import('../storage/storage-types.js').DataCursorOptions} [options]
|
|
466
245
|
* @returns {Object} Metadata with pageSize, hasMore, and optional cursor
|
|
467
|
-
*
|
|
468
|
-
* @example
|
|
469
|
-
* // Input: Page with more records available
|
|
470
|
-
* const records = [
|
|
471
|
-
* { id: 10, created_at: '2024-01-10', title: 'Article 10' },
|
|
472
|
-
* { id: 11, created_at: '2024-01-11', title: 'Article 11' },
|
|
473
|
-
* { id: 12, created_at: '2024-01-12', title: 'Article 12' }
|
|
474
|
-
* ];
|
|
475
|
-
* const meta = buildCursorMeta(records, 20, true, ['created_at', 'id']);
|
|
476
|
-
*
|
|
477
|
-
* // Output:
|
|
478
|
-
* // {
|
|
479
|
-
* // pageSize: 20,
|
|
480
|
-
* // hasMore: true,
|
|
481
|
-
* // cursor: {
|
|
482
|
-
* // next: 'created_at:2024-01-12,id:12' // Cursor from last record
|
|
483
|
-
* // }
|
|
484
|
-
* // }
|
|
485
|
-
*
|
|
486
|
-
* @example
|
|
487
|
-
* // Input: Last page (no more records)
|
|
488
|
-
* const records = [
|
|
489
|
-
* { id: 98, name: 'Last Item' }
|
|
490
|
-
* ];
|
|
491
|
-
* const meta = buildCursorMeta(records, 20, false, ['id']);
|
|
492
|
-
*
|
|
493
|
-
* // Output:
|
|
494
|
-
* // {
|
|
495
|
-
* // pageSize: 20,
|
|
496
|
-
* // hasMore: false
|
|
497
|
-
* // // No cursor property since hasMore is false
|
|
498
|
-
* // }
|
|
499
|
-
*
|
|
500
|
-
* @description
|
|
501
|
-
* Used by:
|
|
502
|
-
* - rest-api-knex-plugin adds this to response.meta for cursor pagination
|
|
503
|
-
* - Provides cursor that client can use directly if needed
|
|
504
|
-
*
|
|
505
|
-
* Purpose:
|
|
506
|
-
* - Gives clients direct access to cursor for custom pagination
|
|
507
|
-
* - Indicates whether more pages exist without counting
|
|
508
|
-
* - Simpler than offset pagination metadata (no total count)
|
|
509
|
-
*
|
|
510
|
-
* Data flow:
|
|
511
|
-
* 1. Called after fetching records with cursor query
|
|
512
|
-
* 2. Creates cursor from last record if more exist
|
|
513
|
-
* 3. Added to response.meta.pagination
|
|
514
|
-
* 4. Clients can use cursor directly or use the next link
|
|
515
246
|
*/
|
|
516
247
|
export const buildCursorMeta = (records, pageSize, hasMore, sortFields = ['id'], { schemaInfo = null, definitions = {}, before = false } = {}) => {
|
|
517
248
|
const meta = {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { buildIncludedResources } from './knex-relationship-includes.js'
|
|
2
|
-
import {
|
|
2
|
+
import { wrapUnexpectedError } from '../../../../lib/error-context.js'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Processes the ?include= parameter to load related resources efficiently
|
|
@@ -7,95 +7,7 @@ import { isRestApiError, wrapUnexpectedError } from '../../../../lib/error-conte
|
|
|
7
7
|
* @param {Object} scope - The scope object for the primary resource
|
|
8
8
|
* @param {Array<Object>} records - The primary records to load includes for
|
|
9
9
|
* @param {Object} deps - Dependencies object containing log, scopes, knex, and context
|
|
10
|
-
* @returns {Promise<
|
|
11
|
-
*
|
|
12
|
-
* @example
|
|
13
|
-
* // Input: Articles with author relationship
|
|
14
|
-
* const articles = [
|
|
15
|
-
* { id: 1, title: 'First Article', author_id: 10 },
|
|
16
|
-
* { id: 2, title: 'Second Article', author_id: 11 },
|
|
17
|
-
* { id: 3, title: 'Third Article', author_id: 10 } // Same author as article 1
|
|
18
|
-
* ];
|
|
19
|
-
*
|
|
20
|
-
* const deps = {
|
|
21
|
-
* context: {
|
|
22
|
-
* scopeName: 'articles',
|
|
23
|
-
* queryParams: { include: ['author'] },
|
|
24
|
-
* schemaInfo: { idProperty: 'id' }
|
|
25
|
-
* }
|
|
26
|
-
* };
|
|
27
|
-
*
|
|
28
|
-
* const included = await processIncludes(scope, articles, deps);
|
|
29
|
-
*
|
|
30
|
-
* // Output: Deduplicated authors (only 2, not 3)
|
|
31
|
-
* // [
|
|
32
|
-
* // { type: 'users', id: '10', attributes: { name: 'Alice', email: 'alice@example.com' } },
|
|
33
|
-
* // { type: 'users', id: '11', attributes: { name: 'Bob', email: 'bob@example.com' } }
|
|
34
|
-
* // ]
|
|
35
|
-
*
|
|
36
|
-
* @example
|
|
37
|
-
* // Input: Nested includes with dot notation
|
|
38
|
-
* const articles = [{ id: 1, title: 'Article', author_id: 10 }];
|
|
39
|
-
*
|
|
40
|
-
* const deps = {
|
|
41
|
-
* context: {
|
|
42
|
-
* queryParams: {
|
|
43
|
-
* include: ['author', 'comments.user'] // Load author AND comment users
|
|
44
|
-
* }
|
|
45
|
-
* }
|
|
46
|
-
* };
|
|
47
|
-
*
|
|
48
|
-
* const included = await processIncludes(scope, articles, deps);
|
|
49
|
-
*
|
|
50
|
-
* // Output: All related resources in flat array
|
|
51
|
-
* // [
|
|
52
|
-
* // { type: 'users', id: '10', attributes: { name: 'Author' } },
|
|
53
|
-
* // { type: 'comments', id: '1', attributes: { text: 'Great!' } },
|
|
54
|
-
* // { type: 'comments', id: '2', attributes: { text: 'Nice!' } },
|
|
55
|
-
* // { type: 'users', id: '20', attributes: { name: 'Commenter1' } },
|
|
56
|
-
* // { type: 'users', id: '21', attributes: { name: 'Commenter2' } }
|
|
57
|
-
* // ]
|
|
58
|
-
*
|
|
59
|
-
* @example
|
|
60
|
-
* // Input: With sparse fieldsets limiting included data
|
|
61
|
-
* const deps = {
|
|
62
|
-
* context: {
|
|
63
|
-
* queryParams: {
|
|
64
|
-
* include: ['author'],
|
|
65
|
-
* fields: {
|
|
66
|
-
* users: 'name' // Only include name field for users
|
|
67
|
-
* }
|
|
68
|
-
* }
|
|
69
|
-
* }
|
|
70
|
-
* };
|
|
71
|
-
*
|
|
72
|
-
* const included = await processIncludes(scope, articles, deps);
|
|
73
|
-
*
|
|
74
|
-
* // Output: Users with only requested fields
|
|
75
|
-
* // [
|
|
76
|
-
* // { type: 'users', id: '10', attributes: { name: 'Alice' } }
|
|
77
|
-
* // // email field excluded due to sparse fieldset
|
|
78
|
-
* // ]
|
|
79
|
-
*
|
|
80
|
-
* @description
|
|
81
|
-
* Used by:
|
|
82
|
-
* - rest-api-knex-plugin's dataGet method when includes are requested
|
|
83
|
-
* - rest-api-knex-plugin's dataQuery method for collection includes
|
|
84
|
-
* - Called after primary records are fetched but before response assembly
|
|
85
|
-
*
|
|
86
|
-
* Purpose:
|
|
87
|
-
* - Implements JSON:API compound documents with primary data and includes
|
|
88
|
-
* - Prevents N+1 queries by batch loading all related resources
|
|
89
|
-
* - Handles complex nested includes like 'comments.author.company'
|
|
90
|
-
* - Automatically deduplicates included resources
|
|
91
|
-
* - Respects sparse fieldsets on included resources
|
|
92
|
-
*
|
|
93
|
-
* Data flow:
|
|
94
|
-
* 1. Receives primary records and include directives
|
|
95
|
-
* 2. Parses include parameter (comma-separated, possibly nested with dots)
|
|
96
|
-
* 3. Delegates to buildIncludedResources for actual loading
|
|
97
|
-
* 4. Returns flat array of all included resources
|
|
98
|
-
* 5. These resources go into the 'included' section of JSON:API response
|
|
10
|
+
* @returns {Promise<import('../storage/storage-types.js').DataResource[]>} Array of included resources in JSON:API format
|
|
99
11
|
*/
|
|
100
12
|
export const processIncludes = async (scope, records, deps) => {
|
|
101
13
|
try {
|
|
@@ -104,9 +16,6 @@ export const processIncludes = async (scope, records, deps) => {
|
|
|
104
16
|
const scopeName = context.scopeName
|
|
105
17
|
|
|
106
18
|
if (!scopeName) {
|
|
107
|
-
log.error('[PROCESS-INCLUDES] scopeName is undefined in context!', {
|
|
108
|
-
contextKeys: Object.keys(context || {})
|
|
109
|
-
})
|
|
110
19
|
throw new Error('scopeName is undefined in context')
|
|
111
20
|
}
|
|
112
21
|
|
|
@@ -117,7 +26,7 @@ export const processIncludes = async (scope, records, deps) => {
|
|
|
117
26
|
return []
|
|
118
27
|
}
|
|
119
28
|
|
|
120
|
-
log.debug('[PROCESS-INCLUDES] Processing includes:',
|
|
29
|
+
log.debug('[PROCESS-INCLUDES] Processing includes:', { scopeName, recordCount: records.length })
|
|
121
30
|
|
|
122
31
|
const includeResult = await buildIncludedResources(
|
|
123
32
|
{
|
|
@@ -144,25 +53,11 @@ export const processIncludes = async (scope, records, deps) => {
|
|
|
144
53
|
|
|
145
54
|
return includeResult.included
|
|
146
55
|
} catch (error) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const { log, context } = deps || {}
|
|
150
|
-
|
|
151
|
-
if (log) {
|
|
152
|
-
log.error('[PROCESS-INCLUDES] Error processing includes:', {
|
|
153
|
-
scopeName: context?.scopeName,
|
|
154
|
-
recordCount: records?.length || 0,
|
|
155
|
-
includeParam: context?.queryParams?.include,
|
|
156
|
-
error: error.message,
|
|
157
|
-
stack: error.stack
|
|
158
|
-
})
|
|
159
|
-
} else {
|
|
160
|
-
console.error('[PROCESS-INCLUDES] Error processing includes:', error)
|
|
161
|
-
}
|
|
162
|
-
|
|
56
|
+
const { context } = deps || {}
|
|
163
57
|
throw wrapUnexpectedError(error, {
|
|
164
58
|
message: `Failed to process includes${context?.scopeName ? ` for scope '${context.scopeName}'` : ''}`,
|
|
165
59
|
context: {
|
|
60
|
+
phase: 'include',
|
|
166
61
|
scopeName: context?.scopeName,
|
|
167
62
|
includeParam: context?.queryParams?.include,
|
|
168
63
|
recordCount: records?.length || 0
|
|
@@ -1,59 +1,14 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { isImmediateExpression } from '../querying-writing/query-field-helpers.js'
|
|
1
3
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* @param {
|
|
5
|
-
* @param {string} tableName
|
|
6
|
-
* @param {
|
|
7
|
-
* @param {boolean} useTablePrefix
|
|
8
|
-
* @
|
|
9
|
-
*
|
|
10
|
-
* @example
|
|
11
|
-
* // Input: Basic query without specific fields
|
|
12
|
-
* const query = knex('articles');
|
|
13
|
-
* buildQuerySelection(query, 'articles', '*', false);
|
|
14
|
-
*
|
|
15
|
-
* // Effect on query: No modification, Knex defaults to SELECT *
|
|
16
|
-
* // SQL generated: SELECT * FROM articles
|
|
17
|
-
*
|
|
18
|
-
* @example
|
|
19
|
-
* // Input: Query with sparse fieldset
|
|
20
|
-
* const query = knex('articles');
|
|
21
|
-
* const fields = ['id', 'title', 'author_id'];
|
|
22
|
-
* buildQuerySelection(query, 'articles', fields, false);
|
|
23
|
-
*
|
|
24
|
-
* // Effect on query: Adds select() with specific fields
|
|
25
|
-
* // SQL generated: SELECT id, title, author_id FROM articles
|
|
26
|
-
* // Response will only include these 3 fields, reducing payload size
|
|
27
|
-
*
|
|
28
|
-
* @example
|
|
29
|
-
* // Input: Query with joins needing table prefixes
|
|
30
|
-
* const query = knex('articles')
|
|
31
|
-
* .join('users', 'articles.author_id', 'users.id');
|
|
32
|
-
* const fields = ['id', 'title', 'created_at as published'];
|
|
33
|
-
* buildQuerySelection(query, 'articles', fields, true);
|
|
34
|
-
*
|
|
35
|
-
* // Effect on query: Prefixes all fields with table name
|
|
36
|
-
* // SQL generated: SELECT articles.id, articles.title, articles.created_at as published
|
|
37
|
-
* // Prevents "column 'id' is ambiguous" errors since both tables have id
|
|
38
|
-
*
|
|
39
|
-
* @description
|
|
40
|
-
* Used by:
|
|
41
|
-
* - knex-query-helpers.js calls this when building queries with sparse fieldsets
|
|
42
|
-
* - rest-api-knex-plugin uses this in dataQuery to optimize SELECT clauses
|
|
43
|
-
* - Called whenever fields parameter limits which attributes to return
|
|
44
|
-
*
|
|
45
|
-
* Purpose:
|
|
46
|
-
* - Implements JSON:API sparse fieldsets by selecting only requested fields
|
|
47
|
-
* - Prevents ambiguous column errors in queries with joins
|
|
48
|
-
* - Reduces database transfer by not selecting unnecessary columns
|
|
49
|
-
* - Handles field aliases (e.g., "created_at as published")
|
|
50
|
-
*
|
|
51
|
-
* Data flow:
|
|
52
|
-
* 1. Query parser extracts fields parameter (e.g., fields[articles]=title,author)
|
|
53
|
-
* 2. This function adds appropriate SELECT clause to query
|
|
54
|
-
* 3. Database returns only selected columns
|
|
55
|
-
* 4. Smaller result sets improve query performance
|
|
56
|
-
* 5. JSON:API response includes only requested attributes
|
|
4
|
+
* Add logical field selections to an unexecuted builder, using the caller's translator.
|
|
5
|
+
* `*` retains wildcard behavior. Aliases are null when table prefixing is disabled.
|
|
6
|
+
* @param {import('../storage/storage-types.js').StorageQuery} query
|
|
7
|
+
* @param {string} tableName
|
|
8
|
+
* @param {readonly string[] | string} fieldsToSelect
|
|
9
|
+
* @param {boolean} [useTablePrefix]
|
|
10
|
+
* @param {{ translateColumn?: (field: string, alias?: string | null) => string | import('knex').Knex.Raw<unknown> | Record<string, string | import('knex').Knex.Raw<unknown>> | null | undefined }} [options]
|
|
11
|
+
* @returns {import('../storage/storage-types.js').StorageQuery}
|
|
57
12
|
*/
|
|
58
13
|
export const buildQuerySelection = (
|
|
59
14
|
query,
|
|
@@ -64,6 +19,7 @@ export const buildQuerySelection = (
|
|
|
64
19
|
) => {
|
|
65
20
|
const translateColumn = options.translateColumn
|
|
66
21
|
|
|
22
|
+
/** @param {string} field @param {string | null} aliasForField */
|
|
67
23
|
const applyTranslation = (field, aliasForField) => {
|
|
68
24
|
if (translateColumn) {
|
|
69
25
|
const translated = translateColumn(field, aliasForField)
|
|
@@ -88,6 +44,7 @@ export const buildQuerySelection = (
|
|
|
88
44
|
return useTablePrefix ? query.select(`${tableName}.*`) : query
|
|
89
45
|
}
|
|
90
46
|
|
|
47
|
+
/** @type {readonly string[]} */
|
|
91
48
|
const fieldsArray = Array.isArray(fieldsToSelect)
|
|
92
49
|
? fieldsToSelect
|
|
93
50
|
: Array.from(fieldsToSelect || [])
|
|
@@ -97,8 +54,16 @@ export const buildQuerySelection = (
|
|
|
97
54
|
|
|
98
55
|
const aliasMatch = field.match(/\s+as\s+/i)
|
|
99
56
|
if (aliasMatch) {
|
|
100
|
-
const [source, alias] = field.split(/\s+as\s+/i)
|
|
57
|
+
const [source = '', alias = ''] = field.split(/\s+as\s+/i)
|
|
101
58
|
const translatedSource = applyTranslation(source.trim(), targetAlias)
|
|
59
|
+
if (typeof translatedSource !== 'string') {
|
|
60
|
+
if (isImmediateExpression(translatedSource)) return { [alias.trim()]: translatedSource }
|
|
61
|
+
const values = Object.values(translatedSource)
|
|
62
|
+
if (values.length !== 1 || values[0] === undefined) {
|
|
63
|
+
throw new Error(`Cannot apply alias '${alias.trim()}' to a translation containing multiple or missing expressions`)
|
|
64
|
+
}
|
|
65
|
+
return { [alias.trim()]: values[0] }
|
|
66
|
+
}
|
|
102
67
|
return `${translatedSource} as ${alias.trim()}`
|
|
103
68
|
}
|
|
104
69
|
|