json-rest-api 1.0.29 → 2.1.0

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