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,21 +1,29 @@
1
+ // @ts-check
1
2
  import {
2
- getFieldValue as getLegacyFieldValue,
3
+ getFieldValue as getTableFieldValue,
3
4
  getIdColumn,
4
5
  getStorageInfo,
5
6
  getStorageColumn,
6
7
  translateAttributesForStorage,
8
+ serializeFieldValueForStorage,
9
+ assertWritableKnexColumns,
7
10
  } from './storage-mapping.js'
8
11
  import {
9
12
  getCanonicalFieldValue,
10
13
  getCanonicalResourceIdColumn,
11
14
  translateCanonicalAttributesForStorage,
12
15
  } from './canonical-storage-mapping.js'
16
+ import { assertScalarQueryField } from '../querying-writing/field-utils.js'
13
17
  import { normalizeValueForDatabaseStorage } from '../querying-writing/database-value-normalizers.js'
14
18
 
19
+ /** @import { BaseStorageAdapterOptions, CanonicalDescriptor, SelectTranslator, StorageAdapter, StorageAdapterOptions, StorageAdapterLookupOptions, StorageSchemaInfo, StorageFieldDefinition } from './storage-types.js' */
20
+
21
+ /** @template T @param {T} value @returns {T} */
15
22
  const passthrough = (value) => value
23
+ /** @type {StorageAdapter['translateFilterValue']} */
16
24
  const identityTranslate = (_field, value) => value
17
- const identityScope = (query) => query
18
25
 
