json-rest-api 1.0.29 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (292) hide show
  1. package/LICENSE +10 -670
  2. package/LICENSE-GPL-3.0 +674 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +39 -30
  5. package/docs/API.md +744 -1273
  6. package/docs/GUIDE/01-getting-started.md +338 -0
  7. package/docs/GUIDE/02-resources-and-relationships.md +33 -0
  8. package/docs/GUIDE/03-running-example.md +87 -0
  9. package/docs/GUIDE/04-creating-and-querying.md +181 -0
  10. package/docs/GUIDE/05-belongs-to.md +177 -0
  11. package/docs/GUIDE/06-has-many.md +184 -0
  12. package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
  13. package/docs/GUIDE/08-many-to-many.md +184 -0
  14. package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
  15. package/docs/GUIDE/10-put-and-patch.md +174 -0
  16. package/docs/GUIDE/11-relationship-endpoints.md +174 -0
  17. package/docs/GUIDE/12-field-transformations.md +287 -0
  18. package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
  19. package/docs/GUIDE/14-custom-resource-methods.md +125 -0
  20. package/docs/GUIDE/15-query-projections.md +266 -0
  21. package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
  22. package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
  23. package/docs/GUIDE/18-labels.md +86 -0
  24. package/docs/GUIDE/19-managed-transactions.md +179 -0
  25. package/docs/GUIDE/20-transaction-outcomes.md +207 -0
  26. package/docs/GUIDE/21-schema-and-migrations.md +634 -0
  27. package/docs/GUIDE/22-version-field-migrations.md +208 -0
  28. package/docs/GUIDE/23-fastify.md +179 -0
  29. package/docs/GUIDE/24-cors.md +172 -0
  30. package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
  31. package/docs/GUIDE/26-file-uploads.md +278 -0
  32. package/docs/GUIDE/27-bulk-operations.md +277 -0
  33. package/docs/GUIDE/28-socketio.md +325 -0
  34. package/docs/GUIDE/29-writing-plugins.md +278 -0
  35. package/docs/GUIDE/30-backend-capabilities.md +100 -0
  36. package/docs/GUIDE/31-positioning.md +181 -0
  37. package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +74 -30
  38. package/docs/GUIDE/33-migrating-to-v2.md +3827 -0
  39. package/docs/GUIDE/index.md +51 -59
  40. package/docs/QUICKSTART.md +120 -287
  41. package/docs/architecture.md +75 -0
  42. package/docs/contributing.md +178 -0
  43. package/index.d.ts +36 -0
  44. package/index.js +1 -22
  45. package/lib/enhanced-logger.js +91 -85
  46. package/lib/error-context.js +445 -5
  47. package/lib/error-formatter.js +219 -59
  48. package/lib/knex-transaction.js +64 -0
  49. package/lib/logger-types.d.ts +22 -0
  50. package/lib/missing-package.js +4 -0
  51. package/lib/rest-api-errors.js +103 -8
  52. package/lib/runtime/json-rest-api.js +172 -0
  53. package/lib/transaction-types.d.ts +59 -0
  54. package/package.json +97 -27
  55. package/plugins/core/bulk-operations-plugin.js +191 -184
  56. package/plugins/core/connectors/express-plugin.js +148 -219
  57. package/plugins/core/connectors/fastify-plugin.js +125 -82
  58. package/plugins/core/connectors/lib/busboy-detector.js +74 -156
  59. package/plugins/core/connectors/lib/connector-core.js +144 -20
  60. package/plugins/core/connectors/lib/formidable-detector.js +63 -120
  61. package/plugins/core/connectors/lib/http-validators.js +77 -0
  62. package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
  63. package/plugins/core/connectors/lib/request-helpers.js +10 -1
  64. package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
  65. package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
  66. package/plugins/core/file-handling-plugin.js +87 -177
  67. package/plugins/core/lib/anyapi/anyapi-registry.js +213 -164
  68. package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
  69. package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
  70. package/plugins/core/lib/anyapi/schema-utils.js +25 -16
  71. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
  72. package/plugins/core/lib/dbIntrospection.js +228 -21
  73. package/plugins/core/lib/dbTablesOperations.js +409 -92
  74. package/plugins/core/lib/fractional-positioning.js +12 -48
  75. package/plugins/core/lib/querying/identifier-query.js +19 -0
  76. package/plugins/core/lib/querying/include-query-helpers.js +364 -0
  77. package/plugins/core/lib/querying/include-to-many.js +428 -0
  78. package/plugins/core/lib/querying/include-to-one.js +579 -0
  79. package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
  80. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
  81. package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
  82. package/plugins/core/lib/querying/knex-process-includes.js +5 -110
  83. package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
  84. package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
  85. package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
  86. package/plugins/core/lib/querying/knex-window-queries.js +62 -253
  87. package/plugins/core/lib/querying/query-builder-utils.js +44 -4
  88. package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
  89. package/plugins/core/lib/querying/query-constraint.js +27 -0
  90. package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
  91. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  92. package/plugins/core/lib/querying/sort-helpers.js +13 -1
  93. package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
  94. package/plugins/core/lib/querying/url-helpers.js +11 -4
  95. package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
  96. package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
  97. package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
  98. package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
  99. package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
  100. package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
  101. package/plugins/core/lib/querying-writing/field-utils.js +113 -92
  102. package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
  103. package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
  104. package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
  105. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
  106. package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
  107. package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
  108. package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
  109. package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
  110. package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
  111. package/plugins/core/lib/querying-writing/response-options.js +35 -0
  112. package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
  113. package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
  114. package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
  115. package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
  116. package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
  117. package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
  118. package/plugins/core/lib/storage/storage-adapter.js +128 -62
  119. package/plugins/core/lib/storage/storage-mapping.js +78 -28
  120. package/plugins/core/lib/storage/storage-types.d.ts +502 -0
  121. package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
  122. package/plugins/core/lib/writing/relationship-processor.js +94 -158
  123. package/plugins/core/lib/writing/resource-version.js +172 -0
  124. package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
  125. package/plugins/core/rest-api-anyapi-knex-plugin.js +116 -2485
  126. package/plugins/core/rest-api-autofilter-plugin.js +17 -19
  127. package/plugins/core/rest-api-cors-plugin.js +52 -205
  128. package/plugins/core/rest-api-knex-plugin.js +45 -1077
  129. package/plugins/core/rest-api-label-plugin.js +25 -38
  130. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
  131. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
  132. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
  133. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
  134. package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
  135. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
  136. package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
  137. package/plugins/core/rest-api-plugin-methods/common.js +291 -395
  138. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
  139. package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
  140. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
  141. package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
  142. package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
  143. package/plugins/core/rest-api-plugin-methods/get.js +36 -68
  144. package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
  145. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
  146. package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
  147. package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
  148. package/plugins/core/rest-api-plugin-methods/post.js +51 -51
  149. package/plugins/core/rest-api-plugin-methods/put.js +69 -148
  150. package/plugins/core/rest-api-plugin-methods/query.js +37 -81
  151. package/plugins/core/rest-api-plugin-methods/release.js +2 -1
  152. package/plugins/core/rest-api-plugin.js +32 -46
  153. package/plugins/core/rest-api-positioning-plugin.js +183 -628
  154. package/plugins/core/rest-api-query-projections-plugin.js +19 -102
  155. package/plugins/core/rest-api-row-policy-plugin.js +3 -3
  156. package/plugins/core/socketio-plugin.js +364 -344
  157. package/plugins/storage/local-storage.js +40 -36
  158. package/types/autofilter.d.ts +57 -0
  159. package/types/bulk-methods.d.ts +63 -0
  160. package/types/errors.d.ts +118 -0
  161. package/types/fastify-options.d.ts +19 -0
  162. package/types/file-detectors.d.ts +33 -0
  163. package/types/file-storage.d.ts +48 -0
  164. package/types/hook-context.d.ts +180 -0
  165. package/types/plugin-options.d.ts +53 -0
  166. package/types/relationship-methods.d.ts +74 -0
  167. package/types/representations.d.ts +50 -0
  168. package/types/resource-methods.d.ts +95 -0
  169. package/types/resource-schema.d.ts +157 -0
  170. package/types/row-policy.d.ts +31 -0
  171. package/types/runtime.d.ts +132 -0
  172. package/types/transactions.d.ts +21 -0
  173. package/.claude/agents/library-guide-expert.md +0 -30
  174. package/.claude/agents/rest-api-internals-expert.md +0 -54
  175. package/.claude/settings.local.json +0 -70
  176. package/AGENTS.md +0 -34
  177. package/CLAUDE.md +0 -160
  178. package/CRUSH.md +0 -55
  179. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
  180. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
  181. package/TODO.todo +0 -13
  182. package/connectors_ideas/fastify.js +0 -217
  183. package/connectors_ideas/koa-plugin.js +0 -112
  184. package/docs/.bundle/config +0 -2
  185. package/docs/COMPARISON.md +0 -433
  186. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
  187. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
  188. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
  189. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
  190. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
  191. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
  192. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
  193. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
  194. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
  195. package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
  196. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
  197. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
  198. package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
  199. package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
  200. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
  201. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
  202. package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
  203. package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
  204. package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
  205. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
  206. package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
  207. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
  208. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
  209. package/docs/Gemfile +0 -6
  210. package/docs/Gemfile.lock +0 -308
  211. package/docs/ONBOARDING.md +0 -315
  212. package/docs/_config.yml +0 -36
  213. package/docs/_layouts/default.html +0 -123
  214. package/docs/apple-touch-icon.svg +0 -4
  215. package/docs/assets/css/style.scss +0 -392
  216. package/docs/favicon.svg +0 -4
  217. package/docs/index.md +0 -164
  218. package/docs/mini-guides/file-uploads-complete.md +0 -161
  219. package/eslint.config.js +0 -13
  220. package/examples/url-override-example.js +0 -137
  221. package/fixing_plan.md +0 -112
  222. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
  223. package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
  224. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
  225. package/plugins/storage/s3-storage.js +0 -93
  226. package/quickTest.js +0 -136
  227. package/scripts/debug-anyapi-review-filter.mjs +0 -63
  228. package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
  229. package/scripts/inspect-anyapi-descriptor.mjs +0 -19
  230. package/scripts/run-docs.js +0 -133
  231. package/test-debug.js +0 -59
  232. package/tests/README.md +0 -202
  233. package/tests/TEST_TEMPLATE.test.js +0 -178
  234. package/tests/anyapi-basic.test.js +0 -105
  235. package/tests/anyapi-cursor-pagination.test.js +0 -148
  236. package/tests/anyapi-custom-idproperty.test.js +0 -163
  237. package/tests/autofilter.test.js +0 -463
  238. package/tests/bulk-operations.test.js +0 -657
  239. package/tests/computed-fields.test.js +0 -586
  240. package/tests/cors.test.js +0 -834
  241. package/tests/curl.test.js +0 -467
  242. package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
  243. package/tests/custom-id-simplified-post.test.js +0 -75
  244. package/tests/custom-idproperty-relationships.test.js +0 -932
  245. package/tests/db-introspection.test.js +0 -521
  246. package/tests/db-table-operations.test.js +0 -798
  247. package/tests/fastify-plugin.test.js +0 -184
  248. package/tests/field-getters.test.js +0 -370
  249. package/tests/field-setters.test.js +0 -306
  250. package/tests/file-handling.test.js +0 -155
  251. package/tests/fixtures/api-configs.js +0 -2566
  252. package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
  253. package/tests/helpers/fake-fastify.js +0 -99
  254. package/tests/helpers/storage-mode.js +0 -65
  255. package/tests/helpers/test-utils.js +0 -203
  256. package/tests/http-connectors-parity.test.js +0 -395
  257. package/tests/id-normalization.test.js +0 -444
  258. package/tests/include-depth-validation.test.js +0 -331
  259. package/tests/includes.test.js +0 -427
  260. package/tests/local-storage.test.js +0 -96
  261. package/tests/logical-id-write-validation.test.js +0 -151
  262. package/tests/nested-includes.test.js +0 -617
  263. package/tests/pagination-boundaries.test.js +0 -154
  264. package/tests/pagination-contract.test.js +0 -122
  265. package/tests/pagination-cursor-multifield.test.js +0 -641
  266. package/tests/pagination-enhanced.test.js +0 -563
  267. package/tests/polymorphic-relationships.test.js +0 -347
  268. package/tests/polymorphic-simple.test.js +0 -148
  269. package/tests/positioning-advanced.test.js +0 -692
  270. package/tests/positioning-extended.test.js +0 -915
  271. package/tests/positioning-multifilter.test.js +0 -699
  272. package/tests/positioning.test.js +0 -560
  273. package/tests/post-commit-hooks.test.js +0 -120
  274. package/tests/projected-fields.test.js +0 -319
  275. package/tests/public-exports.test.js +0 -15
  276. package/tests/put-create-parity.test.js +0 -216
  277. package/tests/queries.test.js +0 -521
  278. package/tests/query-limits.test.js +0 -584
  279. package/tests/relationship-endpoints.test.js +0 -620
  280. package/tests/relationship-include-storage-adapter.test.js +0 -56
  281. package/tests/relationships.test.js +0 -681
  282. package/tests/rest-api.test.js +0 -337
  283. package/tests/return-record-settings.test.js +0 -184
  284. package/tests/row-policy.test.js +0 -489
  285. package/tests/s3-storage.test.js +0 -38
  286. package/tests/searchschema-knex-behavior.test.js +0 -187
  287. package/tests/searchschema-merge.test.js +0 -151
  288. package/tests/socketio.test.js +0 -860
  289. package/tests/storage-mapping.test.js +0 -362
  290. package/tests/temporal-boundaries.test.js +0 -208
  291. package/tests/test-development-guide.md +0 -370
  292. package/tests/virtual-fields.test.js +0 -404
