json-rest-api 1.0.28 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. package/LICENSE +10 -670
  2. package/LICENSE-GPL-3.0 +674 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +39 -30
  5. package/docs/API.md +746 -1273
  6. package/docs/GUIDE/01-getting-started.md +338 -0
  7. package/docs/GUIDE/02-resources-and-relationships.md +33 -0
  8. package/docs/GUIDE/03-running-example.md +87 -0
  9. package/docs/GUIDE/04-creating-and-querying.md +181 -0
  10. package/docs/GUIDE/05-belongs-to.md +177 -0
  11. package/docs/GUIDE/06-has-many.md +184 -0
  12. package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
  13. package/docs/GUIDE/08-many-to-many.md +184 -0
  14. package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
  15. package/docs/GUIDE/10-put-and-patch.md +174 -0
  16. package/docs/GUIDE/11-relationship-endpoints.md +174 -0
  17. package/docs/GUIDE/12-field-transformations.md +287 -0
  18. package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
  19. package/docs/GUIDE/14-custom-resource-methods.md +125 -0
  20. package/docs/GUIDE/15-query-projections.md +266 -0
  21. package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
  22. package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
  23. package/docs/GUIDE/18-labels.md +86 -0
  24. package/docs/GUIDE/19-managed-transactions.md +179 -0
  25. package/docs/GUIDE/20-transaction-outcomes.md +207 -0
  26. package/docs/GUIDE/21-schema-and-migrations.md +634 -0
  27. package/docs/GUIDE/22-version-field-migrations.md +208 -0
  28. package/docs/GUIDE/23-fastify.md +179 -0
  29. package/docs/GUIDE/24-cors.md +172 -0
  30. package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
  31. package/docs/GUIDE/26-file-uploads.md +278 -0
  32. package/docs/GUIDE/27-bulk-operations.md +277 -0
  33. package/docs/GUIDE/28-socketio.md +325 -0
  34. package/docs/GUIDE/29-writing-plugins.md +278 -0
  35. package/docs/GUIDE/30-backend-capabilities.md +100 -0
  36. package/docs/GUIDE/31-positioning.md +181 -0
  37. package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +90 -31
  38. package/docs/GUIDE/33-migrating-to-v2.md +3827 -0
  39. package/docs/GUIDE/index.md +51 -59
  40. package/docs/QUICKSTART.md +120 -287
  41. package/docs/architecture.md +75 -0
  42. package/docs/contributing.md +178 -0
  43. package/index.d.ts +36 -0
  44. package/index.js +1 -22
  45. package/lib/enhanced-logger.js +91 -85
  46. package/lib/error-context.js +445 -5
  47. package/lib/error-formatter.js +219 -59
  48. package/lib/knex-transaction.js +64 -0
  49. package/lib/logger-types.d.ts +22 -0
  50. package/lib/missing-package.js +4 -0
  51. package/lib/rest-api-errors.js +103 -8
  52. package/lib/runtime/json-rest-api.js +172 -0
  53. package/lib/transaction-types.d.ts +59 -0
  54. package/package.json +97 -27
  55. package/plugins/core/bulk-operations-plugin.js +191 -184
  56. package/plugins/core/connectors/express-plugin.js +148 -219
  57. package/plugins/core/connectors/fastify-plugin.js +125 -82
  58. package/plugins/core/connectors/lib/busboy-detector.js +74 -156
  59. package/plugins/core/connectors/lib/connector-core.js +144 -20
  60. package/plugins/core/connectors/lib/formidable-detector.js +63 -120
  61. package/plugins/core/connectors/lib/http-validators.js +77 -0
  62. package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
  63. package/plugins/core/connectors/lib/request-helpers.js +10 -1
  64. package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
  65. package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
  66. package/plugins/core/file-handling-plugin.js +87 -174
  67. package/plugins/core/lib/anyapi/anyapi-registry.js +213 -164
  68. package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
  69. package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
  70. package/plugins/core/lib/anyapi/schema-utils.js +25 -16
  71. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
  72. package/plugins/core/lib/dbIntrospection.js +228 -21
  73. package/plugins/core/lib/dbTablesOperations.js +409 -92
  74. package/plugins/core/lib/fractional-positioning.js +12 -48
  75. package/plugins/core/lib/querying/identifier-query.js +19 -0
  76. package/plugins/core/lib/querying/include-query-helpers.js +364 -0
  77. package/plugins/core/lib/querying/include-to-many.js +428 -0
  78. package/plugins/core/lib/querying/include-to-one.js +579 -0
  79. package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
  80. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
  81. package/plugins/core/lib/querying/knex-pagination-helpers.js +67 -330
  82. package/plugins/core/lib/querying/knex-process-includes.js +5 -110
  83. package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
  84. package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
  85. package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
  86. package/plugins/core/lib/querying/knex-window-queries.js +62 -253
  87. package/plugins/core/lib/querying/query-builder-utils.js +44 -4
  88. package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
  89. package/plugins/core/lib/querying/query-constraint.js +27 -0
  90. package/plugins/core/lib/querying/query-field-sort-helpers.js +205 -30
  91. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  92. package/plugins/core/lib/querying/sort-helpers.js +20 -1
  93. package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
  94. package/plugins/core/lib/querying/url-helpers.js +11 -4
  95. package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
  96. package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
  97. package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
  98. package/plugins/core/lib/querying-writing/database-value-normalizers.js +238 -187
  99. package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
  100. package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
  101. package/plugins/core/lib/querying-writing/field-utils.js +113 -92
  102. package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
  103. package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
  104. package/plugins/core/lib/querying-writing/knex-field-helpers.js +111 -287
  105. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
  106. package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
  107. package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
  108. package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
  109. package/plugins/core/lib/querying-writing/request-contracts.js +109 -44
  110. package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
  111. package/plugins/core/lib/querying-writing/response-options.js +35 -0
  112. package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
  113. package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
  114. package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
  115. package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
  116. package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
  117. package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
  118. package/plugins/core/lib/storage/storage-adapter.js +142 -62
  119. package/plugins/core/lib/storage/storage-mapping.js +78 -28
  120. package/plugins/core/lib/storage/storage-types.d.ts +502 -0
  121. package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
  122. package/plugins/core/lib/writing/relationship-processor.js +94 -158
  123. package/plugins/core/lib/writing/resource-version.js +172 -0
  124. package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
  125. package/plugins/core/rest-api-anyapi-knex-plugin.js +116 -2509
  126. package/plugins/core/rest-api-autofilter-plugin.js +17 -19
  127. package/plugins/core/rest-api-cors-plugin.js +52 -205
  128. package/plugins/core/rest-api-knex-plugin.js +45 -1069
  129. package/plugins/core/rest-api-label-plugin.js +25 -38
  130. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
  131. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
  132. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
  133. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
  134. package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
  135. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
  136. package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
  137. package/plugins/core/rest-api-plugin-methods/common.js +291 -385
  138. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
  139. package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
  140. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -145
  141. package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
  142. package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
  143. package/plugins/core/rest-api-plugin-methods/get.js +38 -69
  144. package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
  145. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
  146. package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
  147. package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
  148. package/plugins/core/rest-api-plugin-methods/post.js +51 -51
  149. package/plugins/core/rest-api-plugin-methods/put.js +69 -148
  150. package/plugins/core/rest-api-plugin-methods/query.js +39 -82
  151. package/plugins/core/rest-api-plugin-methods/release.js +2 -1
  152. package/plugins/core/rest-api-plugin.js +32 -46
  153. package/plugins/core/rest-api-positioning-plugin.js +183 -628
  154. package/plugins/core/rest-api-query-projections-plugin.js +19 -102
  155. package/plugins/core/rest-api-row-policy-plugin.js +3 -3
  156. package/plugins/core/socketio-plugin.js +364 -344
  157. package/plugins/storage/local-storage.js +40 -36
  158. package/types/autofilter.d.ts +57 -0
  159. package/types/bulk-methods.d.ts +63 -0
  160. package/types/errors.d.ts +118 -0
  161. package/types/fastify-options.d.ts +19 -0
  162. package/types/file-detectors.d.ts +33 -0
  163. package/types/file-storage.d.ts +48 -0
  164. package/types/hook-context.d.ts +180 -0
  165. package/types/plugin-options.d.ts +53 -0
  166. package/types/relationship-methods.d.ts +74 -0
  167. package/types/representations.d.ts +50 -0
  168. package/types/resource-methods.d.ts +95 -0
  169. package/types/resource-schema.d.ts +157 -0
  170. package/types/row-policy.d.ts +31 -0
  171. package/types/runtime.d.ts +132 -0
  172. package/types/transactions.d.ts +21 -0
  173. package/.claude/agents/library-guide-expert.md +0 -30
  174. package/.claude/agents/rest-api-internals-expert.md +0 -54
  175. package/.claude/settings.local.json +0 -70
  176. package/AGENTS.md +0 -34
  177. package/CLAUDE.md +0 -160
  178. package/CRUSH.md +0 -55
  179. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
  180. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
  181. package/TODO.todo +0 -13
  182. package/connectors_ideas/fastify.js +0 -217
  183. package/connectors_ideas/koa-plugin.js +0 -112
  184. package/docs/.bundle/config +0 -2
  185. package/docs/COMPARISON.md +0 -433
  186. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
  187. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
  188. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
  189. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
  190. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
  191. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
  192. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
  193. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1238
  194. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
  195. package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
  196. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
  197. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
  198. package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
  199. package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
  200. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
  201. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
  202. package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
  203. package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
  204. package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
  205. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
  206. package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
  207. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
  208. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
  209. package/docs/Gemfile +0 -6
  210. package/docs/Gemfile.lock +0 -308
  211. package/docs/ONBOARDING.md +0 -315
  212. package/docs/_config.yml +0 -36
  213. package/docs/_layouts/default.html +0 -123
  214. package/docs/apple-touch-icon.svg +0 -4
  215. package/docs/assets/css/style.scss +0 -392
  216. package/docs/favicon.svg +0 -4
  217. package/docs/index.md +0 -164
  218. package/docs/mini-guides/file-uploads-complete.md +0 -161
  219. package/eslint.config.js +0 -13
  220. package/examples/url-override-example.js +0 -137
  221. package/fixing_plan.md +0 -112
  222. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
  223. package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
  224. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
  225. package/plugins/storage/s3-storage.js +0 -93
  226. package/quickTest.js +0 -136
  227. package/scripts/debug-anyapi-review-filter.mjs +0 -63
  228. package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
  229. package/scripts/inspect-anyapi-descriptor.mjs +0 -19
  230. package/scripts/run-docs.js +0 -133
  231. package/test-debug.js +0 -59
  232. package/tests/README.md +0 -202
  233. package/tests/TEST_TEMPLATE.test.js +0 -178
  234. package/tests/anyapi-basic.test.js +0 -105
  235. package/tests/anyapi-cursor-pagination.test.js +0 -150
  236. package/tests/anyapi-custom-idproperty.test.js +0 -163
  237. package/tests/autofilter.test.js +0 -463
  238. package/tests/bulk-operations.test.js +0 -657
  239. package/tests/computed-fields.test.js +0 -586
  240. package/tests/cors.test.js +0 -834
  241. package/tests/curl.test.js +0 -467
  242. package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
  243. package/tests/custom-id-simplified-post.test.js +0 -75
  244. package/tests/custom-idproperty-relationships.test.js +0 -932
  245. package/tests/db-introspection.test.js +0 -521
  246. package/tests/db-table-operations.test.js +0 -798
  247. package/tests/fastify-plugin.test.js +0 -184
  248. package/tests/field-getters.test.js +0 -370
  249. package/tests/field-setters.test.js +0 -306
  250. package/tests/file-handling.test.js +0 -118
  251. package/tests/fixtures/api-configs.js +0 -2509
  252. package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
  253. package/tests/helpers/fake-fastify.js +0 -99
  254. package/tests/helpers/storage-mode.js +0 -65
  255. package/tests/helpers/test-utils.js +0 -203
  256. package/tests/http-connectors-parity.test.js +0 -395
  257. package/tests/id-normalization.test.js +0 -444
  258. package/tests/include-depth-validation.test.js +0 -331
  259. package/tests/includes.test.js +0 -427
  260. package/tests/local-storage.test.js +0 -96
  261. package/tests/logical-id-write-validation.test.js +0 -151
  262. package/tests/nested-includes.test.js +0 -617
  263. package/tests/pagination-cursor-multifield.test.js +0 -643
  264. package/tests/pagination-enhanced.test.js +0 -563
  265. package/tests/polymorphic-relationships.test.js +0 -347
  266. package/tests/polymorphic-simple.test.js +0 -148
  267. package/tests/positioning-advanced.test.js +0 -692
  268. package/tests/positioning-extended.test.js +0 -915
  269. package/tests/positioning-multifilter.test.js +0 -699
  270. package/tests/positioning.test.js +0 -560
  271. package/tests/post-commit-hooks.test.js +0 -120
  272. package/tests/projected-fields.test.js +0 -319
  273. package/tests/public-exports.test.js +0 -15
  274. package/tests/put-create-parity.test.js +0 -216
  275. package/tests/queries.test.js +0 -521
  276. package/tests/query-limits.test.js +0 -584
  277. package/tests/relationship-endpoints.test.js +0 -620
  278. package/tests/relationship-include-storage-adapter.test.js +0 -56
  279. package/tests/relationships.test.js +0 -681
  280. package/tests/rest-api.test.js +0 -337
  281. package/tests/return-record-settings.test.js +0 -184
  282. package/tests/row-policy.test.js +0 -489
  283. package/tests/s3-storage.test.js +0 -38
  284. package/tests/searchschema-knex-behavior.test.js +0 -187
  285. package/tests/searchschema-merge.test.js +0 -151
  286. package/tests/socketio.test.js +0 -860
  287. package/tests/storage-mapping.test.js +0 -362
  288. package/tests/test-development-guide.md +0 -370
  289. package/tests/virtual-fields.test.js +0 -404
