json-rest-api 1.0.14 → 1.0.15

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 (118) hide show
  1. package/connectors_ideas/fastify.js +66 -68
  2. package/connectors_ideas/koa-plugin.js +50 -51
  3. package/eslint.config.js +11 -2
  4. package/examples/url-override-example.js +63 -63
  5. package/index.js +13 -13
  6. package/lib/enhanced-logger.js +51 -51
  7. package/lib/error-formatter.js +61 -61
  8. package/lib/rest-api-errors.js +28 -28
  9. package/package.json +1 -1
  10. package/plugins/core/bulk-operations-plugin.js +142 -145
  11. package/plugins/core/connectors/express-plugin.js +210 -217
  12. package/plugins/core/connectors/lib/busboy-detector.js +84 -84
  13. package/plugins/core/connectors/lib/formidable-detector.js +50 -50
  14. package/plugins/core/connectors/lib/request-helpers.js +74 -74
  15. package/plugins/core/file-handling-plugin.js +125 -125
  16. package/plugins/core/lib/anyapi/anyapi-registry.js +254 -254
  17. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +287 -287
  18. package/plugins/core/lib/anyapi/schema-utils.js +101 -101
  19. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +41 -41
  20. package/plugins/core/lib/dbTablesOperations.js +134 -135
  21. package/plugins/core/lib/fractional-positioning.js +74 -76
  22. package/plugins/core/lib/querying/knex-cross-table-search.js +320 -320
  23. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +144 -144
  24. package/plugins/core/lib/querying/knex-pagination-helpers.js +164 -164
  25. package/plugins/core/lib/querying/knex-process-includes.js +50 -50
  26. package/plugins/core/lib/querying/knex-query-helpers-base.js +31 -31
  27. package/plugins/core/lib/querying/knex-query-helpers.js +451 -452
  28. package/plugins/core/lib/querying/knex-relationship-includes.js +1160 -1170
  29. package/plugins/core/lib/querying/knex-window-queries.js +89 -89
  30. package/plugins/core/lib/querying/url-helpers.js +35 -35
  31. package/plugins/core/lib/querying-writing/compile-schemas.js +97 -98
  32. package/plugins/core/lib/querying-writing/connectors-query-parser.js +31 -31
  33. package/plugins/core/lib/querying-writing/database-capabilities.js +94 -94
  34. package/plugins/core/lib/querying-writing/database-value-normalizers.js +76 -76
  35. package/plugins/core/lib/querying-writing/default-data-helpers.js +27 -27
  36. package/plugins/core/lib/querying-writing/field-utils.js +44 -42
  37. package/plugins/core/lib/querying-writing/knex-constants.js +15 -16
  38. package/plugins/core/lib/querying-writing/knex-field-helpers.js +117 -115
  39. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +41 -41
  40. package/plugins/core/lib/querying-writing/payload-validators.js +275 -275
  41. package/plugins/core/lib/querying-writing/schema-helpers.js +129 -129
  42. package/plugins/core/lib/querying-writing/scope-validations.js +143 -146
  43. package/plugins/core/lib/querying-writing/simplified-helpers.js +133 -129
  44. package/plugins/core/lib/storage/storage-adapter.js +68 -68
  45. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +31 -31
  46. package/plugins/core/lib/writing/many-to-many-manipulations.js +81 -81
  47. package/plugins/core/lib/writing/relationship-processor.js +58 -58
  48. package/plugins/core/rest-api-access.js +8 -8
  49. package/plugins/core/rest-api-anyapi-knex-plugin.js +920 -920
  50. package/plugins/core/rest-api-cors-plugin.js +170 -171
  51. package/plugins/core/rest-api-knex-plugin.js +482 -493
  52. package/plugins/core/rest-api-multihome-plugin.js +247 -251
  53. package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +5 -5
  54. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +20 -21
  55. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +44 -44
  56. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +23 -23
  57. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +15 -15
  58. package/plugins/core/rest-api-plugin-methods/add-route.js +9 -9
  59. package/plugins/core/rest-api-plugin-methods/check-permissions.js +5 -6
  60. package/plugins/core/rest-api-plugin-methods/common.js +295 -297
  61. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +35 -37
  62. package/plugins/core/rest-api-plugin-methods/delete.js +57 -59
  63. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +172 -171
  64. package/plugins/core/rest-api-plugin-methods/get-related.js +96 -96
  65. package/plugins/core/rest-api-plugin-methods/get-relationship.js +16 -18
  66. package/plugins/core/rest-api-plugin-methods/get.js +86 -86
  67. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +16 -17
  68. package/plugins/core/rest-api-plugin-methods/patch.js +63 -64
  69. package/plugins/core/rest-api-plugin-methods/post-relationship.js +54 -55
  70. package/plugins/core/rest-api-plugin-methods/post.js +61 -62
  71. package/plugins/core/rest-api-plugin-methods/put.js +86 -87
  72. package/plugins/core/rest-api-plugin-methods/query.js +88 -88
  73. package/plugins/core/rest-api-plugin-methods/release.js +5 -6
  74. package/plugins/core/rest-api-plugin.js +76 -78
  75. package/plugins/core/rest-api-positioning-plugin.js +251 -252
  76. package/plugins/core/socketio-plugin.js +240 -236
  77. package/plugins/storage/local-storage.js +103 -103
  78. package/plugins/storage/s3-storage.js +41 -42
  79. package/quickTest.js +37 -37
  80. package/scripts/debug-anyapi-review-filter.mjs +18 -18
  81. package/scripts/inspect-anyapi-descriptor-country.mjs +10 -10
  82. package/scripts/inspect-anyapi-descriptor.mjs +10 -10
  83. package/scripts/run-docs.js +112 -113
  84. package/test-debug.js +28 -28
  85. package/tests/TEST_TEMPLATE.test.js +51 -51
  86. package/tests/anyapi-basic.test.js +42 -42
  87. package/tests/anyapi-cursor-pagination.test.js +48 -48
  88. package/tests/bulk-operations.test.js +208 -208
  89. package/tests/computed-fields.test.js +204 -204
  90. package/tests/cors.test.js +412 -413
  91. package/tests/curl.test.js +275 -276
  92. package/tests/custom-idproperty-relationships.test.js +266 -266
  93. package/tests/field-getters.test.js +132 -132
  94. package/tests/field-setters.test.js +110 -110
  95. package/tests/fixtures/api-configs.js +699 -699
  96. package/tests/helpers/storage-mode.js +32 -32
  97. package/tests/helpers/test-utils.js +76 -76
  98. package/tests/include-depth-validation.test.js +76 -76
  99. package/tests/includes.test.js +145 -145
  100. package/tests/multihome.test.js +175 -175
  101. package/tests/nested-includes.test.js +255 -255
  102. package/tests/pagination-cursor-multifield.test.js +164 -164
  103. package/tests/pagination-enhanced.test.js +225 -225
  104. package/tests/polymorphic-relationships.test.js +96 -96
  105. package/tests/polymorphic-simple.test.js +44 -44
  106. package/tests/positioning-advanced.test.js +226 -226
  107. package/tests/positioning-extended.test.js +399 -399
  108. package/tests/positioning-multifilter.test.js +204 -204
  109. package/tests/positioning.test.js +266 -266
  110. package/tests/queries.test.js +184 -184
  111. package/tests/query-limits.test.js +154 -155
  112. package/tests/relationship-endpoints.test.js +127 -127
  113. package/tests/relationships.test.js +178 -179
  114. package/tests/rest-api-access.test.js +86 -86
  115. package/tests/rest-api.test.js +103 -103
  116. package/tests/searchschema-merge.test.js +93 -93
  117. package/tests/socketio.test.js +326 -326
  118. package/tests/virtual-fields.test.js +164 -164
