json-rest-api 1.0.28 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. package/LICENSE +10 -670
  2. package/LICENSE-GPL-3.0 +674 -0
  3. package/LICENSE-MIT +21 -0
  4. package/README.md +39 -30
  5. package/docs/API.md +746 -1273
  6. package/docs/GUIDE/01-getting-started.md +338 -0
  7. package/docs/GUIDE/02-resources-and-relationships.md +33 -0
  8. package/docs/GUIDE/03-running-example.md +87 -0
  9. package/docs/GUIDE/04-creating-and-querying.md +181 -0
  10. package/docs/GUIDE/05-belongs-to.md +177 -0
  11. package/docs/GUIDE/06-has-many.md +184 -0
  12. package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
  13. package/docs/GUIDE/08-many-to-many.md +184 -0
  14. package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
  15. package/docs/GUIDE/10-put-and-patch.md +174 -0
  16. package/docs/GUIDE/11-relationship-endpoints.md +174 -0
  17. package/docs/GUIDE/12-field-transformations.md +287 -0
  18. package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
  19. package/docs/GUIDE/14-custom-resource-methods.md +125 -0
  20. package/docs/GUIDE/15-query-projections.md +266 -0
  21. package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
  22. package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
  23. package/docs/GUIDE/18-labels.md +86 -0
  24. package/docs/GUIDE/19-managed-transactions.md +179 -0
  25. package/docs/GUIDE/20-transaction-outcomes.md +207 -0
  26. package/docs/GUIDE/21-schema-and-migrations.md +634 -0
  27. package/docs/GUIDE/22-version-field-migrations.md +208 -0
  28. package/docs/GUIDE/23-fastify.md +179 -0
  29. package/docs/GUIDE/24-cors.md +172 -0
  30. package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
  31. package/docs/GUIDE/26-file-uploads.md +278 -0
  32. package/docs/GUIDE/27-bulk-operations.md +277 -0
  33. package/docs/GUIDE/28-socketio.md +325 -0
  34. package/docs/GUIDE/29-writing-plugins.md +278 -0
  35. package/docs/GUIDE/30-backend-capabilities.md +100 -0
  36. package/docs/GUIDE/31-positioning.md +181 -0
  37. package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +90 -31
  38. package/docs/GUIDE/33-migrating-to-v2.md +3827 -0
  39. package/docs/GUIDE/index.md +51 -59
  40. package/docs/QUICKSTART.md +120 -287
  41. package/docs/architecture.md +75 -0
  42. package/docs/contributing.md +178 -0
  43. package/index.d.ts +36 -0
  44. package/index.js +1 -22
  45. package/lib/enhanced-logger.js +91 -85
  46. package/lib/error-context.js +445 -5
  47. package/lib/error-formatter.js +219 -59
  48. package/lib/knex-transaction.js +64 -0
  49. package/lib/logger-types.d.ts +22 -0
  50. package/lib/missing-package.js +4 -0
  51. package/lib/rest-api-errors.js +103 -8
  52. package/lib/runtime/json-rest-api.js +172 -0
  53. package/lib/transaction-types.d.ts +59 -0
  54. package/package.json +97 -27
  55. package/plugins/core/bulk-operations-plugin.js +191 -184
  56. package/plugins/core/connectors/express-plugin.js +148 -219
  57. package/plugins/core/connectors/fastify-plugin.js +125 -82
  58. package/plugins/core/connectors/lib/busboy-detector.js +74 -156
  59. package/plugins/core/connectors/lib/connector-core.js +144 -20
  60. package/plugins/core/connectors/lib/formidable-detector.js +63 -120
  61. package/plugins/core/connectors/lib/http-validators.js +77 -0
  62. package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
  63. package/plugins/core/connectors/lib/request-helpers.js +10 -1
  64. package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
  65. package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
  66. package/plugins/core/file-handling-plugin.js +87 -174
  67. package/plugins/core/lib/anyapi/anyapi-registry.js +213 -164
  68. package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
  69. package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
  70. package/plugins/core/lib/anyapi/schema-utils.js +25 -16
  71. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
  72. package/plugins/core/lib/dbIntrospection.js +228 -21
  73. package/plugins/core/lib/dbTablesOperations.js +409 -92
  74. package/plugins/core/lib/fractional-positioning.js +12 -48
  75. package/plugins/core/lib/querying/identifier-query.js +19 -0
  76. package/plugins/core/lib/querying/include-query-helpers.js +364 -0
  77. package/plugins/core/lib/querying/include-to-many.js +428 -0
  78. package/plugins/core/lib/querying/include-to-one.js +579 -0
  79. package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
  80. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
  81. package/plugins/core/lib/querying/knex-pagination-helpers.js +67 -330
  82. package/plugins/core/lib/querying/knex-process-includes.js +5 -110
  83. package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
  84. package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
  85. package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
  86. package/plugins/core/lib/querying/knex-window-queries.js +62 -253
  87. package/plugins/core/lib/querying/query-builder-utils.js +44 -4
  88. package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
  89. package/plugins/core/lib/querying/query-constraint.js +27 -0
  90. package/plugins/core/lib/querying/query-field-sort-helpers.js +205 -30
  91. package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
  92. package/plugins/core/lib/querying/sort-helpers.js +20 -1
  93. package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
  94. package/plugins/core/lib/querying/url-helpers.js +11 -4
  95. package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
  96. package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
  97. package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
  98. package/plugins/core/lib/querying-writing/database-value-normalizers.js +238 -187
  99. package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
  100. package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
  101. package/plugins/core/lib/querying-writing/field-utils.js +113 -92
  102. package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
  103. package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
  104. package/plugins/core/lib/querying-writing/knex-field-helpers.js +111 -287
  105. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
  106. package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
  107. package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
  108. package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
  109. package/plugins/core/lib/querying-writing/request-contracts.js +109 -44
  110. package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
  111. package/plugins/core/lib/querying-writing/response-options.js +35 -0
  112. package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
  113. package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
  114. package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
  115. package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
  116. package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
  117. package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
  118. package/plugins/core/lib/storage/storage-adapter.js +142 -62
  119. package/plugins/core/lib/storage/storage-mapping.js +78 -28
  120. package/plugins/core/lib/storage/storage-types.d.ts +502 -0
  121. package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
  122. package/plugins/core/lib/writing/relationship-processor.js +94 -158
  123. package/plugins/core/lib/writing/resource-version.js +172 -0
  124. package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
  125. package/plugins/core/rest-api-anyapi-knex-plugin.js +116 -2509
  126. package/plugins/core/rest-api-autofilter-plugin.js +17 -19
  127. package/plugins/core/rest-api-cors-plugin.js +52 -205
  128. package/plugins/core/rest-api-knex-plugin.js +45 -1069
  129. package/plugins/core/rest-api-label-plugin.js +25 -38
  130. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
  131. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
  132. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
  133. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
  134. package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
  135. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
  136. package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
  137. package/plugins/core/rest-api-plugin-methods/common.js +291 -385
  138. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
  139. package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
  140. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -145
  141. package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
  142. package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
  143. package/plugins/core/rest-api-plugin-methods/get.js +38 -69
  144. package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
  145. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
  146. package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
  147. package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
  148. package/plugins/core/rest-api-plugin-methods/post.js +51 -51
  149. package/plugins/core/rest-api-plugin-methods/put.js +69 -148
  150. package/plugins/core/rest-api-plugin-methods/query.js +39 -82
  151. package/plugins/core/rest-api-plugin-methods/release.js +2 -1
  152. package/plugins/core/rest-api-plugin.js +32 -46
  153. package/plugins/core/rest-api-positioning-plugin.js +183 -628
  154. package/plugins/core/rest-api-query-projections-plugin.js +19 -102
  155. package/plugins/core/rest-api-row-policy-plugin.js +3 -3
  156. package/plugins/core/socketio-plugin.js +364 -344
  157. package/plugins/storage/local-storage.js +40 -36
  158. package/types/autofilter.d.ts +57 -0
  159. package/types/bulk-methods.d.ts +63 -0
  160. package/types/errors.d.ts +118 -0
  161. package/types/fastify-options.d.ts +19 -0
  162. package/types/file-detectors.d.ts +33 -0
  163. package/types/file-storage.d.ts +48 -0
  164. package/types/hook-context.d.ts +180 -0
  165. package/types/plugin-options.d.ts +53 -0
  166. package/types/relationship-methods.d.ts +74 -0
  167. package/types/representations.d.ts +50 -0
  168. package/types/resource-methods.d.ts +95 -0
  169. package/types/resource-schema.d.ts +157 -0
  170. package/types/row-policy.d.ts +31 -0
  171. package/types/runtime.d.ts +132 -0
  172. package/types/transactions.d.ts +21 -0
  173. package/.claude/agents/library-guide-expert.md +0 -30
  174. package/.claude/agents/rest-api-internals-expert.md +0 -54
  175. package/.claude/settings.local.json +0 -70
  176. package/AGENTS.md +0 -34
  177. package/CLAUDE.md +0 -160
  178. package/CRUSH.md +0 -55
  179. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
  180. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
  181. package/TODO.todo +0 -13
  182. package/connectors_ideas/fastify.js +0 -217
  183. package/connectors_ideas/koa-plugin.js +0 -112
  184. package/docs/.bundle/config +0 -2
  185. package/docs/COMPARISON.md +0 -433
  186. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
  187. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
  188. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
  189. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
  190. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
  191. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
  192. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
  193. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1238
  194. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
  195. package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
  196. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
  197. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
  198. package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
  199. package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
  200. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
  201. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
  202. package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
  203. package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
  204. package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
  205. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
  206. package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
  207. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
  208. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
  209. package/docs/Gemfile +0 -6
  210. package/docs/Gemfile.lock +0 -308
  211. package/docs/ONBOARDING.md +0 -315
  212. package/docs/_config.yml +0 -36
  213. package/docs/_layouts/default.html +0 -123
  214. package/docs/apple-touch-icon.svg +0 -4
  215. package/docs/assets/css/style.scss +0 -392
  216. package/docs/favicon.svg +0 -4
  217. package/docs/index.md +0 -164
  218. package/docs/mini-guides/file-uploads-complete.md +0 -161
  219. package/eslint.config.js +0 -13
  220. package/examples/url-override-example.js +0 -137
  221. package/fixing_plan.md +0 -112
  222. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
  223. package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
  224. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
  225. package/plugins/storage/s3-storage.js +0 -93
  226. package/quickTest.js +0 -136
  227. package/scripts/debug-anyapi-review-filter.mjs +0 -63
  228. package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
  229. package/scripts/inspect-anyapi-descriptor.mjs +0 -19
  230. package/scripts/run-docs.js +0 -133
  231. package/test-debug.js +0 -59
  232. package/tests/README.md +0 -202
  233. package/tests/TEST_TEMPLATE.test.js +0 -178
  234. package/tests/anyapi-basic.test.js +0 -105
  235. package/tests/anyapi-cursor-pagination.test.js +0 -150
  236. package/tests/anyapi-custom-idproperty.test.js +0 -163
  237. package/tests/autofilter.test.js +0 -463
  238. package/tests/bulk-operations.test.js +0 -657
  239. package/tests/computed-fields.test.js +0 -586
  240. package/tests/cors.test.js +0 -834
  241. package/tests/curl.test.js +0 -467
  242. package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
  243. package/tests/custom-id-simplified-post.test.js +0 -75
  244. package/tests/custom-idproperty-relationships.test.js +0 -932
  245. package/tests/db-introspection.test.js +0 -521
  246. package/tests/db-table-operations.test.js +0 -798
  247. package/tests/fastify-plugin.test.js +0 -184
  248. package/tests/field-getters.test.js +0 -370
  249. package/tests/field-setters.test.js +0 -306
  250. package/tests/file-handling.test.js +0 -118
  251. package/tests/fixtures/api-configs.js +0 -2509
  252. package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
  253. package/tests/helpers/fake-fastify.js +0 -99
  254. package/tests/helpers/storage-mode.js +0 -65
  255. package/tests/helpers/test-utils.js +0 -203
  256. package/tests/http-connectors-parity.test.js +0 -395
  257. package/tests/id-normalization.test.js +0 -444
  258. package/tests/include-depth-validation.test.js +0 -331
  259. package/tests/includes.test.js +0 -427
  260. package/tests/local-storage.test.js +0 -96
  261. package/tests/logical-id-write-validation.test.js +0 -151
  262. package/tests/nested-includes.test.js +0 -617
  263. package/tests/pagination-cursor-multifield.test.js +0 -643
  264. package/tests/pagination-enhanced.test.js +0 -563
  265. package/tests/polymorphic-relationships.test.js +0 -347
  266. package/tests/polymorphic-simple.test.js +0 -148
  267. package/tests/positioning-advanced.test.js +0 -692
  268. package/tests/positioning-extended.test.js +0 -915
  269. package/tests/positioning-multifilter.test.js +0 -699
  270. package/tests/positioning.test.js +0 -560
  271. package/tests/post-commit-hooks.test.js +0 -120
  272. package/tests/projected-fields.test.js +0 -319
  273. package/tests/public-exports.test.js +0 -15
  274. package/tests/put-create-parity.test.js +0 -216
  275. package/tests/queries.test.js +0 -521
  276. package/tests/query-limits.test.js +0 -584
  277. package/tests/relationship-endpoints.test.js +0 -620
  278. package/tests/relationship-include-storage-adapter.test.js +0 -56
  279. package/tests/relationships.test.js +0 -681
  280. package/tests/rest-api.test.js +0 -337
  281. package/tests/return-record-settings.test.js +0 -184
  282. package/tests/row-policy.test.js +0 -489
  283. package/tests/s3-storage.test.js +0 -38
  284. package/tests/searchschema-knex-behavior.test.js +0 -187
  285. package/tests/searchschema-merge.test.js +0 -151
  286. package/tests/socketio.test.js +0 -860
  287. package/tests/storage-mapping.test.js +0 -362
  288. package/tests/test-development-guide.md +0 -370
  289. package/tests/virtual-fields.test.js +0 -404