@@ -1,20 +1,29 @@
1
+ // @ts-check
1
2
  import {
2
- getFieldValue as getLegacyFieldValue,
3
+ getFieldValue as getTableFieldValue,
3
4
  getIdColumn,
5
+ getStorageInfo,
4
6
  getStorageColumn,
5
7
  translateAttributesForStorage,
8
+ serializeFieldValueForStorage,
9
+ assertWritableKnexColumns,
6
10
  } from './storage-mapping.js'
7
11
  import {
8
12
  getCanonicalFieldValue,
9
13
  getCanonicalResourceIdColumn,
10
14
  translateCanonicalAttributesForStorage,
11
15
  } from './canonical-storage-mapping.js'
16
+ import { assertScalarQueryField } from '../querying-writing/field-utils.js'
12
17
  import { normalizeValueForDatabaseStorage } from '../querying-writing/database-value-normalizers.js'
13
18
 
19
+ /** @import { BaseStorageAdapterOptions, CanonicalDescriptor, SelectTranslator, StorageAdapter, StorageAdapterOptions, StorageAdapterLookupOptions, StorageSchemaInfo, StorageFieldDefinition } from './storage-types.js' */
20
+
21
+ /** @template T @param {T} value @returns {T} */
14
22
  const passthrough = (value) => value