@@ -1,24 +1,24 @@
1
1
  /**
2
2
  * Calculates pagination metadata from query results
3
- *
3
+ *
4
4
  * @param {number} total - Total number of records in the dataset
5
5
  * @param {number} page - Current page number (1-based)
6
6
  * @param {number} pageSize - Number of records per page
7
7
  * @returns {Object} Pagination metadata including page count and hasMore flag
8
8
  * @throws {Error} If pageSize <= 0 or page < 1
9
- *
9
+ *
10
10
  * @example
11
11
  * // Input: 100 total records, viewing page 2 of 20 records each
12
12
  * const meta = calculatePaginationMeta(100, 2, 20);
13
13
  * // Output:
14
14
  * // {
15
15
  * // page: 2, // current page
16
- * // pageSize: 20, // records per page
16
+ * // pageSize: 20, // records per page
17
17
  * // pageCount: 5, // total pages (100/20)
18
18
  * // total: 100, // total records
19
19
  * // hasMore: true // page 2 < 5, so more pages exist
20
20
  * // }
21
- *
21
+ *
22
22
  * @example
23
23
  * // Input: Last page scenario
24
24
  * const meta = calculatePaginationMeta(45, 5, 10);
@@ -30,18 +30,18 @@
30
30
  * // total: 45,
31
31
  * // hasMore: false // on last page
32
32
  * // }
33
- *
33
+ *
34
34
  * @description
35
35
  * Used by:
36
36
  * - rest-api-knex-plugin's dataQuery method after counting total records
37
37
  * - generatePaginationLinks to build navigation links
38
38
  * - Applied when offset-based pagination is enabled
39
- *
39
+ *
40
40
  * Purpose:
41
41
  * - Provides consistent pagination metadata across all collection responses
42
42
  * - Calculates derived values like pageCount and hasMore flag
43
43
  * - Validates pagination parameters to prevent invalid states
44
- *
44
+ *
45
45
  * Data flow:
46
46
  * 1. Query method counts total records when pagination.counts is enabled
47
47
  * 2. calculatePaginationMeta processes the count with current page/size
@@ -50,34 +50,34 @@
50
50
  */
