json-rest-api 1.0.21 → 1.0.22
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/audit/00-charter.md +103 -0
- package/audit/01-scope-map.md +84 -0
- package/audit/02-coverage-matrix.csv +174 -0
- package/audit/03-invariants.md +123 -0
- package/audit/04-standards-rubric.md +56 -0
- package/audit/05-competitor-rubric.md +77 -0
- package/audit/06-findings.md +160 -0
- package/audit/07-risk-register.csv +10 -0
- package/audit/08-remediation-roadmap.md +177 -0
- package/audit/09-signoff-check.md +23 -0
- package/audit/10-final-synthesis.md +29 -0
- package/audit/evidence/code/all-files-json-rest-api.txt +168 -0
- package/audit/evidence/code/all-files-json-rest-schema.txt +12 -0
- package/audit/evidence/code/boundary-leak-checks.txt +72 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__bulk-operations-plugin.js.txt +480 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__express-plugin.js.txt +532 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__busboy-detector.js.txt +175 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__formidable-detector.js.txt +138 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__request-helpers.js.txt +222 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__file-handling-plugin.js.txt +342 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__dbTablesOperations.js.txt +304 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__fractional-positioning.js.txt +188 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__compile-schemas.js.txt +336 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__connectors-query-parser.js.txt +165 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-capabilities.js.txt +265 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-value-normalizers.js.txt +369 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__default-data-helpers.js.txt +72 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__field-utils.js.txt +139 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-constants.js.txt +45 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-field-helpers.js.txt +396 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-json-api-transformers.js.txt +162 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__payload-validators.js.txt +1314 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__schema-helpers.js.txt +488 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__scope-validations.js.txt +484 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__simplified-helpers.js.txt +857 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-cross-table-search.js.txt +565 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-json-api-transformers-querying.js.txt +485 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-pagination-helpers.js.txt +560 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-process-includes.js.txt +167 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers-base.js.txt +109 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers.js.txt +1064 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-relationship-includes.js.txt +1983 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-window-queries.js.txt +328 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__url-helpers.js.txt +119 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__storage__storage-adapter.js.txt +160 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__many-to-many-manipulations.js.txt +300 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__relationship-processor.js.txt +245 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-access.js.txt +459 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-cors-plugin.js.txt +237 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-knex-plugin.js.txt +1135 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-multihome-plugin.js.txt +314 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__compile-resource-schemas.js.txt +7 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-relationship-routes.js.txt +71 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-scope-routes.js.txt +102 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__turn-scope-init-into-vars.js.txt +49 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__validate-include-configurations.js.txt +39 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__add-route.js.txt +23 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__check-permissions.js.txt +13 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__common.js.txt +639 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete-relationship.js.txt +117 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete.js.txt +104 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__enrich-attributes.js.txt +199 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-related.js.txt +353 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-relationship.js.txt +61 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get.js.txt +184 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch-relationship.js.txt +55 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch.js.txt +191 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post-relationship.js.txt +110 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post.js.txt +168 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__put.js.txt +253 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__query.js.txt +207 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__release.js.txt +12 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin.js.txt +165 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-positioning-plugin.js.txt +668 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__socketio-plugin.js.txt +621 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__local-storage.js.txt +218 -0
- package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__s3-storage.js.txt +124 -0
- package/audit/evidence/code/critical-pass1/json-rest-schema__src__CorePlugin.js.txt +269 -0
- package/audit/evidence/code/critical-pass1/json-rest-schema__src__Schema.js.txt +242 -0
- package/audit/evidence/code/critical-pass1/json-rest-schema__src__index.js.txt +77 -0
- package/audit/evidence/code/critical-pass1-risk-keywords.txt +238 -0
- package/audit/evidence/code/critical-pass1-summary.csv +67 -0
- package/audit/evidence/code/critical-pass2-summary.csv +67 -0
- package/audit/evidence/code/early-silent-zones-no-test-token.txt +57 -0
- package/audit/evidence/code/error-model-signals.txt +98 -0
- package/audit/evidence/code/git-files-json-rest-api.txt +171 -0
- package/audit/evidence/code/git-files-json-rest-schema.txt +27 -0
- package/audit/evidence/code/hook-flow-signals.txt +282 -0
- package/audit/evidence/code/idproperty-signals.txt +157 -0
- package/audit/evidence/code/import-cycles-non-anyapi.txt +3 -0
- package/audit/evidence/code/import-hotspots-non-anyapi.txt +96 -0
- package/audit/evidence/code/imports-json-rest-api.txt +354 -0
- package/audit/evidence/code/imports-json-rest-schema.txt +15 -0
- package/audit/evidence/code/in-scope-files-json-rest-api.txt +160 -0
- package/audit/evidence/code/in-scope-files-json-rest-schema.txt +13 -0
- package/audit/evidence/code/non-anyapi-core-files.txt +101 -0
- package/audit/evidence/code/permission-signals.txt +193 -0
- package/audit/evidence/code/query-include-signals.txt +687 -0
- package/audit/evidence/code/transaction-signals.txt +28 -0
- package/audit/evidence/code/validation-signals.txt +650 -0
- package/audit/evidence/code/write-path-signals.txt +61 -0
- package/audit/evidence/docs/docs-claims-signals.txt +1075 -0
- package/audit/evidence/docs/schema-docs-claims-signals.txt +179 -0
- package/audit/evidence/external/corpus-freeze.md +21 -0
- package/audit/evidence/external/npm-competitor-metadata.txt +70 -0
- package/audit/evidence/external/npm-download-signals.txt +23 -0
- package/audit/evidence/external/npm-jsonapi-server.txt +8 -0
- package/audit/evidence/external/npm-modern-jsonapi-metadata.txt +38 -0
- package/audit/evidence/external/readme-fortune-signals.txt +26 -0
- package/audit/evidence/external/readme-fortune.md +165 -0
- package/audit/evidence/external/readme-json-api-serializer-signals.txt +80 -0
- package/audit/evidence/external/readme-json-api-serializer.md +690 -0
- package/audit/evidence/external/readme-jsonapi-serde-server-signals.txt +0 -0
- package/audit/evidence/external/readme-jsonapi-serde-server.md +22 -0
- package/audit/evidence/external/readme-jsonapi-serializer-signals.txt +63 -0
- package/audit/evidence/external/readme-jsonapi-serializer.md +315 -0
- package/audit/evidence/external/readme-jsonapi-server-signals.txt +19 -0
- package/audit/evidence/external/readme-jsonapi-server.md +104 -0
- package/audit/evidence/external/readme-nestjs-json-api-signals.txt +4 -0
- package/audit/evidence/external/readme-nestjs-json-api.md +94 -0
- package/audit/evidence/external/readme-yaser-json-api-nestjs-signals.txt +47 -0
- package/audit/evidence/external/readme-yaser-json-api-nestjs.md +447 -0
- package/audit/evidence/external/standard-jsonapi-snippets.txt +84 -0
- package/audit/evidence/external/standard-rfc-snippets.txt +168 -0
- package/audit/evidence/external/standards-sources.txt +6 -0
- package/audit/evidence/runtime/baseline-commits.txt +2 -0
- package/audit/evidence/runtime/db-adapter-assumptions.txt +3 -0
- package/audit/evidence/runtime/environment.txt +4 -0
- package/audit/evidence/runtime/invariant-runtime-mapping.md +38 -0
- package/audit/evidence/runtime/probe-patch-relationship-error.exitcode +1 -0
- package/audit/evidence/runtime/probe-patch-relationship-error.log +76 -0
- package/audit/evidence/runtime/probe-patch-relationship-error.mjs +67 -0
- package/audit/evidence/runtime/probe-patch-relationship-referenceerror.exitcode +1 -0
- package/audit/evidence/runtime/probe-patch-relationship-referenceerror.log +62 -0
- package/audit/evidence/runtime/probe-patch-relationship-referenceerror.mjs +47 -0
- package/audit/evidence/runtime/test-exitcodes.txt +4 -0
- package/audit/evidence/runtime/test-summary.md +107 -0
- package/audit/evidence/runtime/trace-anchors.txt +61 -0
- package/audit/evidence/tests/access-multitenancy-test-signals.txt +125 -0
- package/audit/evidence/tests/failing-tests-summary.txt +25 -0
- package/audit/evidence/tests/npm-test-json-rest-api.exitcode +1 -0
- package/audit/evidence/tests/npm-test-json-rest-api.log +5885 -0
- package/audit/evidence/tests/npm-test-json-rest-schema.exitcode +1 -0
- package/audit/evidence/tests/npm-test-json-rest-schema.log +356 -0
- package/audit/evidence/tests/query-include-test-signals.txt +948 -0
- package/audit/evidence/tests/relationship-test-signals.txt +223 -0
- package/audit/evidence/tests/skipped-tests.txt +10 -0
- package/audit/evidence/tests/test-suite-list-non-anyapi.txt +30 -0
- package/audit/evidence/tests/test-suite-list.txt +32 -0
- package/audit/subsystems/docs-drift.md +49 -0
- package/audit/subsystems/pass2-cross-review.md +131 -0
- package/audit/subsystems/plugins-access.md +51 -0
- package/audit/subsystems/plugins-bulk.md +50 -0
- package/audit/subsystems/plugins-cors.md +48 -0
- package/audit/subsystems/plugins-file.md +53 -0
- package/audit/subsystems/plugins-multihome.md +50 -0
- package/audit/subsystems/plugins-positioning.md +54 -0
- package/audit/subsystems/plugins-socketio.md +50 -0
- package/audit/subsystems/query-engine.md +61 -0
- package/audit/subsystems/relationships.md +55 -0
- package/audit/subsystems/rest-orchestration.md +66 -0
- package/audit/subsystems/schema-core.md +64 -0
- package/audit/subsystems/storage-knex.md +57 -0
- package/audit/subsystems/tests.md +48 -0
- package/audit/subsystems/transport-express.md +55 -0
- package/audit/traces/trace-read-get.md +22 -0
- package/audit/traces/trace-read-query.md +25 -0
- package/audit/traces/trace-relationship-endpoints.md +31 -0
- package/audit/traces/trace-write-delete.md +17 -0
- package/audit/traces/trace-write-patch.md +26 -0
- package/audit/traces/trace-write-post.md +22 -0
- package/audit/traces/trace-write-put.md +23 -0
- package/package.json +3 -3
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
1 /**
|
|
2
|
+
2 * Calculates pagination metadata from query results
|
|
3
|
+
3 *
|
|
4
|
+
4 * @param {number} total - Total number of records in the dataset
|
|
5
|
+
5 * @param {number} page - Current page number (1-based)
|
|
6
|
+
6 * @param {number} pageSize - Number of records per page
|
|
7
|
+
7 * @returns {Object} Pagination metadata including page count and hasMore flag
|
|
8
|
+
8 * @throws {Error} If pageSize <= 0 or page < 1
|
|
9
|
+
9 *
|
|
10
|
+
10 * @example
|
|
11
|
+
11 * // Input: 100 total records, viewing page 2 of 20 records each
|
|
12
|
+
12 * const meta = calculatePaginationMeta(100, 2, 20);
|
|
13
|
+
13 * // Output:
|
|
14
|
+
14 * // {
|
|
15
|
+
15 * // page: 2, // current page
|
|
16
|
+
16 * // pageSize: 20, // records per page
|
|
17
|
+
17 * // pageCount: 5, // total pages (100/20)
|
|
18
|
+
18 * // total: 100, // total records
|
|
19
|
+
19 * // hasMore: true // page 2 < 5, so more pages exist
|
|
20
|
+
20 * // }
|
|
21
|
+
21 *
|
|
22
|
+
22 * @example
|
|
23
|
+
23 * // Input: Last page scenario
|
|
24
|
+
24 * const meta = calculatePaginationMeta(45, 5, 10);
|
|
25
|
+
25 * // Output:
|
|
26
|
+
26 * // {
|
|
27
|
+
27 * // page: 5,
|
|
28
|
+
28 * // pageSize: 10,
|
|
29
|
+
29 * // pageCount: 5, // 45/10 = 4.5, rounds up to 5
|
|
30
|
+
30 * // total: 45,
|
|
31
|
+
31 * // hasMore: false // on last page
|
|
32
|
+
32 * // }
|
|
33
|
+
33 *
|
|
34
|
+
34 * @description
|
|
35
|
+
35 * Used by:
|
|
36
|
+
36 * - rest-api-knex-plugin's dataQuery method after counting total records
|
|
37
|
+
37 * - generatePaginationLinks to build navigation links
|
|
38
|
+
38 * - Applied when offset-based pagination is enabled
|
|
39
|
+
39 *
|
|
40
|
+
40 * Purpose:
|
|
41
|
+
41 * - Provides consistent pagination metadata across all collection responses
|
|
42
|
+
42 * - Calculates derived values like pageCount and hasMore flag
|
|
43
|
+
43 * - Validates pagination parameters to prevent invalid states
|
|
44
|
+
44 *
|
|
45
|
+
45 * Data flow:
|
|
46
|
+
46 * 1. Query method counts total records when pagination.counts is enabled
|
|
47
|
+
47 * 2. calculatePaginationMeta processes the count with current page/size
|
|
48
|
+
48 * 3. Returns metadata that goes into response.meta.pagination
|
|
49
|
+
49 * 4. Used by generatePaginationLinks to determine which links to include
|
|
50
|
+
50 */
|
|
51
|
+
51 export const calculatePaginationMeta = (total, page, pageSize) => {
|
|
52
|
+
52 if (pageSize <= 0) {
|
|
53
|
+
53 throw new Error('Page size must be greater than 0')
|
|
54
|
+
54 }
|
|
55
|
+
55 if (page < 1) {
|
|
56
|
+
56 throw new Error('Page number must be greater than 0')
|
|
57
|
+
57 }
|
|
58
|
+
58
|
|
59
|
+
59 const pageCount = Math.ceil(total / pageSize)
|
|
60
|
+
60 const currentPage = page || 1
|
|
61
|
+
61 const hasMore = currentPage < pageCount
|
|
62
|
+
62
|
|
63
|
+
63 return {
|
|
64
|
+
64 page: currentPage,
|
|
65
|
+
65 pageSize,
|
|
66
|
+
66 pageCount,
|
|
67
|
+
67 total,
|
|
68
|
+
68 hasMore
|
|
69
|
+
69 }
|
|
70
|
+
70 }
|
|
71
|
+
71
|
|
72
|
+
72 /**
|
|
73
|
+
73 * Generates JSON:API compliant pagination links for offset-based pagination
|
|
74
|
+
74 *
|
|
75
|
+
75 * @param {string} urlPrefix - Base URL prefix for the API
|
|
76
|
+
76 * @param {string} scopeName - Resource type name
|
|
77
|
+
77 * @param {Object} queryParams - Current query parameters
|
|
78
|
+
78 * @param {Object} paginationMeta - Pagination metadata from calculatePaginationMeta
|
|
79
|
+
79 * @returns {Object|null} Links object with self, first, last, prev, next
|
|
80
|
+
80 *
|
|
81
|
+
81 * @example
|
|
82
|
+
82 * // Input data:
|
|
83
|
+
83 * const urlPrefix = '/api/v1';
|
|
84
|
+
84 * const scopeName = 'articles';
|
|
85
|
+
85 * const queryParams = {
|
|
86
|
+
86 * filter: { status: 'published' },
|
|
87
|
+
87 * sort: ['-created_at'],
|
|
88
|
+
88 * page: { number: 2, size: 20 }
|
|
89
|
+
89 * };
|
|
90
|
+
90 * const paginationMeta = {
|
|
91
|
+
91 * page: 2,
|
|
92
|
+
92 * pageSize: 20,
|
|
93
|
+
93 * pageCount: 5,
|
|
94
|
+
94 * total: 100,
|
|
95
|
+
95 * hasMore: true
|
|
96
|
+
96 * };
|
|
97
|
+
97 *
|
|
98
|
+
98 * const links = generatePaginationLinks(urlPrefix, scopeName, queryParams, paginationMeta);
|
|
99
|
+
99 *
|
|
100
|
+
100 * // Output - complete URLs with all parameters preserved:
|
|
101
|
+
101 * // {
|
|
102
|
+
102 * // self: '/api/articles?filter[status]=published&sort=-created_at&page[number]=2&page[size]=20',
|
|
103
|
+
103 * // first: '/api/articles?filter[status]=published&sort=-created_at&page[number]=1&page[size]=20',
|
|
104
|
+
104 * // last: '/api/articles?filter[status]=published&sort=-created_at&page[number]=5&page[size]=20',
|
|
105
|
+
105 * // prev: '/api/articles?filter[status]=published&sort=-created_at&page[number]=1&page[size]=20',
|
|
106
|
+
106 * // next: '/api/articles?filter[status]=published&sort=-created_at&page[number]=3&page[size]=20'
|
|
107
|
+
107 * // }
|
|
108
|
+
108 *
|
|
109
|
+
109 * @example
|
|
110
|
+
110 * // Input: First page (no prev link)
|
|
111
|
+
111 * const paginationMeta = { page: 1, pageSize: 10, pageCount: 3 };
|
|
112
|
+
112 * const links = generatePaginationLinks('/api', 'users', {}, paginationMeta);
|
|
113
|
+
113 *
|
|
114
|
+
114 * // Output - notice no 'prev' property:
|
|
115
|
+
115 * // {
|
|
116
|
+
116 * // self: '/api/users?page[number]=1&page[size]=10',
|
|
117
|
+
117 * // first: '/api/users?page[number]=1&page[size]=10',
|
|
118
|
+
118 * // last: '/api/users?page[number]=3&page[size]=10',
|
|
119
|
+
119 * // next: '/api/users?page[number]=2&page[size]=10'
|
|
120
|
+
120 * // }
|
|
121
|
+
121 *
|
|
122
|
+
122 * @description
|
|
123
|
+
123 * Used by:
|
|
124
|
+
124 * - rest-api-knex-plugin's dataQuery adds these links to response.links
|
|
125
|
+
125 * - Called when offset-based pagination is used (not cursor-based)
|
|
126
|
+
126 *
|
|
127
|
+
127 * Purpose:
|
|
128
|
+
128 * - JSON:API spec requires pagination links for easy navigation
|
|
129
|
+
129 * - Preserves all other query parameters (filters, sorts, includes)
|
|
130
|
+
130 * - Handles complex nested parameters like filter[author][name]=John
|
|
131
|
+
131 * - Only includes prev/next when applicable
|
|
132
|
+
132 *
|
|
133
|
+
133 * Data flow:
|
|
134
|
+
134 * 1. After records are fetched and pagination calculated
|
|
135
|
+
135 * 2. Builds complete URLs preserving all query parameters
|
|
136
|
+
136 * 3. Conditionally includes prev (not on page 1) and next (not on last page)
|
|
137
|
+
137 * 4. Added to response.links for client navigation
|
|
138
|
+
138 */
|
|
139
|
+
139 export const generatePaginationLinks = (urlPrefix, scopeName, queryParams, paginationMeta) => {
|
|
140
|
+
140 // Allow empty urlPrefix to generate relative links
|
|
141
|
+
141 // if (!urlPrefix) return null;
|
|
142
|
+
142
|
|
143
|
+
143 const { page, pageCount, pageSize } = paginationMeta
|
|
144
|
+
144 const links = {}
|
|
145
|
+
145
|
|
146
|
+
146 const otherParams = Object.entries(queryParams)
|
|
147
|
+
147 .filter(([key]) => key !== 'page')
|
|
148
|
+
148 .flatMap(([key, value]) => {
|
|
149
|
+
149 if (Array.isArray(value)) {
|
|
150
|
+
150 return value.length === 0
|
|
151
|
+
151 ? []
|
|
152
|
+
152 : value.map((v) => `${key}=${encodeURIComponent(v)}`)
|
|
153
|
+
153 }
|
|
154
|
+
154 if (typeof value === 'object' && value !== null) {
|
|
155
|
+
155 const parts = []
|
|
156
|
+
156 const processObject = (obj, prefix) => {
|
|
157
|
+
157 Object.entries(obj).forEach(([k, v]) => {
|
|
158
|
+
158 const newKey = prefix ? `${prefix}[${k}]` : `${key}[${k}]`
|
|
159
|
+
159 if (typeof v === 'object' && v !== null && !Array.isArray(v)) {
|
|
160
|
+
160 processObject(v, newKey)
|
|
161
|
+
161 } else if (v !== undefined && v !== null && !(Array.isArray(v) && v.length === 0)) {
|
|
162
|
+
162 parts.push(`${newKey}=${encodeURIComponent(v)}`)
|
|
163
|
+
163 }
|
|
164
|
+
164 })
|
|
165
|
+
165 }
|
|
166
|
+
166 processObject(value, key)
|
|
167
|
+
167 return parts
|
|
168
|
+
168 }
|
|
169
|
+
169 if (value === undefined || value === null) {
|
|
170
|
+
170 return []
|
|
171
|
+
171 }
|
|
172
|
+
172 return [`${key}=${encodeURIComponent(value)}`]
|
|
173
|
+
173 })
|
|
174
|
+
174 .filter(Boolean)
|
|
175
|
+
175 .join('&')
|
|
176
|
+
176
|
|
177
|
+
177 const baseUrl = `${urlPrefix}/${scopeName}`
|
|
178
|
+
178 const queryPrefix = otherParams ? `?${otherParams}&` : '?'
|
|
179
|
+
179
|
|
180
|
+
180 links.self = `${baseUrl}${queryPrefix}page[number]=${page}&page[size]=${pageSize}`
|
|
181
|
+
181
|
|
182
|
+
182 if (pageCount !== undefined) {
|
|
183
|
+
183 links.first = `${baseUrl}${queryPrefix}page[number]=1&page[size]=${pageSize}`
|
|
184
|
+
184 links.last = `${baseUrl}${queryPrefix}page[number]=${pageCount}&page[size]=${pageSize}`
|
|
185
|
+
185
|
|
186
|
+
186 if (page > 1) {
|
|
187
|
+
187 links.prev = `${baseUrl}${queryPrefix}page[number]=${page - 1}&page[size]=${pageSize}`
|
|
188
|
+
188 }
|
|
189
|
+
189
|
|
190
|
+
190 if (page < pageCount) {
|
|
191
|
+
191 links.next = `${baseUrl}${queryPrefix}page[number]=${page + 1}&page[size]=${pageSize}`
|
|
192
|
+
192 }
|
|
193
|
+
193 }
|
|
194
|
+
194
|
|
195
|
+
195 return links
|
|
196
|
+
196 }
|
|
197
|
+
197
|
|
198
|
+
198 /**
|
|
199
|
+
199 * Creates an opaque cursor string from record data for cursor-based pagination
|
|
200
|
+
200 *
|
|
201
|
+
201 * @param {Object} record - Database record to create cursor from
|
|
202
|
+
202 * @param {Array<string>} sortFields - Fields to include in cursor (default: ['id'])
|
|
203
|
+
203 * @returns {string} URL-safe cursor string
|
|
204
|
+
204 *
|
|
205
|
+
205 * @example
|
|
206
|
+
206 * // Input: Simple ID-based cursor
|
|
207
|
+
207 * const record = {
|
|
208
|
+
208 * id: 123,
|
|
209
|
+
209 * title: 'My Article',
|
|
210
|
+
210 * created_at: '2024-01-01T10:00:00Z'
|
|
211
|
+
211 * };
|
|
212
|
+
212 * const cursor = createCursor(record, ['id']);
|
|
213
|
+
213 *
|
|
214
|
+
214 * // Output: "id:123"
|
|
215
|
+
215 * // This cursor marks position at record with id=123
|
|
216
|
+
216 *
|
|
217
|
+
217 * @example
|
|
218
|
+
218 * // Input: Multi-field cursor for complex sorting
|
|
219
|
+
219 * const record = {
|
|
220
|
+
220 * id: 5,
|
|
221
|
+
221 * created_at: new Date('2024-01-15T08:30:00Z'),
|
|
222
|
+
222 * title: 'Article: Part 2'
|
|
223
|
+
223 * };
|
|
224
|
+
224 * const cursor = createCursor(record, ['created_at', 'id']);
|
|
225
|
+
225 *
|
|
226
|
+
226 * // Output: "created_at:2024-01-15T08%3A30%3A00.000Z,id:5"
|
|
227
|
+
227 * // URL-encoded to handle the colons in timestamp
|
|
228
|
+
228 * // Used for queries like: WHERE (created_at, id) > ('2024-01-15T08:30:00.000Z', 5)
|
|
229
|
+
229 *
|
|
230
|
+
230 * @example
|
|
231
|
+
231 * // Input: Handling special characters
|
|
232
|
+
232 * const record = {
|
|
233
|
+
233 * category: 'Tech & Science',
|
|
234
|
+
234 * title: 'AI: The Future?',
|
|
235
|
+
235 * id: 42
|
|
236
|
+
236 * };
|
|
237
|
+
237 * const cursor = createCursor(record, ['category', 'title', 'id']);
|
|
238
|
+
238 *
|
|
239
|
+
239 * // Output: "category:Tech%20%26%20Science,title:AI%3A%20The%20Future%3F,id:42"
|
|
240
|
+
240 * // Spaces encoded as %20, & as %26, : as %3A, ? as %3F
|
|
241
|
+
241 *
|
|
242
|
+
242 * @description
|
|
243
|
+
243 * Used by:
|
|
244
|
+
244 * - generateCursorPaginationLinks to create next/prev cursors
|
|
245
|
+
245 * - buildCursorMeta to provide cursor in response metadata
|
|
246
|
+
246 *
|
|
247
|
+
247 * Purpose:
|
|
248
|
+
248 * - Cursor-based pagination is more stable than offset for changing data
|
|
249
|
+
249 * - Encodes multiple sort fields to maintain stable ordering
|
|
250
|
+
250 * - URL-encodes values to handle special characters safely
|
|
251
|
+
251 * - Simple format that's easy to parse back
|
|
252
|
+
252 *
|
|
253
|
+
253 * Data flow:
|
|
254
|
+
254 * 1. After fetching records, takes the last record
|
|
255
|
+
255 * 2. Extracts values for all sort fields
|
|
256
|
+
256 * 3. Creates cursor encoding those values
|
|
257
|
+
257 * 4. Cursor used in 'next' link for fetching subsequent pages
|
|
258
|
+
258 * 5. Enables efficient "WHERE (field1, field2) > (val1, val2)" queries
|
|
259
|
+
259 */
|
|
260
|
+
260 export const createCursor = (record, sortFields = ['id']) => {
|
|
261
|
+
261 const parts = []
|
|
262
|
+
262 sortFields.forEach(field => {
|
|
263
|
+
263 if (record[field] !== undefined) {
|
|
264
|
+
264 const value = record[field]
|
|
265
|
+
265 const stringValue = value instanceof Date ? value.toISOString() : String(value)
|
|
266
|
+
266 parts.push(`${field}:${encodeURIComponent(stringValue)}`)
|
|
267
|
+
267 }
|
|
268
|
+
268 })
|
|
269
|
+
269 return parts.join(',')
|
|
270
|
+
270 }
|
|
271
|
+
271
|
|
272
|
+
272 /**
|
|
273
|
+
273 * Parses a cursor string back into field/value pairs
|
|
274
|
+
274 *
|
|
275
|
+
275 * @param {string} cursor - Cursor string to parse
|
|
276
|
+
276 * @returns {Object} Object with field names as keys and decoded values
|
|
277
|
+
277 * @throws {Error} If cursor format is invalid
|
|
278
|
+
278 *
|
|
279
|
+
279 * @example
|
|
280
|
+
280 * // Input: Simple cursor
|
|
281
|
+
281 * const cursor = "id:123";
|
|
282
|
+
282 * const data = parseCursor(cursor);
|
|
283
|
+
283 *
|
|
284
|
+
284 * // Output: { id: "123" }
|
|
285
|
+
285 * // Ready to use in SQL: WHERE id > '123'
|
|
286
|
+
286 *
|
|
287
|
+
287 * @example
|
|
288
|
+
288 * // Input: Multi-field cursor with URL-encoded values
|
|
289
|
+
289 * const cursor = "created_at:2024-01-15T08%3A30%3A00.000Z,id:5";
|
|
290
|
+
290 * const data = parseCursor(cursor);
|
|
291
|
+
291 *
|
|
292
|
+
292 * // Output:
|
|
293
|
+
293 * // {
|
|
294
|
+
294 * // created_at: "2024-01-15T08:30:00.000Z", // Decoded
|
|
295
|
+
295 * // id: "5"
|
|
296
|
+
296 * // }
|
|
297
|
+
297 * // Used for: WHERE (created_at, id) > ('2024-01-15T08:30:00.000Z', '5')
|
|
298
|
+
298 *
|
|
299
|
+
299 * @example
|
|
300
|
+
300 * // Input: Invalid cursor (throws error)
|
|
301
|
+
301 * try {
|
|
302
|
+
302 * const data = parseCursor("invalid-no-colon");
|
|
303
|
+
303 * } catch (e) {
|
|
304
|
+
304 * console.log(e.message);
|
|
305
|
+
305 * // "Invalid cursor format: Invalid cursor format: missing colon separator"
|
|
306
|
+
306 * }
|
|
307
|
+
307 *
|
|
308
|
+
308 * @description
|
|
309
|
+
309 * Used by:
|
|
310
|
+
310 * - rest-api-knex-plugin's dataQuery when processing page[after] parameter
|
|
311
|
+
311 * - Used to build WHERE clause for cursor-based queries
|
|
312
|
+
312 *
|
|
313
|
+
313 * Purpose:
|
|
314
|
+
314 * - Decodes cursor back to usable values for SQL queries
|
|
315
|
+
315 * - Handles URL-encoded special characters
|
|
316
|
+
316 * - Validates cursor format to prevent injection attacks
|
|
317
|
+
317 *
|
|
318
|
+
318 * Data flow:
|
|
319
|
+
319 * 1. Client sends page[after]=cursor parameter
|
|
320
|
+
320 * 2. parseCursor extracts field values from cursor
|
|
321
|
+
321 * 3. Values used to build WHERE clause like "WHERE id > 123"
|
|
322
|
+
322 * 4. Ensures pagination continues from exact position
|
|
323
|
+
323 */
|
|
324
|
+
324 export const parseCursor = (cursor) => {
|
|
325
|
+
325 try {
|
|
326
|
+
326 const data = {}
|
|
327
|
+
327 if (!cursor || cursor.trim() === '') {
|
|
328
|
+
328 throw new Error('Empty cursor')
|
|
329
|
+
329 }
|
|
330
|
+
330
|
|
331
|
+
331 const pairs = cursor.split(',')
|
|
332
|
+
332
|
|
333
|
+
333 for (const pair of pairs) {
|
|
334
|
+
334 const colonIndex = pair.indexOf(':')
|
|
335
|
+
335 if (colonIndex === -1) {
|
|
336
|
+
336 throw new Error('Invalid cursor format: missing colon separator')
|
|
337
|
+
337 }
|
|
338
|
+
338
|
|
339
|
+
339 const field = pair.substring(0, colonIndex)
|
|
340
|
+
340 const encodedValue = pair.substring(colonIndex + 1)
|
|
341
|
+
341
|
|
342
|
+
342 if (!field) {
|
|
343
|
+
343 throw new Error('Invalid cursor format: empty field name')
|
|
344
|
+
344 }
|
|
345
|
+
345
|
|
346
|
+
346 data[field] = decodeURIComponent(encodedValue)
|
|
347
|
+
347 }
|
|
348
|
+
348
|
|
349
|
+
349 return data
|
|
350
|
+
350 } catch (e) {
|
|
351
|
+
351 throw new Error(`Invalid cursor format: ${e.message}`)
|
|
352
|
+
352 }
|
|
353
|
+
353 }
|
|
354
|
+
354
|
|
355
|
+
355 /**
|
|
356
|
+
356 * Generates pagination links for cursor-based pagination
|
|
357
|
+
357 *
|
|
358
|
+
358 * @param {string} urlPrefix - Base URL prefix
|
|
359
|
+
359 * @param {string} scopeName - Resource type name
|
|
360
|
+
360 * @param {Object} queryParams - Current query parameters
|
|
361
|
+
361 * @param {Array<Object>} records - Current page records
|
|
362
|
+
362 * @param {number} pageSize - Records per page
|
|
363
|
+
363 * @param {boolean} hasMore - Whether more records exist
|
|
364
|
+
364 * @param {Array<string>} sortFields - Fields used for cursor
|
|
365
|
+
365 * @returns {Object|null} Links object with self, first, next
|
|
366
|
+
366 *
|
|
367
|
+
367 * @example
|
|
368
|
+
368 * // Input: First page of results
|
|
369
|
+
369 * const urlPrefix = '/api/v1';
|
|
370
|
+
370 * const scopeName = 'articles';
|
|
371
|
+
371 * const queryParams = {
|
|
372
|
+
372 * filter: { status: 'published' },
|
|
373
|
+
373 * page: { size: 20 }
|
|
374
|
+
374 * };
|
|
375
|
+
375 * const records = [
|
|
376
|
+
376 * { id: 1, created_at: '2024-01-01', title: 'First' },
|
|
377
|
+
377 * { id: 2, created_at: '2024-01-02', title: 'Second' },
|
|
378
|
+
378 * { id: 3, created_at: '2024-01-03', title: 'Third' }
|
|
379
|
+
379 * ];
|
|
380
|
+
380 * const hasMore = true; // More records exist
|
|
381
|
+
381 *
|
|
382
|
+
382 * const links = generateCursorPaginationLinks(
|
|
383
|
+
383 * urlPrefix, scopeName, queryParams, records, 20, hasMore, ['created_at', 'id']
|
|
384
|
+
384 * );
|
|
385
|
+
385 *
|
|
386
|
+
386 * // Output:
|
|
387
|
+
387 * // {
|
|
388
|
+
388 * // self: '/api/v1/articles?filter[status]=published&page[size]=20',
|
|
389
|
+
389 * // first: '/api/v1/articles?filter[status]=published&page[size]=20',
|
|
390
|
+
390 * // next: '/api/v1/articles?filter[status]=published&page[size]=20&page[after]=created_at:2024-01-03,id:3'
|
|
391
|
+
391 * // }
|
|
392
|
+
392 * // The 'next' cursor points after the last record (id:3)
|
|
393
|
+
393 *
|
|
394
|
+
394 * @example
|
|
395
|
+
395 * // Input: Last page (no more records)
|
|
396
|
+
396 * const records = [
|
|
397
|
+
397 * { id: 98, created_at: '2024-03-01' },
|
|
398
|
+
398 * { id: 99, created_at: '2024-03-02' }
|
|
399
|
+
399 * ];
|
|
400
|
+
400 * const hasMore = false; // No more records
|
|
401
|
+
401 *
|
|
402
|
+
402 * const links = generateCursorPaginationLinks(
|
|
403
|
+
403 * '/api', 'articles', {}, records, 20, hasMore, ['id']
|
|
404
|
+
404 * );
|
|
405
|
+
405 *
|
|
406
|
+
406 * // Output - no 'next' link:
|
|
407
|
+
407 * // {
|
|
408
|
+
408 * // self: '/api/articles?page[size]=20',
|
|
409
|
+
409 * // first: '/api/articles?page[size]=20'
|
|
410
|
+
410 * // }
|
|
411
|
+
411 *
|
|
412
|
+
412 * @description
|
|
413
|
+
413 * Used by:
|
|
414
|
+
414 * - rest-api-knex-plugin when cursor pagination is enabled
|
|
415
|
+
415 * - Called after fetching records with cursor-based query
|
|
416
|
+
416 *
|
|
417
|
+
417 * Purpose:
|
|
418
|
+
418 * - Cursor pagination is more efficient for large datasets
|
|
419
|
+
419 * - Stable pagination when data is being added/removed
|
|
420
|
+
420 * - Only includes 'next' link when more data exists
|
|
421
|
+
421 * - Preserves all other query parameters
|
|
422
|
+
422 *
|
|
423
|
+
423 * Data flow:
|
|
424
|
+
424 * 1. After cursor-based query fetches records
|
|
425
|
+
425 * 2. If hasMore is true, creates cursor from last record
|
|
426
|
+
426 * 3. Builds next link with page[after] parameter
|
|
427
|
+
427 * 4. Added to response.links for navigation
|
|
428
|
+
428 * 5. Client uses next link to fetch subsequent pages
|
|
429
|
+
429 */
|
|
430
|
+
430 export const generateCursorPaginationLinks = (urlPrefix, scopeName, queryParams, records, pageSize, hasMore, sortFields = ['id']) => {
|
|
431
|
+
431 // Allow empty urlPrefix to generate relative links
|
|
432
|
+
432 if (!records.length) return null
|
|
433
|
+
433
|
|
434
|
+
434 const links = {}
|
|
435
|
+
435 const baseUrl = `${urlPrefix}/${scopeName}`
|
|
436
|
+
436
|
|
437
|
+
437 const otherParams = Object.entries(queryParams)
|
|
438
|
+
438 .filter(([key]) => key !== 'page')
|
|
439
|
+
439 .flatMap(([key, value]) => {
|
|
440
|
+
440 if (Array.isArray(value)) {
|
|
441
|
+
441 return value.length === 0
|
|
442
|
+
442 ? []
|
|
443
|
+
443 : value.map((v) => `${key}=${encodeURIComponent(v)}`)
|
|
444
|
+
444 }
|
|
445
|
+
445 if (typeof value === 'object' && value !== null) {
|
|
446
|
+
446 const parts = []
|
|
447
|
+
447 const processObject = (obj, prefix) => {
|
|
448
|
+
448 Object.entries(obj).forEach(([k, v]) => {
|
|
449
|
+
449 const newKey = prefix ? `${prefix}[${k}]` : `${key}[${k}]`
|
|
450
|
+
450 if (typeof v === 'object' && v !== null && !Array.isArray(v)) {
|
|
451
|
+
451 processObject(v, newKey)
|
|
452
|
+
452 } else if (v !== undefined && v !== null && !(Array.isArray(v) && v.length === 0)) {
|
|
453
|
+
453 parts.push(`${newKey}=${encodeURIComponent(v)}`)
|
|
454
|
+
454 }
|
|
455
|
+
455 })
|
|
456
|
+
456 }
|
|
457
|
+
457 processObject(value, key)
|
|
458
|
+
458 return parts
|
|
459
|
+
459 }
|
|
460
|
+
460 if (value === undefined || value === null) {
|
|
461
|
+
461 return []
|
|
462
|
+
462 }
|
|
463
|
+
463 return [`${key}=${encodeURIComponent(value)}`]
|
|
464
|
+
464 })
|
|
465
|
+
465 .filter(Boolean)
|
|
466
|
+
466 .join('&')
|
|
467
|
+
467
|
|
468
|
+
468 const queryPrefix = otherParams ? `?${otherParams}&` : '?'
|
|
469
|
+
469
|
|
470
|
+
470 if (queryParams.page?.after) {
|
|
471
|
+
471 links.self = `${baseUrl}${queryPrefix}page[size]=${pageSize}&page[after]=${queryParams.page.after}`
|
|
472
|
+
472 } else if (queryParams.page?.before) {
|
|
473
|
+
473 links.self = `${baseUrl}${queryPrefix}page[size]=${pageSize}&page[before]=${queryParams.page.before}`
|
|
474
|
+
474 } else {
|
|
475
|
+
475 links.self = `${baseUrl}${queryPrefix}page[size]=${pageSize}`
|
|
476
|
+
476 }
|
|
477
|
+
477
|
|
478
|
+
478 links.first = `${baseUrl}${queryPrefix}page[size]=${pageSize}`
|
|
479
|
+
479
|
|
480
|
+
480 if (hasMore && records.length > 0) {
|
|
481
|
+
481 const lastRecord = records[records.length - 1]
|
|
482
|
+
482 const nextCursor = createCursor(lastRecord, sortFields)
|
|
483
|
+
483 links.next = `${baseUrl}${queryPrefix}page[size]=${pageSize}&page[after]=${nextCursor}`
|
|
484
|
+
484 }
|
|
485
|
+
485
|
|
486
|
+
486 return links
|
|
487
|
+
487 }
|
|
488
|
+
488
|
|
489
|
+
489 /**
|
|
490
|
+
490 * Builds cursor pagination metadata for the response
|
|
491
|
+
491 *
|
|
492
|
+
492 * @param {Array<Object>} records - Current page records
|
|
493
|
+
493 * @param {number} pageSize - Records per page
|
|
494
|
+
494 * @param {boolean} hasMore - Whether more records exist
|
|
495
|
+
495 * @param {Array<string>} sortFields - Fields used for cursor
|
|
496
|
+
496 * @returns {Object} Metadata with pageSize, hasMore, and optional cursor
|
|
497
|
+
497 *
|
|
498
|
+
498 * @example
|
|
499
|
+
499 * // Input: Page with more records available
|
|
500
|
+
500 * const records = [
|
|
501
|
+
501 * { id: 10, created_at: '2024-01-10', title: 'Article 10' },
|
|
502
|
+
502 * { id: 11, created_at: '2024-01-11', title: 'Article 11' },
|
|
503
|
+
503 * { id: 12, created_at: '2024-01-12', title: 'Article 12' }
|
|
504
|
+
504 * ];
|
|
505
|
+
505 * const meta = buildCursorMeta(records, 20, true, ['created_at', 'id']);
|
|
506
|
+
506 *
|
|
507
|
+
507 * // Output:
|
|
508
|
+
508 * // {
|
|
509
|
+
509 * // pageSize: 20,
|
|
510
|
+
510 * // hasMore: true,
|
|
511
|
+
511 * // cursor: {
|
|
512
|
+
512 * // next: 'created_at:2024-01-12,id:12' // Cursor from last record
|
|
513
|
+
513 * // }
|
|
514
|
+
514 * // }
|
|
515
|
+
515 *
|
|
516
|
+
516 * @example
|
|
517
|
+
517 * // Input: Last page (no more records)
|
|
518
|
+
518 * const records = [
|
|
519
|
+
519 * { id: 98, name: 'Last Item' }
|
|
520
|
+
520 * ];
|
|
521
|
+
521 * const meta = buildCursorMeta(records, 20, false, ['id']);
|
|
522
|
+
522 *
|
|
523
|
+
523 * // Output:
|
|
524
|
+
524 * // {
|
|
525
|
+
525 * // pageSize: 20,
|
|
526
|
+
526 * // hasMore: false
|
|
527
|
+
527 * // // No cursor property since hasMore is false
|
|
528
|
+
528 * // }
|
|
529
|
+
529 *
|
|
530
|
+
530 * @description
|
|
531
|
+
531 * Used by:
|
|
532
|
+
532 * - rest-api-knex-plugin adds this to response.meta for cursor pagination
|
|
533
|
+
533 * - Provides cursor that client can use directly if needed
|
|
534
|
+
534 *
|
|
535
|
+
535 * Purpose:
|
|
536
|
+
536 * - Gives clients direct access to cursor for custom pagination
|
|
537
|
+
537 * - Indicates whether more pages exist without counting
|
|
538
|
+
538 * - Simpler than offset pagination metadata (no total count)
|
|
539
|
+
539 *
|
|
540
|
+
540 * Data flow:
|
|
541
|
+
541 * 1. Called after fetching records with cursor query
|
|
542
|
+
542 * 2. Creates cursor from last record if more exist
|
|
543
|
+
543 * 3. Added to response.meta.pagination
|
|
544
|
+
544 * 4. Clients can use cursor directly or use the next link
|
|
545
|
+
545 */
|
|
546
|
+
546 export const buildCursorMeta = (records, pageSize, hasMore, sortFields = ['id']) => {
|
|
547
|
+
547 const meta = {
|
|
548
|
+
548 pageSize,
|
|
549
|
+
549 hasMore
|
|
550
|
+
550 }
|
|
551
|
+
551
|
|
552
|
+
552 if (hasMore && records.length > 0) {
|
|
553
|
+
553 const lastRecord = records[records.length - 1]
|
|
554
|
+
554 meta.cursor = {
|
|
555
|
+
555 next: createCursor(lastRecord, sortFields)
|
|
556
|
+
556 }
|
|
557
|
+
557 }
|
|
558
|
+
558
|
|
559
|
+
559 return meta
|
|
560
|
+
560 }
|