json-rest-api 1.0.29 → 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.
Files changed (292) hide show
  1. package/LICENSE +10 -670
  2. package/LICENSE-GPL-3.0 +674 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +39 -30
  5. package/docs/API.md +744 -1273
  6. package/docs/GUIDE/01-getting-started.md +338 -0
  7. package/docs/GUIDE/02-resources-and-relationships.md +33 -0
  8. package/docs/GUIDE/03-running-example.md +87 -0
  9. package/docs/GUIDE/04-creating-and-querying.md +181 -0
  10. package/docs/GUIDE/05-belongs-to.md +177 -0
  11. package/docs/GUIDE/06-has-many.md +184 -0
  12. package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
  13. package/docs/GUIDE/08-many-to-many.md +184 -0
  14. package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
  15. package/docs/GUIDE/10-put-and-patch.md +174 -0
  16. package/docs/GUIDE/11-relationship-endpoints.md +174 -0
  17. package/docs/GUIDE/12-field-transformations.md +287 -0
  18. package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
  19. package/docs/GUIDE/14-custom-resource-methods.md +125 -0
  20. package/docs/GUIDE/15-query-projections.md +266 -0
  21. package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
  22. package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
  23. package/docs/GUIDE/18-labels.md +86 -0
  24. package/docs/GUIDE/19-managed-transactions.md +179 -0
  25. package/docs/GUIDE/20-transaction-outcomes.md +207 -0
  26. package/docs/GUIDE/21-schema-and-migrations.md +634 -0
  27. package/docs/GUIDE/22-version-field-migrations.md +208 -0
  28. package/docs/GUIDE/23-fastify.md +179 -0
  29. package/docs/GUIDE/24-cors.md +172 -0
  30. package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
  31. package/docs/GUIDE/26-file-uploads.md +278 -0
  32. package/docs/GUIDE/27-bulk-operations.md +277 -0
  33. package/docs/GUIDE/28-socketio.md +325 -0
  34. package/docs/GUIDE/29-writing-plugins.md +278 -0
  35. package/docs/GUIDE/30-backend-capabilities.md +100 -0
  36. package/docs/GUIDE/31-positioning.md +181 -0
  37. package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +74 -30
  38. package/docs/GUIDE/33-migrating-to-v2.md +3827 -0
  39. package/docs/GUIDE/index.md +51 -59
  40. package/docs/QUICKSTART.md +120 -287
  41. package/docs/architecture.md +75 -0
  42. package/docs/contributing.md +178 -0
  43. package/index.d.ts +36 -0
  44. package/index.js +1 -22
  45. package/lib/enhanced-logger.js +91 -85
  46. package/lib/error-context.js +445 -5
  47. package/lib/error-formatter.js +219 -59
  48. package/lib/knex-transaction.js +64 -0
  49. package/lib/logger-types.d.ts +22 -0
  50. package/lib/missing-package.js +4 -0
  51. package/lib/rest-api-errors.js +103 -8
  52. package/lib/runtime/json-rest-api.js +172 -0
  53. package/lib/transaction-types.d.ts +59 -0
  54. package/package.json +97 -27
  55. package/plugins/core/bulk-operations-plugin.js +191 -184
  56. package/plugins/core/connectors/express-plugin.js +148 -219
  57. package/plugins/core/connectors/fastify-plugin.js +125 -82
  58. package/plugins/core/connectors/lib/busboy-detector.js +74 -156
  59. package/plugins/core/connectors/lib/connector-core.js +144 -20
  60. package/plugins/core/connectors/lib/formidable-detector.js +63 -120
  61. package/plugins/core/connectors/lib/http-validators.js +77 -0
  62. package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
  63. package/plugins/core/connectors/lib/request-helpers.js +10 -1
  64. package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
  65. package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
  66. package/plugins/core/file-handling-plugin.js +87 -177
  67. package/plugins/core/lib/anyapi/anyapi-registry.js +213 -164
  68. package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
  69. package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
  70. package/plugins/core/lib/anyapi/schema-utils.js +25 -16
  71. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
  72. package/plugins/core/lib/dbIntrospection.js +228 -21
  73. package/plugins/core/lib/dbTablesOperations.js +409 -92
  74. package/plugins/core/lib/fractional-positioning.js +12 -48
  75. package/plugins/core/lib/querying/identifier-query.js +19 -0
  76. package/plugins/core/lib/querying/include-query-helpers.js +364 -0
  77. package/plugins/core/lib/querying/include-to-many.js +428 -0
  78. package/plugins/core/lib/querying/include-to-one.js +579 -0
  79. package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
  80. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
  81. package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
  82. package/plugins/core/lib/querying/knex-process-includes.js +5 -110
  83. package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
  84. package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
  85. package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
  86. package/plugins/core/lib/querying/knex-window-queries.js +62 -253
  87. package/plugins/core/lib/querying/query-builder-utils.js +44 -4
  88. package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
  89. package/plugins/core/lib/querying/query-constraint.js +27 -0
  90. package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
  91. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  92. package/plugins/core/lib/querying/sort-helpers.js +13 -1
  93. package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
  94. package/plugins/core/lib/querying/url-helpers.js +11 -4
  95. package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
  96. package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
  97. package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
  98. package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
  99. package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
  100. package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
  101. package/plugins/core/lib/querying-writing/field-utils.js +113 -92
  102. package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
  103. package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
  104. package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
  105. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
  106. package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
  107. package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
  108. package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
  109. package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
  110. package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
  111. package/plugins/core/lib/querying-writing/response-options.js +35 -0
  112. package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
  113. package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
  114. package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
  115. package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
  116. package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
  117. package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
  118. package/plugins/core/lib/storage/storage-adapter.js +128 -62
  119. package/plugins/core/lib/storage/storage-mapping.js +78 -28
  120. package/plugins/core/lib/storage/storage-types.d.ts +502 -0
  121. package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
  122. package/plugins/core/lib/writing/relationship-processor.js +94 -158
  123. package/plugins/core/lib/writing/resource-version.js +172 -0
  124. package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
  125. package/plugins/core/rest-api-anyapi-knex-plugin.js +116 -2485
  126. package/plugins/core/rest-api-autofilter-plugin.js +17 -19
  127. package/plugins/core/rest-api-cors-plugin.js +52 -205
  128. package/plugins/core/rest-api-knex-plugin.js +45 -1077
  129. package/plugins/core/rest-api-label-plugin.js +25 -38
  130. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
  131. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
  132. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
  133. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
  134. package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
  135. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
  136. package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
  137. package/plugins/core/rest-api-plugin-methods/common.js +291 -395
  138. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
  139. package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
  140. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
  141. package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
  142. package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
  143. package/plugins/core/rest-api-plugin-methods/get.js +36 -68
  144. package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
  145. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
  146. package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
  147. package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
  148. package/plugins/core/rest-api-plugin-methods/post.js +51 -51
  149. package/plugins/core/rest-api-plugin-methods/put.js +69 -148
  150. package/plugins/core/rest-api-plugin-methods/query.js +37 -81
  151. package/plugins/core/rest-api-plugin-methods/release.js +2 -1
  152. package/plugins/core/rest-api-plugin.js +32 -46
  153. package/plugins/core/rest-api-positioning-plugin.js +183 -628
  154. package/plugins/core/rest-api-query-projections-plugin.js +19 -102
  155. package/plugins/core/rest-api-row-policy-plugin.js +3 -3
  156. package/plugins/core/socketio-plugin.js +364 -344
  157. package/plugins/storage/local-storage.js +40 -36
  158. package/types/autofilter.d.ts +57 -0
  159. package/types/bulk-methods.d.ts +63 -0
  160. package/types/errors.d.ts +118 -0
  161. package/types/fastify-options.d.ts +19 -0
  162. package/types/file-detectors.d.ts +33 -0
  163. package/types/file-storage.d.ts +48 -0
  164. package/types/hook-context.d.ts +180 -0
  165. package/types/plugin-options.d.ts +53 -0
  166. package/types/relationship-methods.d.ts +74 -0
  167. package/types/representations.d.ts +50 -0
  168. package/types/resource-methods.d.ts +95 -0
  169. package/types/resource-schema.d.ts +157 -0
  170. package/types/row-policy.d.ts +31 -0
  171. package/types/runtime.d.ts +132 -0
  172. package/types/transactions.d.ts +21 -0
  173. package/.claude/agents/library-guide-expert.md +0 -30
  174. package/.claude/agents/rest-api-internals-expert.md +0 -54
  175. package/.claude/settings.local.json +0 -70
  176. package/AGENTS.md +0 -34
  177. package/CLAUDE.md +0 -160
  178. package/CRUSH.md +0 -55
  179. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
  180. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
  181. package/TODO.todo +0 -13
  182. package/connectors_ideas/fastify.js +0 -217
  183. package/connectors_ideas/koa-plugin.js +0 -112
  184. package/docs/.bundle/config +0 -2
  185. package/docs/COMPARISON.md +0 -433
  186. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
  187. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
  188. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
  189. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
  190. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
  191. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
  192. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
  193. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
  194. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
  195. package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
  196. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
  197. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
  198. package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
  199. package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
  200. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
  201. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
  202. package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
  203. package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
  204. package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
  205. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
  206. package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
  207. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
  208. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
  209. package/docs/Gemfile +0 -6
  210. package/docs/Gemfile.lock +0 -308
  211. package/docs/ONBOARDING.md +0 -315
  212. package/docs/_config.yml +0 -36
  213. package/docs/_layouts/default.html +0 -123
  214. package/docs/apple-touch-icon.svg +0 -4
  215. package/docs/assets/css/style.scss +0 -392
  216. package/docs/favicon.svg +0 -4
  217. package/docs/index.md +0 -164
  218. package/docs/mini-guides/file-uploads-complete.md +0 -161
  219. package/eslint.config.js +0 -13
  220. package/examples/url-override-example.js +0 -137
  221. package/fixing_plan.md +0 -112
  222. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
  223. package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
  224. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
  225. package/plugins/storage/s3-storage.js +0 -93
  226. package/quickTest.js +0 -136
  227. package/scripts/debug-anyapi-review-filter.mjs +0 -63
  228. package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
  229. package/scripts/inspect-anyapi-descriptor.mjs +0 -19
  230. package/scripts/run-docs.js +0 -133
  231. package/test-debug.js +0 -59
  232. package/tests/README.md +0 -202
  233. package/tests/TEST_TEMPLATE.test.js +0 -178
  234. package/tests/anyapi-basic.test.js +0 -105
  235. package/tests/anyapi-cursor-pagination.test.js +0 -148
  236. package/tests/anyapi-custom-idproperty.test.js +0 -163
  237. package/tests/autofilter.test.js +0 -463
  238. package/tests/bulk-operations.test.js +0 -657
  239. package/tests/computed-fields.test.js +0 -586
  240. package/tests/cors.test.js +0 -834
  241. package/tests/curl.test.js +0 -467
  242. package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
  243. package/tests/custom-id-simplified-post.test.js +0 -75
  244. package/tests/custom-idproperty-relationships.test.js +0 -932
  245. package/tests/db-introspection.test.js +0 -521
  246. package/tests/db-table-operations.test.js +0 -798
  247. package/tests/fastify-plugin.test.js +0 -184
  248. package/tests/field-getters.test.js +0 -370
  249. package/tests/field-setters.test.js +0 -306
  250. package/tests/file-handling.test.js +0 -155
  251. package/tests/fixtures/api-configs.js +0 -2566
  252. package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
  253. package/tests/helpers/fake-fastify.js +0 -99
  254. package/tests/helpers/storage-mode.js +0 -65
  255. package/tests/helpers/test-utils.js +0 -203
  256. package/tests/http-connectors-parity.test.js +0 -395
  257. package/tests/id-normalization.test.js +0 -444
  258. package/tests/include-depth-validation.test.js +0 -331
  259. package/tests/includes.test.js +0 -427
  260. package/tests/local-storage.test.js +0 -96
  261. package/tests/logical-id-write-validation.test.js +0 -151
  262. package/tests/nested-includes.test.js +0 -617
  263. package/tests/pagination-boundaries.test.js +0 -154
  264. package/tests/pagination-contract.test.js +0 -122
  265. package/tests/pagination-cursor-multifield.test.js +0 -641
  266. package/tests/pagination-enhanced.test.js +0 -563
  267. package/tests/polymorphic-relationships.test.js +0 -347
  268. package/tests/polymorphic-simple.test.js +0 -148
  269. package/tests/positioning-advanced.test.js +0 -692
  270. package/tests/positioning-extended.test.js +0 -915
  271. package/tests/positioning-multifilter.test.js +0 -699
  272. package/tests/positioning.test.js +0 -560
  273. package/tests/post-commit-hooks.test.js +0 -120
  274. package/tests/projected-fields.test.js +0 -319
  275. package/tests/public-exports.test.js +0 -15
  276. package/tests/put-create-parity.test.js +0 -216
  277. package/tests/queries.test.js +0 -521
  278. package/tests/query-limits.test.js +0 -584
  279. package/tests/relationship-endpoints.test.js +0 -620
  280. package/tests/relationship-include-storage-adapter.test.js +0 -56
  281. package/tests/relationships.test.js +0 -681
  282. package/tests/rest-api.test.js +0 -337
  283. package/tests/return-record-settings.test.js +0 -184
  284. package/tests/row-policy.test.js +0 -489
  285. package/tests/s3-storage.test.js +0 -38
  286. package/tests/searchschema-knex-behavior.test.js +0 -187
  287. package/tests/searchschema-merge.test.js +0 -151
  288. package/tests/socketio.test.js +0 -860
  289. package/tests/storage-mapping.test.js +0 -362
  290. package/tests/temporal-boundaries.test.js +0 -208
  291. package/tests/test-development-guide.md +0 -370
  292. package/tests/virtual-fields.test.js +0 -404
