json-rest-api 1.0.22 → 1.0.23

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.
Files changed (260) hide show
  1. package/CLAUDE.md +1 -2
  2. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +180 -0
  3. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +172 -0
  4. package/docs/API.md +51 -14
  5. package/docs/COMPARISON.md +2 -2
  6. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +28 -1
  7. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +5 -1
  8. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +3 -1
  9. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +12 -60
  10. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +2 -2
  11. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -1
  12. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +2 -0
  13. package/docs/GUIDE/GUIDE_X_Autofiltering.md +355 -0
  14. package/docs/GUIDE/GUIDE_X_Fastify.md +78 -0
  15. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +250 -0
  16. package/docs/GUIDE/GUIDE_X_Query_Projections.md +159 -0
  17. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +199 -0
  18. package/docs/GUIDE/index.md +18 -9
  19. package/docs/QUICKSTART.md +5 -1
  20. package/docs/index.md +1 -1
  21. package/index.js +3 -2
  22. package/package.json +2 -2
  23. package/plugins/core/connectors/express-plugin.js +97 -184
  24. package/plugins/core/connectors/fastify-plugin.js +206 -0
  25. package/plugins/core/connectors/lib/connector-core.js +195 -0
  26. package/plugins/core/connectors/lib/transport-http-helpers.js +122 -0
  27. package/plugins/core/connectors/lib/transport-route-schemas.js +92 -0
  28. package/plugins/core/lib/dbIntrospection.js +1078 -0
  29. package/plugins/core/lib/dbTablesOperations.js +1104 -160
  30. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +5 -2
  31. package/plugins/core/lib/querying/knex-process-includes.js +3 -4
  32. package/plugins/core/lib/querying/knex-query-helpers.js +17 -71
  33. package/plugins/core/lib/querying/knex-relationship-includes.js +466 -206
  34. package/plugins/core/lib/querying/query-builder-utils.js +19 -0
  35. package/plugins/core/lib/querying/query-field-sort-helpers.js +135 -0
  36. package/plugins/core/lib/querying/sort-helpers.js +33 -0
  37. package/plugins/core/lib/querying/storage-adapter-utils.js +51 -0
  38. package/plugins/core/lib/querying/url-helpers.js +8 -2
  39. package/plugins/core/lib/querying-writing/compile-schemas.js +12 -1
  40. package/plugins/core/lib/querying-writing/knex-field-helpers.js +83 -13
  41. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +13 -12
  42. package/plugins/core/lib/querying-writing/query-field-helpers.js +88 -0
  43. package/plugins/core/lib/querying-writing/request-contracts.js +590 -0
  44. package/plugins/core/lib/storage/canonical-storage-mapping.js +113 -0
  45. package/plugins/core/lib/storage/storage-adapter.js +79 -2
  46. package/plugins/core/lib/storage/storage-mapping.js +204 -0
  47. package/plugins/core/rest-api-anyapi-knex-plugin.js +355 -228
  48. package/plugins/core/rest-api-autofilter-plugin.js +414 -0
  49. package/plugins/core/rest-api-knex-plugin.js +249 -211
  50. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +10 -5
  51. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +12 -6
  52. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +41 -0
  53. package/plugins/core/rest-api-plugin-methods/common.js +121 -83
  54. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +6 -1
  55. package/plugins/core/rest-api-plugin-methods/get.js +23 -14
  56. package/plugins/core/rest-api-plugin-methods/patch.js +28 -12
  57. package/plugins/core/rest-api-plugin-methods/post.js +13 -8
  58. package/plugins/core/rest-api-plugin-methods/put.js +45 -21
  59. package/plugins/core/rest-api-plugin-methods/query.js +15 -53
  60. package/plugins/core/rest-api-plugin.js +2 -0
  61. package/plugins/core/rest-api-positioning-plugin.js +1 -1
  62. package/plugins/core/rest-api-query-projections-plugin.js +130 -0
  63. package/plugins/core/socketio-plugin.js +1 -3
  64. package/tests/autofilter.test.js +463 -0
  65. package/tests/custom-id-simplified-post.test.js +75 -0
  66. package/tests/custom-idproperty-relationships.test.js +33 -0
  67. package/tests/db-introspection.test.js +518 -0
  68. package/tests/db-table-operations.test.js +781 -0
  69. package/tests/fastify-plugin.test.js +141 -0
  70. package/tests/field-getters.test.js +1 -2
  71. package/tests/field-setters.test.js +0 -4
  72. package/tests/fixtures/api-configs.js +188 -219
  73. package/tests/helpers/fake-fastify.js +99 -0
  74. package/tests/http-connectors-parity.test.js +235 -0
  75. package/tests/projected-fields.test.js +319 -0
  76. package/tests/relationships.test.js +3 -3
  77. package/tests/searchschema-knex-behavior.test.js +187 -0
  78. package/tests/searchschema-merge.test.js +21 -69
  79. package/tests/storage-mapping.test.js +210 -0
  80. package/audit/00-charter.md +0 -103
  81. package/audit/01-scope-map.md +0 -84
  82. package/audit/02-coverage-matrix.csv +0 -174
  83. package/audit/03-invariants.md +0 -123
  84. package/audit/04-standards-rubric.md +0 -56
  85. package/audit/05-competitor-rubric.md +0 -77
  86. package/audit/06-findings.md +0 -160
  87. package/audit/07-risk-register.csv +0 -10
  88. package/audit/08-remediation-roadmap.md +0 -177
  89. package/audit/09-signoff-check.md +0 -23
  90. package/audit/10-final-synthesis.md +0 -29
  91. package/audit/evidence/code/all-files-json-rest-api.txt +0 -168
  92. package/audit/evidence/code/all-files-json-rest-schema.txt +0 -12
  93. package/audit/evidence/code/boundary-leak-checks.txt +0 -72
  94. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__bulk-operations-plugin.js.txt +0 -480
  95. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__express-plugin.js.txt +0 -532
  96. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__busboy-detector.js.txt +0 -175
  97. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__formidable-detector.js.txt +0 -138
  98. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__connectors__lib__request-helpers.js.txt +0 -222
  99. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__file-handling-plugin.js.txt +0 -342
  100. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__dbTablesOperations.js.txt +0 -304
  101. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__fractional-positioning.js.txt +0 -188
  102. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__compile-schemas.js.txt +0 -336
  103. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__connectors-query-parser.js.txt +0 -165
  104. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-capabilities.js.txt +0 -265
  105. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__database-value-normalizers.js.txt +0 -369
  106. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__default-data-helpers.js.txt +0 -72
  107. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__field-utils.js.txt +0 -139
  108. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-constants.js.txt +0 -45
  109. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-field-helpers.js.txt +0 -396
  110. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__knex-json-api-transformers.js.txt +0 -162
  111. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__payload-validators.js.txt +0 -1314
  112. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__schema-helpers.js.txt +0 -488
  113. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__scope-validations.js.txt +0 -484
  114. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying-writing__simplified-helpers.js.txt +0 -857
  115. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-cross-table-search.js.txt +0 -565
  116. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-json-api-transformers-querying.js.txt +0 -485
  117. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-pagination-helpers.js.txt +0 -560
  118. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-process-includes.js.txt +0 -167
  119. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers-base.js.txt +0 -109
  120. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-query-helpers.js.txt +0 -1064
  121. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-relationship-includes.js.txt +0 -1983
  122. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__knex-window-queries.js.txt +0 -328
  123. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__querying__url-helpers.js.txt +0 -119
  124. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__storage__storage-adapter.js.txt +0 -160
  125. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__many-to-many-manipulations.js.txt +0 -300
  126. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__lib__writing__relationship-processor.js.txt +0 -245
  127. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-access.js.txt +0 -459
  128. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-cors-plugin.js.txt +0 -237
  129. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-knex-plugin.js.txt +0 -1135
  130. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-multihome-plugin.js.txt +0 -314
  131. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__compile-resource-schemas.js.txt +0 -7
  132. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-relationship-routes.js.txt +0 -71
  133. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__register-scope-routes.js.txt +0 -102
  134. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__turn-scope-init-into-vars.js.txt +0 -49
  135. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-hooks__validate-include-configurations.js.txt +0 -39
  136. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__add-route.js.txt +0 -23
  137. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__check-permissions.js.txt +0 -13
  138. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__common.js.txt +0 -639
  139. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete-relationship.js.txt +0 -117
  140. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__delete.js.txt +0 -104
  141. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__enrich-attributes.js.txt +0 -199
  142. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-related.js.txt +0 -353
  143. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get-relationship.js.txt +0 -61
  144. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__get.js.txt +0 -184
  145. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch-relationship.js.txt +0 -55
  146. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__patch.js.txt +0 -191
  147. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post-relationship.js.txt +0 -110
  148. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__post.js.txt +0 -168
  149. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__put.js.txt +0 -253
  150. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__query.js.txt +0 -207
  151. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin-methods__release.js.txt +0 -12
  152. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-plugin.js.txt +0 -165
  153. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__rest-api-positioning-plugin.js.txt +0 -668
  154. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__core__socketio-plugin.js.txt +0 -621
  155. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__local-storage.js.txt +0 -218
  156. package/audit/evidence/code/critical-pass1/json-rest-api__plugins__storage__s3-storage.js.txt +0 -124
  157. package/audit/evidence/code/critical-pass1/json-rest-schema__src__CorePlugin.js.txt +0 -269
  158. package/audit/evidence/code/critical-pass1/json-rest-schema__src__Schema.js.txt +0 -242
  159. package/audit/evidence/code/critical-pass1/json-rest-schema__src__index.js.txt +0 -77
  160. package/audit/evidence/code/critical-pass1-risk-keywords.txt +0 -238
  161. package/audit/evidence/code/critical-pass1-summary.csv +0 -67
  162. package/audit/evidence/code/critical-pass2-summary.csv +0 -67
  163. package/audit/evidence/code/early-silent-zones-no-test-token.txt +0 -57
  164. package/audit/evidence/code/error-model-signals.txt +0 -98
  165. package/audit/evidence/code/git-files-json-rest-api.txt +0 -171
  166. package/audit/evidence/code/git-files-json-rest-schema.txt +0 -27
  167. package/audit/evidence/code/hook-flow-signals.txt +0 -282
  168. package/audit/evidence/code/idproperty-signals.txt +0 -157
  169. package/audit/evidence/code/import-cycles-non-anyapi.txt +0 -3
  170. package/audit/evidence/code/import-hotspots-non-anyapi.txt +0 -96
  171. package/audit/evidence/code/imports-json-rest-api.txt +0 -354
  172. package/audit/evidence/code/imports-json-rest-schema.txt +0 -15
  173. package/audit/evidence/code/in-scope-files-json-rest-api.txt +0 -160
  174. package/audit/evidence/code/in-scope-files-json-rest-schema.txt +0 -13
  175. package/audit/evidence/code/non-anyapi-core-files.txt +0 -101
  176. package/audit/evidence/code/permission-signals.txt +0 -193
  177. package/audit/evidence/code/query-include-signals.txt +0 -687
  178. package/audit/evidence/code/transaction-signals.txt +0 -28
  179. package/audit/evidence/code/validation-signals.txt +0 -650
  180. package/audit/evidence/code/write-path-signals.txt +0 -61
  181. package/audit/evidence/docs/docs-claims-signals.txt +0 -1075
  182. package/audit/evidence/docs/schema-docs-claims-signals.txt +0 -179
  183. package/audit/evidence/external/corpus-freeze.md +0 -21
  184. package/audit/evidence/external/npm-competitor-metadata.txt +0 -70
  185. package/audit/evidence/external/npm-download-signals.txt +0 -23
  186. package/audit/evidence/external/npm-jsonapi-server.txt +0 -8
  187. package/audit/evidence/external/npm-modern-jsonapi-metadata.txt +0 -38
  188. package/audit/evidence/external/readme-fortune-signals.txt +0 -26
  189. package/audit/evidence/external/readme-fortune.md +0 -165
  190. package/audit/evidence/external/readme-json-api-serializer-signals.txt +0 -80
  191. package/audit/evidence/external/readme-json-api-serializer.md +0 -690
  192. package/audit/evidence/external/readme-jsonapi-serde-server-signals.txt +0 -0
  193. package/audit/evidence/external/readme-jsonapi-serde-server.md +0 -22
  194. package/audit/evidence/external/readme-jsonapi-serializer-signals.txt +0 -63
  195. package/audit/evidence/external/readme-jsonapi-serializer.md +0 -315
  196. package/audit/evidence/external/readme-jsonapi-server-signals.txt +0 -19
  197. package/audit/evidence/external/readme-jsonapi-server.md +0 -104
  198. package/audit/evidence/external/readme-nestjs-json-api-signals.txt +0 -4
  199. package/audit/evidence/external/readme-nestjs-json-api.md +0 -94
  200. package/audit/evidence/external/readme-yaser-json-api-nestjs-signals.txt +0 -47
  201. package/audit/evidence/external/readme-yaser-json-api-nestjs.md +0 -447
  202. package/audit/evidence/external/standard-jsonapi-snippets.txt +0 -84
  203. package/audit/evidence/external/standard-rfc-snippets.txt +0 -168
  204. package/audit/evidence/external/standards-sources.txt +0 -6
  205. package/audit/evidence/runtime/baseline-commits.txt +0 -2
  206. package/audit/evidence/runtime/db-adapter-assumptions.txt +0 -3
  207. package/audit/evidence/runtime/environment.txt +0 -4
  208. package/audit/evidence/runtime/invariant-runtime-mapping.md +0 -38
  209. package/audit/evidence/runtime/probe-patch-relationship-error.exitcode +0 -1
  210. package/audit/evidence/runtime/probe-patch-relationship-error.log +0 -76
  211. package/audit/evidence/runtime/probe-patch-relationship-error.mjs +0 -67
  212. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.exitcode +0 -1
  213. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.log +0 -62
  214. package/audit/evidence/runtime/probe-patch-relationship-referenceerror.mjs +0 -47
  215. package/audit/evidence/runtime/test-exitcodes.txt +0 -4
  216. package/audit/evidence/runtime/test-summary.md +0 -107
  217. package/audit/evidence/runtime/trace-anchors.txt +0 -61
  218. package/audit/evidence/tests/access-multitenancy-test-signals.txt +0 -125
  219. package/audit/evidence/tests/failing-tests-summary.txt +0 -25
  220. package/audit/evidence/tests/npm-test-json-rest-api.exitcode +0 -1
  221. package/audit/evidence/tests/npm-test-json-rest-api.log +0 -5885
  222. package/audit/evidence/tests/npm-test-json-rest-schema.exitcode +0 -1
  223. package/audit/evidence/tests/npm-test-json-rest-schema.log +0 -356
  224. package/audit/evidence/tests/query-include-test-signals.txt +0 -948
  225. package/audit/evidence/tests/relationship-test-signals.txt +0 -223
  226. package/audit/evidence/tests/skipped-tests.txt +0 -10
  227. package/audit/evidence/tests/test-suite-list-non-anyapi.txt +0 -30
  228. package/audit/evidence/tests/test-suite-list.txt +0 -32
  229. package/audit/subsystems/docs-drift.md +0 -49
  230. package/audit/subsystems/pass2-cross-review.md +0 -131
  231. package/audit/subsystems/plugins-access.md +0 -51
  232. package/audit/subsystems/plugins-bulk.md +0 -50
  233. package/audit/subsystems/plugins-cors.md +0 -48
  234. package/audit/subsystems/plugins-file.md +0 -53
  235. package/audit/subsystems/plugins-multihome.md +0 -50
  236. package/audit/subsystems/plugins-positioning.md +0 -54
  237. package/audit/subsystems/plugins-socketio.md +0 -50
  238. package/audit/subsystems/query-engine.md +0 -61
  239. package/audit/subsystems/relationships.md +0 -55
  240. package/audit/subsystems/rest-orchestration.md +0 -66
  241. package/audit/subsystems/schema-core.md +0 -64
  242. package/audit/subsystems/storage-knex.md +0 -57
  243. package/audit/subsystems/tests.md +0 -48
  244. package/audit/subsystems/transport-express.md +0 -55
  245. package/audit/traces/trace-read-get.md +0 -22
  246. package/audit/traces/trace-read-query.md +0 -25
  247. package/audit/traces/trace-relationship-endpoints.md +0 -31
  248. package/audit/traces/trace-write-delete.md +0 -17
  249. package/audit/traces/trace-write-patch.md +0 -26
  250. package/audit/traces/trace-write-post.md +0 -22
  251. package/audit/traces/trace-write-put.md +0 -23
  252. package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +0 -707
  253. package/docs/GUIDE/GUIDE_X_Multihome.md +0 -666
  254. package/plugins/core/lib/querying-writing/payload-validators.js +0 -1314
  255. package/plugins/core/rest-api-access.js +0 -459
  256. package/plugins/core/rest-api-multihome-plugin.js +0 -314
  257. package/tests/multihome.test.js +0 -553
  258. package/tests/multihome.test.js.bak +0 -550
  259. package/tests/ownership-optout.test.js +0 -76
  260. package/tests/rest-api-access.test.js +0 -206
@@ -1,560 +0,0 @@
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 }