51
51
  export const calculatePaginationMeta = (total, page, pageSize) => {
52
52
  if (pageSize <= 0) {
53
- throw new Error('Page size must be greater than 0');
53
+ throw new Error('Page size must be greater than 0')
54
54
  }
55
55
  if (page < 1) {
56
- throw new Error('Page number must be greater than 0');
56
+ throw new Error('Page number must be greater than 0')
57
57
  }
58
-
59
- const pageCount = Math.ceil(total / pageSize);
60
- const currentPage = page || 1;
61
- const hasMore = currentPage < pageCount;
62
-
58
+
59
+ const pageCount = Math.ceil(total / pageSize)
60
+ const currentPage = page || 1
61
+ const hasMore = currentPage < pageCount
62
+
63
63
  return {
64
64
  page: currentPage,
65
65
  pageSize,
66
66
  pageCount,
67
67
  total,
68
68
  hasMore
69
- };
70
- };
69
+ }
70
+ }
71
71
 
72
72
  /**
73
73
  * Generates JSON:API compliant pagination links for offset-based pagination
74
- *
74
+ *
75
75
  * @param {string} urlPrefix - Base URL prefix for the API
76
76
  * @param {string} scopeName - Resource type name
77
77
  * @param {Object} queryParams - Current query parameters
78
78
  * @param {Object} paginationMeta - Pagination metadata from calculatePaginationMeta
79
79
  * @returns {Object|null} Links object with self, first, last, prev, next
80
- *
80
+ *
81
81
  * @example
82
82
  * // Input data:
83
83
  * const urlPrefix = '/api/v1';
@@ -94,9 +94,9 @@ export const calculatePaginationMeta = (total, page, pageSize) => {
94
94
  * total: 100,
95
95
  * hasMore: true
96
96
  * };
97
- *
97
+ *
98
98
  * const links = generatePaginationLinks(urlPrefix, scopeName, queryParams, paginationMeta);
99
- *
99
+ *
100
100
  * // Output - complete URLs with all parameters preserved:
101
101
  * // {
102
102
  * // self: '/api/articles?filter[status]=published&sort=-created_at&page[number]=2&page[size]=20',
@@ -105,12 +105,12 @@ export const calculatePaginationMeta = (total, page, pageSize) => {
105
105
  * // prev: '/api/articles?filter[status]=published&sort=-created_at&page[number]=1&page[size]=20',
106
106
  * // next: '/api/articles?filter[status]=published&sort=-created_at&page[number]=3&page[size]=20'
107
107
  * // }
108
- *
108
+ *
109
109
  * @example
110
110
  * // Input: First page (no prev link)
111
111
  * const paginationMeta = { page: 1, pageSize: 10, pageCount: 3 };
112
112
  * const links = generatePaginationLinks('/api', 'users', {}, paginationMeta);
113
- *
113
+ *
114
114
  * // Output - notice no 'prev' property:
115
115
  * // {
116
116
  * // self: '/api/users?page[number]=1&page[size]=10',
@@ -118,18 +118,18 @@ export const calculatePaginationMeta = (total, page, pageSize) => {
118
118
  * // last: '/api/users?page[number]=3&page[size]=10',
119
119
  * // next: '/api/users?page[number]=2&page[size]=10'
120
120
  * // }
121
- *
121
+ *
122
122
  * @description
123
123
  * Used by:
124
124
  * - rest-api-knex-plugin's dataQuery adds these links to response.links
125
125
  * - Called when offset-based pagination is used (not cursor-based)
126
- *
126
+ *
127
127
  * Purpose:
128
128
  * - JSON:API spec requires pagination links for easy navigation
129
129
  * - Preserves all other query parameters (filters, sorts, includes)
130
130
  * - Handles complex nested parameters like filter[author][name]=John
131
131
  * - Only includes prev/next when applicable
132
- *
132
+ *
133
133
  * Data flow:
134
134
  * 1. After records are fetched and pagination calculated
135
135
  * 2. Builds complete URLs preserving all query parameters
@@ -139,81 +139,81 @@ export const calculatePaginationMeta = (total, page, pageSize) => {
139
139
  export const generatePaginationLinks = (urlPrefix, scopeName, queryParams, paginationMeta) => {
140
140
  // Allow empty urlPrefix to generate relative links
141
141
  // if (!urlPrefix) return null;
142
-
143
- const { page, pageCount, pageSize } = paginationMeta;
144
- const links = {};
145
-
142
+
143
+ const { page, pageCount, pageSize } = paginationMeta
144
+ const links = {}
145
+
146
146
  const otherParams = Object.entries(queryParams)
147
147
  .filter(([key]) => key !== 'page')
148
148
  .flatMap(([key, value]) => {
149
149
  if (Array.isArray(value)) {
150
150
  return value.length === 0
151
151
  ? []
152
- : value.map((v) => `${key}=${encodeURIComponent(v)}`);
152
+ : value.map((v) => `${key}=${encodeURIComponent(v)}`)
153
153
  }
154
154
  if (typeof value === 'object' && value !== null) {
155
- const parts = [];
155
+ const parts = []
156
156
  const processObject = (obj, prefix) => {
157
157
  Object.entries(obj).forEach(([k, v]) => {
158
- const newKey = prefix ? `${prefix}[${k}]` : `${key}[${k}]`;
158
+ const newKey = prefix ? `${prefix}[${k}]` : `${key}[${k}]`
159
159
  if (typeof v === 'object' && v !== null && !Array.isArray(v)) {
160
- processObject(v, newKey);
160
+ processObject(v, newKey)
161
161
  } else if (v !== undefined && v !== null && !(Array.isArray(v) && v.length === 0)) {
162
- parts.push(`${newKey}=${encodeURIComponent(v)}`);
162
+ parts.push(`${newKey}=${encodeURIComponent(v)}`)
163
163
  }
164
- });
165
- };
166
- processObject(value, key);
167
- return parts;
164
+ })
165
+ }
166
+ processObject(value, key)
167
+ return parts
168
168
  }
169
169
  if (value === undefined || value === null) {
170
- return [];
170
+ return []
171
171
  }
172
- return [`${key}=${encodeURIComponent(value)}`];
172
+ return [`${key}=${encodeURIComponent(value)}`]
173
173
  })
174
174
  .filter(Boolean)
175
- .join('&');
176
-
177
- const baseUrl = `${urlPrefix}/${scopeName}`;
178
- const queryPrefix = otherParams ? `?${otherParams}&` : '?';
179
-
180
- links.self = `${baseUrl}${queryPrefix}page[number]=${page}&page[size]=${pageSize}`;
181
-
175
+ .join('&')
176
+
177
+ const baseUrl = `${urlPrefix}/${scopeName}`
178
+ const queryPrefix = otherParams ? `?${otherParams}&` : '?'
179
+
180
+ links.self = `${baseUrl}${queryPrefix}page[number]=${page}&page[size]=${pageSize}`
181
+
182
182
  if (pageCount !== undefined) {
183
- links.first = `${baseUrl}${queryPrefix}page[number]=1&page[size]=${pageSize}`;
184
- links.last = `${baseUrl}${queryPrefix}page[number]=${pageCount}&page[size]=${pageSize}`;
185
-
183
+ links.first = `${baseUrl}${queryPrefix}page[number]=1&page[size]=${pageSize}`
184
+ links.last = `${baseUrl}${queryPrefix}page[number]=${pageCount}&page[size]=${pageSize}`
185
+
186
186
  if (page > 1) {
187
- links.prev = `${baseUrl}${queryPrefix}page[number]=${page - 1}&page[size]=${pageSize}`;
187
+ links.prev = `${baseUrl}${queryPrefix}page[number]=${page - 1}&page[size]=${pageSize}`
188
188
  }
189
-
189
+
190
190
  if (page < pageCount) {
191
- links.next = `${baseUrl}${queryPrefix}page[number]=${page + 1}&page[size]=${pageSize}`;
191
+ links.next = `${baseUrl}${queryPrefix}page[number]=${page + 1}&page[size]=${pageSize}`
192
192
  }
193
193
  }
194
-
195
- return links;
196
- };
194
+
195
+ return links
196
+ }
197
197
 
198
198
  /**
199
199
  * Creates an opaque cursor string from record data for cursor-based pagination
200
- *
200
+ *
201
201
  * @param {Object} record - Database record to create cursor from
202
202
  * @param {Array<string>} sortFields - Fields to include in cursor (default: ['id'])
203
203
  * @returns {string} URL-safe cursor string
204
- *
204
+ *
205
205
  * @example
206
206
  * // Input: Simple ID-based cursor
207
- * const record = {
208
- * id: 123,
209
- * title: 'My Article',
210
- * created_at: '2024-01-01T10:00:00Z'
207
+ * const record = {
208
+ * id: 123,
209
+ * title: 'My Article',
210
+ * created_at: '2024-01-01T10:00:00Z'
211
211
  * };
212
212
  * const cursor = createCursor(record, ['id']);
213
- *
213
+ *
214
214
  * // Output: "id:123"
215
215
  * // This cursor marks position at record with id=123
216
- *
216
+ *
217
217
  * @example
218
218
  * // Input: Multi-field cursor for complex sorting
219
219
  * const record = {
@@ -222,12 +222,12 @@ export const generatePaginationLinks = (urlPrefix, scopeName, queryParams, pagin
222
222
  * title: 'Article: Part 2'
223
223
  * };
224
224
  * const cursor = createCursor(record, ['created_at', 'id']);
225
- *
225
+ *
226
226
  * // Output: "created_at:2024-01-15T08%3A30%3A00.000Z,id:5"
227
227
  * // URL-encoded to handle the colons in timestamp
228
228
  * // Used for queries like: WHERE (created_at, id) > ('2024-01-15T08:30:00.000Z', 5)
229
- *
230
- * @example
229
+ *
230
+ * @example
231
231
  * // Input: Handling special characters
232
232
  * const record = {
233
233
  * category: 'Tech & Science',
@@ -235,21 +235,21 @@ export const generatePaginationLinks = (urlPrefix, scopeName, queryParams, pagin
235
235
  * id: 42
236
236
  * };
237
237
  * const cursor = createCursor(record, ['category', 'title', 'id']);
238
- *
238
+ *
239
239
  * // Output: "category:Tech%20%26%20Science,title:AI%3A%20The%20Future%3F,id:42"
240
240
  * // Spaces encoded as %20, & as %26, : as %3A, ? as %3F
241
- *
241
+ *
242
242
  * @description
243
243
  * Used by:
244
244
  * - generateCursorPaginationLinks to create next/prev cursors
245
245
  * - buildCursorMeta to provide cursor in response metadata
246
- *
246
+ *
247
247
  * Purpose:
248
248
  * - Cursor-based pagination is more stable than offset for changing data
249
249
  * - Encodes multiple sort fields to maintain stable ordering
250
250
  * - URL-encodes values to handle special characters safely
251
251
  * - Simple format that's easy to parse back
252
- *
252
+ *
253
253
  * Data flow:
254
254
  * 1. After fetching records, takes the last record
255
255
  * 2. Extracts values for all sort fields
@@ -258,44 +258,44 @@ export const generatePaginationLinks = (urlPrefix, scopeName, queryParams, pagin
258
258
  * 5. Enables efficient "WHERE (field1, field2) > (val1, val2)" queries
259
259
  */
