json-rest-api 1.0.29 → 2.0.0

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