26
+ /** @type {StorageAdapter['selectColumns']} */
19
27
  const selectColumnsOnBuilder = (builder, columns) => {
20
28
  if (!columns) return builder
21
29
  if (Array.isArray(columns)) {
@@ -26,16 +34,16 @@ const selectColumnsOnBuilder = (builder, columns) => {
26
34
  return builder
27
35
  }
28
36
 
29
- const normalizeArray = (value, normalizeFn) => value.map((entry) => normalizeFn(entry))
30
-
37
+ /** @param {unknown} value @returns {unknown} */
31
38
  const normalizeBelongsToValue = (value) => {
32
39
  if (value === null || value === undefined) return value
33
40
  if (Array.isArray(value)) {
34
- return normalizeArray(value, normalizeBelongsToValue)
41
+ return value.map(entry => normalizeBelongsToValue(entry))
35
42
  }
36
43
  return String(value)
37
44
  }
38
45
 
46
+ /** @param {unknown} value @returns {boolean} */
39
47
  const normalizeBooleanValue = (value) => {
40
48
  if (typeof value === 'boolean') return value
41
49
  if (typeof value === 'string') {
@@ -50,13 +58,16 @@ const normalizeBooleanValue = (value) => {
50
58
  return Boolean(value)
51
59
  }
52
60
 
53
- const normalizeFilterValueForDefinition = (value, definition = {}, { isRelationship = false } = {}) => {
61
+ /**
62
+ * @param {unknown} value
63
+ * @param {StorageFieldDefinition} [definition]
64
+ * @param {{ isRelationship?: boolean, databaseClient?: string, textStorage?: boolean }} [options]
65
+ * @returns {unknown}
66
+ */
67
+ const normalizeFilterValueForDefinition = (value, definition = {}, { isRelationship = false, databaseClient, textStorage } = {}) => {
54
68
  if (value === null || value === undefined) return value
55
69
  if (Array.isArray(value)) {
56
- return normalizeArray(
57
- value,
58
- (entry) => normalizeFilterValueForDefinition(entry, definition, { isRelationship })
59
- )
70
+ return value.map(entry => normalizeFilterValueForDefinition(entry, definition, { isRelationship, databaseClient, textStorage }))
60
71
  }
61
72
 
62
73
  if (isRelationship) {
@@ -79,6 +90,8 @@ const normalizeFilterValueForDefinition = (value, definition = {}, { isRelations
79
90
 
80
91
  if (['date', 'dateTime', 'time'].includes(type)) {
81
92
  return normalizeValueForDatabaseStorage(value, type, {
93
+ databaseClient,
94
+ textStorage,
82
95
  temporalPrecision: definition?.temporalPrecision
83
96
  })
84
97
  }
@@ -86,11 +99,9 @@ const normalizeFilterValueForDefinition = (value, definition = {}, { isRelations
86
99
  return value
87
100
  }
88
101
 
89
- const translateSourceColumn = (source, adapter) => {
90
- if (!adapter || source === '*') return source
91
- return adapter.translateColumn(source) || source
92
- }
93
-
102
+ /** @overload @param {StorageAdapter} adapter @returns {SelectTranslator} */
103
+ /** @overload @param {StorageAdapter | null | undefined} adapter @returns {SelectTranslator | null} */
104
+ /** @param {StorageAdapter | null | undefined} adapter @returns {SelectTranslator | null} */
94
105
  export const createSelectTranslator = (adapter) => {
95
106
  if (!adapter) return null
96
107
 
@@ -119,40 +130,23 @@ export const createSelectTranslator = (adapter) => {
119
130
  }
120
131
  }
121
132
 
122
- export const translateSelectFieldsForAdapter = (fields, adapter) => {
123
- if (!adapter || !fields) return fields
124
-
125
- const translateField = createSelectTranslator(adapter)
126
-
127
- return fields.map((field) => {
128
- if (typeof field !== 'string') return field
129
- if (field === '*') return '*'
130
-
131
- const aliasMatch = field.match(/\s+as\s+/i)
132
- if (aliasMatch) {
133
- const [source, alias] = field.split(/\s+as\s+/i)
134
- const translatedSource = translateSourceColumn(source.trim(), adapter)
135
- return `${translatedSource} as ${alias.trim()}`
136
- }
137
-
138
- return translateField(field)
139
- })
140
- }
141
-
133
+ /** @param {BaseStorageAdapterOptions} options @returns {StorageAdapter} */
142
134
  const baseAdapter = ({
143
135
  knex,
144
136
  tableName,
145
137
  idColumn,
146
138
  translateColumn,
147
139
  translateFilterValue = identityTranslate,
148
- applyResourceScope = identityScope,
140
+ translateCursorValue = identityTranslate,
141
+ applyResourceScope = passthrough,
149
142
  toStorageRow = passthrough,
150
143
  getFieldValue = (record, fieldName) => record?.[fieldName],
151
144
  isCanonical = false,
152
145
  }) => {
153
- const buildBaseQuery = ({ transaction } = {}) => {
154
- const query = (transaction || knex)(tableName)
155
- return applyResourceScope(query)
146
+ /** @type {StorageAdapter['buildBaseQuery']} */
147
+ const buildBaseQuery = ({ transaction, tableAlias } = {}) => {
148
+ const query = (transaction || knex)(tableAlias ? { [tableAlias]: tableName } : tableName)
149
+ return applyResourceScope(query, tableAlias)
156
150
  }
157
151
 
158
152
  return {
@@ -161,28 +155,45 @@ const baseAdapter = ({
161
155
  getIdColumn: () => idColumn,
162
156
  translateColumn,
163
157
  translateFilterValue,
158
+ translateCursorValue,
164
159
  applyResourceScope,
165
160
  toStorageRow,
166
161
  getFieldValue,
167
162
  buildBaseQuery,
168
- selectColumns: (builder, columns) => selectColumnsOnBuilder(builder, columns),
163
+ selectColumns: selectColumnsOnBuilder,
169
164
  }
170
165
  }
171
166
 
172
- const createLegacyAdapter = ({ knex, schemaInfo }) => {
167
+ /** @param {StorageAdapterOptions} options @returns {StorageAdapter} */
168
+ const createTableAdapter = ({ knex, schemaInfo }) => {
169
+ assertWritableKnexColumns(getStorageInfo(schemaInfo))
173
170
  const tableName = schemaInfo.tableName
174
171
  const idColumn = getIdColumn(schemaInfo)
175
172
 
173
+ /** @type {StorageAdapter['translateColumn']} */
176
174
  const translateColumn = (field) => getStorageColumn(schemaInfo, field)
177
- const toStorageRow = (attributes, options = {}) => translateAttributesForStorage(attributes, schemaInfo, options)
178
- const getFieldValue = (record, fieldName) => getLegacyFieldValue(record, schemaInfo, fieldName)
175
+ /** @type {StorageAdapter['toStorageRow']} */
176
+ const toStorageRow = (attributes, options = {}) => translateAttributesForStorage(attributes, schemaInfo, { ...options, databaseClient: knex.client.config.client })
177
+ /** @type {StorageAdapter['getFieldValue']} */
178
+ const getFieldValue = (record, fieldName) => getTableFieldValue(record, schemaInfo, fieldName)
179
+ /** @type {StorageAdapter['translateCursorValue']} */
180
+ const translateCursorValue = (field, value) => {
181
+ const definition = schemaInfo.schemaStructure?.[field]
182
+ return normalizeFilterValueForDefinition(value, definition, {
183
+ isRelationship: Boolean(definition?.belongsTo || definition?.belongsToPolymorphic),
184
+ databaseClient: knex.client.config.client
185
+ })
186
+ }
187
+ /** @type {StorageAdapter['translateFilterValue']} */
179
188
  const translateFilterValue = (field, value) => {
180
189
  const searchField = schemaInfo.searchSchemaStructure?.[field]
181
190
  const fieldName = searchField?.actualField || field
182
191
  const schemaField = schemaInfo.schemaStructure?.[fieldName]
192
+ assertScalarQueryField(schemaField, fieldName, 'filter')
183
193
  const fieldStorage = getStorageInfo(schemaInfo).fields[fieldName]
184
194
  if (fieldStorage?.serialize) {
185
- const serialize = (entry) => fieldStorage.serialize(entry, {
195
+ /** @param {unknown} entry */
196
+ const serialize = (entry) => serializeFieldValueForStorage(entry, {
186
197
  fieldName,
187
198
  columnName: fieldStorage.column,
188
199
  definition: fieldStorage.definition,
@@ -198,7 +209,7 @@ const createLegacyAdapter = ({ knex, schemaInfo }) => {
198
209
  schemaField?.belongsTo ||
199
210
  schemaField?.belongsToPolymorphic
200
211
  )
201
- return normalizeFilterValueForDefinition(value, definition, { isRelationship })
212
+ return normalizeFilterValueForDefinition(value, definition, { isRelationship, databaseClient: knex.client.config.client })
202
213
  }
203
214
 
204
215
  return baseAdapter({
@@ -207,25 +218,28 @@ const createLegacyAdapter = ({ knex, schemaInfo }) => {
207
218
  idColumn,
208
219
  translateColumn,
209
220
  translateFilterValue,
210
- applyResourceScope: identityScope,
221
+ translateCursorValue,
222
+ applyResourceScope: passthrough,
211
223
  toStorageRow,
212
224
  getFieldValue,
213
225
  isCanonical: false,
214
226
  })
215
227
  }
216
228
 
217
- const createCanonicalAdapter = ({ knex, schemaInfo }) => {
218
- const descriptor = schemaInfo.descriptor || {}
219
- const canonical = descriptor.canonical || {}
229
+ /** @param {StorageAdapterOptions & { descriptor: CanonicalDescriptor }} options @returns {StorageAdapter} */
230
+ const createCanonicalAdapter = ({ knex, schemaInfo, descriptor }) => {
231
+ const canonical = descriptor.canonical
220
232
  const canonicalFieldMap = descriptor.canonicalFieldMap || {}
221
233
  const fieldsInfo = descriptor.fields || {}
222
234
  const belongsToInfo = descriptor.belongsTo || {}
223
235
  const idProperty = schemaInfo.idProperty || descriptor.idProperty || 'id'
236
+ /** @param {string} field */
224
237
  const isLogicalIdField = (field) => field === 'id' || field === idProperty
225
238
  const descriptorWithIdProperty = descriptor.idProperty === idProperty
226
239
  ? descriptor
227
240
  : { ...descriptor, idProperty }
228
241
 
242
+ /** @type {StorageAdapter['translateColumn']} */
229
243
  const translateColumn = (field) => {
230
244
  if (!field) return field
231
245
  if (isLogicalIdField(field)) return getCanonicalResourceIdColumn(descriptor)
@@ -251,7 +265,7 @@ const createCanonicalAdapter = ({ knex, schemaInfo }) => {
251
265
  if (!canonicalEntry && field.endsWith('_type')) {
252
266
  const alias = field.slice(0, -5)
253
267
  const aliasEntry = canonicalFieldMap[alias]
254
- if (aliasEntry?.typeSlot) return aliasEntry.typeSlot
268
+ if (typeof aliasEntry === 'object' && aliasEntry?.typeSlot) return aliasEntry.typeSlot
255
269
  }
256
270
 
257
271
  const fieldInfo = fieldsInfo[field]
@@ -263,9 +277,27 @@ const createCanonicalAdapter = ({ knex, schemaInfo }) => {
263
277
  return field
264
278
  }
265
279
 
280
+ /** @type {StorageAdapter['translateFilterValue']} */
266
281
  const translateFilterValue = (field, value) => {
267
282
  const searchField = schemaInfo.searchSchemaStructure?.[field]
268
- const schemaField = schemaInfo.schemaStructure?.[field]
283
+ const fieldName = searchField?.actualField || field
284
+ const schemaField = schemaInfo.schemaStructure?.[fieldName]
285
+ assertScalarQueryField(schemaField, fieldName, 'filter')
286
+ if (schemaField?.storage?.serialize) {
287
+ /** @param {unknown} entry */
288
+ const serialize = entry => {
289
+ const serialized = serializeFieldValueForStorage(entry, {
290
+ fieldName,
291
+ columnName: translateColumn(fieldName),
292
+ definition: schemaField,
293
+ schemaInfo,
294
+ operation: 'filter'
295
+ })
296
+ return schemaField.type === 'id' ? normalizeBelongsToValue(serialized) : serialized
297
+ }
298
+ return Array.isArray(value) ? value.map(serialize) : serialize(value)
299
+ }
300
+ if (isLogicalIdField(fieldName) || schemaField?.type === 'id') return normalizeBelongsToValue(value)
269
301
  const isRelationship = Boolean(
270
302
  searchField?.isRelationship ||
271
303
  schemaField?.belongsTo ||
@@ -273,20 +305,33 @@ const createCanonicalAdapter = ({ knex, schemaInfo }) => {
273
305
  )
274
306
  const definition = searchField || schemaField || {}
275
307
 
276
- return normalizeFilterValueForDefinition(value, definition, { isRelationship })
308
+ return normalizeFilterValueForDefinition(value, definition, { isRelationship, databaseClient: knex.client.config.client, textStorage: fieldsInfo[fieldName]?.slotType === 'string' })
277
309
  }
278
310
 
279
- const applyResourceScope = (query) => {
311
+ /** @type {StorageAdapter['translateCursorValue']} */
312
+ const translateCursorValue = (field, value) => {
313
+ const definition = schemaInfo.schemaStructure?.[field]
314
+ if (isLogicalIdField(field) || definition?.type === 'id') return normalizeBelongsToValue(value)
315
+ return normalizeFilterValueForDefinition(value, definition, {
316
+ isRelationship: Boolean(definition?.belongsTo || definition?.belongsToPolymorphic),
317
+ databaseClient: knex.client.config.client,
318
+ textStorage: fieldsInfo[field]?.slotType === 'string'
319
+ })
320
+ }
321
+
322
+ /** @type {StorageAdapter['applyResourceScope']} */
323
+ const applyResourceScope = (query, tableAlias) => {
324
+ const prefix = tableAlias ? `${tableAlias}.` : ''
280
325
  return query
281
- .where(canonical.tenantColumn, descriptor.tenant)
282
- .where(canonical.resourceColumn, descriptor.resource)
326
+ .where(`${prefix}${canonical.tenantColumn}`, descriptor.tenant)
327
+ .where(`${prefix}${canonical.resourceColumn}`, descriptor.resource)
283
328
  }
284
329
 
285
- const toStorageRow = (attributes) => translateCanonicalAttributesForStorage(attributes, descriptor)
330
+ /** @type {StorageAdapter['toStorageRow']} */
331
+ const toStorageRow = (attributes, options = {}) => translateCanonicalAttributesForStorage(attributes, descriptor, { ...options, schemaInfo, databaseClient: knex.client.config.client })
332
+ /** @type {StorageAdapter['getFieldValue']} */
286
333
  const getFieldValue = (record, fieldName) => (
287
- isLogicalIdField(fieldName)
288
- ? getCanonicalFieldValue(record, descriptorWithIdProperty, fieldName)
289
- : getCanonicalFieldValue(record, descriptor, fieldName)
334
+ getCanonicalFieldValue(record, isLogicalIdField(fieldName) ? descriptorWithIdProperty : descriptor, fieldName)
290
335
  )
291
336
 
292
337
  return baseAdapter({
@@ -295,6 +340,7 @@ const createCanonicalAdapter = ({ knex, schemaInfo }) => {
295
340
  idColumn: getCanonicalResourceIdColumn(descriptor),
296
341
  translateColumn,
297
342
  translateFilterValue,
343
+ translateCursorValue,
298
344
  applyResourceScope,
299
345
  toStorageRow,
300
346
  getFieldValue,
@@ -302,14 +348,34 @@ const createCanonicalAdapter = ({ knex, schemaInfo }) => {
302
348
  })
303
349
  }
304
350
 
351
+ /** @param {StorageAdapterOptions} options @returns {StorageAdapter} */
305
352
  export const createStorageAdapter = ({ knex, schemaInfo }) => {
306
353
  if (!schemaInfo) {
307
354
  throw new Error('createStorageAdapter requires schemaInfo')
308
355
  }
309
356
 
310
- if (schemaInfo?.descriptor?.canonical?.tableName) {
311
- return createCanonicalAdapter({ knex, schemaInfo })
357
+ const descriptor = schemaInfo.descriptor
358
+ if (descriptor?.canonical?.tableName) {
359
+ return createCanonicalAdapter({ knex, schemaInfo, descriptor })
312
360
  }
313
361
 
314
- return createLegacyAdapter({ knex, schemaInfo })
362
+ return createTableAdapter({ knex, schemaInfo })
363
+ }
364
+
365
+ /** @param {StorageAdapterLookupOptions} options @returns {(scopeName: string) => StorageAdapter | null} */
366
+ export function createStorageAdapterLookup ({ knex, getResource }) {
367
+ /** @type {Map<string, { schemaInfo: StorageSchemaInfo, adapter: StorageAdapter }>} */
368
+ const adapters = new Map()
369
+ return scopeName => {
370
+ if (!scopeName) return null
371
+ const resource = getResource(scopeName)
372
+ const schemaInfo = resource?.vars?.schemaInfo
373
+ if (!schemaInfo) return null
374
+ const cached = adapters.get(scopeName)
375
+ if (cached && cached.schemaInfo === schemaInfo) return cached.adapter
376
+ const adapter = createStorageAdapter({ knex, schemaInfo })
377
+ adapters.set(scopeName, { adapter, schemaInfo })
378
+ if (resource?.vars) resource.vars.storageAdapter = adapter
379
+ return adapter
380
+ }
315
381
  }
@@ -1,5 +1,10 @@
1
+ // @ts-check
1
2
  import { normalizeValueForDatabaseStorage } from '../querying-writing/database-value-normalizers.js'
3
+ import { assertFieldNameMap } from '../querying-writing/field-utils.js'
2
4
 
5
+ /** @import { FieldStorage, StorageConfig, StorageFieldDefinition, StorageInfo, StorageNaming, StorageRow, StorageSchemaInfo, StorageSerializerDetails, StorageWriteOptions } from './storage-types.js' */
6
+
7
+ /** @type {StorageInfo} */
3
8
  const EMPTY_STORAGE_INFO = Object.freeze({
4
9
  idColumn: 'id',
5
10
  storage: Object.freeze({
@@ -9,6 +14,7 @@ const EMPTY_STORAGE_INFO = Object.freeze({
9
14
  columns: Object.freeze({})
10
15
  })
11
16
 
17
+ /** @param {unknown} value @returns {value is Record<string, unknown>} */
12
18
  const isPlainObject = (value) => (
13
19
  value !== null &&
14
20
  typeof value === 'object' &&
@@ -18,6 +24,7 @@ const isPlainObject = (value) => (
18
24
  const SNAKE_CASE_STORAGE_NAMING = 'snake_case'
19
25
  const EXACT_STORAGE_NAMING = 'exact'
20
26
 
27
+ /** @type {Readonly<Partial<Record<string, StorageNaming>>>} */
21
28
  const STORAGE_NAMING_ALIASES = Object.freeze({
22
29
  snake: SNAKE_CASE_STORAGE_NAMING,
23
30
  snake_case: SNAKE_CASE_STORAGE_NAMING,
@@ -27,11 +34,13 @@ const STORAGE_NAMING_ALIASES = Object.freeze({
27
34
  verbatim: EXACT_STORAGE_NAMING
28
35
  })
29
36
 
37
+ /** @param {unknown} value */
30
38
  const toSnakeCase = (value) => String(value)
31
39
  .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
32
40
  .replace(/([A-Z]+)([A-Z][a-z0-9])/g, '$1_$2')
33
41
  .toLowerCase()
34
42
 
43
+ /** @param {unknown} [storage] @returns {StorageInfo['storage']} */
35
44
  export const normalizeStorageConfig = (storage = undefined) => {
36
45
  if (storage === undefined || storage === null) {
37
46
  return {
@@ -46,7 +55,9 @@ export const normalizeStorageConfig = (storage = undefined) => {
46
55
  const rawNaming = storage.naming
47
56
  const naming = rawNaming === undefined
48
57
  ? SNAKE_CASE_STORAGE_NAMING
49
- : STORAGE_NAMING_ALIASES[rawNaming]
58
+ : typeof rawNaming === 'string' && Object.hasOwn(STORAGE_NAMING_ALIASES, rawNaming)
59
+ ? STORAGE_NAMING_ALIASES[rawNaming]
60
+ : undefined
50
61
 
51
62
  if (!naming) {
52
63
  throw new Error(
@@ -60,6 +71,7 @@ export const normalizeStorageConfig = (storage = undefined) => {
60
71
  }
61
72
  }
62
73
 
74
+ /** @param {string} fieldName @param {{ idColumn?: string, storageConfig?: StorageConfig }} [options] */
63
75
  const resolveDefaultStorageColumn = (fieldName, { idColumn = 'id', storageConfig = undefined } = {}) => {
64
76
  if (fieldName === 'id') {
65
77
  return idColumn
@@ -73,6 +85,12 @@ const resolveDefaultStorageColumn = (fieldName, { idColumn = 'id', storageConfig
73
85
  return toSnakeCase(fieldName)
74
86
  }
75
87
 
88
+ /**
89
+ * @param {string} fieldName
90
+ * @param {StorageFieldDefinition} [definition]
91
+ * @param {{ idColumn?: string, storageConfig?: StorageConfig }} [options]
92
+ * @returns {Omit<FieldStorage, 'definition'>}
93
+ */
76
94
  export const normalizeFieldStorage = (fieldName, definition = {}, { idColumn = 'id', storageConfig = undefined } = {}) => {
77
95
  const fieldStorage = definition.storage
78
96
 
@@ -105,10 +123,14 @@ export const normalizeFieldStorage = (fieldName, definition = {}, { idColumn = '
105
123
  }
106
124
  }
107
125
 
126
+ /** @param {Partial<Pick<StorageSchemaInfo, 'schemaStructure' | 'idProperty' | 'storage'>>} [options] @returns {StorageInfo} */
108
127
  export const buildStorageInfo = ({ schemaStructure = {}, idProperty = 'id', storage = undefined } = {}) => {
128
+ assertFieldNameMap(schemaStructure, 'storage schema')
109
129
  const idColumn = idProperty || 'id'
110
130
  const storageConfig = normalizeStorageConfig(storage)
131
+ /** @type {StorageInfo['fields']} */
111
132
  const fields = {}
133
+ /** @type {StorageInfo['columns']} */
112
134
  const columns = {}
113
135
  const usedColumns = new Map([[idColumn, 'id']])
114
136
 
@@ -151,6 +173,7 @@ export const buildStorageInfo = ({ schemaStructure = {}, idProperty = 'id', stor
151
173
  }
152
174
  }
153
175
 
176
+ /** @param {Partial<StorageSchemaInfo>} [schemaInfo] @returns {StorageInfo} */
154
177
  export const getStorageInfo = (schemaInfo = {}) => {
155
178
  if (schemaInfo.storageInfo) {
156
179
  return schemaInfo.storageInfo
@@ -167,10 +190,12 @@ export const getStorageInfo = (schemaInfo = {}) => {
167
190
  })
168
191
  }
169
192
 
193
+ /** @param {Partial<StorageSchemaInfo>} [schemaInfo] @returns {string} */
170
194
  export const getIdColumn = (schemaInfo = {}) => {
171
195
  return getStorageInfo(schemaInfo).idColumn || schemaInfo.idProperty || 'id'
172
196
  }
173
197
 
198
+ /** @param {Partial<StorageSchemaInfo>} schemaInfo @param {string} fieldName @returns {string} */
174
199
  export const getStorageColumn = (schemaInfo = {}, fieldName) => {
175
200
  if (!fieldName) return fieldName
176
201
  if (fieldName === 'id') return getIdColumn(schemaInfo)
@@ -179,14 +204,7 @@ export const getStorageColumn = (schemaInfo = {}, fieldName) => {
179
204
  return fieldStorage?.column || fieldName
180
205
  }
181
206
 
182
- export const getLogicalFieldName = (schemaInfo = {}, columnName) => {
183
- if (!columnName) return columnName
184
- if (columnName === getIdColumn(schemaInfo)) return 'id'
185
-
186
- const logicalField = getStorageInfo(schemaInfo).columns[columnName]
187
- return logicalField || columnName
188
- }
189
-
207
+ /** @param {StorageRow | null | undefined} record @param {Partial<StorageSchemaInfo>} schemaInfo @param {string} fieldName @returns {unknown} */
190
208
  export const getFieldValue = (record, schemaInfo = {}, fieldName) => {
191
209
  if (!record || !fieldName) return undefined
192
210
 
@@ -212,18 +230,57 @@ export const getFieldValue = (record, schemaInfo = {}, fieldName) => {
212
230
  return record[columnName]
213
231
  }
214
232
 
215
- return translateRecordFromStorage(record, schemaInfo)[fieldName]
233
+ const translated = translateRecordFromStorage(record, schemaInfo)
234
+ return Object.hasOwn(translated, fieldName) ? translated[fieldName] : undefined
216
235
  }
217
236
 
237
+ /** @param {StorageInfo} storageInfo */
238
+ export const assertWritableKnexColumns = (storageInfo) => {
239
+ // Knex's update compiler discards these keys, even in null-prototype input objects.
240
+ const unsupported = new Set(['constructor', 'prototype', '__proto__'])
241
+ for (const [fieldName, field] of Object.entries(storageInfo.fields)) {
242
+ if (field.persisted && unsupported.has(field.column)) {
243
+ throw new Error(`Field '${fieldName}' maps to unsupported Knex storage column '${field.column}'. Set storage.column to a different column name; the public field name can stay the same.`)
244
+ }
245
+ }
246
+ if (unsupported.has(storageInfo.idColumn)) {
247
+ throw new Error(`Unsupported Knex id column '${storageInfo.idColumn}'. Set idProperty to a different column name.`)
248
+ }
249
+ }
250
+
251
+ /** @param {unknown} value @param {Partial<StorageSerializerDetails> & { textStorage?: boolean }} [options] @returns {unknown} */
252
+ export const serializeFieldValueForStorage = (value, {
253
+ fieldName, columnName, definition = {}, schemaInfo, context = null, operation = null, databaseClient, textStorage
254
+ } = {}) => {
255
+ if (definition.storage?.serialize) {
256
+ const serialized = definition.storage.serialize(value, { fieldName, columnName, definition, schemaInfo, context, operation })
257
+ // Probe the optional property directly: proxies can supply it without a `has` result.
258
+ if (serialized && typeof (/** @type {{ then?: unknown }} */ (serialized)).then === 'function') {
259
+ // Own the rejected promise even though asynchronous serializers are invalid.
260
+ Promise.resolve(serialized).catch(() => {})
261
+ throw new Error(`Field '${fieldName}' storage.serialize must be synchronous. Use a field setter for asynchronous preparation.`)
262
+ }
263
+ return serialized
264
+ }
265
+ return normalizeValueForDatabaseStorage(value, definition.type, {
266
+ databaseClient,
267
+ textStorage,
268
+ temporalPrecision: definition.temporalPrecision,
269
+ fieldName,
270
+ resourceType: context?.scopeName || schemaInfo?.tableName
271
+ })
272
+ }
273
+
274
+ /** @param {StorageRow | null | undefined} attributes @param {Partial<StorageSchemaInfo>} schemaInfo @param {StorageWriteOptions} [options] @returns {StorageRow} */
218
275
  export const translateAttributesForStorage = (attributes, schemaInfo = {}, options = {}) => {
219
276
  if (!attributes) return attributes || {}
220
277
 
221
- const { context = null, operation = null } = options
278
+ const { context = null, operation = null, databaseClient } = options
222
279
  const storageInfo = getStorageInfo(schemaInfo)
223
280
  const computed = schemaInfo.computed || {}
224
281
 
225
282
  return Object.entries(attributes).reduce((translated, [fieldName, value]) => {
226
- if (fieldName in computed) {
283
+ if (Object.hasOwn(computed, fieldName)) {
227
284
  return translated
228
285
  }
229
286
 
@@ -235,30 +292,23 @@ export const translateAttributesForStorage = (attributes, schemaInfo = {}, optio
235
292
  const columnName = fieldName === 'id'
236
293
  ? storageInfo.idColumn
237
294
  : fieldStorage?.column || fieldName
238
- const serializedValue = fieldStorage?.serialize
239
- ? fieldStorage.serialize(value, {
240
- fieldName,
241
- columnName,
242
- definition: fieldStorage.definition,
243
- schemaInfo,
244
- context,
245
- operation
246
- })
247
- : normalizeValueForDatabaseStorage(value, fieldStorage?.definition?.type, {
248
- temporalPrecision: fieldStorage?.definition?.temporalPrecision,
249
- fieldName,
250
- resourceType: context?.scopeName || schemaInfo.tableName
251
- })
295
+ const serializedValue = serializeFieldValueForStorage(value, {
296
+ fieldName, columnName, definition: fieldStorage?.definition, schemaInfo, context, operation, databaseClient
297
+ })
252
298
 
253
299
  translated[columnName] = serializedValue
254
300
  return translated
255
- }, {})
301
+ }, /** @type {StorageRow} */ ({}))
256
302
  }
257
303
 
304
+ /** @overload @param {StorageRow} record @param {Partial<StorageSchemaInfo>} [schemaInfo] @returns {StorageRow} */
305
+ /** @overload @param {StorageRow | null | undefined} record @param {Partial<StorageSchemaInfo>} [schemaInfo] @returns {StorageRow | null | undefined} */
306
+ /** @param {StorageRow | null | undefined} record @param {Partial<StorageSchemaInfo>} [schemaInfo] @returns {StorageRow | null | undefined} */
258
307
  export const translateRecordFromStorage = (record, schemaInfo = {}) => {
259
308
  if (!record || !schemaInfo) return record
260
309
 
261
310
  const storageInfo = getStorageInfo(schemaInfo)
311
+ /** @type {StorageRow} */
262
312
  const translated = {}
263
313
 
264
314
  for (const [columnName, value] of Object.entries(record)) {
@@ -267,7 +317,7 @@ export const translateRecordFromStorage = (record, schemaInfo = {}) => {
267
317
  continue
268
318
  }
269
319
 
270
- const logicalField = storageInfo.columns[columnName]
320
+ const logicalField = Object.hasOwn(storageInfo.columns, columnName) ? storageInfo.columns[columnName] : undefined
271
321
  if (logicalField) {
272
322
  translated[logicalField] = value
273
323
  continue