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,11 +1,40 @@
1
1
  import { createSchema } from 'json-rest-schema'
2
2
  import { RestApiTemporalDataError, RestApiValidationError } from '../../../../lib/rest-api-errors.js'
3
+ import { filterResponseFields } from './field-utils.js'
4
+ import { wrapUnexpectedError } from '../../../../lib/error-context.js'
5
+ import { COMPUTED_DEPENDENCIES_KEY } from './knex-constants.js'
6
+ import { getTemporalStorageCapabilities, SERIALIZATION_CAPABILITIES } from './database-capabilities.js'
3
7
 
4
8
  const DATE_VALUE_PATTERN = /^\d{4}-\d{2}-\d{2}$/
5
9
  const RFC3339_DATETIME_PATTERN = /^(\d{4}-\d{2}-\d{2})T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/
6
- const SQL_DATETIME_PATTERN = /^(\d{4}-\d{2}-\d{2}) ((?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?)$/
10
+ const SQL_DATETIME_PATTERN = /^(\d{4}-\d{2}-\d{2}) ((?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?)(?:([+-])([01]\d|2[0-3])(?::([0-5]\d))?(?::([0-5]\d))?)?( BC)?$/
7
11
  const INTEGER_VALUE_PATTERN = /^-?\d+$/
8
12
  const temporalContractCache = new Map()
13
+ const CACHED_TEMPORAL_TYPES = new Set(['date', 'dateTime', 'time', 'epochMilliseconds', 'epochSeconds'])
14
+
15
+ export function applyDatabaseReadOptions (query) {
16
+ const client = query.client.config.client
17
+ if (client === 'pg' || client === 'postgresql') {
18
+ const types = query.client.config.connection?.types || query.client.driver.types
19
+ query.options({
20
+ types: {
21
+ getTypeParser: (oid, format) => [1082, 1114, 1184].includes(oid) && format !== 'binary'
22
+ ? value => value
23
+ : types.getTypeParser(oid, format)
24
+ }
25
+ })
26
+ } else if (client === 'mysql' || client === 'mysql2') {
27
+ query.options({ dateStrings: true, supportBigNumbers: true })
28
+ }
29
+ return query
30
+ }
31
+
32
+ // SQLite's driver decodes INTEGER as Number. Preserve only values it cannot represent.
33
+ // Keep this in SELECT/RETURNING expressions; predicates and ordering use native columns.
34
+ export function databaseIdentityExpression (db, column) {
35
+ if (!['better-sqlite3', 'sqlite3'].includes(db.client.config.client)) return column
36
+ return db.raw("CASE WHEN typeof(??) = 'integer' AND (?? > 9007199254740991 OR ?? < -9007199254740991) THEN CAST(?? AS TEXT) ELSE ?? END", [column, column, column, column, column])
37
+ }
9
38
 
10
39
  const limitFractionalSecondPrecision = (value, temporalPrecision) => {
11
40
  if (!Number.isInteger(temporalPrecision)) return value
@@ -21,15 +50,18 @@ const limitFractionalSecondPrecision = (value, temporalPrecision) => {
21
50
 
22
51
  const getTemporalContract = (type, temporalPrecision) => {
23
52
  const cacheKey = `${type}:${Number.isInteger(temporalPrecision) ? temporalPrecision : 'default'}`
24
- if (!temporalContractCache.has(cacheKey)) {
25
- temporalContractCache.set(cacheKey, createSchema({
26
- value: {
27
- type,
28
- ...(Number.isInteger(temporalPrecision) ? { temporalPrecision } : {})
29
- }
30
- }))
31
- }
32
- return temporalContractCache.get(cacheKey)
53
+ // Cache common SQL precision; unusual declarations retain validation without permanent entries.
54
+ const cacheable = CACHED_TEMPORAL_TYPES.has(type) &&
55
+ (!Number.isInteger(temporalPrecision) || (temporalPrecision >= 0 && temporalPrecision <= 6))
56
+ if (cacheable && temporalContractCache.has(cacheKey)) return temporalContractCache.get(cacheKey)
57
+ const contract = createSchema({
58
+ value: {
59
+ type,
60
+ ...(Number.isInteger(temporalPrecision) ? { temporalPrecision } : {})
61
+ }
62
+ })
63
+ if (cacheable) temporalContractCache.set(cacheKey, contract)
64
+ return contract
33
65
  }
34
66
 
35
67
  const throwTemporalDataError = ({ fieldName, resourceType, type, source }) => {
@@ -86,9 +118,17 @@ const parseKnownDatabaseDate = (value, { fieldName, resourceType, type, source }
86
118
  const sqlMatch = SQL_DATETIME_PATTERN.exec(trimmed)
87
119
  const rfc3339Match = RFC3339_DATETIME_PATTERN.exec(trimmed)
88
120
  let dateTimeValue = null
121
+ let offsetSeconds = 0
89
122
 
90
123
  if (sqlMatch) {
91
- dateTimeValue = `${sqlMatch[1]}T${sqlMatch[2]}Z`
124
+ const date = sqlMatch[7] && sqlMatch[1].startsWith('0001-')
125
+ ? sqlMatch[1].replace(/^0001/, '0000')
126
+ : sqlMatch[1]
127
+ if (sqlMatch[7] && !sqlMatch[1].startsWith('0001-')) {
128
+ throwTemporalDataError({ fieldName, resourceType, type, source })
129
+ }
130
+ dateTimeValue = `${date}T${sqlMatch[2]}Z`
131
+ offsetSeconds = (Number(sqlMatch[4] || 0) * 3600 + Number(sqlMatch[5] || 0) * 60 + Number(sqlMatch[6] || 0)) * (sqlMatch[3] === '-' ? -1 : 1)
92
132
  } else if (rfc3339Match) {
93
133
  dateTimeValue = trimmed
94
134
  }
@@ -102,14 +142,14 @@ const parseKnownDatabaseDate = (value, { fieldName, resourceType, type, source }
102
142
  resourceType,
103
143
  source
104
144
  })
105
- const parsed = new Date(dateTimeValue)
145
+ const parsed = new Date(new Date(dateTimeValue).getTime() - offsetSeconds * 1000)
106
146
  if (Number.isNaN(parsed.getTime())) {
107
147
  throwTemporalDataError({ fieldName, resourceType, type, source })
108
148
  }
109
149
  // Date handles the offset, but cannot retain digits beyond milliseconds.
110
150
  const fraction = /\.(\d+)(?:Z|[+-]\d{2}:\d{2})$/.exec(dateTimeValue)?.[1]
111
151
  return fraction
112
- ? parsed.toISOString().replace(/\.\d{3}Z$/, `.${fraction}Z`)
152
+ ? parsed.toISOString().replace(/\.\d{3}Z$/, `.${fraction.padEnd(3, '0')}Z`)
113
153
  : parsed.toISOString()
114
154
  }
115
155
 
@@ -134,16 +174,21 @@ export function normalizeDateValue (value, type, {
134
174
  if (value instanceof Date && !Number.isNaN(value.getTime())) {
135
175
  timeValue = limitFractionalSecondPrecision(value.toISOString().slice(11, -1), temporalPrecision)
136
176
  }
137
- return validateTemporalJsonValue(timeValue, type, {
177
+ const validated = validateTemporalJsonValue(timeValue, type, {
138
178
  temporalPrecision,
139
179
  fieldName,
140
180
  resourceType,
141
181
  source
142
182
  })
183
+ const [whole, fraction = ''] = validated.split('.')
184
+ const seconds = whole.length === 5 ? `${whole}:00` : whole
185
+ const digits = Number.isInteger(temporalPrecision) ? fraction.padEnd(temporalPrecision, '0') : fraction.replace(/0+$/, '')
186
+ return `${seconds}${digits ? `.${digits}` : ''}`
143
187
  }
144
188
 
145
- if (type === 'date' && typeof value === 'string' && DATE_VALUE_PATTERN.test(value.trim())) {
146
- return validateTemporalJsonValue(value.trim(), type, {
189
+ const dateValue = typeof value === 'string' ? value.trim().replace(/^0001-(\d{2}-\d{2}) BC$/, '0000-$1') : value
190
+ if (type === 'date' && typeof dateValue === 'string' && DATE_VALUE_PATTERN.test(dateValue)) {
191
+ return validateTemporalJsonValue(dateValue, type, {
147
192
  fieldName,
148
193
  resourceType,
149
194
  source
@@ -153,7 +198,7 @@ export function normalizeDateValue (value, type, {
153
198
  const isoValue = parseKnownDatabaseDate(value, { fieldName, resourceType, type, source })
154
199
  const normalized = type === 'date'
155
200
  ? isoValue.slice(0, 10)
156
- : limitFractionalSecondPrecision(isoValue, temporalPrecision)
201
+ : limitFractionalSecondPrecision(isoValue.replace(/(\.\d{3}\d*?)0+Z$/, '$1Z'), temporalPrecision)
157
202
  return validateTemporalJsonValue(normalized, type, {
158
203
  temporalPrecision,
159
204
  fieldName,
@@ -163,30 +208,57 @@ export function normalizeDateValue (value, type, {
163
208
  }
164
209
 
165
210
  /**
166
- * Converts validated JSON temporal values to the native values expected by
211
+ * Converts structured JSON and temporal values to the values expected by
167
212
  * database drivers. JSON schema validation keeps date and dateTime values as
168
213
  * strings; database writes and comparisons must normalize them consistently.
169
214
  * Time values remain strings because database drivers accept their wire shape.
215
+ * @param {unknown} value
216
+ * @param {string | undefined} type
217
+ * @param {{ databaseClient?: string, textStorage?: boolean, temporalPrecision?: number, fieldName?: string, resourceType?: string, source?: string }} [options]
218
+ * @returns {unknown}
170
219
  */
171
220
  export function normalizeValueForDatabaseStorage (value, type, {
221
+ databaseClient,
222
+ textStorage = false,
172
223
  temporalPrecision,
173
224
  fieldName,
174
225
  resourceType,
175
226
  source = 'storage'
176
227
  } = {}) {
228
+ if (SERIALIZATION_CAPABILITIES.structuredTypes.includes(type)) {
229
+ const structured = normalizeStructuredValue(value, type, { fieldName, resourceType, source })
230
+ try {
231
+ return structured == null ? structured : JSON.stringify(structured)
232
+ } catch (error) {
233
+ throw wrapUnexpectedError(error, {
234
+ message: `Cannot serialize ${type} field '${[resourceType, fieldName].filter(Boolean).join('.')}' as JSON`,
235
+ context: { fieldName, resourceType, source, phase: 'serialization' }
236
+ })
237
+ }
238
+ }
177
239
  if (!['date', 'dateTime', 'time'].includes(type)) return value
178
240
  if (value === null || value === undefined) return null
241
+ const capabilities = getTemporalStorageCapabilities(databaseClient, { textStorage })
179
242
 
180
243
  if (type === 'time') {
181
244
  if (typeof value !== 'string') {
182
245
  throwTemporalDataError({ fieldName, resourceType, type, source })
183
246
  }
184
- return validateTemporalJsonValue(value, type, {
247
+ const validated = validateTemporalJsonValue(value, type, {
185
248
  temporalPrecision,
186
249
  fieldName,
187
250
  resourceType,
188
251
  source
189
252
  })
253
+ const normalized = normalizeDateValue(validated, type, { temporalPrecision, fieldName, resourceType, source })
254
+ if (capabilities.timeFractionDigits !== null && (normalized.split('.')[1]?.length || 0) > capabilities.timeFractionDigits) {
255
+ const field = fieldName ? `data.attributes.${fieldName}` : 'value'
256
+ throw new RestApiValidationError('SQL time storage supports at most six fractional digits.', {
257
+ fields: [field],
258
+ violations: [{ field, rule: 'storage_precision', message: 'Use a text storage mapping for finer time precision.' }]
259
+ })
260
+ }
261
+ return normalized
190
262
  }
191
263
 
192
264
  let publicValue = value
@@ -205,10 +277,11 @@ export function normalizeValueForDatabaseStorage (value, type, {
205
277
  resourceType,
206
278
  source
207
279
  })
280
+ if (type === 'date' && textStorage) return validatedValue
208
281
  const fraction = type === 'dateTime'
209
282
  ? /\.(\d+)(?:Z|[+-]\d{2}:\d{2})$/.exec(validatedValue)?.[1]
210
283
  : null
211
- if (fraction && /[1-9]/.test(fraction.slice(3))) {
284
+ if (fraction && /[1-9]/.test(fraction.slice(capabilities.dateTimeFractionDigits))) {
212
285
  const field = fieldName ? `data.attributes.${fieldName}` : 'value'
213
286
  throw new RestApiValidationError('Built-in dateTime storage supports millisecond precision.', {
214
287
  fields: [field],
@@ -225,85 +298,57 @@ export function normalizeValueForDatabaseStorage (value, type, {
225
298
  if (Number.isNaN(databaseValue.getTime())) {
226
299
  throwTemporalDataError({ fieldName, resourceType, type, source })
227
300
  }
301
+ if (databaseValue.getUTCFullYear() < 0 || databaseValue.getUTCFullYear() > capabilities.maxYear) {
302
+ const field = fieldName ? `data.attributes.${fieldName}` : 'value'
303
+ throw new RestApiValidationError('UTC dateTime must remain within years 0000 through 9999.', {
304
+ fields: [field],
305
+ violations: [{ field, rule: 'storage_range', message: 'The timezone offset moves this value outside the public dateTime range.' }]
306
+ })
307
+ }
308
+ if (databaseClient === 'pg' || databaseClient === 'postgresql') {
309
+ const iso = databaseValue.toISOString()
310
+ const sqlValue = type === 'date' ? iso.slice(0, 10) : iso.replace('T', ' ')
311
+ return sqlValue.startsWith('0000-') ? `${sqlValue.replace(/^0000/, '0001')} BC` : sqlValue
312
+ }
313
+ if (databaseClient === 'mysql' || databaseClient === 'mysql2') {
314
+ if (databaseValue.getUTCFullYear() < (type === 'date' ? capabilities.dateMinYear : capabilities.dateTimeMinYear)) {
315
+ const field = fieldName ? `data.attributes.${fieldName}` : 'value'
316
+ throw new RestApiValidationError('MySQL date storage supports years 1000 through 9999.', {
317
+ fields: [field],
318
+ violations: [{ field, rule: 'storage_range', message: 'Use a custom storage mapping for dates outside the MySQL date range.' }]
319
+ })
320
+ }
321
+ const iso = databaseValue.toISOString()
322
+ return type === 'date' ? iso.slice(0, 10) : iso.replace('T', ' ').replace(/Z$/, '')
323
+ }
228
324
  return databaseValue
229
325
  }
230
326
 
327
+ export function normalizeStructuredValue (value, type, { fieldName, resourceType, source = 'database' } = {}) {
328
+ if (value === null || value === undefined) return value
329
+ try {
330
+ // Writes receive logical values; only reads decode a driver's JSON text.
331
+ const parsed = source !== 'storage' && typeof value === 'string' ? JSON.parse(value) : value
332
+ const valid = type === 'array'
333
+ ? Array.isArray(parsed)
334
+ : parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed) &&
335
+ [Object.prototype, null].includes(Object.getPrototypeOf(parsed))
336
+ if (!valid) throw new TypeError(`Expected a JSON ${type}`)
337
+ return parsed
338
+ } catch (error) {
339
+ throw wrapUnexpectedError(error, {
340
+ message: `Invalid ${source} value for ${type} field '${[resourceType, fieldName].filter(Boolean).join('.')}'`,
341
+ context: { fieldName, resourceType, source, phase: 'normalization' }
342
+ })
343
+ }
344
+ }
345
+
231
346
  /**
232
347
  * Normalizes database values in an attributes object
233
348
  *
234
349
  * @param {Object} attributes - The attributes object to normalize
235
350
  * @param {Object} schemaStructure - The schema structure defining field types
236
351
  * @returns {Object} The normalized attributes object
237
- *
238
- * @example
239
- * // Input: Raw database values
240
- * const attributes = {
241
- * id: 1,
242
- * is_active: 1, // Boolean as 0/1
243
- * created_at: '2024-01-15 10:30:00', // MySQL datetime
244
- * birth_date: '1990-05-20', // Date string
245
- * shift_time: '09:00:00' // Time string
246
- * };
247
- *
248
- * const schema = {
249
- * is_active: { type: 'boolean' },
250
- * created_at: { type: 'dateTime' },
251
- * birth_date: { type: 'date' },
252
- * shift_time: { type: 'time' }
253
- * };
254
- *
255
- * const normalized = normalizeAttributes(attributes, schema);
256
- *
257
- * // Output: JSON-safe values
258
- * // {
259
- * // id: 1,
260
- * // is_active: true, // 1 → true
261
- * // created_at: '2024-01-15T10:30:00.000Z',
262
- * // birth_date: '1990-05-20',
263
- * // shift_time: '09:00:00'
264
- * // }
265
- *
266
- * @example
267
- * // Input: Boolean edge cases
268
- * const attributes = {
269
- * flag1: 0, // Number 0
270
- * flag2: '1', // String '1'
271
- * flag3: true, // Already boolean
272
- * flag4: null // Null value
273
- * };
274
- *
275
- * const normalized = normalizeAttributes(attributes, {
276
- * flag1: { type: 'boolean' },
277
- * flag2: { type: 'boolean' },
278
- * flag3: { type: 'boolean' },
279
- * flag4: { type: 'boolean' }
280
- * });
281
- *
282
- * // Output:
283
- * // {
284
- * // flag1: false, // 0 → false
285
- * // flag2: true, // '1' → true
286
- * // flag3: true, // unchanged
287
- * // flag4: null // null preserved
288
- * // }
289
- *
290
- * @description
291
- * Used by:
292
- * - normalizeRecordAttributes for each record's attributes
293
- * - Applied after fetching data from database
294
- *
295
- * Purpose:
296
- * - Handles databases without native boolean support (SQLite, older MySQL)
297
- * - Normalizes date formats from different databases
298
- * - Ensures consistent data types in API responses
299
- * - Preserves null values appropriately
300
- *
301
- * Data flow:
302
- * 1. Creates copy of attributes to avoid mutation
303
- * 2. Iterates through each field with schema definition
304
- * 3. Normalizes booleans: 1/0 or '1'/'0' to true/false
305
- * 4. Normalizes dates using normalizeDateValue
306
- * 5. Returns new object with normalized values
307
352
  */
308
353
  export function normalizeAttributes (attributes, schemaStructure, {
309
354
  resourceType,
@@ -322,6 +367,25 @@ export function normalizeAttributes (attributes, schemaStructure, {
322
367
  // Skip if no field definition found
323
368
  if (!fieldDef) continue
324
369
 
370
+ if (['object', 'array'].includes(fieldDef.type)) {
371
+ // A custom getter owns decoding its stored representation.
372
+ if (source === 'database' && typeof fieldDef.getter === 'function') continue
373
+ normalized[fieldName] = normalizeStructuredValue(value, fieldDef.type, { fieldName, resourceType, source })
374
+ continue
375
+ }
376
+
377
+ if (fieldDef.type === 'file' && value instanceof Uint8Array) {
378
+ if (source === 'database' && typeof fieldDef.getter === 'function') continue
379
+ try {
380
+ normalized[fieldName] = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(value)
381
+ } catch (error) {
382
+ throw wrapUnexpectedError(error, {
383
+ message: `Invalid ${source} file handle '${[resourceType, fieldName].filter(Boolean).join('.')}'`,
384
+ context: { fieldName, resourceType, source, phase: 'normalization' }
385
+ })
386
+ }
387
+ }
388
+
325
389
  // Normalize boolean values
326
390
  if (fieldDef.type === 'boolean') {
327
391
  if (value === 1 || value === '1') {
@@ -353,120 +417,46 @@ export function normalizeAttributes (attributes, schemaStructure, {
353
417
  }
354
418
 
355
419
  /**
356
- * Normalizes all records in a JSON:API response
357
- *
358
- * @param {Object} record - The JSON:API response object
359
- * @param {Object} scopes - All available scopes for schema lookup
360
- * @returns {Object} The response with normalized values
361
- *
362
- * @example
363
- * // Input: JSON:API response with various data types
364
- * const response = {
365
- * data: {
366
- * type: 'articles',
367
- * id: '1',
368
- * attributes: {
369
- * title: 'My Article',
370
- * is_published: 1, // Boolean as number
371
- * published_at: '2024-01-15 10:00:00' // MySQL datetime
372
- * }
373
- * },
374
- * included: [{
375
- * type: 'users',
376
- * id: '10',
377
- * attributes: {
378
- * name: 'John',
379
- * is_admin: 0, // Boolean as number
380
- * last_login: '2024-01-14 15:30:00'
381
- * }
382
- * }]
383
- * };
384
- *
385
- * const normalized = normalizeRecordAttributes(response, scopes);
386
- *
387
- * // Output: All booleans and dates normalized
388
- * // {
389
- * // data: {
390
- * // type: 'articles',
391
- * // id: '1',
392
- * // attributes: {
393
- * // title: 'My Article',
394
- * // is_published: true,
395
- * // published_at: '2024-01-15T10:00:00.000Z'
396
- * // }
397
- * // },
398
- * // included: [{
399
- * // type: 'users',
400
- * // id: '10',
401
- * // attributes: {
402
- * // name: 'John',
403
- * // is_admin: false,
404
- * // last_login: '2024-01-14T15:30:00.000Z'
405
- * // }
406
- * // }]
407
- * // }
408
- *
409
- * @example
410
- * // Input: Collection response (array of records)
411
- * const response = {
412
- * data: [
413
- * {
414
- * type: 'comments',
415
- * id: '1',
416
- * attributes: { approved: 1, created_at: '2024-01-01 09:00:00' }
417
- * },
418
- * {
419
- * type: 'comments',
420
- * id: '2',
421
- * attributes: { approved: 0, created_at: '2024-01-02 10:00:00' }
422
- * }
423
- * ]
424
- * };
420
+ * Normalize a JSON:API document or one plain resource using compiled output fields.
421
+ * JSON:API primary/included attributes are replaced on the supplied document;
422
+ * plain resources are copied recursively with shared/cyclic identities tracked.
423
+ * The source distinguishes database conversion from final-response validation.
424
+ * Final response normalization removes internal enrichment metadata and reapplies
425
+ * the supplied fieldset so dependency-only values do not escape in output.
425
426
  *
426
- * // Each record in the array is normalized
427
- *
428
- * @description
429
- * Used by:
430
- * - rest-api-knex-plugin after fetching data, before sending response
431
- * - Applied to both GET (single) and QUERY (collection) responses
432
- *
433
- * Purpose:
434
- * - Ensures consistent data types in API responses
435
- * - Handles both primary data and included resources
436
- * - Works with single records and collections
437
- * - Uses schema definitions to determine field types
438
- *
439
- * Data flow:
440
- * 1. Checks if data is array (collection) or object (single)
441
- * 2. For each record, looks up its schema by type
442
- * 3. Calls normalizeAttributes with schema structure
443
- * 4. Processes included array the same way
444
- * 5. Returns complete response with normalized values
427
+ * @template T
428
+ * @param {T} record - JSON:API document, or a plain resource in simplified mode
429
+ * @param {Object} scopes - Resource registry for schema lookup
430
+ * @param {Object} [options]
431
+ * @param {string} [options.source='database'] - Value origin for normalization
432
+ * @param {boolean} [options.simplified=false] - Internal plain-resource selector
433
+ * @param {string} [options.resourceType] - Required for plain-resource schema lookup
434
+ * @param {Object} [options.fields] - Selected fieldsets, if supplied
435
+ * @returns {T} Normalized document or plain resource
445
436
  */
446
437
  export function normalizeRecordAttributes (record, scopes, {
447
438
  source = 'database',
448
439
  simplified = false,
449
- resourceType
440
+ resourceType,
441
+ fields
450
442
  } = {}) {
451
443
  if (!record || !scopes) {
452
444
  return record
453
445
  }
454
446
 
455
447
  if (simplified) {
456
- return normalizeSimplifiedRecord(record, resourceType, scopes, { source })
448
+ return normalizeSimplifiedRecord(record, resourceType, scopes, { source, fields })
457
449
  }
458
450
 
459
451
  const normalizeEntry = (entry) => {
452
+ // Discard enrichment metadata after finish hooks have run.
453
+ if (source === 'response' && entry && typeof entry === 'object') delete entry[COMPUTED_DEPENDENCIES_KEY]
460
454
  const schemaInfo = scopes[entry?.type]?.vars?.schemaInfo
461
455
  if (!schemaInfo || !entry?.attributes) return
462
456
 
463
457
  entry.attributes = normalizeAttributes(
464
458
  entry.attributes,
465
- {
466
- ...(schemaInfo.schemaStructure || {}),
467
- ...(schemaInfo.computed || {}),
468
- ...(scopes[entry.type]?.vars?.queryFields || {})
469
- },
459
+ schemaInfo.outputFields,
470
460
  {
471
461
  resourceType: entry.type,
472
462
  source
@@ -494,6 +484,7 @@ export function normalizeRecordAttributes (record, scopes, {
494
484
  }
495
485
  }
496
486
 
487
+ if (fields) filterResponseFields(record, fields)
497
488
  return record
498
489
  }
499
490
 
@@ -503,16 +494,9 @@ function normalizeSimplifiedRecord (record, resourceType, scopes, options, visit
503
494
  const schemaInfo = scopes[resourceType]?.vars?.schemaInfo
504
495
  if (!schemaInfo) return record
505
496
 
506
- const schema = { ...schemaInfo.schemaStructure, ...schemaInfo.computed, ...scopes[resourceType]?.vars?.queryFields }
507
- const normalized = normalizeAttributes(record, schema, { ...options, resourceType })
497
+ const normalized = normalizeAttributes(record, schemaInfo.outputFields, { ...options, resourceType })
508
498
  visited.set(record, normalized)
509
- const relationships = { ...schemaInfo.schemaRelationships }
510
- for (const definition of Object.values(schema)) {
511
- if (definition.as && (definition.belongsTo || definition.belongsToPolymorphic)) {
512
- relationships[definition.as] = definition
513
- }
514
- }
515
- for (const [name, definition] of Object.entries(relationships)) {
499
+ for (const [name, definition] of Object.entries(schemaInfo.outputRelationships || {})) {
516
500
  const normalizeRelated = (entry) => normalizeSimplifiedRecord(
517
501
  entry,
518
502
  definition.belongsToPolymorphic
@@ -528,5 +512,6 @@ function normalizeSimplifiedRecord (record, resourceType, scopes, options, visit
528
512
  : normalizeRelated(normalized[name])
529
513
  }
530
514
  }
515
+ if (options.fields) filterResponseFields(normalized, options.fields, { simplified: true, resourceType })
531
516
  return normalized
532
517
  }
@@ -1,72 +1,42 @@
1
+ // @ts-check
2
+ /** @import { DataWriteHelpers, DataReadHelpers } from '../storage/storage-types.js' */
1
3
  /**
2
- * Default data helper implementations that instruct users to install a storage plugin
3
- *
4
- * @description
5
- * These default implementations ensure the REST API plugin fails gracefully
6
- * when no storage plugin is installed. Each method throws a descriptive error
7
- * guiding users to install a storage implementation.
8
- *
9
- * @example
10
- * // When no storage plugin is installed:
11
- * try {
12
- * await api.resources.articles.get({ id: 1 });
13
- * } catch (error) {
14
- * console.log(error.message);
15
- * // "No storage implementation for get. Install a storage plugin."
16
- * }
17
- *
18
- * @example
19
- * // After installing rest-api-knex-plugin:
20
- * api.use(RestApiKnexPlugin, { knex });
21
- * // Now data helpers are replaced with actual implementations
22
- * const article = await api.resources.articles.get({ id: 1 });
23
- * // Works! Returns article from database
24
- *
25
- * Used by:
26
- * - rest-api-plugin sets these as initial data helpers
27
- * - Storage plugins replace these with actual implementations
28
- *
29
- * Purpose:
30
- * - Provides clear error messages when storage is missing
31
- * - Ensures REST API plugin can be installed independently
32
- * - Allows storage plugins to be swapped without changing API
33
- *
34
- * Data flow:
35
- * 1. REST API plugin initializes with these defaults
36
- * 2. User tries to use API without storage plugin
37
- * 3. Clear error message guides them to install storage
38
- * 4. Storage plugin replaces these with real implementations
4
+ * Missing-storage placeholders installed by RestApiPlugin.
5
+ * Storage plugins replace these helpers. They are not a complete custom-backend
6
+ * interface: resource operations also require transaction and query integration.
7
+ * Earlier lifecycle validation may reject a call before one of these runs.
8
+ * @satisfies {DataWriteHelpers<never, never> & DataReadHelpers<never>}
39
9
  */
40
10
  export const defaultDataHelpers = {
41
- dataExists: async function (scope, deps) {
11
+ dataExists: async function (request) {
42
12
  throw new Error('No storage implementation for exists. Install a storage plugin.')
43
13
  },
44
14
 
45
- dataGet: async function (scope, deps) {
15
+ dataGet: async function (request) {
46
16
  throw new Error('No storage implementation for get. Install a storage plugin.')
47
17
  },
48
18
 
49
- dataGetMinimal: async function (scope, deps) {
19
+ dataGetMinimal: async function (request) {
50
20
  throw new Error('No storage implementation for getMinimal. Install a storage plugin.')
51
21
  },
52
22
 
53
- dataQuery: async function (scope, deps) {
23
+ dataQuery: async function (request) {
54
24
  throw new Error('No storage implementation for query. Install a storage plugin.')
55
25
  },
56
26
 
57
- dataPost: async function (scope, deps) {
27
+ dataPost: async function (request) {
58
28
  throw new Error('No storage implementation for post. Install a storage plugin.')
59
29
  },
60
30
 
61
- dataPatch: async function (scope, deps) {
31
+ dataPatch: async function (request) {
62
32
  throw new Error('No storage implementation for patch. Install a storage plugin.')
63
33
  },
64
34
 
65
- dataPut: async function (scope, deps) {
35
+ dataPut: async function (request) {
66
36
  throw new Error('No storage implementation for put. Install a storage plugin.')
67
37
  },
68
38
 
69
- dataDelete: async function (scope, deps) {
39
+ dataDelete: async function (request) {
70
40
  throw new Error('No storage implementation for delete. Install a storage plugin.')
71
41
  }
72
42
  }
@@ -0,0 +1,43 @@
1
+ import type { ResourceConversionSchema, StorageAdapter, StorageContext, StorageDatabase, StorageFieldDefinition, StorageQuery } from '../storage/storage-types.js'
2
+ import type { CompiledQueryField, ProjectionRuntime } from './query-field-types.js'
3
+ import type { Fieldsets } from './field-utils.js'
4
+
5
+ export interface SelectionSchema extends Partial<ResourceConversionSchema> {
6
+ queryFields?: Record<string, CompiledQueryField>
7
+ outputRelationships?: Record<string, StorageFieldDefinition>
8
+ readDependencies?: Record<string, readonly string[]>
9
+ }
10
+
11
+ export interface SelectionScope {
12
+ vars?: { schemaInfo?: SelectionSchema }
13
+ }
14
+
15
+ export interface SelectionContext extends StorageContext {
16
+ scopeName: string
17
+ queryParams?: { fields?: Fieldsets; sort?: unknown }
18
+ }
19
+
20
+ export interface FieldSelection {
21
+ fieldsToSelect: string[]
22
+ queryFieldsToSelect: string[]
23
+ requestedFields: readonly string[] | null
24
+ computedDependencies: string[]
25
+ idProperty: 'id'
26
+ }
27
+
28
+ export interface SelectionQueryRequest {
29
+ query: StorageQuery
30
+ scope?: SelectionScope | null
31
+ fieldSelectionInfo?: Partial<FieldSelection> | null
32
+ tableName: string
33
+ useTablePrefix?: boolean
34
+ storageAdapter?: StorageAdapter | null
35
+ db?: StorageDatabase | null
36
+ context?: StorageContext | null
37
+ scopeName?: string
38
+ }
39
+
40
+ export interface SelectedQuery {
41
+ query: StorageQuery
42
+ queryFieldRuntimeByField: Map<string, ProjectionRuntime>
43
+ }