@@ -1,11 +1,40 @@
1
1
  import { createSchema } from 'json-rest-schema'
2
- import { RestApiTemporalDataError } from '../../../../lib/rest-api-errors.js'
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 }) => {
@@ -71,12 +103,12 @@ const parseEpochDatabaseValue = (value) => {
71
103
 
72
104
  const parseKnownDatabaseDate = (value, { fieldName, resourceType, type, source }) => {
73
105
  if (value instanceof Date) {
74
- if (!Number.isNaN(value.getTime())) return value
106
+ if (!Number.isNaN(value.getTime())) return value.toISOString()
75
107
  throwTemporalDataError({ fieldName, resourceType, type, source })
76
108
  }
77
109
 
78
110
  const epochDate = parseEpochDatabaseValue(value)
79
- if (epochDate) return epochDate
111
+ if (epochDate) return epochDate.toISOString()
80
112
 
81
113
  if (typeof value !== 'string') {
82
114
  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,11 +142,15 @@ 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
- return parsed
149
+ // Date handles the offset, but cannot retain digits beyond milliseconds.
150
+ const fraction = /\.(\d+)(?:Z|[+-]\d{2}:\d{2})$/.exec(dateTimeValue)?.[1]
151
+ return fraction
152
+ ? parsed.toISOString().replace(/\.\d{3}Z$/, `.${fraction.padEnd(3, '0')}Z`)
153
+ : parsed.toISOString()
110
154
  }
111
155
 
112
156
  /**
@@ -130,27 +174,31 @@ export function normalizeDateValue (value, type, {
130
174
  if (value instanceof Date && !Number.isNaN(value.getTime())) {
131
175
  timeValue = limitFractionalSecondPrecision(value.toISOString().slice(11, -1), temporalPrecision)
132
176
  }
133
- return validateTemporalJsonValue(timeValue, type, {
177
+ const validated = validateTemporalJsonValue(timeValue, type, {
134
178
  temporalPrecision,
135
179
  fieldName,
136
180
  resourceType,
137
181
  source
138
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}` : ''}`
139
187
  }
140
188
 
141
- if (type === 'date' && typeof value === 'string' && DATE_VALUE_PATTERN.test(value.trim())) {
142
- 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, {
143
192
  fieldName,
144
193
  resourceType,
145
194
  source
146
195
  })
147
196
  }
148
197
 
149
- const dateValue = parseKnownDatabaseDate(value, { fieldName, resourceType, type, source })
150
- const isoValue = dateValue.toISOString()
198
+ const isoValue = parseKnownDatabaseDate(value, { fieldName, resourceType, type, source })
151
199
  const normalized = type === 'date'
152
200
  ? isoValue.slice(0, 10)
153
- : limitFractionalSecondPrecision(isoValue, temporalPrecision)
201
+ : limitFractionalSecondPrecision(isoValue.replace(/(\.\d{3}\d*?)0+Z$/, '$1Z'), temporalPrecision)
154
202
  return validateTemporalJsonValue(normalized, type, {
155
203
  temporalPrecision,
156
204
  fieldName,
@@ -160,30 +208,57 @@ export function normalizeDateValue (value, type, {
160
208
  }
161
209
 
162
210
  /**
163
- * Converts validated JSON temporal values to the native values expected by
211
+ * Converts structured JSON and temporal values to the values expected by
164
212
  * database drivers. JSON schema validation keeps date and dateTime values as
165
213
  * strings; database writes and comparisons must normalize them consistently.
166
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}
167
219
  */
168
220
  export function normalizeValueForDatabaseStorage (value, type, {
221
+ databaseClient,
222
+ textStorage = false,
169
223
  temporalPrecision,
170
224
  fieldName,
171
225
  resourceType,
172
226
  source = 'storage'
173
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
+ }
174
239
  if (!['date', 'dateTime', 'time'].includes(type)) return value
175
240
  if (value === null || value === undefined) return null
241
+ const capabilities = getTemporalStorageCapabilities(databaseClient, { textStorage })
176
242
 
177
243
  if (type === 'time') {
178
244
  if (typeof value !== 'string') {
179
245
  throwTemporalDataError({ fieldName, resourceType, type, source })
180
246
  }
181
- return validateTemporalJsonValue(value, type, {
247
+ const validated = validateTemporalJsonValue(value, type, {
182
248
  temporalPrecision,
183
249
  fieldName,
184
250
  resourceType,
185
251
  source
186
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
187
262
  }
188
263
 
189
264
  let publicValue = value
@@ -202,91 +277,78 @@ export function normalizeValueForDatabaseStorage (value, type, {
202
277
  resourceType,
203
278
  source
204
279
  })
280
+ if (type === 'date' && textStorage) return validatedValue
281
+ const fraction = type === 'dateTime'
282
+ ? /\.(\d+)(?:Z|[+-]\d{2}:\d{2})$/.exec(validatedValue)?.[1]
283
+ : null
284
+ if (fraction && /[1-9]/.test(fraction.slice(capabilities.dateTimeFractionDigits))) {
285
+ const field = fieldName ? `data.attributes.${fieldName}` : 'value'
286
+ throw new RestApiValidationError('Built-in dateTime storage supports millisecond precision.', {
287
+ fields: [field],
288
+ violations: [{
289
+ field,
290
+ rule: 'storage_precision',
291
+ message: 'Submillisecond values require a custom storage serializer.'
292
+ }]
293
+ })
294
+ }
205
295
  const databaseValue = new Date(
206
296
  type === 'date' ? `${validatedValue}T00:00:00Z` : validatedValue
207
297
  )
208
298
  if (Number.isNaN(databaseValue.getTime())) {
209
299
  throwTemporalDataError({ fieldName, resourceType, type, source })
210
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
+ }
211
324
  return databaseValue
212
325
  }
213
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
+
214
346
  /**
215
347
  * Normalizes database values in an attributes object
216
348
  *
217
349
  * @param {Object} attributes - The attributes object to normalize
218
350
  * @param {Object} schemaStructure - The schema structure defining field types
219
351
  * @returns {Object} The normalized attributes object
220
- *
221
- * @example
222
- * // Input: Raw database values
223
- * const attributes = {
224
- * id: 1,
225
- * is_active: 1, // Boolean as 0/1
226
- * created_at: '2024-01-15 10:30:00', // MySQL datetime
227
- * birth_date: '1990-05-20', // Date string
228
- * shift_time: '09:00:00' // Time string
229
- * };
230
- *
231
- * const schema = {
232
- * is_active: { type: 'boolean' },
233
- * created_at: { type: 'dateTime' },
234
- * birth_date: { type: 'date' },
235
- * shift_time: { type: 'time' }
236
- * };
237
- *
238
- * const normalized = normalizeAttributes(attributes, schema);
239
- *
240
- * // Output: JSON-safe values
241
- * // {
242
- * // id: 1,
243
- * // is_active: true, // 1 → true
244
- * // created_at: '2024-01-15T10:30:00.000Z',
245
- * // birth_date: '1990-05-20',
246
- * // shift_time: '09:00:00'
247
- * // }
248
- *
249
- * @example
250
- * // Input: Boolean edge cases
251
- * const attributes = {
252
- * flag1: 0, // Number 0
253
- * flag2: '1', // String '1'
254
- * flag3: true, // Already boolean
255
- * flag4: null // Null value
256
- * };
257
- *
258
- * const normalized = normalizeAttributes(attributes, {
259
- * flag1: { type: 'boolean' },
260
- * flag2: { type: 'boolean' },
261
- * flag3: { type: 'boolean' },
262
- * flag4: { type: 'boolean' }
263
- * });
264
- *
265
- * // Output:
266
- * // {
267
- * // flag1: false, // 0 → false
268
- * // flag2: true, // '1' → true
269
- * // flag3: true, // unchanged
270
- * // flag4: null // null preserved
271
- * // }
272
- *
273
- * @description
274
- * Used by:
275
- * - normalizeRecordAttributes for each record's attributes
276
- * - Applied after fetching data from database
277
- *
278
- * Purpose:
279
- * - Handles databases without native boolean support (SQLite, older MySQL)
280
- * - Normalizes date formats from different databases
281
- * - Ensures consistent data types in API responses
282
- * - Preserves null values appropriately
283
- *
284
- * Data flow:
285
- * 1. Creates copy of attributes to avoid mutation
286
- * 2. Iterates through each field with schema definition
287
- * 3. Normalizes booleans: 1/0 or '1'/'0' to true/false
288
- * 4. Normalizes dates using normalizeDateValue
289
- * 5. Returns new object with normalized values
290
352
  */
291
353
  export function normalizeAttributes (attributes, schemaStructure, {
292
354
  resourceType,
@@ -305,6 +367,25 @@ export function normalizeAttributes (attributes, schemaStructure, {
305
367
  // Skip if no field definition found
306
368
  if (!fieldDef) continue
307
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
+
308
389
  // Normalize boolean values
309
390
  if (fieldDef.type === 'boolean') {
310
391
  if (value === 1 || value === '1') {
@@ -323,6 +404,12 @@ export function normalizeAttributes (attributes, schemaStructure, {
323
404
  resourceType,
324
405
  source
325
406
  })
407
+ } else if (['epochMilliseconds', 'epochSeconds'].includes(fieldDef.type) && value != null) {
408
+ normalized[fieldName] = validateTemporalJsonValue(
409
+ typeof value === 'bigint' ? String(value) : value,
410
+ fieldDef.type,
411
+ { fieldName, resourceType, source }
412
+ )
326
413
  }
327
414
  }
328
415
 
@@ -330,111 +417,46 @@ export function normalizeAttributes (attributes, schemaStructure, {
330
417
  }
331
418
 
332
419
  /**
333
- * Normalizes all records in a JSON:API response
334
- *
335
- * @param {Object} record - The JSON:API response object
336
- * @param {Object} scopes - All available scopes for schema lookup
337
- * @returns {Object} The response with normalized values
338
- *
339
- * @example
340
- * // Input: JSON:API response with various data types
341
- * const response = {
342
- * data: {
343
- * type: 'articles',
344
- * id: '1',
345
- * attributes: {
346
- * title: 'My Article',
347
- * is_published: 1, // Boolean as number
348
- * published_at: '2024-01-15 10:00:00' // MySQL datetime
349
- * }
350
- * },
351
- * included: [{
352
- * type: 'users',
353
- * id: '10',
354
- * attributes: {
355
- * name: 'John',
356
- * is_admin: 0, // Boolean as number
357
- * last_login: '2024-01-14 15:30:00'
358
- * }
359
- * }]
360
- * };
361
- *
362
- * const normalized = normalizeRecordAttributes(response, scopes);
363
- *
364
- * // Output: All booleans and dates normalized
365
- * // {
366
- * // data: {
367
- * // type: 'articles',
368
- * // id: '1',
369
- * // attributes: {
370
- * // title: 'My Article',
371
- * // is_published: true,
372
- * // published_at: '2024-01-15T10:00:00.000Z'
373
- * // }
374
- * // },
375
- * // included: [{
376
- * // type: 'users',
377
- * // id: '10',
378
- * // attributes: {
379
- * // name: 'John',
380
- * // is_admin: false,
381
- * // last_login: '2024-01-14T15:30:00.000Z'
382
- * // }
383
- * // }]
384
- * // }
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.
385
426
  *
386
- * @example
387
- * // Input: Collection response (array of records)
388
- * const response = {
389
- * data: [
390
- * {
391
- * type: 'comments',
392
- * id: '1',
393
- * attributes: { approved: 1, created_at: '2024-01-01 09:00:00' }
394
- * },
395
- * {
396
- * type: 'comments',
397
- * id: '2',
398
- * attributes: { approved: 0, created_at: '2024-01-02 10:00:00' }
399
- * }
400
- * ]
401
- * };
402
- *
403
- * // Each record in the array is normalized
404
- *
405
- * @description
406
- * Used by:
407
- * - rest-api-knex-plugin after fetching data, before sending response
408
- * - Applied to both GET (single) and QUERY (collection) responses
409
- *
410
- * Purpose:
411
- * - Ensures consistent data types in API responses
412
- * - Handles both primary data and included resources
413
- * - Works with single records and collections
414
- * - Uses schema definitions to determine field types
415
- *
416
- * Data flow:
417
- * 1. Checks if data is array (collection) or object (single)
418
- * 2. For each record, looks up its schema by type
419
- * 3. Calls normalizeAttributes with schema structure
420
- * 4. Processes included array the same way
421
- * 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
422
436
  */
423
- export function normalizeRecordAttributes (record, scopes, { source = 'database' } = {}) {
437
+ export function normalizeRecordAttributes (record, scopes, {
438
+ source = 'database',
439
+ simplified = false,
440
+ resourceType,
441
+ fields
442
+ } = {}) {
424
443
  if (!record || !scopes) {
425
444
  return record
426
445
  }
427
446
 
447
+ if (simplified) {
448
+ return normalizeSimplifiedRecord(record, resourceType, scopes, { source, fields })
449
+ }
450
+
428
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]
429
454
  const schemaInfo = scopes[entry?.type]?.vars?.schemaInfo
430
455
  if (!schemaInfo || !entry?.attributes) return
431
456
 
432
457
  entry.attributes = normalizeAttributes(
433
458
  entry.attributes,
434
- {
435
- ...(schemaInfo.schemaStructure || {}),
436
- ...(schemaInfo.computed || {})
437
- },
459
+ schemaInfo.outputFields,
438
460
  {
439
461
  resourceType: entry.type,
440
462
  source
@@ -462,5 +484,34 @@ export function normalizeRecordAttributes (record, scopes, { source = 'database'
462
484
  }
463
485
  }
464
486
 
487
+ if (fields) filterResponseFields(record, fields)
465
488
  return record
466
489
  }
490
+
491
+ function normalizeSimplifiedRecord (record, resourceType, scopes, options, visited = new WeakMap()) {
492
+ if (!record || typeof record !== 'object') return record
493
+ if (visited.has(record)) return visited.get(record)
494
+ const schemaInfo = scopes[resourceType]?.vars?.schemaInfo
495
+ if (!schemaInfo) return record
496
+
497
+ const normalized = normalizeAttributes(record, schemaInfo.outputFields, { ...options, resourceType })
498
+ visited.set(record, normalized)
499
+ for (const [name, definition] of Object.entries(schemaInfo.outputRelationships || {})) {
500
+ const normalizeRelated = (entry) => normalizeSimplifiedRecord(
501
+ entry,
502
+ definition.belongsToPolymorphic
503
+ ? entry?._type
504
+ : definition.belongsTo || definition.target || name,
505
+ scopes,
506
+ options,
507
+ visited
508
+ )
509
+ if (Object.hasOwn(normalized, name)) {
510
+ normalized[name] = Array.isArray(normalized[name])
511
+ ? normalized[name].map(normalizeRelated)
512
+ : normalizeRelated(normalized[name])
513
+ }
514
+ }
515
+ if (options.fields) filterResponseFields(normalized, options.fields, { simplified: true, resourceType })
516
+ return normalized
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
  }