260
260
  export const createCursor = (record, sortFields = ['id']) => {
261
- const parts = [];
261
+ const parts = []
262
262
  sortFields.forEach(field => {
263
263
  if (record[field] !== undefined) {
264
- const value = record[field];
265
- const stringValue = value instanceof Date ? value.toISOString() : String(value);
266
- parts.push(`${field}:${encodeURIComponent(stringValue)}`);
264
+ const value = record[field]
265
+ const stringValue = value instanceof Date ? value.toISOString() : String(value)
266
+ parts.push(`${field}:${encodeURIComponent(stringValue)}`)
267
267
  }
268
- });
269
- return parts.join(',');
270
- };
268
+ })
269
+ return parts.join(',')
270
+ }
271
271
 
272
272
  /**
273
273
  * Parses a cursor string back into field/value pairs
274
- *
274
+ *
275
275
  * @param {string} cursor - Cursor string to parse
276
276
  * @returns {Object} Object with field names as keys and decoded values
277
277
  * @throws {Error} If cursor format is invalid
278
- *
278
+ *
279
279
  * @example
280
280
  * // Input: Simple cursor
281
281
  * const cursor = "id:123";
282
282
  * const data = parseCursor(cursor);
283
- *
283
+ *
284
284
  * // Output: { id: "123" }
285
285
  * // Ready to use in SQL: WHERE id > '123'
286
- *
286
+ *
287
287
  * @example
288
288
  * // Input: Multi-field cursor with URL-encoded values
289
289
  * const cursor = "created_at:2024-01-15T08%3A30%3A00.000Z,id:5";
290
290
  * const data = parseCursor(cursor);
291
- *
292
- * // Output:
291
+ *
292
+ * // Output:
293
293
  * // {
294
294
  * // created_at: "2024-01-15T08:30:00.000Z", // Decoded
295
295
  * // id: "5"
296
296
  * // }
297
297
  * // Used for: WHERE (created_at, id) > ('2024-01-15T08:30:00.000Z', '5')
298
- *
298
+ *
299
299
  * @example
300
300
  * // Input: Invalid cursor (throws error)
301
301
  * try {
@@ -304,17 +304,17 @@ export const createCursor = (record, sortFields = ['id']) => {
304
304
  * console.log(e.message);
305
305
  * // "Invalid cursor format: Invalid cursor format: missing colon separator"
306
306
  * }
307
- *
307
+ *
308
308
  * @description
309
309
  * Used by:
310
310
  * - rest-api-knex-plugin's dataQuery when processing page[after] parameter
311
311
  * - Used to build WHERE clause for cursor-based queries
312
- *
312
+ *
313
313
  * Purpose:
314
314
  * - Decodes cursor back to usable values for SQL queries
315
315
  * - Handles URL-encoded special characters
316
316
  * - Validates cursor format to prevent injection attacks
317
- *
317
+ *
318
318
  * Data flow:
319
319
  * 1. Client sends page[after]=cursor parameter
320
320
  * 2. parseCursor extracts field values from cursor
@@ -323,38 +323,38 @@ export const createCursor = (record, sortFields = ['id']) => {
323
323
  */