23
+ /** @type {StorageAdapter['translateFilterValue']} */
15
24
  const identityTranslate = (_field, value) => value
16
- const identityScope = (query) => query
17
25
 
26
+ /** @type {StorageAdapter['selectColumns']} */
18
27
  const selectColumnsOnBuilder = (builder, columns) => {
19
28
  if (!columns) return builder
20
29
  if (Array.isArray(columns)) {
@@ -25,16 +34,16 @@ const selectColumnsOnBuilder = (builder, columns) => {
25
34
  return builder
26
35
  }
27
36
 
28
- const normalizeArray = (value, normalizeFn) => value.map((entry) => normalizeFn(entry))
29
-
37
+ /** @param {unknown} value @returns {unknown} */
30
38
  const normalizeBelongsToValue = (value) => {
31
39
  if (value === null || value === undefined) return value
32
40
  if (Array.isArray(value)) {
33
- return normalizeArray(value, normalizeBelongsToValue)
41
+ return value.map(entry => normalizeBelongsToValue(entry))
34
42
  }
35
43
  return String(value)
36
44
  }
37
45
 
46
+ /** @param {unknown} value @returns {boolean} */
38
47
  const normalizeBooleanValue = (value) => {
39
48
  if (typeof value === 'boolean') return value
40
49
  if (typeof value === 'string') {
@@ -49,13 +58,16 @@ const normalizeBooleanValue = (value) => {
49
58
  return Boolean(value)
50
59
  }
51
60
 
52
- 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 } = {}) => {
53
68
  if (value === null || value === undefined) return value
54
69
  if (Array.isArray(value)) {
55
- return normalizeArray(
56
- value,
57
- (entry) => normalizeFilterValueForDefinition(entry, definition, { isRelationship })
58
- )
70
+ return value.map(entry => normalizeFilterValueForDefinition(entry, definition, { isRelationship, databaseClient, textStorage }))
59
71
  }
60
72
 
61
73
  if (isRelationship) {
@@ -78,6 +90,8 @@ const normalizeFilterValueForDefinition = (value, definition = {}, { isRelations
78
90
 
79
91
  if (['date', 'dateTime', 'time'].includes(type)) {
80
92
  return normalizeValueForDatabaseStorage(value, type, {
93
+ databaseClient,
94
+ textStorage,
81
95
  temporalPrecision: definition?.temporalPrecision
82
96
  })
83
97
  }
@@ -85,11 +99,9 @@ const normalizeFilterValueForDefinition = (value, definition = {}, { isRelations
85
99
  return value
86
100
  }
87
101
 
88
- const translateSourceColumn = (source, adapter) => {
89
- if (!adapter || source === '*') return source
90
- return adapter.translateColumn(source) || source
91
- }
92
-
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} */
93
105
  export const createSelectTranslator = (adapter) => {
94
106
  if (!adapter) return null
95
107
 
@@ -118,40 +130,23 @@ export const createSelectTranslator = (adapter) => {
118
130
  }
119
131
  }
120
132
 
121
- export const translateSelectFieldsForAdapter = (fields, adapter) => {
122
- if (!adapter || !fields) return fields
123
-
124
- const translateField = createSelectTranslator(adapter)
125
-
126
- return fields.map((field) => {
127
- if (typeof field !== 'string') return field
128
- if (field === '*') return '*'
129
-
130
- const aliasMatch = field.match(/\s+as\s+/i)
131
- if (aliasMatch) {
132
- const [source, alias] = field.split(/\s+as\s+/i)
133
- const translatedSource = translateSourceColumn(source.trim(), adapter)
134
- return `${translatedSource} as ${alias.trim()}`
135
- }
136
-
137
- return translateField(field)
138
- })
139
- }
140
-
133
+ /** @param {BaseStorageAdapterOptions} options @returns {StorageAdapter} */
141
134
  const baseAdapter = ({
142
135
  knex,
143
136
  tableName,
144
137
  idColumn,
145
138
  translateColumn,
146
139
  translateFilterValue = identityTranslate,
147
- applyResourceScope = identityScope,
140
+ translateCursorValue = identityTranslate,
141
+ applyResourceScope = passthrough,
148
142
  toStorageRow = passthrough,
149
143
  getFieldValue = (record, fieldName) => record?.[fieldName],
150
144
  isCanonical = false,
151
145
  }) => {
152
- const buildBaseQuery = ({ transaction } = {}) => {
153
- const query = (transaction || knex)(tableName)
154
- 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)
155
150
  }
156
151
 
157
152
  return {
@@ -160,31 +155,61 @@ const baseAdapter = ({
160
155
  getIdColumn: () => idColumn,
161
156
  translateColumn,
162
157
  translateFilterValue,
158
+ translateCursorValue,
163
159
  applyResourceScope,
164
160
  toStorageRow,
165
161
  getFieldValue,
166
162
  buildBaseQuery,
167
- selectColumns: (builder, columns) => selectColumnsOnBuilder(builder, columns),
163
+ selectColumns: selectColumnsOnBuilder,
168
164
  }
169
165
  }
170
166
 
171
- const createLegacyAdapter = ({ knex, schemaInfo }) => {
167
+ /** @param {StorageAdapterOptions} options @returns {StorageAdapter} */
168
+ const createTableAdapter = ({ knex, schemaInfo }) => {
169
+ assertWritableKnexColumns(getStorageInfo(schemaInfo))
172
170
  const tableName = schemaInfo.tableName
173
171
  const idColumn = getIdColumn(schemaInfo)
174
172
 
173
+ /** @type {StorageAdapter['translateColumn']} */
175
174
  const translateColumn = (field) => getStorageColumn(schemaInfo, field)
176
- const toStorageRow = (attributes, options = {}) => translateAttributesForStorage(attributes, schemaInfo, options)
177
- 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']} */
178
188
  const translateFilterValue = (field, value) => {
179
189
  const searchField = schemaInfo.searchSchemaStructure?.[field]
180
- const schemaField = schemaInfo.schemaStructure?.[field]
190
+ const fieldName = searchField?.actualField || field
191
+ const schemaField = schemaInfo.schemaStructure?.[fieldName]
192
+ assertScalarQueryField(schemaField, fieldName, 'filter')
193
+ const fieldStorage = getStorageInfo(schemaInfo).fields[fieldName]
194
+ if (fieldStorage?.serialize) {
195
+ /** @param {unknown} entry */
196
+ const serialize = (entry) => serializeFieldValueForStorage(entry, {
197
+ fieldName,
198
+ columnName: fieldStorage.column,
199
+ definition: fieldStorage.definition,
200
+ schemaInfo,
201
+ context: null,
202
+ operation: 'filter'
203
+ })
204
+ return Array.isArray(value) ? value.map(serialize) : serialize(value)
205
+ }
181
206
  const definition = searchField || schemaField || {}
182
207
  const isRelationship = Boolean(
183
208
  searchField?.isRelationship ||
184
209
  schemaField?.belongsTo ||
185
210
  schemaField?.belongsToPolymorphic
186
211
  )
187
- return normalizeFilterValueForDefinition(value, definition, { isRelationship })
212
+ return normalizeFilterValueForDefinition(value, definition, { isRelationship, databaseClient: knex.client.config.client })
188
213
  }
189
214
 
190
215
  return baseAdapter({
@@ -193,25 +218,28 @@ const createLegacyAdapter = ({ knex, schemaInfo }) => {
193
218
  idColumn,
194
219
  translateColumn,
195
220
  translateFilterValue,
196
- applyResourceScope: identityScope,
221
+ translateCursorValue,
222
+ applyResourceScope: passthrough,
197
223
  toStorageRow,
198
224
  getFieldValue,
199
225
  isCanonical: false,
200
226
  })
201
227
  }
202
228
 
203
- const createCanonicalAdapter = ({ knex, schemaInfo }) => {
204
- const descriptor = schemaInfo.descriptor || {}
205
- const canonical = descriptor.canonical || {}
229
+ /** @param {StorageAdapterOptions & { descriptor: CanonicalDescriptor }} options @returns {StorageAdapter} */
230
+ const createCanonicalAdapter = ({ knex, schemaInfo, descriptor }) => {
231
+ const canonical = descriptor.canonical
206
232
  const canonicalFieldMap = descriptor.canonicalFieldMap || {}
207
233
  const fieldsInfo = descriptor.fields || {}
208
234
  const belongsToInfo = descriptor.belongsTo || {}
209
235
  const idProperty = schemaInfo.idProperty || descriptor.idProperty || 'id'
236
+ /** @param {string} field */
210
237
  const isLogicalIdField = (field) => field === 'id' || field === idProperty
211
238
  const descriptorWithIdProperty = descriptor.idProperty === idProperty
212
239
  ? descriptor
213
240
  : { ...descriptor, idProperty }
214
241
 
242
+ /** @type {StorageAdapter['translateColumn']} */
215
243
  const translateColumn = (field) => {
216
244
  if (!field) return field
217
245
  if (isLogicalIdField(field)) return getCanonicalResourceIdColumn(descriptor)
@@ -237,7 +265,7 @@ const createCanonicalAdapter = ({ knex, schemaInfo }) => {
237
265
  if (!canonicalEntry && field.endsWith('_type')) {
238
266
  const alias = field.slice(0, -5)
239
267
  const aliasEntry = canonicalFieldMap[alias]
240
- if (aliasEntry?.typeSlot) return aliasEntry.typeSlot
268
+ if (typeof aliasEntry === 'object' && aliasEntry?.typeSlot) return aliasEntry.typeSlot
241
269
  }
242
270
 
243
271
  const fieldInfo = fieldsInfo[field]
@@ -249,9 +277,27 @@ const createCanonicalAdapter = ({ knex, schemaInfo }) => {
249
277
  return field
250
278
  }
251
279
 
280
+ /** @type {StorageAdapter['translateFilterValue']} */
252
281
  const translateFilterValue = (field, value) => {
253
282
  const searchField = schemaInfo.searchSchemaStructure?.[field]
254
- 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)
255
301
  const isRelationship = Boolean(
256
302
  searchField?.isRelationship ||
257
303
  schemaField?.belongsTo ||
@@ -259,20 +305,33 @@ const createCanonicalAdapter = ({ knex, schemaInfo }) => {
259
305
  )
260
306
  const definition = searchField || schemaField || {}
261
307
 
262
- return normalizeFilterValueForDefinition(value, definition, { isRelationship })
308
+ return normalizeFilterValueForDefinition(value, definition, { isRelationship, databaseClient: knex.client.config.client, textStorage: fieldsInfo[fieldName]?.slotType === 'string' })
263
309
  }
264
310
 
265
- 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}.` : ''
266
325
  return query
267
- .where(canonical.tenantColumn, descriptor.tenant)
268
- .where(canonical.resourceColumn, descriptor.resource)
326
+ .where(`${prefix}${canonical.tenantColumn}`, descriptor.tenant)
327
+ .where(`${prefix}${canonical.resourceColumn}`, descriptor.resource)
269
328
  }
270
329
 
271
- 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']} */
272
333
  const getFieldValue = (record, fieldName) => (
273
- isLogicalIdField(fieldName)
274
- ? getCanonicalFieldValue(record, descriptorWithIdProperty, fieldName)
275
- : getCanonicalFieldValue(record, descriptor, fieldName)
334
+ getCanonicalFieldValue(record, isLogicalIdField(fieldName) ? descriptorWithIdProperty : descriptor, fieldName)
276
335
  )
277
336
 
278
337
  return baseAdapter({
@@ -281,6 +340,7 @@ const createCanonicalAdapter = ({ knex, schemaInfo }) => {
281
340
  idColumn: getCanonicalResourceIdColumn(descriptor),
282
341
  translateColumn,
283
342
  translateFilterValue,
343
+ translateCursorValue,
284
344
  applyResourceScope,
285
345
  toStorageRow,
286
346
  getFieldValue,
@@ -288,14 +348,34 @@ const createCanonicalAdapter = ({ knex, schemaInfo }) => {
288
348
  })
289
349
  }
290
350
 
351
+ /** @param {StorageAdapterOptions} options @returns {StorageAdapter} */
291
352
  export const createStorageAdapter = ({ knex, schemaInfo }) => {
292
353
  if (!schemaInfo) {
293
354
  throw new Error('createStorageAdapter requires schemaInfo')
294
355
  }
295
356
 
296
- if (schemaInfo?.descriptor?.canonical?.tableName) {
297
- return createCanonicalAdapter({ knex, schemaInfo })
357
+ const descriptor = schemaInfo.descriptor
358
+ if (descriptor?.canonical?.tableName) {
359
+ return createCanonicalAdapter({ knex, schemaInfo, descriptor })
298
360
  }
299
361
 
300
- 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
+ }
301
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