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,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
|
|
@@ -158,7 +97,7 @@ export const generatePaginationLinks = (urlPrefix, scopeName, queryParams, pagin
|
|
|
158
97
|
|
|
159
98
|
if (pageCount !== undefined) {
|
|
160
99
|
links.first = buildJsonApiLink(baseUrl, queryParams, { number: 1, size: pageSize })
|
|
161
|
-
links.last = buildJsonApiLink(baseUrl, queryParams, { number: pageCount, size: pageSize })
|
|
100
|
+
links.last = buildJsonApiLink(baseUrl, queryParams, { number: Math.max(1, pageCount), size: pageSize })
|
|
162
101
|
|
|
163
102
|
if (page > 1) {
|
|
164
103
|
links.prev = buildJsonApiLink(baseUrl, queryParams, { number: page - 1, size: pageSize })
|
|
@@ -177,71 +116,21 @@ 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
|
-
export const createCursor = (record, sortFields = ['id'], { schemaInfo = null } = {}) => {
|
|
122
|
+
export const createCursor = (record, sortFields = ['id'], { schemaInfo = null, definitions = {} } = {}) => {
|
|
238
123
|
const parts = []
|
|
239
124
|
sortFields.forEach(field => {
|
|
240
125
|
let value = schemaInfo
|
|
241
126
|
? getFieldValue(record, schemaInfo, field)
|
|
242
127
|
: record[field]
|
|
243
|
-
const definition = schemaInfo?.schemaStructure?.[field]
|
|
244
|
-
if (
|
|
128
|
+
const definition = definitions[field] || schemaInfo?.schemaStructure?.[field]
|
|
129
|
+
if (value === null) {
|
|
130
|
+
parts.push(`${field}:~null`)
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
if (!definition?.storage?.serialize && !definition?.select && ['date', 'dateTime', 'time'].includes(definition?.type) && value != null) {
|
|
245
134
|
value = normalizeDateValue(value, definition.type, {
|
|
246
135
|
temporalPrecision: definition.temporalPrecision,
|
|
247
136
|
fieldName: field,
|
|
@@ -251,7 +140,7 @@ export const createCursor = (record, sortFields = ['id'], { schemaInfo = null }
|
|
|
251
140
|
}
|
|
252
141
|
if (value !== undefined) {
|
|
253
142
|
const stringValue = value instanceof Date ? value.toISOString() : String(value)
|
|
254
|
-
parts.push(`${field}:${encodeURIComponent(stringValue)}`)
|
|
143
|
+
parts.push(`${field}:${encodeURIComponent(stringValue).replace(/^~/, '%7E')}`)
|
|
255
144
|
}
|
|
256
145
|
})
|
|
257
146
|
return parts.join(',')
|
|
@@ -263,55 +152,10 @@ export const createCursor = (record, sortFields = ['id'], { schemaInfo = null }
|
|
|
263
152
|
* @param {string} cursor - Cursor string to parse
|
|
264
153
|
* @returns {Object} Object with field names as keys and decoded values
|
|
265
154
|
* @throws {Error} If cursor format is invalid
|
|
266
|
-
*
|
|
267
|
-
* @example
|
|
268
|
-
* // Input: Simple cursor
|
|
269
|
-
* const cursor = "id:123";
|
|
270
|
-
* const data = parseCursor(cursor);
|
|
271
|
-
*
|
|
272
|
-
* // Output: { id: "123" }
|
|
273
|
-
* // Ready to use in SQL: WHERE id > '123'
|
|
274
|
-
*
|
|
275
|
-
* @example
|
|
276
|
-
* // Input: Multi-field cursor with URL-encoded values
|
|
277
|
-
* const cursor = "created_at:2024-01-15T08%3A30%3A00.000Z,id:5";
|
|
278
|
-
* const data = parseCursor(cursor);
|
|
279
|
-
*
|
|
280
|
-
* // Output:
|
|
281
|
-
* // {
|
|
282
|
-
* // created_at: "2024-01-15T08:30:00.000Z", // Decoded
|
|
283
|
-
* // id: "5"
|
|
284
|
-
* // }
|
|
285
|
-
* // Used for: WHERE (created_at, id) > ('2024-01-15T08:30:00.000Z', '5')
|
|
286
|
-
*
|
|
287
|
-
* @example
|
|
288
|
-
* // Input: Invalid cursor (throws error)
|
|
289
|
-
* try {
|
|
290
|
-
* const data = parseCursor("invalid-no-colon");
|
|
291
|
-
* } catch (e) {
|
|
292
|
-
* console.log(e.message);
|
|
293
|
-
* // "Invalid cursor format: Invalid cursor format: missing colon separator"
|
|
294
|
-
* }
|
|
295
|
-
*
|
|
296
|
-
* @description
|
|
297
|
-
* Used by:
|
|
298
|
-
* - rest-api-knex-plugin's dataQuery when processing page[after] parameter
|
|
299
|
-
* - Used to build WHERE clause for cursor-based queries
|
|
300
|
-
*
|
|
301
|
-
* Purpose:
|
|
302
|
-
* - Decodes cursor back to usable values for SQL queries
|
|
303
|
-
* - Handles URL-encoded special characters
|
|
304
|
-
* - Validates cursor format to prevent injection attacks
|
|
305
|
-
*
|
|
306
|
-
* Data flow:
|
|
307
|
-
* 1. Client sends page[after]=cursor parameter
|
|
308
|
-
* 2. parseCursor extracts field values from cursor
|
|
309
|
-
* 3. Values used to build WHERE clause like "WHERE id > 123"
|
|
310
|
-
* 4. Ensures pagination continues from exact position
|
|
311
155
|
*/
|
|
312
156
|
export const parseCursor = (cursor) => {
|
|
313
157
|
try {
|
|
314
|
-
const data =
|
|
158
|
+
const data = Object.create(null)
|
|
315
159
|
if (!cursor || cursor.trim() === '') {
|
|
316
160
|
throw new Error('Empty cursor')
|
|
317
161
|
}
|
|
@@ -331,7 +175,8 @@ export const parseCursor = (cursor) => {
|
|
|
331
175
|
throw new Error('Invalid cursor format: empty field name')
|
|
332
176
|
}
|
|
333
177
|
|
|
334
|
-
data
|
|
178
|
+
if (Object.hasOwn(data, field)) throw new Error(`Duplicate cursor field '${field}'`)
|
|
179
|
+
data[field] = encodedValue === '~null' ? null : decodeURIComponent(encodedValue)
|
|
335
180
|
}
|
|
336
181
|
|
|
337
182
|
return data
|
|
@@ -350,70 +195,8 @@ export const parseCursor = (cursor) => {
|
|
|
350
195
|
* @param {number} pageSize - Records per page
|
|
351
196
|
* @param {boolean} hasMore - Whether more records exist
|
|
352
197
|
* @param {Array<string>} sortFields - Fields used for cursor
|
|
353
|
-
* @
|
|
354
|
-
*
|
|
355
|
-
* @example
|
|
356
|
-
* // Input: First page of results
|
|
357
|
-
* const urlPrefix = '/api/v1';
|
|
358
|
-
* const scopeName = 'articles';
|
|
359
|
-
* const queryParams = {
|
|
360
|
-
* filter: { status: 'published' },
|
|
361
|
-
* page: { size: 20 }
|
|
362
|
-
* };
|
|
363
|
-
* const records = [
|
|
364
|
-
* { id: 1, created_at: '2024-01-01', title: 'First' },
|
|
365
|
-
* { id: 2, created_at: '2024-01-02', title: 'Second' },
|
|
366
|
-
* { id: 3, created_at: '2024-01-03', title: 'Third' }
|
|
367
|
-
* ];
|
|
368
|
-
* const hasMore = true; // More records exist
|
|
369
|
-
*
|
|
370
|
-
* const links = generateCursorPaginationLinks(
|
|
371
|
-
* urlPrefix, scopeName, queryParams, records, 20, hasMore, ['created_at', 'id']
|
|
372
|
-
* );
|
|
373
|
-
*
|
|
374
|
-
* // Output:
|
|
375
|
-
* // {
|
|
376
|
-
* // self: '/api/v1/articles?filter[status]=published&page[size]=20',
|
|
377
|
-
* // first: '/api/v1/articles?filter[status]=published&page[size]=20',
|
|
378
|
-
* // next: '/api/v1/articles?filter[status]=published&page[size]=20&page[after]=created_at:2024-01-03,id:3'
|
|
379
|
-
* // }
|
|
380
|
-
* // The 'next' cursor points after the last record (id:3)
|
|
381
|
-
*
|
|
382
|
-
* @example
|
|
383
|
-
* // Input: Last page (no more records)
|
|
384
|
-
* const records = [
|
|
385
|
-
* { id: 98, created_at: '2024-03-01' },
|
|
386
|
-
* { id: 99, created_at: '2024-03-02' }
|
|
387
|
-
* ];
|
|
388
|
-
* const hasMore = false; // No more records
|
|
389
|
-
*
|
|
390
|
-
* const links = generateCursorPaginationLinks(
|
|
391
|
-
* '/api', 'articles', {}, records, 20, hasMore, ['id']
|
|
392
|
-
* );
|
|
393
|
-
*
|
|
394
|
-
* // Output - no 'next' link:
|
|
395
|
-
* // {
|
|
396
|
-
* // self: '/api/articles?page[size]=20',
|
|
397
|
-
* // first: '/api/articles?page[size]=20'
|
|
398
|
-
* // }
|
|
399
|
-
*
|
|
400
|
-
* @description
|
|
401
|
-
* Used by:
|
|
402
|
-
* - rest-api-knex-plugin when cursor pagination is enabled
|
|
403
|
-
* - Called after fetching records with cursor-based query
|
|
404
|
-
*
|
|
405
|
-
* Purpose:
|
|
406
|
-
* - Cursor pagination is more efficient for large datasets
|
|
407
|
-
* - Stable pagination when data is being added/removed
|
|
408
|
-
* - Only includes 'next' link when more data exists
|
|
409
|
-
* - Preserves all other query parameters
|
|
410
|
-
*
|
|
411
|
-
* Data flow:
|
|
412
|
-
* 1. After cursor-based query fetches records
|
|
413
|
-
* 2. If hasMore is true, creates cursor from last record
|
|
414
|
-
* 3. Builds next link with page[after] parameter
|
|
415
|
-
* 4. Added to response.links for navigation
|
|
416
|
-
* 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
|
|
417
200
|
*/
|
|
418
201
|
export const generateCursorPaginationLinks = (
|
|
419
202
|
urlPrefix,
|
|
@@ -423,7 +206,7 @@ export const generateCursorPaginationLinks = (
|
|
|
423
206
|
pageSize,
|
|
424
207
|
hasMore,
|
|
425
208
|
sortFields = ['id'],
|
|
426
|
-
{ schemaInfo = null } = {}
|
|
209
|
+
{ schemaInfo = null, definitions = {} } = {}
|
|
427
210
|
) => {
|
|
428
211
|
// Allow empty urlPrefix to generate relative links
|
|
429
212
|
if (!records.length) return null
|
|
@@ -442,9 +225,10 @@ export const generateCursorPaginationLinks = (
|
|
|
442
225
|
links.first = buildJsonApiLink(baseUrl, queryParams, { size: pageSize })
|
|
443
226
|
|
|
444
227
|
if (hasMore && records.length > 0) {
|
|
445
|
-
const
|
|
446
|
-
const
|
|
447
|
-
|
|
228
|
+
const direction = queryParams.page?.before ? 'before' : 'after'
|
|
229
|
+
const boundary = direction === 'before' ? records[0] : records[records.length - 1]
|
|
230
|
+
const nextCursor = createCursor(boundary, sortFields, { schemaInfo, definitions })
|
|
231
|
+
links.next = buildJsonApiLink(baseUrl, queryParams, { size: pageSize, [direction]: nextCursor })
|
|
448
232
|
}
|
|
449
233
|
|
|
450
234
|
return links
|
|
@@ -457,66 +241,19 @@ export const generateCursorPaginationLinks = (
|
|
|
457
241
|
* @param {number} pageSize - Records per page
|
|
458
242
|
* @param {boolean} hasMore - Whether more records exist
|
|
459
243
|
* @param {Array<string>} sortFields - Fields used for cursor
|
|
244
|
+
* @param {import('../storage/storage-types.js').DataCursorOptions} [options]
|
|
460
245
|
* @returns {Object} Metadata with pageSize, hasMore, and optional cursor
|
|
461
|
-
*
|
|
462
|
-
* @example
|
|
463
|
-
* // Input: Page with more records available
|
|
464
|
-
* const records = [
|
|
465
|
-
* { id: 10, created_at: '2024-01-10', title: 'Article 10' },
|
|
466
|
-
* { id: 11, created_at: '2024-01-11', title: 'Article 11' },
|
|
467
|
-
* { id: 12, created_at: '2024-01-12', title: 'Article 12' }
|
|
468
|
-
* ];
|
|
469
|
-
* const meta = buildCursorMeta(records, 20, true, ['created_at', 'id']);
|
|
470
|
-
*
|
|
471
|
-
* // Output:
|
|
472
|
-
* // {
|
|
473
|
-
* // pageSize: 20,
|
|
474
|
-
* // hasMore: true,
|
|
475
|
-
* // cursor: {
|
|
476
|
-
* // next: 'created_at:2024-01-12,id:12' // Cursor from last record
|
|
477
|
-
* // }
|
|
478
|
-
* // }
|
|
479
|
-
*
|
|
480
|
-
* @example
|
|
481
|
-
* // Input: Last page (no more records)
|
|
482
|
-
* const records = [
|
|
483
|
-
* { id: 98, name: 'Last Item' }
|
|
484
|
-
* ];
|
|
485
|
-
* const meta = buildCursorMeta(records, 20, false, ['id']);
|
|
486
|
-
*
|
|
487
|
-
* // Output:
|
|
488
|
-
* // {
|
|
489
|
-
* // pageSize: 20,
|
|
490
|
-
* // hasMore: false
|
|
491
|
-
* // // No cursor property since hasMore is false
|
|
492
|
-
* // }
|
|
493
|
-
*
|
|
494
|
-
* @description
|
|
495
|
-
* Used by:
|
|
496
|
-
* - rest-api-knex-plugin adds this to response.meta for cursor pagination
|
|
497
|
-
* - Provides cursor that client can use directly if needed
|
|
498
|
-
*
|
|
499
|
-
* Purpose:
|
|
500
|
-
* - Gives clients direct access to cursor for custom pagination
|
|
501
|
-
* - Indicates whether more pages exist without counting
|
|
502
|
-
* - Simpler than offset pagination metadata (no total count)
|
|
503
|
-
*
|
|
504
|
-
* Data flow:
|
|
505
|
-
* 1. Called after fetching records with cursor query
|
|
506
|
-
* 2. Creates cursor from last record if more exist
|
|
507
|
-
* 3. Added to response.meta.pagination
|
|
508
|
-
* 4. Clients can use cursor directly or use the next link
|
|
509
246
|
*/
|
|
510
|
-
export const buildCursorMeta = (records, pageSize, hasMore, sortFields = ['id'], { schemaInfo = null } = {}) => {
|
|
247
|
+
export const buildCursorMeta = (records, pageSize, hasMore, sortFields = ['id'], { schemaInfo = null, definitions = {}, before = false } = {}) => {
|
|
511
248
|
const meta = {
|
|
512
249
|
pageSize,
|
|
513
250
|
hasMore
|
|
514
251
|
}
|
|
515
252
|
|
|
516
253
|
if (hasMore && records.length > 0) {
|
|
517
|
-
const lastRecord = records[records.length - 1]
|
|
254
|
+
const lastRecord = before ? records[0] : records[records.length - 1]
|
|
518
255
|
meta.cursor = {
|
|
519
|
-
next: createCursor(lastRecord, sortFields, { schemaInfo })
|
|
256
|
+
next: createCursor(lastRecord, sortFields, { schemaInfo, definitions })
|
|
520
257
|
}
|
|
521
258
|
}
|
|
522
259
|
|
|
@@ -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
|