324
324
  export const parseCursor = (cursor) => {
325
325
  try {
326
- const data = {};
326
+ const data = {}
327
327
  if (!cursor || cursor.trim() === '') {
328
- throw new Error('Empty cursor');
328
+ throw new Error('Empty cursor')
329
329
  }
330
-
331
- const pairs = cursor.split(',');
332
-
330
+
331
+ const pairs = cursor.split(',')
332
+
333
333
  for (const pair of pairs) {
334
- const colonIndex = pair.indexOf(':');
334
+ const colonIndex = pair.indexOf(':')
335
335
  if (colonIndex === -1) {
336
- throw new Error('Invalid cursor format: missing colon separator');
336
+ throw new Error('Invalid cursor format: missing colon separator')
337
337
  }
338
-
339
- const field = pair.substring(0, colonIndex);
340
- const encodedValue = pair.substring(colonIndex + 1);
341
-
338
+
339
+ const field = pair.substring(0, colonIndex)
340
+ const encodedValue = pair.substring(colonIndex + 1)
341
+
342
342
  if (!field) {
343
- throw new Error('Invalid cursor format: empty field name');
343
+ throw new Error('Invalid cursor format: empty field name')
344
344
  }
345
-
346
- data[field] = decodeURIComponent(encodedValue);
345
+
346
+ data[field] = decodeURIComponent(encodedValue)
347
347
  }
348
-
349
- return data;
348
+
349
+ return data
350
350
  } catch (e) {
351
- throw new Error(`Invalid cursor format: ${e.message}`);
351
+ throw new Error(`Invalid cursor format: ${e.message}`)
352
352
  }
353
- };
353
+ }
354
354
 