@@ -1,1247 +0,0 @@
1
- # 2.7 Pagination and ordering
2
-
3
- Pagination and ordering are essential features for working with large datasets. The json-rest-api library provides powerful pagination that applies not just to main resources, but also to included relationships. This means you can limit and order both parent records AND their children independently.
4
-
5
- For derived fields that must participate in `ORDER BY` and cursor pagination, see [Query Projections](GUIDE_X_Query_Projections.md). Regular computed fields are response-layer only and are not the right tool for stable SQL ordering.
6
-
7
- Let's create a comprehensive example with posts, comments, and tags to demonstrate all pagination and ordering features:
8
-
9
- ```javascript
10
- await api.addResource('posts', {
11
- schema: {
12
- title: { type: 'string', required: true, max: 255, search: true, filterOperator: 'like', indexed: true },
13
- content: { type: 'string', required: true },
14
- view_count: { type: 'number', default: 0, indexed: true },
15
- published_at: { type: 'dateTime', indexed: true },
16
- created_at: { type: 'dateTime', temporalPrecision: 3, defaultTo: () => new Date().toISOString(), indexed: true }
17
- },
18
- relationships: {
19
- comments: {
20
- type: 'hasMany',
21
- target: 'comments',
22
- foreignKey: 'post_id',
23
- include: {
24
- limit: 5, // Default limit for included comments
25
- orderBy: ['-created_at'] // Newest comments first
26
- }
27
- },
28
- tags: {
29
- type: 'manyToMany',
30
- through: 'post_tags',
31
- foreignKey: 'post_id',
32
- otherKey: 'tag_id',
33
- include: {
34
- limit: 10,
35
- orderBy: ['name'] // Alphabetical order
36
- }
37
- }
38
- },
39
- // Define which fields can be sorted in queries
40
- sortableFields: ['title', 'view_count', 'published_at', 'created_at'],
41
- // Default sort uses the same format as query sort parameters:
42
- // - String format: '-created_at' (- prefix for DESC)
43
- // - Array format: ['-created_at', 'title'] for multiple sorts
44
- defaultSort: '-created_at', // Newest first by default
45
-
46
- // Set pagination limits for this resource
47
- queryDefaultLimit: 10, // Default page size
48
- queryMaxLimit: 50 // Maximum allowed page size
49
- });
50
- await api.resources.posts.createKnexTable();
51
-
52
- // Define comments resource
53
- await api.addResource('comments', {
54
- schema: {
55
- content: { type: 'string', required: true },
56
- author_name: { type: 'string', required: true, max: 100 },
57
- created_at: { type: 'dateTime', temporalPrecision: 3, defaultTo: () => new Date().toISOString(), indexed: true },
58
- likes: { type: 'number', default: 0, indexed: true },
59
- post_id: { type: 'id', belongsTo: 'posts', as: 'post', required: true }
60
- },
61
- sortableFields: ['created_at', 'likes'],
62
- queryDefaultLimit: 20,
63
- queryMaxLimit: 100
64
- });
65
- await api.resources.comments.createKnexTable();
66
-
67
- // Define tags resource
68
- await api.addResource('tags', {
69
- schema: {
70
- name: { type: 'string', required: true, max: 50, unique: true, indexed: true },
71
- usage_count: { type: 'number', default: 0, indexed: true }
72
- },
73
- relationships: {
74
- posts: {
75
- type: 'manyToMany',
76
- through: 'post_tags',
77
- foreignKey: 'tag_id',
78
- otherKey: 'post_id'
79
- }
80
- },
81
- sortableFields: ['name', 'usage_count'],
82
- queryDefaultLimit: 30
83
- });
84
- await api.resources.tags.createKnexTable();
85
-
86
- // Define pivot table
87
- await api.addResource('post_tags', {
88
- schema: {
89
- post_id: { type: 'id', belongsTo: 'posts', as: 'post', required: true },
90
- tag_id: { type: 'id', belongsTo: 'tags', as: 'tag', required: true }
91
- }
92
- });
93
- await api.resources.post_tags.createKnexTable();
94
-
95
- ```
96
-
97
- **Basic Pagination**
98
-
99
- When `sortableFields` is omitted or empty, visible stored schema fields and relationship aliases can be sorted. Query fields marked `sortable: true` are added to that set. Unknown fields and ordinary computed fields are rejected with 422. If a sort field is repeated, its first occurrence determines the direction.
100
-
101
- The API supports two types of pagination:
102
-
103
- 1. **Offset-based pagination** using `page[number]` and `page[size]` parameters
104
- 2. **Cursor-based pagination** using `page[after]` and `page[before]` parameters
105
-
106
- **How the API chooses pagination mode:**
107
-
108
- The API automatically selects the pagination mode based on your query parameters:
109
- - When you specify a page number (`page[number]=2`), you get traditional offset pagination with page counts and totals
110
- - When you only specify a page size (`page[size]=10`) without a page number, the API switches to cursor pagination for better performance
111
- - When you use cursor parameters (`page[after]` or `page[before]`), you explicitly request cursor pagination
112
-
113
- This design encourages the use of cursor pagination (which is more efficient for large datasets) while still supporting traditional page numbers when needed.
114
-
115
- **Offset-based Pagination:**
116
-
117
- ```javascript
118
- // Get first page with default size (10 posts)
119
- const page1 = await api.resources.posts.query({
120
- queryParams: {
121
- page: { number: 1, size: 5 }
122
- }
123
- });
124
- // HTTP: GET /api/posts?page[number]=1
125
- // Returns: {
126
- // data: [ /* 10 posts */ ],
127
- // meta: {
128
- // pagination: {
129
- // page: 1,
130
- // pageSize: 5,
131
- // pageCount: 3,
132
- // total: 25
133
- // }
134
- // }
135
- // }
136
-
137
- // Get second page with custom size
138
- const page2 = await api.resources.posts.query({
139
- queryParams: {
140
- page: { number: 2, size: 5 }
141
- }
142
- });
143
- // HTTP: GET /api/posts?page[number]=2&page[size]=5
144
- // Returns: {
145
- // data: [ /* 5 posts (posts 6-10) */ ],
146
- // meta: {
147
- // pagination: {
148
- // page: 2,
149
- // pageSize: 5,
150
- // pageCount: 5,
151
- // total: 25
152
- // }
153
- // }
154
- // }
155
-
156
- // Get a large page (but limited by queryMaxLimit)
157
- const largePage = await api.resources.posts.query({
158
- queryParams: {
159
- page: { number: 1, size: 100 } // Will be capped at 50 (queryMaxLimit)
160
- }
161
- });
162
- // HTTP: GET /api/posts?page[number]=1&page[size]=100
163
- // Returns: {
164
- // data: [ /* All 25 posts */ ],
165
- // meta: {
166
- // pagination: {
167
- // page: 1,
168
- // pageSize: 50, // Capped at queryMaxLimit
169
- // pageCount: 1,
170
- // total: 25
171
- // }
172
- // }
173
- // }
174
-
175
- console.log('Page 1 posts:', page1.data.length);
176
- console.log('Page 1 post data:', page1.data);
177
- console.log('Page 2 posts:', page2.data.length);
178
- console.log('Page 2 post data:', page2.data);
179
- console.log('Large page posts:', largePage.data.length);
180
- console.log('Page 1 metadata:', page1.meta);
181
- ```
182
-
183
- **Expected Output**
184
-
185
- ```text
186
- Page 1 posts: 5
187
- Page 1 post data: [
188
- {
189
- id: '25',
190
- title: 'Post 25: Odd Number Post',
191
- content: "This is the content of post number 25. It's a odd numbered post.",
192
- view_count: 823,
193
- published_at: 2025-07-28T03:57:39.711Z,
194
- created_at: 2025-07-28T03:57:39.712Z,
195
- comments_ids: [],
196
- tags_ids: [ '1', '2', '3', '4', '5' ]
197
- },
198
- {
199
- id: '24',
200
- title: 'Post 24: Even Number Post',
201
- content: "This is the content of post number 24. It's a even numbered post.",
202
- view_count: 765,
203
- published_at: 2025-07-27T03:57:39.707Z,
204
- created_at: 2025-07-28T03:57:39.708Z,
205
- comments_ids: [],
206
- tags_ids: [ '1', '2', '3', '4', '5' ]
207
- },
208
- {
209
- id: '23',
210
- title: 'Post 23: Odd Number Post',
211
- content: "This is the content of post number 23. It's a odd numbered post.",
212
- view_count: 196,
213
- published_at: 2025-07-26T03:57:39.705Z,
214
- created_at: 2025-07-28T03:57:39.706Z,
215
- comments_ids: [],
216
- tags_ids: [ '1', '2', '3' ]
217
- },
218
- {
219
- id: '22',
220
- title: 'Post 22: Even Number Post',
221
- content: "This is the content of post number 22. It's a even numbered post.",
222
- view_count: 987,
223
- published_at: 2025-07-25T03:57:39.703Z,
224
- created_at: 2025-07-28T03:57:39.703Z,
225
- comments_ids: [],
226
- tags_ids: [ '1', '2', '3' ]
227
- },
228
- {
229
- id: '21',
230
- title: 'Post 21: Odd Number Post',
231
- content: "This is the content of post number 21. It's a odd numbered post.",
232
- view_count: 28,
233
- published_at: 2025-07-24T03:57:39.701Z,
234
- created_at: 2025-07-28T03:57:39.701Z,
235
- comments_ids: [],
236
- tags_ids: [ '1', '2', '3' ]
237
- }
238
- ]
239
- Page 2 posts: 5
240
- Page 2 post data: [
241
- {
242
- id: '20',
243
- title: 'Post 20: Even Number Post',
244
- content: "This is the content of post number 20. It's a even numbered post.",
245
- view_count: 626,
246
- published_at: 2025-07-23T03:57:39.699Z,
247
- created_at: 2025-07-28T03:57:39.699Z,
248
- comments_ids: [],
249
- tags_ids: [ '1', '2', '3' ]
250
- },
251
- {
252
- id: '19',
253
- title: 'Post 19: Odd Number Post',
254
- content: "This is the content of post number 19. It's a odd numbered post.",
255
- view_count: 487,
256
- published_at: 2025-07-22T03:57:39.696Z,
257
- created_at: 2025-07-28T03:57:39.697Z,
258
- comments_ids: [],
259
- tags_ids: [ '1', '2', '3', '4' ]
260
- },
261
- {
262
- id: '18',
263
- title: 'Post 18: Even Number Post',
264
- content: "This is the content of post number 18. It's a even numbered post.",
265
- view_count: 685,
266
- published_at: 2025-07-21T03:57:39.694Z,
267
- created_at: 2025-07-28T03:57:39.695Z,
268
- comments_ids: [],
269
- tags_ids: [ '1', '2', '3' ]
270
- },
271
- {
272
- id: '17',
273
- title: 'Post 17: Odd Number Post',
274
- content: "This is the content of post number 17. It's a odd numbered post.",
275
- view_count: 402,
276
- published_at: 2025-07-20T03:57:39.690Z,
277
- created_at: 2025-07-28T03:57:39.691Z,
278
- comments_ids: [],
279
- tags_ids: [ '1', '2', '3', '4', '5' ]
280
- },
281
- {
282
- id: '16',
283
- title: 'Post 16: Even Number Post',
284
- content: "This is the content of post number 16. It's a even numbered post.",
285
- view_count: 705,
286
- published_at: 2025-07-19T03:57:39.685Z,
287
- created_at: 2025-07-28T03:57:39.686Z,
288
- comments_ids: [],
289
- tags_ids: [ '1', '2', '3', '4', '5' ]
290
- }
291
- ]
292
- Large page posts: 25
293
- Page 1 metadata: {
294
- pagination: { page: 1, pageSize: 5, pageCount: 5, total: 25, hasMore: true }
295
- }
296
- ```
297
-
298
- **Cursor-based Pagination:**
299
-
300
- Cursor-based pagination is ideal for real-time data or when users are scrolling through results. It provides stable pagination even when new records are added:
301
-
302
- ```javascript
303
- // Get first page of posts (newest first)
304
- const firstPage = await api.resources.posts.query({
305
- queryParams: {
306
- page: { size: 5 }, // Using size without number triggers cursor pagination
307
- sort: ['-created_at']
308
- }
309
- });
310
- // Returns: {
311
- // data: [ /* 5 posts */ ],
312
- // meta: {
313
- // pagination: {
314
- // pageSize: 5,
315
- // hasMore: true,
316
- // cursor: { next: 'created_at:2024-07-25T10%3A30%3A00.000Z' }
317
- // }
318
- // }
319
- // }
320
-
321
- // Get next page using the cursor from meta
322
- const nextPage = await api.resources.posts.query({
323
- queryParams: {
324
- page: {
325
- size: 5,
326
- after: firstPage.meta.pagination.cursor.next
327
- },
328
- sort: ['-created_at']
329
- }
330
- });
331
-
332
- // Get second page using the cursor from first page
333
- const secondPage = await api.resources.posts.query({
334
- queryParams: {
335
- page: {
336
- size: 5,
337
- after: firstPage.meta.pagination.cursor?.next
338
- },
339
- sort: ['-created_at']
340
- }
341
- });
342
-
343
- // For backward pagination (less common), use the 'prev' cursor
344
- // Note: This requires the implementation to provide prev cursors
345
- if (secondPage.meta.pagination.cursor?.prev) {
346
- const backToFirstPage = await api.resources.posts.query({
347
- queryParams: {
348
- page: {
349
- size: 5,
350
- before: secondPage.meta.pagination.cursor.prev
351
- },
352
- sort: ['-created_at']
353
- }
354
- });
355
- }
356
-
357
- console.log('First page:', inspect(firstPage));
358
- console.log('\nSecond page:', inspect(secondPage));
359
- ```
360
-
361
- **Expected output**
362
-
363
- ```
364
- First page: {
365
- data: [
366
- {
367
- id: '25',
368
- title: 'Post 25: Odd Number Post',
369
- content: "This is the content of post number 25. It's a odd numbered post.",
370
- view_count: 142,
371
- published_at: 2025-07-28T04:06:39.821Z,
372
- created_at: 2025-07-28T04:06:39.821Z,
373
- comments_ids: [],
374
- tags_ids: [ '1', '2', '3', '4' ]
375
- },
376
- {
377
- id: '24',
378
- title: 'Post 24: Even Number Post',
379
- content: "This is the content of post number 24. It's a even numbered post.",
380
- view_count: 497,
381
- published_at: 2025-07-27T04:06:39.818Z,
382
- created_at: 2025-07-28T04:06:39.818Z,
383
- comments_ids: [],
384
- tags_ids: [ '1', '2', '3', '4', '5' ]
385
- },
386
- {
387
- id: '23',
388
- title: 'Post 23: Odd Number Post',
389
- content: "This is the content of post number 23. It's a odd numbered post.",
390
- view_count: 167,
391
- published_at: 2025-07-26T04:06:39.815Z,
392
- created_at: 2025-07-28T04:06:39.816Z,
393
- comments_ids: [],
394
- tags_ids: [ '1', '2', '3', '4', '5' ]
395
- },
396
- {
397
- id: '22',
398
- title: 'Post 22: Even Number Post',
399
- content: "This is the content of post number 22. It's a even numbered post.",
400
- view_count: 738,
401
- published_at: 2025-07-25T04:06:39.813Z,
402
- created_at: 2025-07-28T04:06:39.813Z,
403
- comments_ids: [],
404
- tags_ids: [ '1', '2', '3' ]
405
- },
406
- {
407
- id: '21',
408
- title: 'Post 21: Odd Number Post',
409
- content: "This is the content of post number 21. It's a odd numbered post.",
410
- view_count: 41,
411
- published_at: 2025-07-24T04:06:39.810Z,
412
- created_at: 2025-07-28T04:06:39.811Z,
413
- comments_ids: [],
414
- tags_ids: [ '1', '2', '3' ]
415
- }
416
- ],
417
- meta: {
418
- pagination: {
419
- pageSize: 5,
420
- hasMore: true,
421
- cursor: { next: 'created_at:1753675599811' }
422
- }
423
- },
424
- links: {
425
- self: '/api/posts?sort=-created_at&&&page[size]=5',
426
- first: '/api/posts?sort=-created_at&&&page[size]=5',
427
- next: '/api/posts?sort=-created_at&&&page[size]=5&page[after]=created_at:1753675599811'
428
- }
429
- }
430
-
431
- Second page: {
432
- data: [
433
- {
434
- id: '20',
435
- title: 'Post 20: Even Number Post',
436
- content: "This is the content of post number 20. It's a even numbered post.",
437
- view_count: 149,
438
- published_at: 2025-07-23T04:06:39.808Z,
439
- created_at: 2025-07-28T04:06:39.809Z,
440
- comments_ids: [],
441
- tags_ids: [ '1', '2', '3', '4' ]
442
- },
443
- {
444
- id: '19',
445
- title: 'Post 19: Odd Number Post',
446
- content: "This is the content of post number 19. It's a odd numbered post.",
447
- view_count: 562,
448
- published_at: 2025-07-22T04:06:39.805Z,
449
- created_at: 2025-07-28T04:06:39.806Z,
450
- comments_ids: [],
451
- tags_ids: [ '1', '2', '3', '4', '5' ]
452
- },
453
- {
454
- id: '18',
455
- title: 'Post 18: Even Number Post',
456
- content: "This is the content of post number 18. It's a even numbered post.",
457
- view_count: 146,
458
- published_at: 2025-07-21T04:06:39.803Z,
459
- created_at: 2025-07-28T04:06:39.803Z,
460
- comments_ids: [],
461
- tags_ids: [ '1', '2', '3' ]
462
- },
463
- {
464
- id: '17',
465
- title: 'Post 17: Odd Number Post',
466
- content: "This is the content of post number 17. It's a odd numbered post.",
467
- view_count: 689,
468
- published_at: 2025-07-20T04:06:39.798Z,
469
- created_at: 2025-07-28T04:06:39.799Z,
470
- comments_ids: [],
471
- tags_ids: [ '1', '2', '3', '4' ]
472
- },
473
- {
474
- id: '16',
475
- title: 'Post 16: Even Number Post',
476
- content: "This is the content of post number 16. It's a even numbered post.",
477
- view_count: 215,
478
- published_at: 2025-07-19T04:06:39.796Z,
479
- created_at: 2025-07-28T04:06:39.797Z,
480
- comments_ids: [],
481
- tags_ids: [ '1', '2', '3' ]
482
- }
483
- ],
484
- meta: {
485
- pagination: {
486
- pageSize: 5,
487
- hasMore: true,
488
- cursor: { next: 'created_at:1753675599797' }
489
- }
490
- },
491
- links: {
492
- self: '/api/posts?sort=-created_at&&&page[size]=5&page[after]=created_at:1753675599811',
493
- first: '/api/posts?sort=-created_at&&&page[size]=5',
494
- next: '/api/posts?sort=-created_at&&&page[size]=5&page[after]=created_at:1753675599797'
495
- }
496
- }
497
- ```
498
-
499
- When sorting happens
500
-
501
- ```javascript
502
- // Multi-field cursor pagination example
503
- const multiFieldPage = await api.resources.posts.query({
504
- queryParams: {
505
- page: { size: 5 },
506
- sort: ['view_count', '-created_at'] // Sort by view_count first, then by created_at DESC
507
- }
508
- });
509
-
510
- // The cursor will contain both fields: "view_count:142,created_at:2024-07-25T10%3A30%3A00.000Z"
511
- // This ensures no records are skipped even if many posts have the same view_count
512
- const nextMultiFieldPage = await api.resources.posts.query({
513
- queryParams: {
514
- page: {
515
- size: 5,
516
- after: multiFieldPage.meta.pagination.cursor.next
517
- },
518
- sort: ['view_count', '-created_at']
519
- }
520
- });
521
-
522
- console.log('First multiField page:', inspect(multiFieldPage));
523
- console.log('Next multiField page:', inspect(nextMultiFieldPage));
524
- ```
525
-
526
- **Expected output**:
527
-
528
- ```text
529
- First multiField page: {
530
- data: [
531
- {
532
- id: '4',
533
- title: 'Post 4: Even Number Post',
534
- content: "This is the content of post number 4. It's a even numbered post.",
535
- view_count: 19,
536
- published_at: 2025-07-07T04:16:32.004Z,
537
- created_at: 2025-07-28T04:16:32.005Z,
538
- comments_ids: [
539
- '30', '31', '32',
540
- '33', '34', '35',
541
- '36', '37', '38',
542
- '39'
543
- ],
544
- tags_ids: [ '1', '2', '3' ]
545
- },
546
- {
547
- id: '13',
548
- title: 'Post 13: Odd Number Post',
549
- content: "This is the content of post number 13. It's a odd numbered post.",
550
- view_count: 25,
551
- published_at: 2025-07-16T04:16:32.041Z,
552
- created_at: 2025-07-28T04:16:32.042Z,
553
- comments_ids: [],
554
- tags_ids: [ '1', '2', '3', '4', '5' ]
555
- },
556
- {
557
- id: '19',
558
- title: 'Post 19: Odd Number Post',
559
- content: "This is the content of post number 19. It's a odd numbered post.",
560
- view_count: 28,
561
- published_at: 2025-07-22T04:16:32.066Z,
562
- created_at: 2025-07-28T04:16:32.066Z,
563
- comments_ids: [],
564
- tags_ids: [ '1', '2', '3', '4', '5' ]
565
- },
566
- {
567
- id: '7',
568
- title: 'Post 7: Odd Number Post',
569
- content: "This is the content of post number 7. It's a odd numbered post.",
570
- view_count: 41,
571
- published_at: 2025-07-10T04:16:32.018Z,
572
- created_at: 2025-07-28T04:16:32.019Z,
573
- comments_ids: [],
574
- tags_ids: [ '1', '2', '3', '4' ]
575
- },
576
- {
577
- id: '20',
578
- title: 'Post 20: Even Number Post',
579
- content: "This is the content of post number 20. It's a even numbered post.",
580
- view_count: 74,
581
- published_at: 2025-07-23T04:16:32.069Z,
582
- created_at: 2025-07-28T04:16:32.069Z,
583
- comments_ids: [],
584
- tags_ids: [ '1', '2', '3' ]
585
- }
586
- ],
587
- meta: {
588
- pagination: {
589
- pageSize: 5,
590
- hasMore: true,
591
- cursor: { next: 'view_count:74,created_at:1753676192069' }
592
- }
593
- },
594
- links: {
595
- self: '/api/posts?sort=view_count&sort=-created_at&&&page[size]=5',
596
- first: '/api/posts?sort=view_count&sort=-created_at&&&page[size]=5',
597
- next: '/api/posts?sort=view_count&sort=-created_at&&&page[size]=5&page[after]=view_count:74,created_at:1753676192069'
598
- }
599
- }
600
- Next multiField page: {
601
- data: [
602
- {
603
- id: '6',
604
- title: 'Post 6: Even Number Post',
605
- content: "This is the content of post number 6. It's a even numbered post.",
606
- view_count: 148,
607
- published_at: 2025-07-09T04:16:32.011Z,
608
- created_at: 2025-07-28T04:16:32.012Z,
609
- comments_ids: [],
610
- tags_ids: [ '1', '2', '3', '4', '5' ]
611
- },
612
- {
613
- id: '22',
614
- title: 'Post 22: Even Number Post',
615
- content: "This is the content of post number 22. It's a even numbered post.",
616
- view_count: 161,
617
- published_at: 2025-07-25T04:16:32.073Z,
618
- created_at: 2025-07-28T04:16:32.074Z,
619
- comments_ids: [],
620
- tags_ids: [ '1', '2', '3', '4', '5' ]
621
- },
622
- {
623
- id: '5',
624
- title: 'Post 5: Odd Number Post',
625
- content: "This is the content of post number 5. It's a odd numbered post.",
626
- view_count: 235,
627
- published_at: 2025-07-08T04:16:32.007Z,
628
- created_at: 2025-07-28T04:16:32.008Z,
629
- comments_ids: [
630
- '40', '41', '42',
631
- '43', '44', '45',
632
- '46', '47', '48',
633
- '49', '50', '51'
634
- ],
635
- tags_ids: [ '1', '2', '3', '4', '5' ]
636
- },
637
- {
638
- id: '9',
639
- title: 'Post 9: Odd Number Post',
640
- content: "This is the content of post number 9. It's a odd numbered post.",
641
- view_count: 249,
642
- published_at: 2025-07-12T04:16:32.024Z,
643
- created_at: 2025-07-28T04:16:32.026Z,
644
- comments_ids: [],
645
- tags_ids: [ '1', '2', '3', '4', '5' ]
646
- },
647
- {
648
- id: '17',
649
- title: 'Post 17: Odd Number Post',
650
- content: "This is the content of post number 17. It's a odd numbered post.",
651
- view_count: 290,
652
- published_at: 2025-07-20T04:16:32.055Z,
653
- created_at: 2025-07-28T04:16:32.056Z,
654
- comments_ids: [],
655
- tags_ids: [ '1', '2', '3', '4', '5' ]
656
- }
657
- ],
658
- meta: {
659
- pagination: {
660
- pageSize: 5,
661
- hasMore: true,
662
- cursor: { next: 'view_count:290,created_at:1753676192056' }
663
- }
664
- },
665
- links: {
666
- self: '/api/posts?sort=view_count&sort=-created_at&&&page[size]=5&page[after]=view_count:74,created_at:1753676192069',
667
- first: '/api/posts?sort=view_count&sort=-created_at&&&page[size]=5',
668
- next: '/api/posts?sort=view_count&sort=-created_at&&&page[size]=5&page[after]=view_count:290,created_at:1753676192056'
669
- }
670
- }
671
- ```
672
-
673
-
674
- **Cursor Pagination Notes:**
675
- - Cursors are simple strings containing the sort field values (format: `field:value,field2:value2`)
676
- - Sparse fieldsets may omit sort fields from the response; the Knex query still fetches those values internally to construct a complete cursor.
677
- - A cursor missing any active sort field is rejected with a validation error. Keep the same sort order when following a cursor.
678
- - Cursor values are validated against the sort field types. Invalid dates, numbers, or duplicate cursor fields produce HTTP 422.
679
- - `page[size]` and `page[number]` must be positive integers. Choose one of `page[number]`, `page[after]`, or `page[before]`; these cannot be combined.
680
- - Null sort values appear last in either sort direction. Generated cursors distinguish null from the literal string `"null"`; treat cursor strings as opaque values.
681
- - `page[before]` returns the adjacent preceding records in the requested sort order. Use a cursor for the first record of the current page to step back. When a backward response has more preceding records, its `links.next` and `meta.pagination.cursor.next` continue in the `before` direction.
682
- - Requested page sizes above `queryMaxLimit` are capped consistently for returned records, pagination metadata, and links.
683
- - The API automatically generates appropriate WHERE clauses based on sort direction
684
- - Cursor pagination is more efficient for large datasets as it doesn't need to count all records
685
- - Works seamlessly with any sort field, not just timestamps
686
- - **Multi-field sorting is fully supported** - When sorting by multiple fields (e.g., `sort: ['category', 'name']`), the cursor includes all sort fields and correctly handles records with duplicate values in the first sort field
687
-
688
- **Pagination with Sorting**
689
-
690
- You can combine pagination with sorting. The sort order is maintained across pages:
691
-
692
- ```javascript
693
- // Get posts sorted by view count (highest first)
694
- const popularPosts = await api.resources.posts.query({
695
- queryParams: {
696
- sort: ['-view_count'], // Minus prefix means descending
697
- page: { number: 1, size: 5 }
698
- }
699
- });
700
- // HTTP: GET /api/posts?sort=-view_count&page[number]=1&page[size]=5
701
- // Returns: {
702
- // data: [
703
- // { id: '5', title: 'Post 5', view_count: 950, ... },
704
- // { id: '15', title: 'Post 15', view_count: 850, ... },
705
- // { id: '20', title: 'Post 20', view_count: 750, ... },
706
- // { id: '10', title: 'Post 10', view_count: 650, ... },
707
- // { id: '25', title: 'Post 25', view_count: 550, ... }
708
- // ],
709
- // meta: { pagination: { page: 1, pageSize: 5, pageCount: 5, total: 25 } }
710
- // }
711
-
712
- // Get posts sorted by multiple fields
713
- const multiSort = await api.resources.posts.query({
714
- queryParams: {
715
- sort: ['published_at', '-view_count'], // Oldest first, then by views
716
- page: { number: 1, size: 5 }
717
- }
718
- });
719
- // HTTP: GET /api/posts?sort=published_at,-view_count&page[number]=1&page[size]=5
720
- // Returns: {
721
- // data: [
722
- // { id: '1', title: 'Post 1', published_at: '2024-01-01T00:00:00Z', view_count: 100, ... },
723
- // { id: '2', title: 'Post 2', published_at: '2024-01-02T00:00:00Z', view_count: 200, ... },
724
- // // ... posts sorted first by date, then by views within same date
725
- // ],
726
- // meta: { pagination: { page: 1, pageSize: 5, pageCount: 5, total: 25 } }
727
- // }
728
-
729
- console.log('Popular sort:', inspect(popularPosts));
730
- console.log('Multisort:', inspect(multiSort));
731
- ```
732
-
733
- **Expected output**:
734
-
735
- ```text
736
- Popular sort: {
737
- data: [
738
- {
739
- id: '6',
740
- title: 'Post 6: Even Number Post',
741
- content: "This is the content of post number 6. It's a even numbered post.",
742
- view_count: 910,
743
- published_at: 2025-07-09T04:21:13.403Z,
744
- created_at: 2025-07-28T04:21:13.404Z,
745
- comments_ids: [],
746
- tags_ids: [ '1', '2', '3' ]
747
- },
748
- {
749
- id: '24',
750
- title: 'Post 24: Even Number Post',
751
- content: "This is the content of post number 24. It's a even numbered post.",
752
- view_count: 871,
753
- published_at: 2025-07-27T04:21:13.454Z,
754
- created_at: 2025-07-28T04:21:13.454Z,
755
- comments_ids: [],
756
- tags_ids: [ '1', '2', '3', '4' ]
757
- },
758
- {
759
- id: '18',
760
- title: 'Post 18: Even Number Post',
761
- content: "This is the content of post number 18. It's a even numbered post.",
762
- view_count: 870,
763
- published_at: 2025-07-21T04:21:13.437Z,
764
- created_at: 2025-07-28T04:21:13.438Z,
765
- comments_ids: [],
766
- tags_ids: [ '1', '2', '3', '4', '5' ]
767
- },
768
- {
769
- id: '8',
770
- title: 'Post 8: Even Number Post',
771
- content: "This is the content of post number 8. It's a even numbered post.",
772
- view_count: 848,
773
- published_at: 2025-07-11T04:21:13.410Z,
774
- created_at: 2025-07-28T04:21:13.410Z,
775
- comments_ids: [],
776
- tags_ids: [ '1', '2', '3' ]
777
- },
778
- {
779
- id: '22',
780
- title: 'Post 22: Even Number Post',
781
- content: "This is the content of post number 22. It's a even numbered post.",
782
- view_count: 838,
783
- published_at: 2025-07-25T04:21:13.449Z,
784
- created_at: 2025-07-28T04:21:13.450Z,
785
- comments_ids: [],
786
- tags_ids: [ '1', '2', '3', '4' ]
787
- }
788
- ],
789
- meta: {
790
- pagination: { page: 1, pageSize: 5, pageCount: 5, total: 25, hasMore: true }
791
- },
792
- links: {
793
- self: '/api/posts?sort=-view_count&&&page[number]=1&page[size]=5',
794
- first: '/api/posts?sort=-view_count&&&page[number]=1&page[size]=5',
795
- last: '/api/posts?sort=-view_count&&&page[number]=5&page[size]=5',
796
- next: '/api/posts?sort=-view_count&&&page[number]=2&page[size]=5'
797
- }
798
- }
799
- Multisort: {
800
- data: [
801
- {
802
- id: '1',
803
- title: 'Post 1: Odd Number Post',
804
- content: "This is the content of post number 1. It's a odd numbered post.",
805
- view_count: 468,
806
- published_at: 2025-07-04T04:21:13.385Z,
807
- created_at: 2025-07-28T04:21:13.386Z,
808
- comments_ids: [
809
- '1', '2', '3', '4',
810
- '5', '6', '7', '8',
811
- '9', '10', '11', '12'
812
- ],
813
- tags_ids: [ '1', '2', '3' ]
814
- },
815
- {
816
- id: '2',
817
- title: 'Post 2: Even Number Post',
818
- content: "This is the content of post number 2. It's a even numbered post.",
819
- view_count: 652,
820
- published_at: 2025-07-05T04:21:13.389Z,
821
- created_at: 2025-07-28T04:21:13.390Z,
822
- comments_ids: [
823
- '13', '14', '15',
824
- '16', '17', '18',
825
- '19', '20', '21',
826
- '22', '23', '24'
827
- ],
828
- tags_ids: [ '1', '2', '3', '4' ]
829
- },
830
- {
831
- id: '3',
832
- title: 'Post 3: Odd Number Post',
833
- content: "This is the content of post number 3. It's a odd numbered post.",
834
- view_count: 198,
835
- published_at: 2025-07-06T04:21:13.393Z,
836
- created_at: 2025-07-28T04:21:13.394Z,
837
- comments_ids: [
838
- '25', '26', '27',
839
- '28', '29', '30',
840
- '31', '32', '33',
841
- '34', '35', '36'
842
- ],
843
- tags_ids: [ '1', '2', '3', '4', '5' ]
844
- },
845
- {
846
- id: '4',
847
- title: 'Post 4: Even Number Post',
848
- content: "This is the content of post number 4. It's a even numbered post.",
849
- view_count: 606,
850
- published_at: 2025-07-07T04:21:13.396Z,
851
- created_at: 2025-07-28T04:21:13.397Z,
852
- comments_ids: [
853
- '37', '38', '39',
854
- '40', '41', '42',
855
- '43', '44', '45',
856
- '46'
857
- ],
858
- tags_ids: [ '1', '2', '3', '4', '5' ]
859
- },
860
- {
861
- id: '5',
862
- title: 'Post 5: Odd Number Post',
863
- content: "This is the content of post number 5. It's a odd numbered post.",
864
- view_count: 142,
865
- published_at: 2025-07-08T04:21:13.400Z,
866
- created_at: 2025-07-28T04:21:13.401Z,
867
- comments_ids: [
868
- '47', '48', '49',
869
- '50', '51', '52',
870
- '53', '54', '55',
871
- '56'
872
- ],
873
- tags_ids: [ '1', '2', '3', '4', '5' ]
874
- }
875
- ],
876
- meta: {
877
- pagination: { page: 1, pageSize: 5, pageCount: 5, total: 25, hasMore: true }
878
- },
879
- links: {
880
- self: '/api/posts?sort=published_at&sort=-view_count&&&page[number]=1&page[size]=5',
881
- first: '/api/posts?sort=published_at&sort=-view_count&&&page[number]=1&page[size]=5',
882
- last: '/api/posts?sort=published_at&sort=-view_count&&&page[number]=5&page[size]=5',
883
- next: '/api/posts?sort=published_at&sort=-view_count&&&page[number]=2&page[size]=5'
884
- }
885
- }
886
- ```
887
-
888
- **Limits and Ordering for Included Relationships**
889
-
890
- When you include relationships in your query, they use the default `limit` and `orderBy` values defined in the relationship schema. These defaults are applied automatically and cannot be overridden in the query:
891
-
892
- ```javascript
893
- // The relationship was defined with default limits:
894
- // relationships: {
895
- // comments: {
896
- // type: 'hasMany',
897
- // target: 'comments',
898
- // foreignKey: 'post_id',
899
- // include: {
900
- // limit: 5, // Always return max 5 comments
901
- // orderBy: ['-created_at'] // Always order by newest first
902
- // }
903
- // }
904
- // }
905
-
906
- // When you include comments, these defaults are automatically applied:
907
- const postsWithComments = await api.resources.posts.query({
908
- queryParams: {
909
- include: ['comments'], // Just specify which relationships to include
910
- page: { number: 1, size: 3 } // This controls posts pagination only
911
- }
912
- });
913
- // HTTP: GET /api/posts?include=comments&page[number]=1&page[size]=3
914
- // Returns: {
915
- // data: [
916
- // { id: '1', title: 'Post 1', ...,
917
- // comments: [ /* Up to 5 comments, ordered by newest first */ ]
918
- // },
919
- // { id: '2', title: 'Post 2', ...,
920
- // comments: [ /* Up to 5 comments, newest first */ ]
921
- // },
922
- // { id: '3', title: 'Post 3', ...,
923
- // comments: [ /* Up to 5 comments, newest first */ ]
924
- // }
925
- // ],
926
- // meta: { pagination: { page: 1, pageSize: 3, pageCount: 9, total: 25 } }
927
- // }
928
-
929
- // Display the results
930
- console.log('Posts with comments:', inspect(postsWithComments));
931
-
932
- // Get posts with both comments and tags, using their configured limits
933
- const postsWithAll = await api.resources.posts.query({
934
- queryParams: {
935
- include: ['comments', 'tags'],
936
- page: { number: 1, size: 2 }
937
- }
938
- });
939
-
940
- console.log('\nPosts with all relationships:', inspect(postsWithAll));
941
- ```
942
-
943
- **Expected Output**
944
-
945
- ```text
946
- Posts with comments: {
947
- data: [
948
- {
949
- id: '25',
950
- title: 'Post 25: Odd Number Post',
951
- content: "This is the content of post number 25. It's a odd numbered post.",
952
- view_count: 483,
953
- published_at: 2025-07-28T04:50:14.529Z,
954
- created_at: 2025-07-28T04:50:14.530Z,
955
- comments_ids: [],
956
- tags_ids: [ '1', '2', '3', '4' ]
957
- },
958
- {
959
- id: '24',
960
- title: 'Post 24: Even Number Post',
961
- content: "This is the content of post number 24. It's a even numbered post.",
962
- view_count: 719,
963
- published_at: 2025-07-27T04:50:14.526Z,
964
- created_at: 2025-07-28T04:50:14.527Z,
965
- comments_ids: [],
966
- tags_ids: [ '1', '2', '3', '4' ]
967
- },
968
- {
969
- id: '23',
970
- title: 'Post 23: Odd Number Post',
971
- content: "This is the content of post number 23. It's a odd numbered post.",
972
- view_count: 167,
973
- published_at: 2025-07-26T04:50:14.523Z,
974
- created_at: 2025-07-28T04:50:14.524Z,
975
- comments_ids: [],
976
- tags_ids: [ '1', '2', '3', '4', '5' ]
977
- }
978
- ],
979
- meta: {
980
- pagination: { page: 1, pageSize: 3, pageCount: 9, total: 25, hasMore: true }
981
- },
982
- links: {
983
- self: '/api/posts?include=comments&&sort=-created_at&page[number]=1&page[size]=3',
984
- first: '/api/posts?include=comments&&sort=-created_at&page[number]=1&page[size]=3',
985
- last: '/api/posts?include=comments&&sort=-created_at&page[number]=9&page[size]=3',
986
- next: '/api/posts?include=comments&&sort=-created_at&page[number]=2&page[size]=3'
987
- }
988
- }
989
-
990
- Posts with all relationships: {
991
- data: [
992
- {
993
- id: '25',
994
- title: 'Post 25: Odd Number Post',
995
- content: "This is the content of post number 25. It's a odd numbered post.",
996
- view_count: 483,
997
- published_at: 2025-07-28T04:50:14.529Z,
998
- created_at: 2025-07-28T04:50:14.530Z,
999
- comments_ids: [],
1000
- tags_ids: [ '1', '2', '3', '4' ],
1001
- tags: [
1002
- { id: '1', name: 'tag-01', usage_count: 74, posts_ids: [] },
1003
- { id: '2', name: 'tag-02', usage_count: 36, posts_ids: [] },
1004
- { id: '3', name: 'tag-03', usage_count: 59, posts_ids: [] },
1005
- { id: '4', name: 'tag-04', usage_count: 63, posts_ids: [] }
1006
- ]
1007
- },
1008
- {
1009
- id: '24',
1010
- title: 'Post 24: Even Number Post',
1011
- content: "This is the content of post number 24. It's a even numbered post.",
1012
- view_count: 719,
1013
- published_at: 2025-07-27T04:50:14.526Z,
1014
- created_at: 2025-07-28T04:50:14.527Z,
1015
- comments_ids: [],
1016
- tags_ids: [ '1', '2', '3', '4' ],
1017
- tags: [
1018
- { id: '1', name: 'tag-01', usage_count: 74, posts_ids: [] },
1019
- { id: '2', name: 'tag-02', usage_count: 36, posts_ids: [] },
1020
- { id: '3', name: 'tag-03', usage_count: 59, posts_ids: [] },
1021
- { id: '4', name: 'tag-04', usage_count: 63, posts_ids: [] }
1022
- ]
1023
- }
1024
- ],
1025
- meta: {
1026
- pagination: { page: 1, pageSize: 2, pageCount: 13, total: 25, hasMore: true }
1027
- },
1028
- links: {
1029
- self: '/api/posts?include=comments&include=tags&&sort=-created_at&page[number]=1&page[size]=2',
1030
- first: '/api/posts?include=comments&include=tags&&sort=-created_at&page[number]=1&page[size]=2',
1031
- last: '/api/posts?include=comments&include=tags&&sort=-created_at&page[number]=13&page[size]=2',
1032
- next: '/api/posts?include=comments&include=tags&&sort=-created_at&page[number]=2&page[size]=2'
1033
- }
1034
- }
1035
- ```
1036
-
1037
- **Pagination Links**
1038
-
1039
- The API automatically generates pagination links following JSON:API specification:
1040
-
1041
- ```javascript
1042
- // Get a page with full pagination info
1043
- const pagedResult = await api.resources.posts.query({
1044
- queryParams: {
1045
- page: { number: 2, size: 5 },
1046
- sort: ['-published_at']
1047
- },
1048
- simplified: false // Use JSON:API format to see links
1049
- });
1050
- // HTTP: GET /api/posts?page[number]=2&page[size]=5&sort=-published_at
1051
- // Returns (JSON:API): {
1052
- // data: [ /* 5 posts */ ],
1053
- // links: {
1054
- // first: '/api/posts?page[number]=1&page[size]=5&sort=-published_at',
1055
- // prev: '/api/posts?page[number]=1&page[size]=5&sort=-published_at',
1056
- // next: '/api/posts?page[number]=3&page[size]=5&sort=-published_at',
1057
- // last: '/api/posts?page[number]=5&page[size]=5&sort=-published_at'
1058
- // },
1059
- // meta: {
1060
- // pagination: {
1061
- // page: 2,
1062
- // pageSize: 5,
1063
- // pageCount: 5,
1064
- // total: 25
1065
- // }
1066
- // }
1067
- // }
1068
-
1069
- console.log('Pagination links:', JSON.stringify(pagedResult.links, null, 2));
1070
- console.log('Pagination meta:', JSON.stringify(pagedResult.meta, null, 2));
1071
- ```
1072
-
1073
- **Expected Output**
1074
-
1075
- ```text
1076
- Pagination links: {
1077
- "self": "/api/posts?sort=-published_at&&&page[number]=2&page[size]=5",
1078
- "first": "/api/posts?sort=-published_at&&&page[number]=1&page[size]=5",
1079
- "last": "/api/posts?sort=-published_at&&&page[number]=5&page[size]=5",
1080
- "prev": "/api/posts?sort=-published_at&&&page[number]=1&page[size]=5",
1081
- "next": "/api/posts?sort=-published_at&&&page[number]=3&page[size]=5"
1082
- }
1083
- Pagination meta: {
1084
- "pagination": {
1085
- "page": 2,
1086
- "pageSize": 5,
1087
- "pageCount": 5,
1088
- "total": 25,
1089
- "hasMore": true
1090
- }
1091
- }
1092
- ```
1093
-
1094
- **Combining Everything: Filters, Sorting, Pagination, and Includes**
1095
-
1096
- ```javascript
1097
- // Complex query combining all features
1098
- const complexQuery = await api.resources.posts.query({
1099
- queryParams: {
1100
- filters: {
1101
- title: 'Even' // Only posts with "Even" in title
1102
- },
1103
- sort: ['-view_count', 'published_at'], // Most viewed first, then oldest
1104
- page: { number: 1, size: 3 },
1105
- include: ['comments', 'tags']
1106
- }
1107
- });
1108
- // HTTP: GET /api/posts?filter[title]=Even&sort=-view_count,published_at&page[number]=1&page[size]=3&include=comments,tags
1109
- // Returns: {
1110
- // data: [
1111
- // { id: '20', title: 'Even Post 20', view_count: 750,
1112
- // comments: [ /* Up to 5 newest */ ],
1113
- // tags: [ /* Up to 10, alphabetical */ ]
1114
- // },
1115
- // { id: '10', title: 'Even Post 10', view_count: 650,
1116
- // comments: [ /* Up to 5 newest */ ],
1117
- // tags: [ /* Up to 10, alphabetical */ ]
1118
- // },
1119
- // { id: '2', title: 'Even Post 2', view_count: 200,
1120
- // comments: [ /* Up to 5 newest */ ],
1121
- // tags: [ /* Up to 10, alphabetical */ ]
1122
- // }
1123
- // ],
1124
- // meta: {
1125
- // pagination: {
1126
- // page: 1,
1127
- // pageSize: 3,
1128
- // pageCount: 4, // 12 "Even" posts total
1129
- // total: 12
1130
- // }
1131
- // }
1132
- // });
1133
-
1134
- console.log('\nComplex query results:', inspect(complexQuery));
1135
- ```
1136
-
1137
- **Expected results:**
1138
-
1139
- ```text
1140
- Complex query results: {
1141
- data: [
1142
- {
1143
- id: '14',
1144
- title: 'Post 14: Even Number Post',
1145
- content: "This is the content of post number 14. It's a even numbered post.",
1146
- view_count: 968,
1147
- published_at: 2025-07-17T05:03:41.480Z,
1148
- created_at: 2025-07-28T05:03:41.480Z,
1149
- comments_ids: [],
1150
- tags_ids: [ '1', '2', '3' ],
1151
- tags: [
1152
- { id: '1', name: 'tag-01', usage_count: 43, posts_ids: [] },
1153
- { id: '2', name: 'tag-02', usage_count: 80, posts_ids: [] },
1154
- { id: '3', name: 'tag-03', usage_count: 6, posts_ids: [] }
1155
- ]
1156
- },
1157
- {
1158
- id: '2',
1159
- title: 'Post 2: Even Number Post',
1160
- content: "This is the content of post number 2. It's a even numbered post.",
1161
- view_count: 844,
1162
- published_at: 2025-07-05T05:03:41.439Z,
1163
- created_at: 2025-07-28T05:03:41.440Z,
1164
- comments_ids: [ '19', '18', '17', '16', '15' ],
1165
- comments: [
1166
- {
1167
- id: '19',
1168
- content: 'Comment 10 on post 2',
1169
- author_name: 'User 3',
1170
- created_at: 2025-07-28T05:03:41.542Z,
1171
- likes: 27
1172
- },
1173
- {
1174
- id: '18',
1175
- content: 'Comment 9 on post 2',
1176
- author_name: 'User 2',
1177
- created_at: 2025-07-28T04:03:41.541Z,
1178
- likes: 27
1179
- },
1180
- {
1181
- id: '17',
1182
- content: 'Comment 8 on post 2',
1183
- author_name: 'User 7',
1184
- created_at: 2025-07-28T03:03:41.539Z,
1185
- likes: 31
1186
- },
1187
- {
1188
- id: '16',
1189
- content: 'Comment 7 on post 2',
1190
- author_name: 'User 10',
1191
- created_at: 2025-07-28T02:03:41.538Z,
1192
- likes: 27
1193
- },
1194
- {
1195
- id: '15',
1196
- content: 'Comment 6 on post 2',
1197
- author_name: 'User 9',
1198
- created_at: 2025-07-28T01:03:41.535Z,
1199
- likes: 46
1200
- }
1201
- ],
1202
- tags_ids: [ '1', '2', '3', '4' ],
1203
- tags: [
1204
- { id: '1', name: 'tag-01', usage_count: 43, posts_ids: [] },
1205
- { id: '2', name: 'tag-02', usage_count: 80, posts_ids: [] },
1206
- { id: '3', name: 'tag-03', usage_count: 6, posts_ids: [] },
1207
- { id: '4', name: 'tag-04', usage_count: 92, posts_ids: [] }
1208
- ]
1209
- },
1210
- {
1211
- id: '18',
1212
- title: 'Post 18: Even Number Post',
1213
- content: "This is the content of post number 18. It's a even numbered post.",
1214
- view_count: 623,
1215
- published_at: 2025-07-21T05:03:41.492Z,
1216
- created_at: 2025-07-28T05:03:41.492Z,
1217
- comments_ids: [],
1218
- tags_ids: [ '1', '2', '3', '4' ],
1219
- tags: [
1220
- { id: '1', name: 'tag-01', usage_count: 43, posts_ids: [] },
1221
- { id: '2', name: 'tag-02', usage_count: 80, posts_ids: [] },
1222
- { id: '3', name: 'tag-03', usage_count: 6, posts_ids: [] },
1223
- { id: '4', name: 'tag-04', usage_count: 92, posts_ids: [] }
1224
- ]
1225
- }
1226
- ],
1227
- meta: {
1228
- pagination: { page: 1, pageSize: 3, pageCount: 4, total: 12, hasMore: true }
1229
- },
1230
- links: {
1231
- self: '/api/posts?filters[title]=Even&sort=-view_count&sort=published_at&include=comments&include=tags&&page[number]=1&page[size]=3',
1232
- first: '/api/posts?filters[title]=Even&sort=-view_count&sort=published_at&include=comments&include=tags&&page[number]=1&page[size]=3',
1233
- last: '/api/posts?filters[title]=Even&sort=-view_count&sort=published_at&include=comments&include=tags&&page[number]=4&page[size]=3',
1234
- next: '/api/posts?filters[title]=Even&sort=-view_count&sort=published_at&include=comments&include=tags&&page[number]=2&page[size]=3'
1235
- }
1236
- }
1237
- ```
1238
-
1239
- The pagination and ordering system is designed to be efficient and predictable:
1240
- - Parent records are paginated and sorted according to the main query
1241
- - Child records are independently paginated and sorted according to their relationship configuration
1242
- - All limits respect the configured maximums to prevent performance issues
1243
- - The system automatically uses SQL window functions for efficient pagination of included records when supported by the database
1244
-
1245
- ---
1246
-
1247
- [Previous: 2.6 Many to Many Relationships](./GUIDE_2_6_Many_To_Many.md) | [Back to Guide](./README.md)