@@ -1,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
@@ -177,62 +116,8 @@ 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
122
  export const createCursor = (record, sortFields = ['id'], { schemaInfo = null, definitions = {} } = {}) => {
238
123
  const parts = []
@@ -245,7 +130,7 @@ export const createCursor = (record, sortFields = ['id'], { schemaInfo = null, d
245
130
  parts.push(`${field}:~null`)
246
131
  return
247
132
  }
248
- if (['date', 'dateTime', 'time'].includes(definition?.type) && value != null) {
133
+ if (!definition?.storage?.serialize && !definition?.select && ['date', 'dateTime', 'time'].includes(definition?.type) && value != null) {
249
134
  value = normalizeDateValue(value, definition.type, {
250
135
  temporalPrecision: definition.temporalPrecision,
251
136
  fieldName: field,
@@ -267,51 +152,6 @@ export const createCursor = (record, sortFields = ['id'], { schemaInfo = null, d
267
152
  * @param {string} cursor - Cursor string to parse
268
153
  * @returns {Object} Object with field names as keys and decoded values
269
154
  * @throws {Error} If cursor format is invalid
270
- *
271
- * @example
272
- * // Input: Simple cursor
273
- * const cursor = "id:123";
274
- * const data = parseCursor(cursor);
275
- *
276
- * // Output: { id: "123" }
277
- * // Ready to use in SQL: WHERE id > '123'
278
- *
279
- * @example
280
- * // Input: Multi-field cursor with URL-encoded values
281
- * const cursor = "created_at:2024-01-15T08%3A30%3A00.000Z,id:5";
282
- * const data = parseCursor(cursor);
283
- *
284
- * // Output:
285
- * // {
286
- * // created_at: "2024-01-15T08:30:00.000Z", // Decoded
287
- * // id: "5"
288
- * // }
289
- * // Used for: WHERE (created_at, id) > ('2024-01-15T08:30:00.000Z', '5')
290
- *
291
- * @example
292
- * // Input: Invalid cursor (throws error)
293
- * try {
294
- * const data = parseCursor("invalid-no-colon");
295
- * } catch (e) {
296
- * console.log(e.message);
297
- * // "Invalid cursor format: Invalid cursor format: missing colon separator"
298
- * }
299
- *
300
- * @description
301
- * Used by:
302
- * - rest-api-knex-plugin's dataQuery when processing page[after] parameter
303
- * - Used to build WHERE clause for cursor-based queries
304
- *
305
- * Purpose:
306
- * - Decodes cursor back to usable values for SQL queries
307
- * - Handles URL-encoded special characters
308
- * - Validates cursor format to prevent injection attacks
309
- *
310
- * Data flow:
311
- * 1. Client sends page[after]=cursor parameter
312
- * 2. parseCursor extracts field values from cursor
313
- * 3. Values used to build WHERE clause like "WHERE id > 123"
314
- * 4. Ensures pagination continues from exact position
315
155
  */
316
156
  export const parseCursor = (cursor) => {
317
157
  try {
@@ -355,70 +195,8 @@ export const parseCursor = (cursor) => {
355
195
  * @param {number} pageSize - Records per page
356
196
  * @param {boolean} hasMore - Whether more records exist
357
197
  * @param {Array<string>} sortFields - Fields used for cursor
358
- * @returns {Object|null} Links object with self, first, next
359
- *
360
- * @example
361
- * // Input: First page of results
362
- * const urlPrefix = '/api/v1';
363
- * const scopeName = 'articles';
364
- * const queryParams = {
365
- * filter: { status: 'published' },
366
- * page: { size: 20 }
367
- * };
368
- * const records = [
369
- * { id: 1, created_at: '2024-01-01', title: 'First' },
370
- * { id: 2, created_at: '2024-01-02', title: 'Second' },
371
- * { id: 3, created_at: '2024-01-03', title: 'Third' }
372
- * ];
373
- * const hasMore = true; // More records exist
374
- *
375
- * const links = generateCursorPaginationLinks(
376
- * urlPrefix, scopeName, queryParams, records, 20, hasMore, ['created_at', 'id']
377
- * );
378
- *
379
- * // Output:
380
- * // {
381
- * // self: '/api/v1/articles?filter[status]=published&page[size]=20',
382
- * // first: '/api/v1/articles?filter[status]=published&page[size]=20',
383
- * // next: '/api/v1/articles?filter[status]=published&page[size]=20&page[after]=created_at:2024-01-03,id:3'
384
- * // }
385
- * // The 'next' cursor points after the last record (id:3)
386
- *
387
- * @example
388
- * // Input: Last page (no more records)
389
- * const records = [
390
- * { id: 98, created_at: '2024-03-01' },
391
- * { id: 99, created_at: '2024-03-02' }
392
- * ];
393
- * const hasMore = false; // No more records
394
- *
395
- * const links = generateCursorPaginationLinks(
396
- * '/api', 'articles', {}, records, 20, hasMore, ['id']
397
- * );
398
- *
399
- * // Output - no 'next' link:
400
- * // {
401
- * // self: '/api/articles?page[size]=20',
402
- * // first: '/api/articles?page[size]=20'
403
- * // }
404
- *
405
- * @description
406
- * Used by:
407
- * - rest-api-knex-plugin when cursor pagination is enabled
408
- * - Called after fetching records with cursor-based query
409
- *
410
- * Purpose:
411
- * - Cursor pagination is more efficient for large datasets
412
- * - Stable pagination when data is being added/removed
413
- * - Only includes 'next' link when more data exists
414
- * - Preserves all other query parameters
415
- *
416
- * Data flow:
417
- * 1. After cursor-based query fetches records
418
- * 2. If hasMore is true, creates cursor from last record
419
- * 3. Builds next link with page[after] parameter
420
- * 4. Added to response.links for navigation
421
- * 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
422
200
  */
423
201
  export const generateCursorPaginationLinks = (
424
202
  urlPrefix,
@@ -463,55 +241,8 @@ export const generateCursorPaginationLinks = (
463
241
  * @param {number} pageSize - Records per page
464
242
  * @param {boolean} hasMore - Whether more records exist
465
243
  * @param {Array<string>} sortFields - Fields used for cursor
244
+ * @param {import('../storage/storage-types.js').DataCursorOptions} [options]
466
245
  * @returns {Object} Metadata with pageSize, hasMore, and optional cursor
467
- *
468
- * @example
469
- * // Input: Page with more records available
470
- * const records = [
471
- * { id: 10, created_at: '2024-01-10', title: 'Article 10' },
472
- * { id: 11, created_at: '2024-01-11', title: 'Article 11' },
473
- * { id: 12, created_at: '2024-01-12', title: 'Article 12' }
474
- * ];
475
- * const meta = buildCursorMeta(records, 20, true, ['created_at', 'id']);
476
- *
477
- * // Output:
478
- * // {
479
- * // pageSize: 20,
480
- * // hasMore: true,
481
- * // cursor: {
482
- * // next: 'created_at:2024-01-12,id:12' // Cursor from last record
483
- * // }
484
- * // }
485
- *
486
- * @example
487
- * // Input: Last page (no more records)
488
- * const records = [
489
- * { id: 98, name: 'Last Item' }
490
- * ];
491
- * const meta = buildCursorMeta(records, 20, false, ['id']);
492
- *
493
- * // Output:
494
- * // {
495
- * // pageSize: 20,
496
- * // hasMore: false
497
- * // // No cursor property since hasMore is false
498
- * // }
499
- *
500
- * @description
501
- * Used by:
502
- * - rest-api-knex-plugin adds this to response.meta for cursor pagination
503
- * - Provides cursor that client can use directly if needed
504
- *
505
- * Purpose:
506
- * - Gives clients direct access to cursor for custom pagination
507
- * - Indicates whether more pages exist without counting
508
- * - Simpler than offset pagination metadata (no total count)
509
- *
510
- * Data flow:
511
- * 1. Called after fetching records with cursor query
512
- * 2. Creates cursor from last record if more exist
513
- * 3. Added to response.meta.pagination
514
- * 4. Clients can use cursor directly or use the next link
515
246
  */
516
247
  export const buildCursorMeta = (records, pageSize, hasMore, sortFields = ['id'], { schemaInfo = null, definitions = {}, before = false } = {}) => {
517
248
  const meta = {
@@ -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
@@ -1,59 +1,14 @@
1
+ // @ts-check
2
+ import { isImmediateExpression } from '../querying-writing/query-field-helpers.js'
1
3
  /**
2
- * Builds the SELECT clause for a Knex query, handling field selection and table prefixing
3
- *
4
- * @param {Object} query - The Knex query builder instance
5
- * @param {string} tableName - The table name
6
- * @param {Array<string>|string} fieldsToSelect - Fields to select or '*'
7
- * @param {boolean} useTablePrefix - Whether to prefix fields with table name
8
- * @returns {Object} The modified query builder
9
- *
10
- * @example
11
- * // Input: Basic query without specific fields
12
- * const query = knex('articles');
13
- * buildQuerySelection(query, 'articles', '*', false);
14
- *
15
- * // Effect on query: No modification, Knex defaults to SELECT *
16
- * // SQL generated: SELECT * FROM articles
17
- *
18
- * @example
19
- * // Input: Query with sparse fieldset
20
- * const query = knex('articles');
21
- * const fields = ['id', 'title', 'author_id'];
22
- * buildQuerySelection(query, 'articles', fields, false);
23
- *
24
- * // Effect on query: Adds select() with specific fields
25
- * // SQL generated: SELECT id, title, author_id FROM articles
26
- * // Response will only include these 3 fields, reducing payload size
27
- *
28
- * @example
29
- * // Input: Query with joins needing table prefixes
30
- * const query = knex('articles')
31
- * .join('users', 'articles.author_id', 'users.id');
32
- * const fields = ['id', 'title', 'created_at as published'];
33
- * buildQuerySelection(query, 'articles', fields, true);
34
- *
35
- * // Effect on query: Prefixes all fields with table name
36
- * // SQL generated: SELECT articles.id, articles.title, articles.created_at as published
37
- * // Prevents "column 'id' is ambiguous" errors since both tables have id
38
- *
39
- * @description
40
- * Used by:
41
- * - knex-query-helpers.js calls this when building queries with sparse fieldsets
42
- * - rest-api-knex-plugin uses this in dataQuery to optimize SELECT clauses
43
- * - Called whenever fields parameter limits which attributes to return
44
- *
45
- * Purpose:
46
- * - Implements JSON:API sparse fieldsets by selecting only requested fields
47
- * - Prevents ambiguous column errors in queries with joins
48
- * - Reduces database transfer by not selecting unnecessary columns
49
- * - Handles field aliases (e.g., "created_at as published")
50
- *
51
- * Data flow:
52
- * 1. Query parser extracts fields parameter (e.g., fields[articles]=title,author)
53
- * 2. This function adds appropriate SELECT clause to query
54
- * 3. Database returns only selected columns
55
- * 4. Smaller result sets improve query performance
56
- * 5. JSON:API response includes only requested attributes
4
+ * Add logical field selections to an unexecuted builder, using the caller's translator.
5
+ * `*` retains wildcard behavior. Aliases are null when table prefixing is disabled.
6
+ * @param {import('../storage/storage-types.js').StorageQuery} query
7
+ * @param {string} tableName
8
+ * @param {readonly string[] | string} fieldsToSelect
9
+ * @param {boolean} [useTablePrefix]
10
+ * @param {{ translateColumn?: (field: string, alias?: string | null) => string | import('knex').Knex.Raw<unknown> | Record<string, string | import('knex').Knex.Raw<unknown>> | null | undefined }} [options]
11
+ * @returns {import('../storage/storage-types.js').StorageQuery}
57
12
  */
58
13
  export const buildQuerySelection = (
59
14
  query,
@@ -64,6 +19,7 @@ export const buildQuerySelection = (
64
19
  ) => {
65
20
  const translateColumn = options.translateColumn
66
21
 
22
+ /** @param {string} field @param {string | null} aliasForField */
67
23
  const applyTranslation = (field, aliasForField) => {
68
24
  if (translateColumn) {
69
25
  const translated = translateColumn(field, aliasForField)
@@ -88,6 +44,7 @@ export const buildQuerySelection = (
88
44
  return useTablePrefix ? query.select(`${tableName}.*`) : query
89
45
  }
90
46
 
47
+ /** @type {readonly string[]} */
91
48
  const fieldsArray = Array.isArray(fieldsToSelect)
92
49
  ? fieldsToSelect
93
50
  : Array.from(fieldsToSelect || [])
@@ -97,8 +54,16 @@ export const buildQuerySelection = (
97
54
 
98
55
  const aliasMatch = field.match(/\s+as\s+/i)
99
56
  if (aliasMatch) {
100
- const [source, alias] = field.split(/\s+as\s+/i)
57
+ const [source = '', alias = ''] = field.split(/\s+as\s+/i)
101
58
  const translatedSource = applyTranslation(source.trim(), targetAlias)
59
+ if (typeof translatedSource !== 'string') {
60
+ if (isImmediateExpression(translatedSource)) return { [alias.trim()]: translatedSource }
61
+ const values = Object.values(translatedSource)
62
+ if (values.length !== 1 || values[0] === undefined) {
63
+ throw new Error(`Cannot apply alias '${alias.trim()}' to a translation containing multiple or missing expressions`)
64
+ }
65
+ return { [alias.trim()]: values[0] }
66
+ }
102
67
  return `${translatedSource} as ${alias.trim()}`
103
68
  }
104
69