355
355
  /**
356
356
  * Generates pagination links for cursor-based pagination
357
- *
357
+ *
358
358
  * @param {string} urlPrefix - Base URL prefix
359
359
  * @param {string} scopeName - Resource type name
360
360
  * @param {Object} queryParams - Current query parameters
@@ -363,12 +363,12 @@ export const parseCursor = (cursor) => {
363
363
  * @param {boolean} hasMore - Whether more records exist
364
364
  * @param {Array<string>} sortFields - Fields used for cursor
365
365
  * @returns {Object|null} Links object with self, first, next
366
- *
366
+ *
367
367
  * @example
368
368
  * // Input: First page of results
369
369
  * const urlPrefix = '/api/v1';
370
370
  * const scopeName = 'articles';
371
- * const queryParams = {
371
+ * const queryParams = {
372
372
  * filter: { status: 'published' },
373
373
  * page: { size: 20 }
374
374
  * };
@@ -378,11 +378,11 @@ export const parseCursor = (cursor) => {
378
378
  * { id: 3, created_at: '2024-01-03', title: 'Third' }
379
379
  * ];
380
380
  * const hasMore = true; // More records exist
381
- *
381
+ *
382
382
  * const links = generateCursorPaginationLinks(
383
383
  * urlPrefix, scopeName, queryParams, records, 20, hasMore, ['created_at', 'id']
384
384
  * );
385
- *
385
+ *
386
386
  * // Output:
387
387
  * // {
388
388
  * // self: '/api/v1/articles?filter[status]=published&page[size]=20',
@@ -390,7 +390,7 @@ export const parseCursor = (cursor) => {
390
390
  * // next: '/api/v1/articles?filter[status]=published&page[size]=20&page[after]=created_at:2024-01-03,id:3'
391
391
  * // }
392
392
  * // The 'next' cursor points after the last record (id:3)
393
- *
393
+ *
394
394
  * @example
395
395
  * // Input: Last page (no more records)
396
396
  * const records = [
@@ -398,28 +398,28 @@ export const parseCursor = (cursor) => {
398
398
  * { id: 99, created_at: '2024-03-02' }
399
399
  * ];
400
400
  * const hasMore = false; // No more records
401
- *
401
+ *
402
402
  * const links = generateCursorPaginationLinks(
403
403
  * '/api', 'articles', {}, records, 20, hasMore, ['id']
404
404
  * );
405
- *
405
+ *
406
406
  * // Output - no 'next' link:
407
407
  * // {
408
408
  * // self: '/api/articles?page[size]=20',
409
409
  * // first: '/api/articles?page[size]=20'
410
410
  * // }
411
- *
411
+ *
412
412
  * @description
413
413
  * Used by:
414
414
  * - rest-api-knex-plugin when cursor pagination is enabled
415
415
  * - Called after fetching records with cursor-based query
416
- *
416
+ *
417
417
  * Purpose:
418
418
  * - Cursor pagination is more efficient for large datasets
419
419
  * - Stable pagination when data is being added/removed
420
420
  * - Only includes 'next' link when more data exists
421
421
  * - Preserves all other query parameters
422
- *
422
+ *
423
423
  * Data flow:
424
424
  * 1. After cursor-based query fetches records
425
425
  * 2. If hasMore is true, creates cursor from last record
@@ -429,72 +429,72 @@ export const parseCursor = (cursor) => {
429
429
  */
