json-rest-api 1.0.28 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) 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 +746 -1273
  6. package/docs/GUIDE/01-getting-started.md +338 -0
  7. package/docs/GUIDE/02-resources-and-relationships.md +33 -0
  8. package/docs/GUIDE/03-running-example.md +87 -0
  9. package/docs/GUIDE/04-creating-and-querying.md +181 -0
  10. package/docs/GUIDE/05-belongs-to.md +177 -0
  11. package/docs/GUIDE/06-has-many.md +184 -0
  12. package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
  13. package/docs/GUIDE/08-many-to-many.md +184 -0
  14. package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
  15. package/docs/GUIDE/10-put-and-patch.md +174 -0
  16. package/docs/GUIDE/11-relationship-endpoints.md +174 -0
  17. package/docs/GUIDE/12-field-transformations.md +287 -0
  18. package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
  19. package/docs/GUIDE/14-custom-resource-methods.md +125 -0
  20. package/docs/GUIDE/15-query-projections.md +266 -0
  21. package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
  22. package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
  23. package/docs/GUIDE/18-labels.md +86 -0
  24. package/docs/GUIDE/19-managed-transactions.md +179 -0
  25. package/docs/GUIDE/20-transaction-outcomes.md +207 -0
  26. package/docs/GUIDE/21-schema-and-migrations.md +634 -0
  27. package/docs/GUIDE/22-version-field-migrations.md +208 -0
  28. package/docs/GUIDE/23-fastify.md +179 -0
  29. package/docs/GUIDE/24-cors.md +172 -0
  30. package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
  31. package/docs/GUIDE/26-file-uploads.md +278 -0
  32. package/docs/GUIDE/27-bulk-operations.md +277 -0
  33. package/docs/GUIDE/28-socketio.md +325 -0
  34. package/docs/GUIDE/29-writing-plugins.md +278 -0
  35. package/docs/GUIDE/30-backend-capabilities.md +100 -0
  36. package/docs/GUIDE/31-positioning.md +181 -0
  37. package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +90 -31
  38. package/docs/GUIDE/33-migrating-to-v2.md +3827 -0
  39. package/docs/GUIDE/index.md +51 -59
  40. package/docs/QUICKSTART.md +120 -287
  41. package/docs/architecture.md +75 -0
  42. package/docs/contributing.md +178 -0
  43. package/index.d.ts +36 -0
  44. package/index.js +1 -22
  45. package/lib/enhanced-logger.js +91 -85
  46. package/lib/error-context.js +445 -5
  47. package/lib/error-formatter.js +219 -59
  48. package/lib/knex-transaction.js +64 -0
  49. package/lib/logger-types.d.ts +22 -0
  50. package/lib/missing-package.js +4 -0
  51. package/lib/rest-api-errors.js +103 -8
  52. package/lib/runtime/json-rest-api.js +172 -0
  53. package/lib/transaction-types.d.ts +59 -0
  54. package/package.json +97 -27
  55. package/plugins/core/bulk-operations-plugin.js +191 -184
  56. package/plugins/core/connectors/express-plugin.js +148 -219
  57. package/plugins/core/connectors/fastify-plugin.js +125 -82
  58. package/plugins/core/connectors/lib/busboy-detector.js +74 -156
  59. package/plugins/core/connectors/lib/connector-core.js +144 -20
  60. package/plugins/core/connectors/lib/formidable-detector.js +63 -120
  61. package/plugins/core/connectors/lib/http-validators.js +77 -0
  62. package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
  63. package/plugins/core/connectors/lib/request-helpers.js +10 -1
  64. package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
  65. package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
  66. package/plugins/core/file-handling-plugin.js +87 -174
  67. package/plugins/core/lib/anyapi/anyapi-registry.js +213 -164
  68. package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
  69. package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
  70. package/plugins/core/lib/anyapi/schema-utils.js +25 -16
  71. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
  72. package/plugins/core/lib/dbIntrospection.js +228 -21
  73. package/plugins/core/lib/dbTablesOperations.js +409 -92
  74. package/plugins/core/lib/fractional-positioning.js +12 -48
  75. package/plugins/core/lib/querying/identifier-query.js +19 -0
  76. package/plugins/core/lib/querying/include-query-helpers.js +364 -0
  77. package/plugins/core/lib/querying/include-to-many.js +428 -0
  78. package/plugins/core/lib/querying/include-to-one.js +579 -0
  79. package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
  80. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
  81. package/plugins/core/lib/querying/knex-pagination-helpers.js +67 -330
  82. package/plugins/core/lib/querying/knex-process-includes.js +5 -110
  83. package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
  84. package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
  85. package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
  86. package/plugins/core/lib/querying/knex-window-queries.js +62 -253
  87. package/plugins/core/lib/querying/query-builder-utils.js +44 -4
  88. package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
  89. package/plugins/core/lib/querying/query-constraint.js +27 -0
  90. package/plugins/core/lib/querying/query-field-sort-helpers.js +205 -30
  91. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  92. package/plugins/core/lib/querying/sort-helpers.js +20 -1
  93. package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
  94. package/plugins/core/lib/querying/url-helpers.js +11 -4
  95. package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
  96. package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
  97. package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
  98. package/plugins/core/lib/querying-writing/database-value-normalizers.js +238 -187
  99. package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
  100. package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
  101. package/plugins/core/lib/querying-writing/field-utils.js +113 -92
  102. package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
  103. package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
  104. package/plugins/core/lib/querying-writing/knex-field-helpers.js +111 -287
  105. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
  106. package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
  107. package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
  108. package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
  109. package/plugins/core/lib/querying-writing/request-contracts.js +109 -44
  110. package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
  111. package/plugins/core/lib/querying-writing/response-options.js +35 -0
  112. package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
  113. package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
  114. package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
  115. package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
  116. package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
  117. package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
  118. package/plugins/core/lib/storage/storage-adapter.js +142 -62
  119. package/plugins/core/lib/storage/storage-mapping.js +78 -28
  120. package/plugins/core/lib/storage/storage-types.d.ts +502 -0
  121. package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
  122. package/plugins/core/lib/writing/relationship-processor.js +94 -158
  123. package/plugins/core/lib/writing/resource-version.js +172 -0
  124. package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
  125. package/plugins/core/rest-api-anyapi-knex-plugin.js +116 -2509
  126. package/plugins/core/rest-api-autofilter-plugin.js +17 -19
  127. package/plugins/core/rest-api-cors-plugin.js +52 -205
  128. package/plugins/core/rest-api-knex-plugin.js +45 -1069
  129. package/plugins/core/rest-api-label-plugin.js +25 -38
  130. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
  131. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
  132. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
  133. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
  134. package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
  135. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
  136. package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
  137. package/plugins/core/rest-api-plugin-methods/common.js +291 -385
  138. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
  139. package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
  140. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -145
  141. package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
  142. package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
  143. package/plugins/core/rest-api-plugin-methods/get.js +38 -69
  144. package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
  145. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
  146. package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
  147. package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
  148. package/plugins/core/rest-api-plugin-methods/post.js +51 -51
  149. package/plugins/core/rest-api-plugin-methods/put.js +69 -148
  150. package/plugins/core/rest-api-plugin-methods/query.js +39 -82
  151. package/plugins/core/rest-api-plugin-methods/release.js +2 -1
  152. package/plugins/core/rest-api-plugin.js +32 -46
  153. package/plugins/core/rest-api-positioning-plugin.js +183 -628
  154. package/plugins/core/rest-api-query-projections-plugin.js +19 -102
  155. package/plugins/core/rest-api-row-policy-plugin.js +3 -3
  156. package/plugins/core/socketio-plugin.js +364 -344
  157. package/plugins/storage/local-storage.js +40 -36
  158. package/types/autofilter.d.ts +57 -0
  159. package/types/bulk-methods.d.ts +63 -0
  160. package/types/errors.d.ts +118 -0
  161. package/types/fastify-options.d.ts +19 -0
  162. package/types/file-detectors.d.ts +33 -0
  163. package/types/file-storage.d.ts +48 -0
  164. package/types/hook-context.d.ts +180 -0
  165. package/types/plugin-options.d.ts +53 -0
  166. package/types/relationship-methods.d.ts +74 -0
  167. package/types/representations.d.ts +50 -0
  168. package/types/resource-methods.d.ts +95 -0
  169. package/types/resource-schema.d.ts +157 -0
  170. package/types/row-policy.d.ts +31 -0
  171. package/types/runtime.d.ts +132 -0
  172. package/types/transactions.d.ts +21 -0
  173. package/.claude/agents/library-guide-expert.md +0 -30
  174. package/.claude/agents/rest-api-internals-expert.md +0 -54
  175. package/.claude/settings.local.json +0 -70
  176. package/AGENTS.md +0 -34
  177. package/CLAUDE.md +0 -160
  178. package/CRUSH.md +0 -55
  179. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
  180. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
  181. package/TODO.todo +0 -13
  182. package/connectors_ideas/fastify.js +0 -217
  183. package/connectors_ideas/koa-plugin.js +0 -112
  184. package/docs/.bundle/config +0 -2
  185. package/docs/COMPARISON.md +0 -433
  186. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
  187. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
  188. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
  189. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
  190. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
  191. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
  192. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
  193. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1238
  194. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
  195. package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
  196. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
  197. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
  198. package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
  199. package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
  200. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
  201. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
  202. package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
  203. package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
  204. package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
  205. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
  206. package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
  207. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
  208. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
  209. package/docs/Gemfile +0 -6
  210. package/docs/Gemfile.lock +0 -308
  211. package/docs/ONBOARDING.md +0 -315
  212. package/docs/_config.yml +0 -36
  213. package/docs/_layouts/default.html +0 -123
  214. package/docs/apple-touch-icon.svg +0 -4
  215. package/docs/assets/css/style.scss +0 -392
  216. package/docs/favicon.svg +0 -4
  217. package/docs/index.md +0 -164
  218. package/docs/mini-guides/file-uploads-complete.md +0 -161
  219. package/eslint.config.js +0 -13
  220. package/examples/url-override-example.js +0 -137
  221. package/fixing_plan.md +0 -112
  222. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
  223. package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
  224. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
  225. package/plugins/storage/s3-storage.js +0 -93
  226. package/quickTest.js +0 -136
  227. package/scripts/debug-anyapi-review-filter.mjs +0 -63
  228. package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
  229. package/scripts/inspect-anyapi-descriptor.mjs +0 -19
  230. package/scripts/run-docs.js +0 -133
  231. package/test-debug.js +0 -59
  232. package/tests/README.md +0 -202
  233. package/tests/TEST_TEMPLATE.test.js +0 -178
  234. package/tests/anyapi-basic.test.js +0 -105
  235. package/tests/anyapi-cursor-pagination.test.js +0 -150
  236. package/tests/anyapi-custom-idproperty.test.js +0 -163
  237. package/tests/autofilter.test.js +0 -463
  238. package/tests/bulk-operations.test.js +0 -657
  239. package/tests/computed-fields.test.js +0 -586
  240. package/tests/cors.test.js +0 -834
  241. package/tests/curl.test.js +0 -467
  242. package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
  243. package/tests/custom-id-simplified-post.test.js +0 -75
  244. package/tests/custom-idproperty-relationships.test.js +0 -932
  245. package/tests/db-introspection.test.js +0 -521
  246. package/tests/db-table-operations.test.js +0 -798
  247. package/tests/fastify-plugin.test.js +0 -184
  248. package/tests/field-getters.test.js +0 -370
  249. package/tests/field-setters.test.js +0 -306
  250. package/tests/file-handling.test.js +0 -118
  251. package/tests/fixtures/api-configs.js +0 -2509
  252. package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
  253. package/tests/helpers/fake-fastify.js +0 -99
  254. package/tests/helpers/storage-mode.js +0 -65
  255. package/tests/helpers/test-utils.js +0 -203
  256. package/tests/http-connectors-parity.test.js +0 -395
  257. package/tests/id-normalization.test.js +0 -444
  258. package/tests/include-depth-validation.test.js +0 -331
  259. package/tests/includes.test.js +0 -427
  260. package/tests/local-storage.test.js +0 -96
  261. package/tests/logical-id-write-validation.test.js +0 -151
  262. package/tests/nested-includes.test.js +0 -617
  263. package/tests/pagination-cursor-multifield.test.js +0 -643
  264. package/tests/pagination-enhanced.test.js +0 -563
  265. package/tests/polymorphic-relationships.test.js +0 -347
  266. package/tests/polymorphic-simple.test.js +0 -148
  267. package/tests/positioning-advanced.test.js +0 -692
  268. package/tests/positioning-extended.test.js +0 -915
  269. package/tests/positioning-multifilter.test.js +0 -699
  270. package/tests/positioning.test.js +0 -560
  271. package/tests/post-commit-hooks.test.js +0 -120
  272. package/tests/projected-fields.test.js +0 -319
  273. package/tests/public-exports.test.js +0 -15
  274. package/tests/put-create-parity.test.js +0 -216
  275. package/tests/queries.test.js +0 -521
  276. package/tests/query-limits.test.js +0 -584
  277. package/tests/relationship-endpoints.test.js +0 -620
  278. package/tests/relationship-include-storage-adapter.test.js +0 -56
  279. package/tests/relationships.test.js +0 -681
  280. package/tests/rest-api.test.js +0 -337
  281. package/tests/return-record-settings.test.js +0 -184
  282. package/tests/row-policy.test.js +0 -489
  283. package/tests/s3-storage.test.js +0 -38
  284. package/tests/searchschema-knex-behavior.test.js +0 -187
  285. package/tests/searchschema-merge.test.js +0 -151
  286. package/tests/socketio.test.js +0 -860
  287. package/tests/storage-mapping.test.js +0 -362
  288. package/tests/test-development-guide.md +0 -370
  289. package/tests/virtual-fields.test.js +0 -404
