json-rest-api 1.0.29 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (294) 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 +733 -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 +103 -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 +3838 -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 +37 -0
  44. package/index.js +3 -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 +70 -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 +586 -418
  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 +120 -56
  71. package/plugins/core/lib/anyapi/stored-definition.js +109 -0
  72. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
  73. package/plugins/core/lib/dbIntrospection.js +228 -21
  74. package/plugins/core/lib/dbTablesOperations.js +409 -92
  75. package/plugins/core/lib/fractional-positioning.js +12 -48
  76. package/plugins/core/lib/querying/identifier-query.js +19 -0
  77. package/plugins/core/lib/querying/include-query-helpers.js +364 -0
  78. package/plugins/core/lib/querying/include-to-many.js +428 -0
  79. package/plugins/core/lib/querying/include-to-one.js +579 -0
  80. package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
  81. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
  82. package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
  83. package/plugins/core/lib/querying/knex-process-includes.js +5 -110
  84. package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
  85. package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
  86. package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
  87. package/plugins/core/lib/querying/knex-window-queries.js +62 -253
  88. package/plugins/core/lib/querying/query-builder-utils.js +44 -4
  89. package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
  90. package/plugins/core/lib/querying/query-constraint.js +27 -0
  91. package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
  92. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  93. package/plugins/core/lib/querying/sort-helpers.js +13 -1
  94. package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
  95. package/plugins/core/lib/querying/url-helpers.js +11 -4
  96. package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
  97. package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
  98. package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
  99. package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
  100. package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
  101. package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
  102. package/plugins/core/lib/querying-writing/field-utils.js +113 -92
  103. package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
  104. package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
  105. package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
  106. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
  107. package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
  108. package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
  109. package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
  110. package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
  111. package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
  112. package/plugins/core/lib/querying-writing/response-options.js +35 -0
  113. package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
  114. package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
  115. package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
  116. package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
  117. package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
  118. package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
  119. package/plugins/core/lib/storage/storage-adapter.js +128 -62
  120. package/plugins/core/lib/storage/storage-mapping.js +78 -28
  121. package/plugins/core/lib/storage/storage-types.d.ts +502 -0
  122. package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
  123. package/plugins/core/lib/writing/relationship-processor.js +94 -158
  124. package/plugins/core/lib/writing/resource-version.js +172 -0
  125. package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
  126. package/plugins/core/rest-api-anyapi-knex-plugin.js +290 -2475
  127. package/plugins/core/rest-api-autofilter-plugin.js +17 -19
  128. package/plugins/core/rest-api-cors-plugin.js +52 -205
  129. package/plugins/core/rest-api-knex-plugin.js +45 -1077
  130. package/plugins/core/rest-api-label-plugin.js +25 -38
  131. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
  132. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
  133. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
  134. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
  135. package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
  136. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
  137. package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
  138. package/plugins/core/rest-api-plugin-methods/common.js +291 -395
  139. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
  140. package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
  141. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
  142. package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
  143. package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
  144. package/plugins/core/rest-api-plugin-methods/get.js +36 -68
  145. package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
  146. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
  147. package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
  148. package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
  149. package/plugins/core/rest-api-plugin-methods/post.js +51 -51
  150. package/plugins/core/rest-api-plugin-methods/put.js +69 -148
  151. package/plugins/core/rest-api-plugin-methods/query.js +37 -81
  152. package/plugins/core/rest-api-plugin-methods/release.js +2 -1
  153. package/plugins/core/rest-api-plugin.js +32 -46
  154. package/plugins/core/rest-api-positioning-plugin.js +183 -628
  155. package/plugins/core/rest-api-query-projections-plugin.js +19 -102
  156. package/plugins/core/rest-api-row-policy-plugin.js +3 -3
  157. package/plugins/core/socketio-plugin.js +364 -344
  158. package/plugins/storage/local-storage.js +40 -36
  159. package/types/anyapi.d.ts +30 -0
  160. package/types/autofilter.d.ts +57 -0
  161. package/types/bulk-methods.d.ts +63 -0
  162. package/types/errors.d.ts +118 -0
  163. package/types/fastify-options.d.ts +19 -0
  164. package/types/file-detectors.d.ts +33 -0
  165. package/types/file-storage.d.ts +48 -0
  166. package/types/hook-context.d.ts +180 -0
  167. package/types/plugin-options.d.ts +65 -0
  168. package/types/relationship-methods.d.ts +74 -0
  169. package/types/representations.d.ts +50 -0
  170. package/types/resource-methods.d.ts +95 -0
  171. package/types/resource-schema.d.ts +157 -0
  172. package/types/row-policy.d.ts +31 -0
  173. package/types/runtime.d.ts +132 -0
  174. package/types/transactions.d.ts +21 -0
  175. package/.claude/agents/library-guide-expert.md +0 -30
  176. package/.claude/agents/rest-api-internals-expert.md +0 -54
  177. package/.claude/settings.local.json +0 -70
  178. package/AGENTS.md +0 -34
  179. package/CLAUDE.md +0 -160
  180. package/CRUSH.md +0 -55
  181. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
  182. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
  183. package/TODO.todo +0 -13
  184. package/connectors_ideas/fastify.js +0 -217
  185. package/connectors_ideas/koa-plugin.js +0 -112
  186. package/docs/.bundle/config +0 -2
  187. package/docs/COMPARISON.md +0 -433
  188. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
  189. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
  190. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
  191. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
  192. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
  193. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
  194. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
  195. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
  196. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
  197. package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
  198. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
  199. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
  200. package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
  201. package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
  202. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
  203. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
  204. package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
  205. package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
  206. package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
  207. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
  208. package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
  209. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
  210. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
  211. package/docs/Gemfile +0 -6
  212. package/docs/Gemfile.lock +0 -308
  213. package/docs/ONBOARDING.md +0 -315
  214. package/docs/_config.yml +0 -36
  215. package/docs/_layouts/default.html +0 -123
  216. package/docs/apple-touch-icon.svg +0 -4
  217. package/docs/assets/css/style.scss +0 -392
  218. package/docs/favicon.svg +0 -4
  219. package/docs/index.md +0 -164
  220. package/docs/mini-guides/file-uploads-complete.md +0 -161
  221. package/eslint.config.js +0 -13
  222. package/examples/url-override-example.js +0 -137
  223. package/fixing_plan.md +0 -112
  224. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
  225. package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
  226. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
  227. package/plugins/storage/s3-storage.js +0 -93
  228. package/quickTest.js +0 -136
  229. package/scripts/debug-anyapi-review-filter.mjs +0 -63
  230. package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
  231. package/scripts/inspect-anyapi-descriptor.mjs +0 -19
  232. package/scripts/run-docs.js +0 -133
  233. package/test-debug.js +0 -59
  234. package/tests/README.md +0 -202
  235. package/tests/TEST_TEMPLATE.test.js +0 -178
  236. package/tests/anyapi-basic.test.js +0 -105
  237. package/tests/anyapi-cursor-pagination.test.js +0 -148
  238. package/tests/anyapi-custom-idproperty.test.js +0 -163
  239. package/tests/autofilter.test.js +0 -463
  240. package/tests/bulk-operations.test.js +0 -657
  241. package/tests/computed-fields.test.js +0 -586
  242. package/tests/cors.test.js +0 -834
  243. package/tests/curl.test.js +0 -467
  244. package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
  245. package/tests/custom-id-simplified-post.test.js +0 -75
  246. package/tests/custom-idproperty-relationships.test.js +0 -932
  247. package/tests/db-introspection.test.js +0 -521
  248. package/tests/db-table-operations.test.js +0 -798
  249. package/tests/fastify-plugin.test.js +0 -184
  250. package/tests/field-getters.test.js +0 -370
  251. package/tests/field-setters.test.js +0 -306
  252. package/tests/file-handling.test.js +0 -155
  253. package/tests/fixtures/api-configs.js +0 -2566
  254. package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
  255. package/tests/helpers/fake-fastify.js +0 -99
  256. package/tests/helpers/storage-mode.js +0 -65
  257. package/tests/helpers/test-utils.js +0 -203
  258. package/tests/http-connectors-parity.test.js +0 -395
  259. package/tests/id-normalization.test.js +0 -444
  260. package/tests/include-depth-validation.test.js +0 -331
  261. package/tests/includes.test.js +0 -427
  262. package/tests/local-storage.test.js +0 -96
  263. package/tests/logical-id-write-validation.test.js +0 -151
  264. package/tests/nested-includes.test.js +0 -617
  265. package/tests/pagination-boundaries.test.js +0 -154
  266. package/tests/pagination-contract.test.js +0 -122
  267. package/tests/pagination-cursor-multifield.test.js +0 -641
  268. package/tests/pagination-enhanced.test.js +0 -563
  269. package/tests/polymorphic-relationships.test.js +0 -347
  270. package/tests/polymorphic-simple.test.js +0 -148
  271. package/tests/positioning-advanced.test.js +0 -692
  272. package/tests/positioning-extended.test.js +0 -915
  273. package/tests/positioning-multifilter.test.js +0 -699
  274. package/tests/positioning.test.js +0 -560
  275. package/tests/post-commit-hooks.test.js +0 -120
  276. package/tests/projected-fields.test.js +0 -319
  277. package/tests/public-exports.test.js +0 -15
  278. package/tests/put-create-parity.test.js +0 -216
  279. package/tests/queries.test.js +0 -521
  280. package/tests/query-limits.test.js +0 -584
  281. package/tests/relationship-endpoints.test.js +0 -620
  282. package/tests/relationship-include-storage-adapter.test.js +0 -56
  283. package/tests/relationships.test.js +0 -681
  284. package/tests/rest-api.test.js +0 -337
  285. package/tests/return-record-settings.test.js +0 -184
  286. package/tests/row-policy.test.js +0 -489
  287. package/tests/s3-storage.test.js +0 -38
  288. package/tests/searchschema-knex-behavior.test.js +0 -187
  289. package/tests/searchschema-merge.test.js +0 -151
  290. package/tests/socketio.test.js +0 -860
  291. package/tests/storage-mapping.test.js +0 -362
  292. package/tests/temporal-boundaries.test.js +0 -208
  293. package/tests/test-development-guide.md +0 -370
  294. 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)