430
430
  export const generateCursorPaginationLinks = (urlPrefix, scopeName, queryParams, records, pageSize, hasMore, sortFields = ['id']) => {
431
431
  // Allow empty urlPrefix to generate relative links
432
- if (!records.length) return null;
433
-
434
- const links = {};
435
- const baseUrl = `${urlPrefix}/${scopeName}`;
436
-
432
+ if (!records.length) return null
433
+
434
+ const links = {}
435
+ const baseUrl = `${urlPrefix}/${scopeName}`
436
+
437
437
  const otherParams = Object.entries(queryParams)
438
438
  .filter(([key]) => key !== 'page')
439
439
  .flatMap(([key, value]) => {
440
440
  if (Array.isArray(value)) {
441
441
  return value.length === 0
442
442
  ? []
443
- : value.map((v) => `${key}=${encodeURIComponent(v)}`);
443
+ : value.map((v) => `${key}=${encodeURIComponent(v)}`)
444
444
  }
445
445
  if (typeof value === 'object' && value !== null) {
446
- const parts = [];
446
+ const parts = []
447
447
  const processObject = (obj, prefix) => {
448
448
  Object.entries(obj).forEach(([k, v]) => {
449
- const newKey = prefix ? `${prefix}[${k}]` : `${key}[${k}]`;
449
+ const newKey = prefix ? `${prefix}[${k}]` : `${key}[${k}]`
450
450
  if (typeof v === 'object' && v !== null && !Array.isArray(v)) {
451
- processObject(v, newKey);
451
+ processObject(v, newKey)
452
452
  } else if (v !== undefined && v !== null && !(Array.isArray(v) && v.length === 0)) {
453
- parts.push(`${newKey}=${encodeURIComponent(v)}`);
453
+ parts.push(`${newKey}=${encodeURIComponent(v)}`)
454
454
  }
455
- });
456
- };
457
- processObject(value, key);
458
- return parts;
455
+ })
456
+ }
457
+ processObject(value, key)
458
+ return parts
459
459
  }
460
460
  if (value === undefined || value === null) {
461
- return [];
461
+ return []
462
462
  }
463
- return [`${key}=${encodeURIComponent(value)}`];
463
+ return [`${key}=${encodeURIComponent(value)}`]
464
464
  })
465
465
  .filter(Boolean)
