json-rest-api 1.0.7 → 1.0.8

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