json-rest-api 1.0.29 → 2.1.0

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