466
- .join('&');
467
-
468
- const queryPrefix = otherParams ? `?${otherParams}&` : '?';
469
-
466
+ .join('&')
467
+
468
+ const queryPrefix = otherParams ? `?${otherParams}&` : '?'
469
+
470
470
  if (queryParams.page?.after) {
471
- links.self = `${baseUrl}${queryPrefix}page[size]=${pageSize}&page[after]=${queryParams.page.after}`;
471
+ links.self = `${baseUrl}${queryPrefix}page[size]=${pageSize}&page[after]=${queryParams.page.after}`
472
472
  } else if (queryParams.page?.before) {
473
- links.self = `${baseUrl}${queryPrefix}page[size]=${pageSize}&page[before]=${queryParams.page.before}`;
473
+ links.self = `${baseUrl}${queryPrefix}page[size]=${pageSize}&page[before]=${queryParams.page.before}`
474
474
  } else {
475
- links.self = `${baseUrl}${queryPrefix}page[size]=${pageSize}`;
475
+ links.self = `${baseUrl}${queryPrefix}page[size]=${pageSize}`
476
476
  }
477
-
478
- links.first = `${baseUrl}${queryPrefix}page[size]=${pageSize}`;
479
-
477
+
478
+ links.first = `${baseUrl}${queryPrefix}page[size]=${pageSize}`
479
+
480
480
  if (hasMore && records.length > 0) {
481
- const lastRecord = records[records.length - 1];
482
- const nextCursor = createCursor(lastRecord, sortFields);
483
- links.next = `${baseUrl}${queryPrefix}page[size]=${pageSize}&page[after]=${nextCursor}`;
481
+ const lastRecord = records[records.length - 1]
482
+ const nextCursor = createCursor(lastRecord, sortFields)
483
+ links.next = `${baseUrl}${queryPrefix}page[size]=${pageSize}&page[after]=${nextCursor}`
484
484
  }
485
-
486
- return links;
487
- };
485
+
486
+ return links
487
+ }
488
488
 
489
489
  /**
490
490
  * Builds cursor pagination metadata for the response
491
- *
491
+ *
492
492
  * @param {Array<Object>} records - Current page records
493
493
  * @param {number} pageSize - Records per page
494
494
  * @param {boolean} hasMore - Whether more records exist
495
495
  * @param {Array<string>} sortFields - Fields used for cursor
496
496
  * @returns {Object} Metadata with pageSize, hasMore, and optional cursor
497
- *
497
+ *
498
498
  * @example
499
499
  * // Input: Page with more records available
500
500
  * const records = [
@@ -503,7 +503,7 @@ export const generateCursorPaginationLinks = (urlPrefix, scopeName, queryParams,
503
503
  * { id: 12, created_at: '2024-01-12', title: 'Article 12' }
504
504
  * ];
505
505
  * const meta = buildCursorMeta(records, 20, true, ['created_at', 'id']);
506
- *
506
+ *
507
507
  * // Output:
508
508
  * // {
509
509
  * // pageSize: 20,
@@ -512,31 +512,31 @@ export const generateCursorPaginationLinks = (urlPrefix, scopeName, queryParams,
512
512
  * // next: 'created_at:2024-01-12,id:12' // Cursor from last record
513
513
  * // }
514
514
  * // }
515
- *
515
+ *
516
516
  * @example
517
517
  * // Input: Last page (no more records)
518
518
  * const records = [
519
519
  * { id: 98, name: 'Last Item' }
520
520
  * ];
521
521
  * const meta = buildCursorMeta(records, 20, false, ['id']);
522
- *
522
+ *
523
523
  * // Output:
524
524
  * // {
525
525
  * // pageSize: 20,
526
526
  * // hasMore: false
527
527
  * // // No cursor property since hasMore is false
528
528
  * // }
529
- *
529
+ *
530
530
  * @description
531
531
  * Used by:
532
532
  * - rest-api-knex-plugin adds this to response.meta for cursor pagination
533
533
  * - Provides cursor that client can use directly if needed
534
- *
534
+ *
535
535
  * Purpose:
536
536
  * - Gives clients direct access to cursor for custom pagination
537
537
  * - Indicates whether more pages exist without counting
538
538
  * - Simpler than offset pagination metadata (no total count)
539
- *
539
+ *
540
540
  * Data flow:
541
541
  * 1. Called after fetching records with cursor query
542
542
  * 2. Creates cursor from last record if more exist
@@ -547,14 +547,14 @@ export const buildCursorMeta = (records, pageSize, hasMore, sortFields = ['id'])
547
547
  const meta = {
548
548
  pageSize,
549
549
  hasMore
550
- };
551
-
550
+ }
551
+
552
552
  if (hasMore && records.length > 0) {
553
- const lastRecord = records[records.length - 1];
553
+ const lastRecord = records[records.length - 1]
554
554
  meta.cursor = {
555
555
  next: createCursor(lastRecord, sortFields)
556
- };
556
+ }
557
557
  }
558
-
559
- return meta;
560
- };
558
+
559
+ return meta
560
+ }