@@ -1,10 +1,48 @@
1
1
  import { getFieldValue } from '../storage/storage-mapping.js'
2
- import { serializeJsonApiQuery } from '../querying-writing/connectors-query-parser.js'
2
+ import { buildJsonApiLink } from './url-helpers.js'
3
3
  import { normalizeDateValue } from '../querying-writing/database-value-normalizers.js'
4
+ import { DEFAULT_QUERY_LIMIT, DEFAULT_MAX_QUERY_LIMIT } from '../querying-writing/knex-constants.js'
5
+ import { applyCursorPredicate, validateCursorValues } from './query-field-sort-helpers.js'
6
+ import { RestApiValidationError } from '../../../../lib/rest-api-errors.js'
4
7
 
5
- const buildJsonApiLink = (baseUrl, queryParams, page) => {
6
- const queryString = serializeJsonApiQuery(queryParams, { page })
7
- return queryString ? `${baseUrl}?${queryString}` : baseUrl
8
+ // Page parameters have passed the shared request contract. Physical sort
9
+ // columns and built-in value conversion remain with descriptors and adapters.
10
+ export const applyPaginationToQuery = ({ query, page = {}, vars, sortDescriptors, storageAdapter }) => {
11
+ const pageSize = Math.min(page.size ?? (vars.queryDefaultLimit || DEFAULT_QUERY_LIMIT), vars.queryMaxLimit || DEFAULT_MAX_QUERY_LIMIT)
12
+ if (!Number.isInteger(pageSize) || pageSize <= 0) {
13
+ throw new RestApiValidationError('Page size must be a positive integer', {
14
+ fields: ['page.size'],
15
+ violations: [{ field: 'page.size', rule: 'min_value', message: 'Page size must be a positive integer' }]
16
+ })
17
+ }
18
+ const parameter = page.before !== undefined ? 'before' : page.after !== undefined ? 'after' : null
19
+ const mode = page.number !== undefined ? 'offset' : parameter || page.size !== undefined ? 'cursor' : 'default'
20
+ const before = parameter === 'before'
21
+ const pageNumber = page.number ?? 1
22
+
23
+ if (parameter) {
24
+ let values
25
+ try {
26
+ values = parseCursor(page[parameter])
27
+ } catch (error) {
28
+ const field = `page.${parameter}`
29
+ throw new RestApiValidationError(`Invalid cursor format in page[${parameter}] parameter`, {
30
+ fields: [field],
31
+ violations: [{ field, rule: 'invalid_cursor', message: error.message }]
32
+ })
33
+ }
34
+ values = validateCursorValues(sortDescriptors, values, parameter, vars.schemaInfo.schemaInstance)
35
+ applyCursorPredicate(query, sortDescriptors, values, direction => {
36
+ const descending = direction.toUpperCase() === 'DESC'
37
+ return before ? (descending ? '>' : '<') : (descending ? '<' : '>')
38
+ }, (builder, descriptor, operator, value) => builder.where(
39
+ descriptor.column, operator, storageAdapter.translateCursorValue(descriptor.actualField, value)
40
+ ))
41
+ }
42
+
43
+ query.limit(pageSize + (mode === 'cursor' ? 1 : 0))
44
+ if (mode === 'offset') query.offset((pageNumber - 1) * pageSize)
45
+ return { mode, page: pageNumber, pageSize, before }
8
46
  }
9
47
 
10
48
  /**
@@ -15,47 +53,6 @@ const buildJsonApiLink = (baseUrl, queryParams, page) => {
15
53
  * @param {number} pageSize - Number of records per page
16
54
  * @returns {Object} Pagination metadata including page count and hasMore flag
17
55
  * @throws {Error} If pageSize <= 0 or page < 1
18
- *
19
- * @example
20
- * // Input: 100 total records, viewing page 2 of 20 records each
21
- * const meta = calculatePaginationMeta(100, 2, 20);
22
- * // Output:
23
- * // {
24
- * // page: 2, // current page
25
- * // pageSize: 20, // records per page
26
- * // pageCount: 5, // total pages (100/20)
27
- * // total: 100, // total records
28
- * // hasMore: true // page 2 < 5, so more pages exist
29
- * // }
30
- *
31
- * @example
32
- * // Input: Last page scenario
33
- * const meta = calculatePaginationMeta(45, 5, 10);
34
- * // Output:
35
- * // {
36
- * // page: 5,
37
- * // pageSize: 10,
38
- * // pageCount: 5, // 45/10 = 4.5, rounds up to 5
39
- * // total: 45,
40
- * // hasMore: false // on last page
41
- * // }
42
- *
43
- * @description
44
- * Used by:
45
- * - rest-api-knex-plugin's dataQuery method after counting total records
46
- * - generatePaginationLinks to build navigation links
47
- * - Applied when offset-based pagination is enabled
48
- *
49
- * Purpose:
50
- * - Provides consistent pagination metadata across all collection responses
51
- * - Calculates derived values like pageCount and hasMore flag
52
- * - Validates pagination parameters to prevent invalid states
53
- *
54
- * Data flow:
55
- * 1. Query method counts total records when pagination.counts is enabled
56
- * 2. calculatePaginationMeta processes the count with current page/size
57
- * 3. Returns metadata that goes into response.meta.pagination
58
- * 4. Used by generatePaginationLinks to determine which links to include
59
56
  */
60
57
  export const calculatePaginationMeta = (total, page, pageSize) => {
61
58
  if (pageSize <= 0) {
@@ -85,65 +82,7 @@ export const calculatePaginationMeta = (total, page, pageSize) => {
85
82
  * @param {string} scopeName - Resource type name
86
83
  * @param {Object} queryParams - Current query parameters
87
84
  * @param {Object} paginationMeta - Pagination metadata from calculatePaginationMeta
88
- * @returns {Object|null} Links object with self, first, last, prev, next
89
- *
90
- * @example
91
- * // Input data:
92
- * const urlPrefix = '/api/v1';
93
- * const scopeName = 'articles';
94
- * const queryParams = {
95
- * filter: { status: 'published' },
96
- * sort: ['-created_at'],
97
- * page: { number: 2, size: 20 }
98
- * };
99
- * const paginationMeta = {
100
- * page: 2,
101
- * pageSize: 20,
102
- * pageCount: 5,
103
- * total: 100,
104
- * hasMore: true
105
- * };
106
- *
107
- * const links = generatePaginationLinks(urlPrefix, scopeName, queryParams, paginationMeta);
108
- *
109
- * // Output - complete URLs with all parameters preserved:
110
- * // {
111
- * // self: '/api/articles?filter[status]=published&sort=-created_at&page[number]=2&page[size]=20',
112
- * // first: '/api/articles?filter[status]=published&sort=-created_at&page[number]=1&page[size]=20',
113
- * // last: '/api/articles?filter[status]=published&sort=-created_at&page[number]=5&page[size]=20',
114
- * // prev: '/api/articles?filter[status]=published&sort=-created_at&page[number]=1&page[size]=20',
115
- * // next: '/api/articles?filter[status]=published&sort=-created_at&page[number]=3&page[size]=20'
116
- * // }
117
- *
118
- * @example
119
- * // Input: First page (no prev link)
120
- * const paginationMeta = { page: 1, pageSize: 10, pageCount: 3 };
121
- * const links = generatePaginationLinks('/api', 'users', {}, paginationMeta);
122
- *
123
- * // Output - notice no 'prev' property:
124
- * // {
125
- * // self: '/api/users?page[number]=1&page[size]=10',
126
- * // first: '/api/users?page[number]=1&page[size]=10',
127
- * // last: '/api/users?page[number]=3&page[size]=10',
128
- * // next: '/api/users?page[number]=2&page[size]=10'
129
- * // }
130
- *
131
- * @description
132
- * Used by:
133
- * - rest-api-knex-plugin's dataQuery adds these links to response.links
134
- * - Called when offset-based pagination is used (not cursor-based)
135
- *
136
- * Purpose:
137
- * - JSON:API spec requires pagination links for easy navigation
138
- * - Preserves all other query parameters (filters, sorts, includes)
139
- * - Uses the shared JSON:API query serializer for transport-supported params
140
- * - Only includes prev/next when applicable
141
- *
142
- * Data flow:
143
- * 1. After records are fetched and pagination calculated
144
- * 2. Builds complete URLs preserving all query parameters
145
- * 3. Conditionally includes prev (not on page 1) and next (not on last page)
146
- * 4. Added to response.links for client navigation
85
+ * @returns {import('../../../../types/representations.js').JsonApiLinks | null} Links object with self, first, last, prev, next
147
86
  */
148
87
  export const generatePaginationLinks = (urlPrefix, scopeName, queryParams, paginationMeta) => {
149
88
  // Allow empty urlPrefix to generate relative links
@@ -158,7 +97,7 @@ export const generatePaginationLinks = (urlPrefix, scopeName, queryParams, pagin
158
97
 
159
98
  if (pageCount !== undefined) {
160
99
  links.first = buildJsonApiLink(baseUrl, queryParams, { number: 1, size: pageSize })
161
- links.last = buildJsonApiLink(baseUrl, queryParams, { number: pageCount, size: pageSize })
100
+ links.last = buildJsonApiLink(baseUrl, queryParams, { number: Math.max(1, pageCount), size: pageSize })
162
101
 
163
102
  if (page > 1) {
164
103
  links.prev = buildJsonApiLink(baseUrl, queryParams, { number: page - 1, size: pageSize })
@@ -177,71 +116,21 @@ export const generatePaginationLinks = (urlPrefix, scopeName, queryParams, pagin
177
116
  *
178
117
  * @param {Object} record - Database record to create cursor from
179
118
  * @param {Array<string>} sortFields - Fields to include in cursor (default: ['id'])
119
+ * @param {import('../storage/storage-types.js').DataCursorOptions} [options]
180
120
  * @returns {string} URL-safe cursor string
181
- *
182
- * @example
183
- * // Input: Simple ID-based cursor
184
- * const record = {
185
- * id: 123,
186
- * title: 'My Article',
187
- * created_at: '2024-01-01T10:00:00Z'
188
- * };
189
- * const cursor = createCursor(record, ['id']);
190
- *
191
- * // Output: "id:123"
192
- * // This cursor marks position at record with id=123
193
- *
194
- * @example
195
- * // Input: Multi-field cursor for complex sorting
196
- * const record = {
197
- * id: 5,
198
- * created_at: new Date('2024-01-15T08:30:00Z'),
199
- * title: 'Article: Part 2'
200
- * };
201
- * const cursor = createCursor(record, ['created_at', 'id']);
202
- *
203
- * // Output: "created_at:2024-01-15T08%3A30%3A00.000Z,id:5"
204
- * // URL-encoded to handle the colons in timestamp
205
- * // Used for queries like: WHERE (created_at, id) > ('2024-01-15T08:30:00.000Z', 5)
206
- *
207
- * @example
208
- * // Input: Handling special characters
209
- * const record = {
210
- * category: 'Tech & Science',
211
- * title: 'AI: The Future?',
212
- * id: 42
213
- * };
214
- * const cursor = createCursor(record, ['category', 'title', 'id']);
215
- *
216
- * // Output: "category:Tech%20%26%20Science,title:AI%3A%20The%20Future%3F,id:42"
217
- * // Spaces encoded as %20, & as %26, : as %3A, ? as %3F
218
- *
219
- * @description
220
- * Used by:
221
- * - generateCursorPaginationLinks to create next/prev cursors
222
- * - buildCursorMeta to provide cursor in response metadata
223
- *
224
- * Purpose:
225
- * - Cursor-based pagination is more stable than offset for changing data
226
- * - Encodes multiple sort fields to maintain stable ordering
227
- * - URL-encodes values to handle special characters safely
228
- * - Simple format that's easy to parse back
229
- *
230
- * Data flow:
231
- * 1. After fetching records, takes the last record
232
- * 2. Extracts values for all sort fields
233
- * 3. Creates cursor encoding those values
234
- * 4. Cursor used in 'next' link for fetching subsequent pages
235
- * 5. Enables efficient "WHERE (field1, field2) > (val1, val2)" queries
236
121
  */
237
- export const createCursor = (record, sortFields = ['id'], { schemaInfo = null } = {}) => {
122
+ export const createCursor = (record, sortFields = ['id'], { schemaInfo = null, definitions = {} } = {}) => {
238
123
  const parts = []
239
124
  sortFields.forEach(field => {
240
125
  let value = schemaInfo
241
126
  ? getFieldValue(record, schemaInfo, field)
242
127
  : record[field]
243
- const definition = schemaInfo?.schemaStructure?.[field]
244
- if (['date', 'dateTime', 'time'].includes(definition?.type) && value != null) {
128
+ const definition = definitions[field] || schemaInfo?.schemaStructure?.[field]
129
+ if (value === null) {
130
+ parts.push(`${field}:~null`)
131
+ return
132
+ }
133
+ if (!definition?.storage?.serialize && !definition?.select && ['date', 'dateTime', 'time'].includes(definition?.type) && value != null) {
245
134
  value = normalizeDateValue(value, definition.type, {
246
135
  temporalPrecision: definition.temporalPrecision,
247
136
  fieldName: field,
@@ -251,7 +140,7 @@ export const createCursor = (record, sortFields = ['id'], { schemaInfo = null }
251
140
  }
252
141
  if (value !== undefined) {
253
142
  const stringValue = value instanceof Date ? value.toISOString() : String(value)
254
- parts.push(`${field}:${encodeURIComponent(stringValue)}`)
143
+ parts.push(`${field}:${encodeURIComponent(stringValue).replace(/^~/, '%7E')}`)
255
144
  }
256
145
  })
257
146
  return parts.join(',')
@@ -263,55 +152,10 @@ export const createCursor = (record, sortFields = ['id'], { schemaInfo = null }
263
152
  * @param {string} cursor - Cursor string to parse
264
153
  * @returns {Object} Object with field names as keys and decoded values
265
154
  * @throws {Error} If cursor format is invalid
266
- *
267
- * @example
268
- * // Input: Simple cursor
269
- * const cursor = "id:123";
270
- * const data = parseCursor(cursor);
271
- *
272
- * // Output: { id: "123" }
273
- * // Ready to use in SQL: WHERE id > '123'
274
- *
275
- * @example
276
- * // Input: Multi-field cursor with URL-encoded values
277
- * const cursor = "created_at:2024-01-15T08%3A30%3A00.000Z,id:5";
278
- * const data = parseCursor(cursor);
279
- *
280
- * // Output:
281
- * // {
282
- * // created_at: "2024-01-15T08:30:00.000Z", // Decoded
283
- * // id: "5"
284
- * // }
285
- * // Used for: WHERE (created_at, id) > ('2024-01-15T08:30:00.000Z', '5')
286
- *
287
- * @example
288
- * // Input: Invalid cursor (throws error)
289
- * try {
290
- * const data = parseCursor("invalid-no-colon");
291
- * } catch (e) {
292
- * console.log(e.message);
293
- * // "Invalid cursor format: Invalid cursor format: missing colon separator"
294
- * }
295
- *
296
- * @description
297
- * Used by:
298
- * - rest-api-knex-plugin's dataQuery when processing page[after] parameter
299
- * - Used to build WHERE clause for cursor-based queries
300
- *
301
- * Purpose:
302
- * - Decodes cursor back to usable values for SQL queries
303
- * - Handles URL-encoded special characters
304
- * - Validates cursor format to prevent injection attacks
305
- *
306
- * Data flow:
307
- * 1. Client sends page[after]=cursor parameter
308
- * 2. parseCursor extracts field values from cursor
309
- * 3. Values used to build WHERE clause like "WHERE id > 123"
310
- * 4. Ensures pagination continues from exact position
311
155
  */
312
156
  export const parseCursor = (cursor) => {
313
157
  try {
314
- const data = {}
158
+ const data = Object.create(null)
315
159
  if (!cursor || cursor.trim() === '') {
316
160
  throw new Error('Empty cursor')
317
161
  }
@@ -331,7 +175,8 @@ export const parseCursor = (cursor) => {
331
175
  throw new Error('Invalid cursor format: empty field name')
332
176
  }
333
177
 
334
- data[field] = decodeURIComponent(encodedValue)
178
+ if (Object.hasOwn(data, field)) throw new Error(`Duplicate cursor field '${field}'`)
179
+ data[field] = encodedValue === '~null' ? null : decodeURIComponent(encodedValue)
335
180
  }
336
181
 
337
182
  return data
@@ -350,70 +195,8 @@ export const parseCursor = (cursor) => {
350
195
  * @param {number} pageSize - Records per page
351
196
  * @param {boolean} hasMore - Whether more records exist
352
197
  * @param {Array<string>} sortFields - Fields used for cursor
353
- * @returns {Object|null} Links object with self, first, next
354
- *
355
- * @example
356
- * // Input: First page of results
357
- * const urlPrefix = '/api/v1';
358
- * const scopeName = 'articles';
359
- * const queryParams = {
360
- * filter: { status: 'published' },
361
- * page: { size: 20 }
362
- * };
363
- * const records = [
364
- * { id: 1, created_at: '2024-01-01', title: 'First' },
365
- * { id: 2, created_at: '2024-01-02', title: 'Second' },
366
- * { id: 3, created_at: '2024-01-03', title: 'Third' }
367
- * ];
368
- * const hasMore = true; // More records exist
369
- *
370
- * const links = generateCursorPaginationLinks(
371
- * urlPrefix, scopeName, queryParams, records, 20, hasMore, ['created_at', 'id']
372
- * );
373
- *
374
- * // Output:
375
- * // {
376
- * // self: '/api/v1/articles?filter[status]=published&page[size]=20',
377
- * // first: '/api/v1/articles?filter[status]=published&page[size]=20',
378
- * // next: '/api/v1/articles?filter[status]=published&page[size]=20&page[after]=created_at:2024-01-03,id:3'
379
- * // }
380
- * // The 'next' cursor points after the last record (id:3)
381
- *
382
- * @example
383
- * // Input: Last page (no more records)
384
- * const records = [
385
- * { id: 98, created_at: '2024-03-01' },
386
- * { id: 99, created_at: '2024-03-02' }
387
- * ];
388
- * const hasMore = false; // No more records
389
- *
390
- * const links = generateCursorPaginationLinks(
391
- * '/api', 'articles', {}, records, 20, hasMore, ['id']
392
- * );
393
- *
394
- * // Output - no 'next' link:
395
- * // {
396
- * // self: '/api/articles?page[size]=20',
397
- * // first: '/api/articles?page[size]=20'
398
- * // }
399
- *
400
- * @description
401
- * Used by:
402
- * - rest-api-knex-plugin when cursor pagination is enabled
403
- * - Called after fetching records with cursor-based query
404
- *
405
- * Purpose:
406
- * - Cursor pagination is more efficient for large datasets
407
- * - Stable pagination when data is being added/removed
408
- * - Only includes 'next' link when more data exists
409
- * - Preserves all other query parameters
410
- *
411
- * Data flow:
412
- * 1. After cursor-based query fetches records
413
- * 2. If hasMore is true, creates cursor from last record
414
- * 3. Builds next link with page[after] parameter
415
- * 4. Added to response.links for navigation
416
- * 5. Client uses next link to fetch subsequent pages
198
+ * @param {import('../storage/storage-types.js').DataCursorOptions} [options]
199
+ * @returns {import('../../../../types/representations.js').JsonApiLinks | null} Links object with self, first, next
417
200
  */
418
201
  export const generateCursorPaginationLinks = (
419
202
  urlPrefix,
@@ -423,7 +206,7 @@ export const generateCursorPaginationLinks = (
423
206
  pageSize,
424
207
  hasMore,
425
208
  sortFields = ['id'],
426
- { schemaInfo = null } = {}
209
+ { schemaInfo = null, definitions = {} } = {}
427
210
  ) => {
428
211
  // Allow empty urlPrefix to generate relative links
429
212
  if (!records.length) return null
@@ -442,9 +225,10 @@ export const generateCursorPaginationLinks = (
442
225
  links.first = buildJsonApiLink(baseUrl, queryParams, { size: pageSize })
443
226
 
444
227
  if (hasMore && records.length > 0) {
445
- const lastRecord = records[records.length - 1]
446
- const nextCursor = createCursor(lastRecord, sortFields, { schemaInfo })
447
- links.next = buildJsonApiLink(baseUrl, queryParams, { size: pageSize, after: nextCursor })
228
+ const direction = queryParams.page?.before ? 'before' : 'after'
229
+ const boundary = direction === 'before' ? records[0] : records[records.length - 1]
230
+ const nextCursor = createCursor(boundary, sortFields, { schemaInfo, definitions })
231
+ links.next = buildJsonApiLink(baseUrl, queryParams, { size: pageSize, [direction]: nextCursor })
448
232
  }
449
233
 
450
234
  return links
@@ -457,66 +241,19 @@ export const generateCursorPaginationLinks = (
457
241
  * @param {number} pageSize - Records per page
458
242
  * @param {boolean} hasMore - Whether more records exist
459
243
  * @param {Array<string>} sortFields - Fields used for cursor
244
+ * @param {import('../storage/storage-types.js').DataCursorOptions} [options]
460
245
  * @returns {Object} Metadata with pageSize, hasMore, and optional cursor
461
- *
462
- * @example
463
- * // Input: Page with more records available
464
- * const records = [
465
- * { id: 10, created_at: '2024-01-10', title: 'Article 10' },
466
- * { id: 11, created_at: '2024-01-11', title: 'Article 11' },
467
- * { id: 12, created_at: '2024-01-12', title: 'Article 12' }
468
- * ];
469
- * const meta = buildCursorMeta(records, 20, true, ['created_at', 'id']);
470
- *
471
- * // Output:
472
- * // {
473
- * // pageSize: 20,
474
- * // hasMore: true,
475
- * // cursor: {
476
- * // next: 'created_at:2024-01-12,id:12' // Cursor from last record
477
- * // }
478
- * // }
479
- *
480
- * @example
481
- * // Input: Last page (no more records)
482
- * const records = [
483
- * { id: 98, name: 'Last Item' }
484
- * ];
485
- * const meta = buildCursorMeta(records, 20, false, ['id']);
486
- *
487
- * // Output:
488
- * // {
489
- * // pageSize: 20,
490
- * // hasMore: false
491
- * // // No cursor property since hasMore is false
492
- * // }
493
- *
494
- * @description
495
- * Used by:
496
- * - rest-api-knex-plugin adds this to response.meta for cursor pagination
497
- * - Provides cursor that client can use directly if needed
498
- *
499
- * Purpose:
500
- * - Gives clients direct access to cursor for custom pagination
501
- * - Indicates whether more pages exist without counting
502
- * - Simpler than offset pagination metadata (no total count)
503
- *
504
- * Data flow:
505
- * 1. Called after fetching records with cursor query
506
- * 2. Creates cursor from last record if more exist
507
- * 3. Added to response.meta.pagination
508
- * 4. Clients can use cursor directly or use the next link
509
246
  */
510
- export const buildCursorMeta = (records, pageSize, hasMore, sortFields = ['id'], { schemaInfo = null } = {}) => {
247
+ export const buildCursorMeta = (records, pageSize, hasMore, sortFields = ['id'], { schemaInfo = null, definitions = {}, before = false } = {}) => {
511
248
  const meta = {
512
249
  pageSize,
513
250
  hasMore
514
251
  }
515
252
 
516
253
  if (hasMore && records.length > 0) {
517
- const lastRecord = records[records.length - 1]
254
+ const lastRecord = before ? records[0] : records[records.length - 1]
518
255
  meta.cursor = {
519
- next: createCursor(lastRecord, sortFields, { schemaInfo })
256
+ next: createCursor(lastRecord, sortFields, { schemaInfo, definitions })
520
257
  }
521
258
  }
522
259
 
@@ -1,5 +1,5 @@
1
1
  import { buildIncludedResources } from './knex-relationship-includes.js'
2
- import { isRestApiError, wrapUnexpectedError } from '../../../../lib/error-context.js'
2
+ import { wrapUnexpectedError } from '../../../../lib/error-context.js'
3
3
 
4
4
  /**
5
5
  * Processes the ?include= parameter to load related resources efficiently
@@ -7,95 +7,7 @@ import { isRestApiError, wrapUnexpectedError } from '../../../../lib/error-conte
7
7
  * @param {Object} scope - The scope object for the primary resource
8
8
  * @param {Array<Object>} records - The primary records to load includes for
9
9
  * @param {Object} deps - Dependencies object containing log, scopes, knex, and context
10
- * @returns {Promise<Array<Object>>} Array of included resources in JSON:API format
11
- *
12
- * @example
13
- * // Input: Articles with author relationship
14
- * const articles = [
15
- * { id: 1, title: 'First Article', author_id: 10 },
16
- * { id: 2, title: 'Second Article', author_id: 11 },
17
- * { id: 3, title: 'Third Article', author_id: 10 } // Same author as article 1
18
- * ];
19
- *
20
- * const deps = {
21
- * context: {
22
- * scopeName: 'articles',
23
- * queryParams: { include: ['author'] },
24
- * schemaInfo: { idProperty: 'id' }
25
- * }
26
- * };
27
- *
28
- * const included = await processIncludes(scope, articles, deps);
29
- *
30
- * // Output: Deduplicated authors (only 2, not 3)
31
- * // [
32
- * // { type: 'users', id: '10', attributes: { name: 'Alice', email: 'alice@example.com' } },
33
- * // { type: 'users', id: '11', attributes: { name: 'Bob', email: 'bob@example.com' } }
34
- * // ]
35
- *
36
- * @example
37
- * // Input: Nested includes with dot notation
38
- * const articles = [{ id: 1, title: 'Article', author_id: 10 }];
39
- *
40
- * const deps = {
41
- * context: {
42
- * queryParams: {
43
- * include: ['author', 'comments.user'] // Load author AND comment users
44
- * }
45
- * }
46
- * };
47
- *
48
- * const included = await processIncludes(scope, articles, deps);
49
- *
50
- * // Output: All related resources in flat array
51
- * // [
52
- * // { type: 'users', id: '10', attributes: { name: 'Author' } },
53
- * // { type: 'comments', id: '1', attributes: { text: 'Great!' } },
54
- * // { type: 'comments', id: '2', attributes: { text: 'Nice!' } },
55
- * // { type: 'users', id: '20', attributes: { name: 'Commenter1' } },
56
- * // { type: 'users', id: '21', attributes: { name: 'Commenter2' } }
57
- * // ]
58
- *
59
- * @example
60
- * // Input: With sparse fieldsets limiting included data
61
- * const deps = {
62
- * context: {
63
- * queryParams: {
64
- * include: ['author'],
65
- * fields: {
66
- * users: 'name' // Only include name field for users
67
- * }
68
- * }
69
- * }
70
- * };
71
- *
72
- * const included = await processIncludes(scope, articles, deps);
73
- *
74
- * // Output: Users with only requested fields
75
- * // [
76
- * // { type: 'users', id: '10', attributes: { name: 'Alice' } }
77
- * // // email field excluded due to sparse fieldset
78
- * // ]
79
- *
80
- * @description
81
- * Used by:
82
- * - rest-api-knex-plugin's dataGet method when includes are requested
83
- * - rest-api-knex-plugin's dataQuery method for collection includes
84
- * - Called after primary records are fetched but before response assembly
85
- *
86
- * Purpose:
87
- * - Implements JSON:API compound documents with primary data and includes
88
- * - Prevents N+1 queries by batch loading all related resources
89
- * - Handles complex nested includes like 'comments.author.company'
90
- * - Automatically deduplicates included resources
91
- * - Respects sparse fieldsets on included resources
92
- *
93
- * Data flow:
94
- * 1. Receives primary records and include directives
95
- * 2. Parses include parameter (comma-separated, possibly nested with dots)
96
- * 3. Delegates to buildIncludedResources for actual loading
97
- * 4. Returns flat array of all included resources
98
- * 5. These resources go into the 'included' section of JSON:API response
10
+ * @returns {Promise<import('../storage/storage-types.js').DataResource[]>} Array of included resources in JSON:API format
99
11
  */
100
12
  export const processIncludes = async (scope, records, deps) => {
101
13
  try {
@@ -104,9 +16,6 @@ export const processIncludes = async (scope, records, deps) => {
104
16
  const scopeName = context.scopeName
105
17
 
106
18
  if (!scopeName) {
107
- log.error('[PROCESS-INCLUDES] scopeName is undefined in context!', {
108
- contextKeys: Object.keys(context || {})
109
- })
110
19
  throw new Error('scopeName is undefined in context')
111
20
  }
112
21
 
@@ -117,7 +26,7 @@ export const processIncludes = async (scope, records, deps) => {
117
26
  return []
118
27
  }
119
28
 
120
- log.debug('[PROCESS-INCLUDES] Processing includes:', queryParams.include)
29
+ log.debug('[PROCESS-INCLUDES] Processing includes:', { scopeName, recordCount: records.length })
121
30
 
122
31
  const includeResult = await buildIncludedResources(
123
32
  {
@@ -144,25 +53,11 @@ export const processIncludes = async (scope, records, deps) => {
144
53
 
145
54
  return includeResult.included
146
55
  } catch (error) {
147
- if (isRestApiError(error)) throw error
148
-
149
- const { log, context } = deps || {}
150
-
151
- if (log) {
152
- log.error('[PROCESS-INCLUDES] Error processing includes:', {
153
- scopeName: context?.scopeName,
154
- recordCount: records?.length || 0,
155
- includeParam: context?.queryParams?.include,
156
- error: error.message,
157
- stack: error.stack
158
- })
159
- } else {
160
- console.error('[PROCESS-INCLUDES] Error processing includes:', error)
161
- }
162
-
56
+ const { context } = deps || {}
163
57
  throw wrapUnexpectedError(error, {
164
58
  message: `Failed to process includes${context?.scopeName ? ` for scope '${context.scopeName}'` : ''}`,
165
59
  context: {
60
+ phase: 'include',
166
61
  scopeName: context?.scopeName,
167
62
  includeParam: context?.queryParams?.include,
168
63
  recordCount: records?.length || 0