@async/db 0.2.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 (398) hide show
  1. package/CHANGELOG.md +167 -0
  2. package/README.md +431 -0
  3. package/SPEC.md +1429 -0
  4. package/db.config.example.mjs +128 -0
  5. package/dist/cli/args.d.ts +8 -0
  6. package/dist/cli/args.js +16 -0
  7. package/dist/cli/commands/create.d.ts +3 -0
  8. package/dist/cli/commands/create.js +13 -0
  9. package/dist/cli/commands/doctor.d.ts +3 -0
  10. package/dist/cli/commands/doctor.js +31 -0
  11. package/dist/cli/commands/generate.d.ts +6 -0
  12. package/dist/cli/commands/generate.js +24 -0
  13. package/dist/cli/commands/operations.d.ts +12 -0
  14. package/dist/cli/commands/operations.js +61 -0
  15. package/dist/cli/commands/schema.d.ts +11 -0
  16. package/dist/cli/commands/schema.js +1086 -0
  17. package/dist/cli/commands/serve.d.ts +9 -0
  18. package/dist/cli/commands/serve.js +18 -0
  19. package/dist/cli/commands/sync.d.ts +3 -0
  20. package/dist/cli/commands/sync.js +11 -0
  21. package/dist/cli/commands/types.d.ts +7 -0
  22. package/dist/cli/commands/types.js +37 -0
  23. package/dist/cli/commands/viewer.d.ts +6 -0
  24. package/dist/cli/commands/viewer.js +29 -0
  25. package/dist/cli/index.d.ts +2 -0
  26. package/dist/cli/index.js +108 -0
  27. package/dist/cli/output.d.ts +25 -0
  28. package/dist/cli/output.js +149 -0
  29. package/dist/cli/schema-prompt.d.ts +20 -0
  30. package/dist/cli/schema-prompt.js +66 -0
  31. package/dist/cli.d.ts +2 -0
  32. package/dist/cli.js +3 -0
  33. package/dist/client-cache.d.ts +105 -0
  34. package/dist/client-cache.js +916 -0
  35. package/dist/client.d.ts +64 -0
  36. package/dist/client.js +405 -0
  37. package/dist/config-public.d.ts +1 -0
  38. package/dist/config-public.js +1 -0
  39. package/dist/config.d.ts +54 -0
  40. package/dist/config.js +2 -0
  41. package/dist/csv.d.ts +1 -0
  42. package/dist/csv.js +1 -0
  43. package/dist/db.d.ts +3 -0
  44. package/dist/db.js +3 -0
  45. package/dist/doctor.d.ts +1 -0
  46. package/dist/doctor.js +1 -0
  47. package/dist/errors.d.ts +1 -0
  48. package/dist/errors.js +1 -0
  49. package/dist/features/config/defaults.d.ts +98 -0
  50. package/dist/features/config/defaults.js +95 -0
  51. package/dist/features/config/load.d.ts +11 -0
  52. package/dist/features/config/load.js +265 -0
  53. package/dist/features/config/public.d.ts +17 -0
  54. package/dist/features/config/public.js +75 -0
  55. package/dist/features/doctor/duplicate-ids.d.ts +18 -0
  56. package/dist/features/doctor/duplicate-ids.js +79 -0
  57. package/dist/features/doctor/field-consistency.d.ts +17 -0
  58. package/dist/features/doctor/field-consistency.js +48 -0
  59. package/dist/features/doctor/index.d.ts +39 -0
  60. package/dist/features/doctor/index.js +177 -0
  61. package/dist/features/doctor/relations.d.ts +22 -0
  62. package/dist/features/doctor/relations.js +90 -0
  63. package/dist/features/doctor/schema-guidance.d.ts +35 -0
  64. package/dist/features/doctor/schema-guidance.js +184 -0
  65. package/dist/features/generate/registry.d.ts +14 -0
  66. package/dist/features/generate/registry.js +37 -0
  67. package/dist/features/http/registry.d.ts +46 -0
  68. package/dist/features/http/registry.js +86 -0
  69. package/dist/features/operations/index.d.ts +49 -0
  70. package/dist/features/operations/index.js +199 -0
  71. package/dist/features/operations/maps.d.ts +1 -0
  72. package/dist/features/operations/maps.js +10 -0
  73. package/dist/features/operations/readiness.d.ts +30 -0
  74. package/dist/features/operations/readiness.js +228 -0
  75. package/dist/features/operations/runtime.d.ts +57 -0
  76. package/dist/features/operations/runtime.js +288 -0
  77. package/dist/features/runtime/collection.d.ts +51 -0
  78. package/dist/features/runtime/collection.js +198 -0
  79. package/dist/features/runtime/db.d.ts +152 -0
  80. package/dist/features/runtime/db.js +824 -0
  81. package/dist/features/runtime/document.d.ts +43 -0
  82. package/dist/features/runtime/document.js +111 -0
  83. package/dist/features/runtime/fanout.d.ts +24 -0
  84. package/dist/features/runtime/fanout.js +77 -0
  85. package/dist/features/runtime/json-pointer.d.ts +5 -0
  86. package/dist/features/runtime/json-pointer.js +49 -0
  87. package/dist/features/runtime/scope-state.d.ts +44 -0
  88. package/dist/features/runtime/scope-state.js +185 -0
  89. package/dist/features/runtime/state.d.ts +1 -0
  90. package/dist/features/runtime/state.js +1 -0
  91. package/dist/features/schema/api.d.ts +107 -0
  92. package/dist/features/schema/api.js +460 -0
  93. package/dist/features/schema/builders.d.ts +86 -0
  94. package/dist/features/schema/builders.js +110 -0
  95. package/dist/features/schema/fields.d.ts +38 -0
  96. package/dist/features/schema/fields.js +296 -0
  97. package/dist/features/schema/generated.d.ts +29 -0
  98. package/dist/features/schema/generated.js +32 -0
  99. package/dist/features/schema/locator.d.ts +16 -0
  100. package/dist/features/schema/locator.js +135 -0
  101. package/dist/features/schema/manifest.d.ts +91 -0
  102. package/dist/features/schema/manifest.js +384 -0
  103. package/dist/features/schema/metadata.d.ts +30 -0
  104. package/dist/features/schema/metadata.js +75 -0
  105. package/dist/features/schema/project.d.ts +46 -0
  106. package/dist/features/schema/project.js +442 -0
  107. package/dist/features/schema/relations.d.ts +38 -0
  108. package/dist/features/schema/relations.js +109 -0
  109. package/dist/features/schema/resolvers.d.ts +36 -0
  110. package/dist/features/schema/resolvers.js +111 -0
  111. package/dist/features/schema/resource.d.ts +75 -0
  112. package/dist/features/schema/resource.js +253 -0
  113. package/dist/features/schema/source-definitions.d.ts +21 -0
  114. package/dist/features/schema/source-definitions.js +29 -0
  115. package/dist/features/schema/sources.d.ts +83 -0
  116. package/dist/features/schema/sources.js +689 -0
  117. package/dist/features/schema/standard-schema.d.ts +57 -0
  118. package/dist/features/schema/standard-schema.js +232 -0
  119. package/dist/features/schema/validation.d.ts +69 -0
  120. package/dist/features/schema/validation.js +434 -0
  121. package/dist/features/storage/events.d.ts +12 -0
  122. package/dist/features/storage/events.js +30 -0
  123. package/dist/features/storage/json.d.ts +112 -0
  124. package/dist/features/storage/json.js +239 -0
  125. package/dist/features/storage/memory.d.ts +30 -0
  126. package/dist/features/storage/memory.js +44 -0
  127. package/dist/features/storage/resource-json.d.ts +31 -0
  128. package/dist/features/storage/resource-json.js +76 -0
  129. package/dist/features/storage/runtime.d.ts +37 -0
  130. package/dist/features/storage/runtime.js +184 -0
  131. package/dist/features/storage/source-metadata.d.ts +20 -0
  132. package/dist/features/storage/source-metadata.js +25 -0
  133. package/dist/features/storage/source.d.ts +37 -0
  134. package/dist/features/storage/source.js +60 -0
  135. package/dist/features/storage/static.d.ts +29 -0
  136. package/dist/features/storage/static.js +42 -0
  137. package/dist/features/sync/defaults.d.ts +21 -0
  138. package/dist/features/sync/defaults.js +21 -0
  139. package/dist/features/sync/index.d.ts +35 -0
  140. package/dist/features/sync/index.js +85 -0
  141. package/dist/features/sync/mirror-state.d.ts +14 -0
  142. package/dist/features/sync/mirror-state.js +4 -0
  143. package/dist/features/sync/runtime-dirs.d.ts +5 -0
  144. package/dist/features/sync/runtime-dirs.js +9 -0
  145. package/dist/features/sync/source-writes.d.ts +15 -0
  146. package/dist/features/sync/source-writes.js +27 -0
  147. package/dist/features/sync/synthetic-seed.d.ts +26 -0
  148. package/dist/features/sync/synthetic-seed.js +83 -0
  149. package/dist/features/viewer/manifest.d.ts +148 -0
  150. package/dist/features/viewer/manifest.js +165 -0
  151. package/dist/fs-utils.d.ts +1 -0
  152. package/dist/fs-utils.js +1 -0
  153. package/dist/generate/hono/app.d.ts +6 -0
  154. package/dist/generate/hono/app.js +51 -0
  155. package/dist/generate/hono/graphql.d.ts +7 -0
  156. package/dist/generate/hono/graphql.js +53 -0
  157. package/dist/generate/hono/index.d.ts +55 -0
  158. package/dist/generate/hono/index.js +140 -0
  159. package/dist/generate/hono/package.d.ts +6 -0
  160. package/dist/generate/hono/package.js +44 -0
  161. package/dist/generate/hono/readme.d.ts +13 -0
  162. package/dist/generate/hono/readme.js +28 -0
  163. package/dist/generate/hono/repository.d.ts +1 -0
  164. package/dist/generate/hono/repository.js +27 -0
  165. package/dist/generate/hono/rest.d.ts +1 -0
  166. package/dist/generate/hono/rest.js +38 -0
  167. package/dist/generate/hono/schema.d.ts +13 -0
  168. package/dist/generate/hono/schema.js +18 -0
  169. package/dist/generate/hono/sqlite.d.ts +20 -0
  170. package/dist/generate/hono/sqlite.js +266 -0
  171. package/dist/generate/hono/validators.d.ts +1 -0
  172. package/dist/generate/hono/validators.js +141 -0
  173. package/dist/generate/hono.d.ts +1 -0
  174. package/dist/generate/hono.js +1 -0
  175. package/dist/graphql/execute.d.ts +14 -0
  176. package/dist/graphql/execute.js +719 -0
  177. package/dist/graphql/http.d.ts +15 -0
  178. package/dist/graphql/http.js +29 -0
  179. package/dist/graphql/index.d.ts +3 -0
  180. package/dist/graphql/index.js +3 -0
  181. package/dist/graphql/parser.d.ts +54 -0
  182. package/dist/graphql/parser.js +433 -0
  183. package/dist/hono.d.ts +77 -0
  184. package/dist/hono.js +1 -0
  185. package/dist/index.d.ts +1065 -0
  186. package/dist/index.js +14 -0
  187. package/dist/integrations/hono.d.ts +136 -0
  188. package/dist/integrations/hono.js +508 -0
  189. package/dist/integrations/kv.d.ts +69 -0
  190. package/dist/integrations/kv.js +69 -0
  191. package/dist/integrations/postgres.d.ts +52 -0
  192. package/dist/integrations/postgres.js +113 -0
  193. package/dist/integrations/sqlite.d.ts +112 -0
  194. package/dist/integrations/sqlite.js +489 -0
  195. package/dist/integrations/vite.d.ts +45 -0
  196. package/dist/integrations/vite.js +111 -0
  197. package/dist/json.d.ts +48 -0
  198. package/dist/json.js +1 -0
  199. package/dist/jsonc.d.ts +1 -0
  200. package/dist/jsonc.js +1 -0
  201. package/dist/kv.d.ts +24 -0
  202. package/dist/kv.js +1 -0
  203. package/dist/mock.d.ts +1 -0
  204. package/dist/mock.js +1 -0
  205. package/dist/names.d.ts +1 -0
  206. package/dist/names.js +1 -0
  207. package/dist/operations.d.ts +3 -0
  208. package/dist/operations.js +3 -0
  209. package/dist/postgres.d.ts +24 -0
  210. package/dist/postgres.js +1 -0
  211. package/dist/redis.d.ts +14 -0
  212. package/dist/redis.js +1 -0
  213. package/dist/rest/formats.d.ts +80 -0
  214. package/dist/rest/formats.js +318 -0
  215. package/dist/rest/handler.d.ts +111 -0
  216. package/dist/rest/handler.js +833 -0
  217. package/dist/rest/shape.d.ts +33 -0
  218. package/dist/rest/shape.js +218 -0
  219. package/dist/schema-builders.d.ts +1 -0
  220. package/dist/schema-builders.js +1 -0
  221. package/dist/schema-manifest.d.ts +1 -0
  222. package/dist/schema-manifest.js +1 -0
  223. package/dist/schema.d.ts +193 -0
  224. package/dist/schema.js +6 -0
  225. package/dist/server.d.ts +116 -0
  226. package/dist/server.js +601 -0
  227. package/dist/shared/csv.d.ts +8 -0
  228. package/dist/shared/csv.js +149 -0
  229. package/dist/shared/errors.d.ts +40 -0
  230. package/dist/shared/errors.js +55 -0
  231. package/dist/shared/fs-utils.d.ts +4 -0
  232. package/dist/shared/fs-utils.js +30 -0
  233. package/dist/shared/jsonc.d.ts +2 -0
  234. package/dist/shared/jsonc.js +99 -0
  235. package/dist/shared/mock.d.ts +40 -0
  236. package/dist/shared/mock.js +83 -0
  237. package/dist/shared/names.d.ts +28 -0
  238. package/dist/shared/names.js +127 -0
  239. package/dist/shared/operations.d.ts +32 -0
  240. package/dist/shared/operations.js +302 -0
  241. package/dist/sqlite.d.ts +24 -0
  242. package/dist/sqlite.js +1 -0
  243. package/dist/state.d.ts +1 -0
  244. package/dist/state.js +1 -0
  245. package/dist/sync.d.ts +1 -0
  246. package/dist/sync.js +1 -0
  247. package/dist/tracing.d.ts +95 -0
  248. package/dist/tracing.js +260 -0
  249. package/dist/types.d.ts +51 -0
  250. package/dist/types.js +285 -0
  251. package/dist/viewer-manifest.d.ts +1 -0
  252. package/dist/viewer-manifest.js +1 -0
  253. package/dist/vite.d.ts +59 -0
  254. package/dist/vite.js +1 -0
  255. package/dist/web/json-viewer.d.ts +5 -0
  256. package/dist/web/json-viewer.js +176 -0
  257. package/dist/web/viewer.d.ts +12 -0
  258. package/dist/web/viewer.js +1015 -0
  259. package/docs/README.md +42 -0
  260. package/docs/architecture.md +112 -0
  261. package/docs/ci-and-release.md +177 -0
  262. package/docs/cms-storage-patterns.md +108 -0
  263. package/docs/concepts.md +141 -0
  264. package/docs/configuration.md +552 -0
  265. package/docs/fixtures-and-schemas.md +527 -0
  266. package/docs/fork-branch-workflows.md +108 -0
  267. package/docs/generated-files.md +174 -0
  268. package/docs/getting-started.md +165 -0
  269. package/docs/integrations.md +206 -0
  270. package/docs/json-production.md +120 -0
  271. package/docs/package-api.md +418 -0
  272. package/docs/prototype-to-production.md +378 -0
  273. package/docs/server-and-viewer.md +466 -0
  274. package/docs/store-graduation.md +120 -0
  275. package/docs/typescript-schema-sources.md +79 -0
  276. package/examples/advanced/README.md +55 -0
  277. package/examples/advanced/db/projects.schema.jsonc +44 -0
  278. package/examples/advanced/db/settings.jsonc +9 -0
  279. package/examples/advanced/db/users.json +23 -0
  280. package/examples/advanced/db/users.schema.mjs +31 -0
  281. package/examples/advanced/db.config.mjs +18 -0
  282. package/examples/advanced/example.json +5 -0
  283. package/examples/advanced/src/generated/db.types.d.ts +64 -0
  284. package/examples/basic/README.md +95 -0
  285. package/examples/basic/db/operations/get-user.jsonc +8 -0
  286. package/examples/basic/db/settings.json +7 -0
  287. package/examples/basic/db/users.schema.jsonc +36 -0
  288. package/examples/basic/db.config.mjs +68 -0
  289. package/examples/basic/example.json +5 -0
  290. package/examples/basic/src/generated/db.types.d.ts +39 -0
  291. package/examples/cms-json-publish/README.md +21 -0
  292. package/examples/cms-json-publish/db/navigation.json +7 -0
  293. package/examples/cms-json-publish/db/pages.json +18 -0
  294. package/examples/cms-json-publish/example.json +5 -0
  295. package/examples/cms-json-publish/src/cms.mjs +104 -0
  296. package/examples/computed-fields/README.md +93 -0
  297. package/examples/computed-fields/db/orders.schema.mjs +62 -0
  298. package/examples/computed-fields/db/posts.schema.mjs +59 -0
  299. package/examples/computed-fields/db/products.schema.mjs +39 -0
  300. package/examples/computed-fields/db/users.schema.mjs +43 -0
  301. package/examples/computed-fields/db.config.mjs +15 -0
  302. package/examples/computed-fields/example.json +5 -0
  303. package/examples/computed-fields/src/generated/db.types.d.ts +81 -0
  304. package/examples/content-collections/README.md +91 -0
  305. package/examples/content-collections/db/authors.json +12 -0
  306. package/examples/content-collections/db/authors.schema.mjs +20 -0
  307. package/examples/content-collections/db/blog/draft-roadmap.mdx +12 -0
  308. package/examples/content-collections/db/blog/index.schema.mjs +61 -0
  309. package/examples/content-collections/db/blog/launch-notes.mdx +15 -0
  310. package/examples/content-collections/db/docs/index.schema.mjs +32 -0
  311. package/examples/content-collections/db/docs/intro.mdx +11 -0
  312. package/examples/content-collections/db/docs/schema-workflow.mdx +10 -0
  313. package/examples/content-collections/db/site.schema.jsonc +21 -0
  314. package/examples/content-collections/db.config.mjs +26 -0
  315. package/examples/content-collections/example.json +5 -0
  316. package/examples/content-collections/src/content-preview.mjs +66 -0
  317. package/examples/content-collections/src/generated/db.types.d.ts +81 -0
  318. package/examples/csv/README.md +52 -0
  319. package/examples/csv/db/customers.csv +4 -0
  320. package/examples/csv/db.config.mjs +13 -0
  321. package/examples/csv/example.json +5 -0
  322. package/examples/data-first/README.md +54 -0
  323. package/examples/data-first/db/posts.json +16 -0
  324. package/examples/data-first/db/settings.json +8 -0
  325. package/examples/data-first/db/users.json +14 -0
  326. package/examples/data-first/db.config.mjs +13 -0
  327. package/examples/data-first/example.json +5 -0
  328. package/examples/diagnostics/README.md +55 -0
  329. package/examples/diagnostics/db/projects.schema.jsonc +27 -0
  330. package/examples/diagnostics/db/users.json +9 -0
  331. package/examples/diagnostics/db/users.schema.jsonc +23 -0
  332. package/examples/diagnostics/db.config.mjs +16 -0
  333. package/examples/diagnostics/example.json +5 -0
  334. package/examples/free-plan-upgrade/README.md +22 -0
  335. package/examples/free-plan-upgrade/db/appSettings.json +4 -0
  336. package/examples/free-plan-upgrade/db/projects.json +7 -0
  337. package/examples/free-plan-upgrade/example.json +5 -0
  338. package/examples/free-plan-upgrade/src/upgrade-tenant-to-paid.mjs +105 -0
  339. package/examples/hono-auth/README.md +74 -0
  340. package/examples/hono-auth/db/pages.schema.jsonc +44 -0
  341. package/examples/hono-auth/db/users.schema.jsonc +42 -0
  342. package/examples/hono-auth/db.config.mjs +17 -0
  343. package/examples/hono-auth/example.json +5 -0
  344. package/examples/hono-auth/package.json +14 -0
  345. package/examples/hono-auth/src/app.mjs +79 -0
  346. package/examples/hono-auth/src/server.mjs +13 -0
  347. package/examples/production-json/README.md +102 -0
  348. package/examples/production-json/db/appSettings.schema.jsonc +41 -0
  349. package/examples/production-json/db/featureFlags.schema.jsonc +84 -0
  350. package/examples/production-json/db/operations/get-control-plane.jsonc +6 -0
  351. package/examples/production-json/db/operations/get-feature-flag.jsonc +9 -0
  352. package/examples/production-json/db/operations/list-feature-flags.jsonc +8 -0
  353. package/examples/production-json/db/operations/read-public-settings.jsonc +8 -0
  354. package/examples/production-json/db.config.mjs +33 -0
  355. package/examples/production-json/example.json +5 -0
  356. package/examples/production-json/src/client-demo.mjs +28 -0
  357. package/examples/production-json/src/generated/db.types.d.ts +60 -0
  358. package/examples/relations/README.md +56 -0
  359. package/examples/relations/db/posts.schema.jsonc +46 -0
  360. package/examples/relations/db/users.schema.jsonc +34 -0
  361. package/examples/relations/db.config.mjs +13 -0
  362. package/examples/relations/example.json +5 -0
  363. package/examples/rest-client/README.md +54 -0
  364. package/examples/rest-client/db/settings.json +5 -0
  365. package/examples/rest-client/db/users.schema.jsonc +42 -0
  366. package/examples/rest-client/db.config.mjs +13 -0
  367. package/examples/rest-client/example.json +5 -0
  368. package/examples/rest-client/src/client-demo.mjs +24 -0
  369. package/examples/schema-first/README.md +55 -0
  370. package/examples/schema-first/db/auditEvents.schema.jsonc +24 -0
  371. package/examples/schema-first/db/settings.schema.jsonc +29 -0
  372. package/examples/schema-first/db/users.schema.jsonc +36 -0
  373. package/examples/schema-first/db.config.mjs +15 -0
  374. package/examples/schema-first/example.json +5 -0
  375. package/examples/schema-first/src/generated/db.types.d.ts +47 -0
  376. package/examples/schema-manifest/README.md +50 -0
  377. package/examples/schema-manifest/db/projects.schema.jsonc +48 -0
  378. package/examples/schema-manifest/db/users.schema.jsonc +35 -0
  379. package/examples/schema-manifest/db.config.mjs +41 -0
  380. package/examples/schema-manifest/example.json +5 -0
  381. package/examples/schema-manifest/src/generated/db.schema.json +130 -0
  382. package/examples/schema-manifest/src/generated/db.types.d.ts +50 -0
  383. package/examples/schema-ui/README.md +103 -0
  384. package/examples/schema-ui/db/pages.schema.jsonc +53 -0
  385. package/examples/schema-ui/db/users.schema.jsonc +30 -0
  386. package/examples/schema-ui/db.config.mjs +55 -0
  387. package/examples/schema-ui/example.json +5 -0
  388. package/examples/schema-ui/src/cms-ssr.mjs +276 -0
  389. package/examples/schema-ui/src/generated/db.schema.json +133 -0
  390. package/examples/schema-ui/src/generated/db.types.d.ts +46 -0
  391. package/examples/schema-ui/src/render-admin.mjs +175 -0
  392. package/examples/schema-ui/src/schema-ui-ssr-handler.mjs +149 -0
  393. package/examples/schema-ui/src/start-schema-ui-server.mjs +140 -0
  394. package/examples/standard-schema/README.md +55 -0
  395. package/examples/standard-schema/db/settings.schema.mjs +22 -0
  396. package/examples/standard-schema/db/users.schema.mjs +72 -0
  397. package/examples/standard-schema/example.json +5 -0
  398. package/package.json +108 -0
@@ -0,0 +1,833 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { parseCsvRecords } from '../csv.js';
4
+ import { dbError, listChoices, serializeError } from '../errors.js';
5
+ import { resolveResource, resourceNameCandidates } from '../names.js';
6
+ import { makeGeneratedSchema } from '../schema.js';
7
+ import { syncDb } from '../sync.js';
8
+ import { renderViewerManifest } from '../viewer-manifest.js';
9
+ import { renderDbViewer } from '../web/viewer.js';
10
+ import { availableRestFormats, negotiateRestFormat, resolveRestFormat, restFormatMetadata } from './formats.js';
11
+ import { shapeCollectionRead } from './shape.js';
12
+ import { tracePhase, tracePhaseSync } from '../tracing.js';
13
+ export async function handleRestRequest(db, request, response, url = new URL(request.url ?? '/', 'http://db.local'), options = {}) {
14
+ try {
15
+ await handleRestRequestUnsafe(db, request, response, url, options);
16
+ }
17
+ catch (error) {
18
+ asRequestTrace(options.trace)?.setError(error);
19
+ sendJson(response, error.status ?? 500, serializeError(error, 'REST_ERROR'));
20
+ }
21
+ }
22
+ async function handleRestRequestUnsafe(db, request, response, url, options) {
23
+ const routeOptions = normalizeRestRouteOptions(db, options);
24
+ const trace = routeOptions.trace;
25
+ if (request.method === 'GET' && url.pathname === routeOptions.viewerPath) {
26
+ setRestTraceRoute(trace, routeOptions, { route: 'viewer', operation: 'render' });
27
+ sendText(response, 200, renderDbViewer({
28
+ graphqlPath: routeOptions.graphqlPath,
29
+ schemaPath: routeOptions.schemaPath,
30
+ manifestPath: routeOptions.manifestJsonPath,
31
+ eventsPath: routeOptions.eventsPath,
32
+ importPath: routeOptions.importPath,
33
+ restBatchPath: routeOptions.batchPath,
34
+ restBasePath: routeOptions.restBasePath,
35
+ sourceDirLabel: sourceDirLabel(db.config),
36
+ }), 'text/html; charset=utf-8');
37
+ return;
38
+ }
39
+ if (request.method === 'POST' && url.pathname === routeOptions.batchPath) {
40
+ setRestTraceRoute(trace, routeOptions, { operation: 'batch' });
41
+ if (!routeOptions.resourceRoutesEnabled) {
42
+ sendRestDisabled(response, 'REST batch routes are disabled.');
43
+ return;
44
+ }
45
+ const body = await tracePhase(trace, 'request-body', () => readJsonBody(request, {
46
+ maxBytes: maxBodyBytes(db),
47
+ }));
48
+ const result = await tryRest(async () => tracePhase(trace, 'batch-execution', () => executeRestBatch(db, body, routeOptions), {
49
+ itemCount: Array.isArray(body) ? body.length : Array.isArray(body?.requests) ? (body.requests).length : undefined,
50
+ }));
51
+ sendJson(response, result.status, result.body);
52
+ return;
53
+ }
54
+ if (request.method === 'POST' && url.pathname === routeOptions.importPath) {
55
+ setRestTraceRoute(trace, routeOptions, { route: 'import', operation: 'csv' });
56
+ sendJson(response, 201, await tracePhase(trace, 'import-csv', () => importCsvFixture(db, request, routeOptions)));
57
+ return;
58
+ }
59
+ if (request.method === 'GET' && url.pathname === routeOptions.schemaPath) {
60
+ setRestTraceRoute(trace, routeOptions, { route: 'schema', operation: 'read' });
61
+ sendJson(response, 200, makeGeneratedSchema([...db.resources.values()], (db.diagnostics ?? [])));
62
+ return;
63
+ }
64
+ const manifestFormat = request.method === 'GET'
65
+ ? manifestResponseFormat(url, request, routeOptions, db.config)
66
+ : null;
67
+ if (manifestFormat) {
68
+ setRestTraceRoute(trace, routeOptions, { route: 'manifest', operation: 'render' });
69
+ const manifest = tracePhaseSync(trace, 'manifest-build', () => renderViewerManifest([...db.resources.values()], db.config, {
70
+ diagnostics: db.diagnostics ?? [],
71
+ routes: routeOptions,
72
+ }));
73
+ const resolved = resolveRestFormat(db.config, manifestFormat, 'manifest');
74
+ if (!resolved) {
75
+ sendUnknownFormat(response, manifestFormat, db.config, 'manifest');
76
+ return;
77
+ }
78
+ const result = await tracePhase(trace, 'response-formatting', () => resolved.renderer({
79
+ db,
80
+ data: manifest,
81
+ manifest,
82
+ format: resolved.key,
83
+ request,
84
+ url,
85
+ routes: routeOptions,
86
+ target: 'manifest',
87
+ }), {
88
+ format: resolved.key,
89
+ target: 'manifest',
90
+ });
91
+ const normalized = normalizeFormatResult(result, resolved.contentType);
92
+ sendText(response, normalized.status, normalized.body, normalized.contentType);
93
+ return;
94
+ }
95
+ const resourceUrl = tracePhaseSync(trace, 'rest-route', () => restResourceUrl(url, routeOptions));
96
+ const [rawRouteName, rawId] = resourceUrl.pathname.split('/').filter(Boolean);
97
+ const { routeName, id, format } = parseFormattedResourcePath(rawRouteName, rawId);
98
+ if (!routeName) {
99
+ setRestTraceRoute(trace, routeOptions, { operation: 'discovery' });
100
+ const discovery = rootDiscovery(db, routeOptions);
101
+ if (request.method === 'GET' && requestPrefersHtml(db.config, request)) {
102
+ sendText(response, 200, renderRootDiscovery(discovery), 'text/html; charset=utf-8');
103
+ return;
104
+ }
105
+ sendJson(response, 200, discovery);
106
+ return;
107
+ }
108
+ const resource = tracePhaseSync(trace, 'resource-lookup', () => findResourceByRoute(db, routeName), {
109
+ routeName,
110
+ });
111
+ if (!resource) {
112
+ setRestTraceRoute(trace, routeOptions, { resource: routeName, operation: 'unknown' });
113
+ sendJson(response, 404, {
114
+ error: {
115
+ code: 'REST_UNKNOWN_RESOURCE',
116
+ message: `Unknown REST resource "${routeName}".`,
117
+ hint: `Use one of: ${listChoices([...db.resources.values()].map((resource) => resource.routePath))}.`,
118
+ details: {
119
+ routeName,
120
+ resource: routeName,
121
+ requestedResource: routeName,
122
+ normalizedCandidates: resourceNameCandidates(routeName),
123
+ availableResources: db.resourceNames(),
124
+ availableRoutes: [...db.resources.values()].map((resource) => resource.routePath),
125
+ },
126
+ },
127
+ });
128
+ return;
129
+ }
130
+ if (!routeOptions.resourceRoutesEnabled) {
131
+ setRestTraceRoute(trace, routeOptions, { resource: resource.name, operation: 'disabled' });
132
+ sendRestDisabled(response, `REST resource routes are disabled. Cannot serve "${routeName}".`, {
133
+ resource: resource.name,
134
+ routeName,
135
+ });
136
+ return;
137
+ }
138
+ if (resource.kind === 'collection') {
139
+ await handleCollection(db, resource, id, request, response, resourceUrl, format, routeOptions);
140
+ }
141
+ else {
142
+ await handleDocument(db, resource, request, response, format, routeOptions);
143
+ }
144
+ }
145
+ function parseFormattedResourcePath(routeName, id) {
146
+ if (!routeName) {
147
+ return { routeName, id, format: null };
148
+ }
149
+ if (id) {
150
+ const parsedId = splitFormatExtension(id);
151
+ return {
152
+ routeName,
153
+ id: parsedId.name,
154
+ format: parsedId.format,
155
+ };
156
+ }
157
+ const parsedRoute = splitFormatExtension(routeName);
158
+ return {
159
+ routeName: parsedRoute.name,
160
+ id,
161
+ format: parsedRoute.format,
162
+ };
163
+ }
164
+ function splitFormatExtension(value) {
165
+ const match = String(value).match(/^(.+)\.([A-Za-z][A-Za-z0-9_-]*)$/);
166
+ if (!match) {
167
+ return { name: String(value), format: null };
168
+ }
169
+ return {
170
+ name: match[1],
171
+ format: match[2],
172
+ };
173
+ }
174
+ export function findResourceByRoute(db, routeName) {
175
+ return resolveResource(db.resources, routeName).resource
176
+ ?? [...db.resources.values()].find((candidate) => candidate.routePath.slice(1) === routeName);
177
+ }
178
+ export async function executeRestBatch(db, body, options = {}) {
179
+ const requests = Array.isArray(body) ? body : body.requests;
180
+ const batchPath = batchPathForOptions(options, db);
181
+ if (!Array.isArray(requests)) {
182
+ throw dbError('REST_BATCH_INVALID_BODY', 'REST batch body must be an array or an object with a requests array.', {
183
+ status: 400,
184
+ hint: `Send POST ${batchPath} with [{ "method": "GET", "path": "/users" }].`,
185
+ details: {
186
+ receivedType: body === null ? 'null' : Array.isArray(body) ? 'array' : typeof body,
187
+ },
188
+ });
189
+ }
190
+ const results = [];
191
+ for (const [index, request] of requests.entries()) {
192
+ const itemDetails = batchItemTraceDetails(index, request);
193
+ const trace = asRequestTrace(options.trace);
194
+ try {
195
+ const result = await tracePhase(trace, 'batch-item', () => executeRestBatchItem(db, request, options), itemDetails);
196
+ results.push({
197
+ index,
198
+ ...result,
199
+ });
200
+ }
201
+ catch (error) {
202
+ trace?.setError(error);
203
+ trace?.addPhase('batch-item', 0, {
204
+ ...itemDetails,
205
+ error: error.code ? String(error.code) : 'REST_ERROR',
206
+ });
207
+ results.push({
208
+ index,
209
+ status: error.status ?? 500,
210
+ headers: {
211
+ 'content-type': 'application/json; charset=utf-8',
212
+ },
213
+ body: serializeError(error, 'REST_ERROR'),
214
+ });
215
+ }
216
+ }
217
+ return results;
218
+ }
219
+ export async function readRawBody(request, options = {}) {
220
+ const chunks = [];
221
+ const maxBytes = Number(options.maxBytes ?? Infinity);
222
+ let byteLength = 0;
223
+ for await (const chunk of request) {
224
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
225
+ byteLength += buffer.length;
226
+ if (byteLength > maxBytes) {
227
+ throw dbError('JSON_BODY_TOO_LARGE', `Request body is too large. Received more than ${maxBytes} bytes.`, {
228
+ status: 413,
229
+ hint: 'Send a smaller JSON payload or increase server.maxBodyBytes in db.config.mjs for local development.',
230
+ details: {
231
+ maxBodyBytes: maxBytes,
232
+ },
233
+ });
234
+ }
235
+ chunks.push(buffer);
236
+ }
237
+ return Buffer.concat(chunks);
238
+ }
239
+ export async function readJsonBody(request, options = {}) {
240
+ const text = (await readRawBody(request, options)).toString('utf8').trim();
241
+ try {
242
+ return text ? JSON.parse(text) : {};
243
+ }
244
+ catch (error) {
245
+ throw dbError('REST_INVALID_JSON_BODY', 'Request body is not valid JSON.', {
246
+ status: 400,
247
+ hint: 'Check for trailing commas, unquoted property names, or an incomplete JSON object.',
248
+ details: {
249
+ parserMessage: error.message,
250
+ },
251
+ });
252
+ }
253
+ }
254
+ function maxBodyBytes(db) {
255
+ return Number(db.config.server?.maxBodyBytes ?? 1048576);
256
+ }
257
+ function asRequestTrace(value) {
258
+ return value ? value : null;
259
+ }
260
+ function normalizeRestRouteOptions(db, options = {}) {
261
+ const apiBase = normalizeBasePath(options.apiBase ?? db.config.server?.apiBase ?? '/__db');
262
+ return {
263
+ apiBase,
264
+ viewerPath: options.viewerPath ?? apiBase,
265
+ manifestPath: options.manifestPath ?? `${apiBase}/manifest`,
266
+ manifestJsonPath: options.manifestJsonPath ?? `${apiBase}/manifest.json`,
267
+ manifestHtmlPath: options.manifestHtmlPath ?? `${apiBase}/manifest.html`,
268
+ manifestMarkdownPath: options.manifestMarkdownPath ?? `${apiBase}/manifest.md`,
269
+ schemaPath: options.schemaPath ?? `${apiBase}/schema`,
270
+ batchPath: options.batchPath ?? `${apiBase}/batch`,
271
+ importPath: options.importPath ?? `${apiBase}/import`,
272
+ eventsPath: options.eventsPath ?? `${apiBase}/events`,
273
+ graphqlPath: options.graphqlPath ?? db.config.graphql?.path ?? '/graphql',
274
+ restBasePath: options.restBasePath ?? '',
275
+ resourceRoutesEnabled: options.resourceRoutesEnabled ?? db.config.rest?.enabled !== false,
276
+ trace: asRequestTrace(options.trace),
277
+ traceNested: options.traceNested === true,
278
+ };
279
+ }
280
+ function restResourceUrl(url, options) {
281
+ if (!options.restBasePath || !pathStartsWith(url.pathname, options.restBasePath)) {
282
+ return url;
283
+ }
284
+ const next = new URL(url.href);
285
+ const stripped = next.pathname.slice(options.restBasePath.length);
286
+ next.pathname = stripped.startsWith('/') ? stripped : `/${stripped}`;
287
+ return next;
288
+ }
289
+ function pathStartsWith(pathname, basePath) {
290
+ return pathname === basePath || pathname.startsWith(`${basePath}/`);
291
+ }
292
+ function normalizeBasePath(value) {
293
+ const pathValue = `/${String(value ?? '').replace(/^\/+/, '').replace(/\/+$/, '')}`;
294
+ return pathValue === '/' ? '' : pathValue;
295
+ }
296
+ function sourceDirLabel(config) {
297
+ const relative = path.relative(config.cwd, config.sourceDir) || '.';
298
+ return `${relative.split(path.sep).join('/')}/`;
299
+ }
300
+ function rootDiscovery(db, options = {}) {
301
+ const apiBase = normalizeBasePath(options.apiBase ?? db.config.server?.apiBase ?? '/__db');
302
+ const schemaPath = options.schemaPath ?? `${apiBase}/schema`;
303
+ const manifestPath = options.manifestPath ?? `${apiBase}/manifest`;
304
+ const manifestJsonPath = options.manifestJsonPath ?? `${apiBase}/manifest.json`;
305
+ const manifestHtmlPath = options.manifestHtmlPath ?? `${apiBase}/manifest.html`;
306
+ const manifestMarkdownPath = options.manifestMarkdownPath ?? `${apiBase}/manifest.md`;
307
+ const viewerPath = options.viewerPath ?? apiBase;
308
+ const graphqlPath = options.graphqlPath ?? db.config.graphql?.path ?? '/graphql';
309
+ const graphqlEnabled = db.config.graphql?.enabled !== false;
310
+ const resourceRoutesEnabled = options.resourceRoutesEnabled ?? db.config.rest?.enabled !== false;
311
+ const viewers = viewerLinks(db.config, viewerPath);
312
+ const formats = restFormatMetadata(db.config, {
313
+ manifestPath,
314
+ manifestJsonPath,
315
+ manifestHtmlPath,
316
+ manifestMarkdownPath,
317
+ });
318
+ return {
319
+ resources: db.resourceNames(),
320
+ viewer: viewerPath,
321
+ viewers,
322
+ formats,
323
+ manifest: manifestPath,
324
+ manifestJson: manifestJsonPath,
325
+ manifestHtml: manifestHtmlPath,
326
+ manifestMarkdown: manifestMarkdownPath,
327
+ schema: schemaPath,
328
+ graphql: graphqlEnabled ? graphqlPath : null,
329
+ links: {
330
+ viewer: viewerPath,
331
+ viewers,
332
+ formats,
333
+ manifest: manifestPath,
334
+ manifestJson: manifestJsonPath,
335
+ manifestHtml: manifestHtmlPath,
336
+ manifestMarkdown: manifestMarkdownPath,
337
+ schema: schemaPath,
338
+ graphql: graphqlEnabled ? graphqlPath : null,
339
+ resources: resourceRoutesEnabled
340
+ ? Object.fromEntries([...db.resources.values()].map((resource) => [resource.name, joinPaths(options.restBasePath ?? '', resource.routePath)]))
341
+ : {},
342
+ },
343
+ };
344
+ }
345
+ function viewerLinks(config, viewerPath) {
346
+ const configuredLinks = Array.isArray(config.server?.viewerLinks)
347
+ ? config.server.viewerLinks
348
+ : [];
349
+ return [
350
+ {
351
+ label: 'Data Viewer',
352
+ href: viewerPath,
353
+ source: 'built-in',
354
+ },
355
+ ...configuredLinks.map(normalizeViewerLink).filter(Boolean),
356
+ ];
357
+ }
358
+ function normalizeViewerLink(link) {
359
+ if (!link || typeof link !== 'object') {
360
+ return null;
361
+ }
362
+ const record = link;
363
+ const href = typeof record.href === 'string' ? record.href : record.url;
364
+ if (typeof href !== 'string' || href.trim() === '') {
365
+ return null;
366
+ }
367
+ return {
368
+ label: typeof record.label === 'string' && record.label.trim() ? record.label : 'Custom Viewer',
369
+ href,
370
+ source: 'custom',
371
+ };
372
+ }
373
+ function joinPaths(basePath, routePath) {
374
+ if (!basePath) {
375
+ return String(routePath);
376
+ }
377
+ const base = `/${String(basePath).replace(/^\/+/, '').replace(/\/+$/, '')}`;
378
+ const route = `/${String(routePath || '/').replace(/^\/+/, '')}`;
379
+ return `${base}${route === '/' ? '' : route}`;
380
+ }
381
+ function requestPrefersHtml(config, request) {
382
+ return negotiateRestFormat(config, request, 'resource') === 'html';
383
+ }
384
+ function renderRootDiscovery(discovery) {
385
+ const viewerLinksHtml = discovery.links.viewers.map((viewer) => (`<li><a href="${escapeHtml(viewer.href)}">${escapeHtml(viewer.label)}</a> <code>${escapeHtml(viewer.href)}</code></li>`)).join('');
386
+ const resourceLinks = Object.entries(discovery.links.resources).map(([name, routePath]) => (`<li><a href="${escapeHtml(routePath)}">${escapeHtml(name)}</a> <code>${escapeHtml(routePath)}</code></li>`)).join('');
387
+ const graphqlLink = discovery.graphql
388
+ ? `<li><a href="${escapeHtml(discovery.graphql)}">GraphQL</a> <code>${escapeHtml(discovery.graphql)}</code></li>`
389
+ : '';
390
+ return `<!doctype html>
391
+ <html lang="en">
392
+ <head>
393
+ <meta charset="utf-8">
394
+ <meta name="viewport" content="width=device-width, initial-scale=1">
395
+ <title>db</title>
396
+ <style>
397
+ body { margin: 0; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: #111827; background: #f8fafc; }
398
+ main { max-width: 760px; margin: 0 auto; padding: 48px 20px; }
399
+ h1 { margin: 0 0 8px; font-size: 2rem; line-height: 1.1; }
400
+ p { color: #4b5563; }
401
+ section { margin-top: 24px; }
402
+ ul { display: grid; gap: 10px; padding: 0; list-style: none; }
403
+ li { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; padding: 12px; border: 1px solid #d1d5db; border-radius: 8px; background: white; }
404
+ a { font-weight: 700; color: #047857; text-decoration: none; }
405
+ a:hover { text-decoration: underline; }
406
+ code { color: #475569; }
407
+ </style>
408
+ </head>
409
+ <body>
410
+ <main>
411
+ <h1>db</h1>
412
+ <p>Local fixture database resources and tools.</p>
413
+
414
+ <section aria-labelledby="tools-heading">
415
+ <h2 id="tools-heading">Tools</h2>
416
+ <ul>
417
+ ${viewerLinksHtml}
418
+ <li><a href="${escapeHtml(discovery.manifest)}">Viewer Manifest</a> <code>${escapeHtml(discovery.manifest)}</code></li>
419
+ <li><a href="${escapeHtml(discovery.schema)}">Schema</a> <code>${escapeHtml(discovery.schema)}</code></li>
420
+ ${graphqlLink}
421
+ </ul>
422
+ </section>
423
+
424
+ <section aria-labelledby="resources-heading">
425
+ <h2 id="resources-heading">Resources</h2>
426
+ <ul>${resourceLinks || '<li>No resources loaded.</li>'}</ul>
427
+ </section>
428
+ </main>
429
+ </body>
430
+ </html>`;
431
+ }
432
+ function escapeHtml(value) {
433
+ return String(value)
434
+ .replaceAll('&', '&amp;')
435
+ .replaceAll('<', '&lt;')
436
+ .replaceAll('>', '&gt;')
437
+ .replaceAll('"', '&quot;')
438
+ .replaceAll("'", '&#39;');
439
+ }
440
+ async function importCsvFixture(db, request, options = {}) {
441
+ const filename = csvFilenameFromRequest(request);
442
+ const body = await readRawBody(request, {
443
+ maxBytes: maxBodyBytes(db),
444
+ });
445
+ parseCsvRecords(body.toString('utf8'), filename);
446
+ await mkdir(db.config.sourceDir, { recursive: true });
447
+ const outFile = path.join(db.config.sourceDir, filename);
448
+ await writeFile(outFile, body);
449
+ const project = await syncDb(db.config, { allowErrors: true });
450
+ db.resources = new Map(project.resources.map((resource) => [resource.name, resource]));
451
+ db.diagnostics = project.diagnostics;
452
+ db.schemaVersion = Date.now();
453
+ const resourceName = filename.replace(/\.csv$/i, '');
454
+ const resource = db.resources.get(resourceName);
455
+ return {
456
+ resource: resourceName,
457
+ filename,
458
+ dataPath: path.relative(db.config.cwd, outFile),
459
+ statePath: path.relative(db.config.cwd, path.join(db.config.stateDir, 'state', `${resourceName}.json`)),
460
+ routePath: resource?.routePath ?? `/${resourceName}`,
461
+ viewerPath: `${options.viewerPath ?? normalizeBasePath(db.config.server?.apiBase ?? '/__db')}?resource=${encodeURIComponent(resourceName)}`,
462
+ logs: project.logs,
463
+ };
464
+ }
465
+ function csvFilenameFromRequest(request) {
466
+ const rawName = headerValue(request, 'x-db-file-name');
467
+ if (!rawName) {
468
+ throw dbError('CSV_IMPORT_MISSING_FILENAME', 'CSV import requires an x-db-file-name header.', {
469
+ status: 400,
470
+ hint: 'Upload with a filename ending in .csv.',
471
+ });
472
+ }
473
+ if (!String(rawName).toLowerCase().endsWith('.csv')) {
474
+ throw dbError('CSV_IMPORT_INVALID_EXTENSION', `CSV import only accepts .csv files: ${rawName}`, {
475
+ status: 400,
476
+ hint: 'Choose a CSV file such as users.csv or products.csv.',
477
+ });
478
+ }
479
+ const base = path.basename(String(rawName)).replace(/\.csv$/i, '');
480
+ const words = base.match(/[A-Za-z0-9]+/g) ?? [];
481
+ const resourceName = words.map((word, index) => {
482
+ const lower = word.toLowerCase();
483
+ return index === 0 ? lower : lower.charAt(0).toUpperCase() + lower.slice(1);
484
+ }).join('') || 'importedCsv';
485
+ return `${/^\d/.test(resourceName) ? `csv${resourceName}` : resourceName}.csv`;
486
+ }
487
+ function headerValue(request, name) {
488
+ if (typeof request.headers?.get === 'function') {
489
+ return request.headers.get(name);
490
+ }
491
+ return request.headers?.[name] ?? request.headers?.[name.toLowerCase()];
492
+ }
493
+ export function sendJson(response, status, body) {
494
+ if (status === 204) {
495
+ response.writeHead(status);
496
+ response.end();
497
+ return;
498
+ }
499
+ sendText(response, status, `${JSON.stringify(body, null, 2)}\n`, 'application/json; charset=utf-8');
500
+ }
501
+ export function sendText(response, status, body, contentType) {
502
+ response.writeHead(status, {
503
+ 'content-type': contentType,
504
+ });
505
+ response.end(body);
506
+ }
507
+ async function executeRestBatchItem(db, item, options = {}) {
508
+ if (!item || typeof item !== 'object' || Array.isArray(item)) {
509
+ throw dbError('REST_BATCH_INVALID_ITEM', 'Each REST batch item must be an object.', {
510
+ status: 400,
511
+ hint: 'Use an item like { "method": "GET", "path": "/users" }.',
512
+ });
513
+ }
514
+ const batchItem = item;
515
+ const method = String(batchItem.method ?? 'GET').toUpperCase();
516
+ const requestPath = String(batchItem.path ?? '/');
517
+ if (!requestPath.startsWith('/')) {
518
+ throw dbError('REST_BATCH_INVALID_PATH', `REST batch path must start with "/": ${requestPath}`, {
519
+ status: 400,
520
+ hint: `Use absolute local paths such as "/users", "/settings", or "${options.schemaPath ?? `${normalizeBasePath(options.apiBase ?? db.config.server?.apiBase ?? '/__db')}/schema`}".`,
521
+ details: { path: requestPath },
522
+ });
523
+ }
524
+ const batchPath = batchPathForOptions(options, db);
525
+ if (requestPath === batchPath) {
526
+ throw dbError('REST_BATCH_NESTED_UNSUPPORTED', 'Nested REST batch requests are not supported.', {
527
+ status: 400,
528
+ hint: 'Flatten the batch array instead of calling the batch endpoint from inside another batch.',
529
+ });
530
+ }
531
+ const response = makeBatchResponse();
532
+ await handleRestRequest(db, makeBatchRequest(method, batchItem.body), response, new URL(requestPath, 'http://db.local'), { ...options, traceNested: true });
533
+ return {
534
+ status: response.status,
535
+ headers: response.headers,
536
+ body: response.jsonBody(),
537
+ };
538
+ }
539
+ function batchPathForOptions(options = {}, db = null) {
540
+ return options.batchPath ?? `${normalizeBasePath(options.apiBase ?? db?.config?.server?.apiBase ?? '/__db')}/batch`;
541
+ }
542
+ async function tryRest(fn) {
543
+ try {
544
+ const body = await fn();
545
+ return {
546
+ status: 200,
547
+ headers: {
548
+ 'content-type': 'application/json; charset=utf-8',
549
+ },
550
+ body,
551
+ };
552
+ }
553
+ catch (error) {
554
+ return {
555
+ status: error.status ?? 500,
556
+ headers: {
557
+ 'content-type': 'application/json; charset=utf-8',
558
+ },
559
+ body: serializeError(error, 'REST_ERROR'),
560
+ };
561
+ }
562
+ }
563
+ function makeBatchRequest(method, body) {
564
+ return {
565
+ method,
566
+ async *[Symbol.asyncIterator]() {
567
+ if (body !== undefined) {
568
+ yield Buffer.from(JSON.stringify(body));
569
+ }
570
+ },
571
+ };
572
+ }
573
+ function makeBatchResponse() {
574
+ return {
575
+ status: 200,
576
+ headers: {},
577
+ body: '',
578
+ writeHead(status, headers = {}) {
579
+ this.status = status;
580
+ this.headers = headers;
581
+ },
582
+ end(chunk = '') {
583
+ this.body += chunk;
584
+ },
585
+ jsonBody() {
586
+ if (!this.body) {
587
+ return null;
588
+ }
589
+ try {
590
+ return JSON.parse(this.body);
591
+ }
592
+ catch {
593
+ return this.body;
594
+ }
595
+ },
596
+ };
597
+ }
598
+ async function handleCollection(db, resource, id, request, response, url, format, options) {
599
+ const trace = options.trace;
600
+ const collection = db.collection(resource.name);
601
+ const hasQueryId = request.method === 'GET' && !id && url.searchParams.has('id');
602
+ if (hasQueryId && format !== 'json') {
603
+ throw idQueryRequiresJsonRoute(resource, url.searchParams.get('id'));
604
+ }
605
+ const queryId = hasQueryId
606
+ ? url.searchParams.get('id')
607
+ : null;
608
+ const recordId = id ?? queryId;
609
+ if (request.method === 'GET' && !recordId) {
610
+ setRestTraceRoute(trace, options, { resource: resource.name, operation: 'list' });
611
+ const records = await tracePhase(trace, 'collection-read', () => collection.all(), {
612
+ resource: resource.name,
613
+ operation: 'all',
614
+ });
615
+ const shaped = await tracePhase(trace, 'response-shaping', () => shapeCollectionRead(db, resource, records, url, { allowPagination: true }), {
616
+ resource: resource.name,
617
+ });
618
+ await sendFormattedResource(db, response, resource, shaped, format, request, url, trace);
619
+ return;
620
+ }
621
+ if (request.method === 'GET' && recordId) {
622
+ setRestTraceRoute(trace, options, { resource: resource.name, operation: 'get', id: recordId });
623
+ const record = await tracePhase(trace, 'collection-read', () => collection.get(recordId), {
624
+ resource: resource.name,
625
+ operation: 'get',
626
+ });
627
+ const body = record
628
+ ? await tracePhase(trace, 'response-shaping', () => shapeCollectionRead(db, resource, [record], url, { allowPagination: false }), {
629
+ resource: resource.name,
630
+ })
631
+ : null;
632
+ if (!record) {
633
+ sendJson(response, 404, { error: 'Not found' });
634
+ return;
635
+ }
636
+ await sendFormattedResource(db, response, resource, body[0], format, request, url, trace);
637
+ return;
638
+ }
639
+ if (request.method === 'POST' && !id) {
640
+ setRestTraceRoute(trace, options, { resource: resource.name, operation: 'create' });
641
+ const body = await tracePhase(trace, 'request-body', () => readJsonBody(request, {
642
+ maxBytes: maxBodyBytes(db),
643
+ }));
644
+ sendJson(response, 201, await tracePhase(trace, 'collection-write', () => collection.create(body), {
645
+ resource: resource.name,
646
+ operation: 'create',
647
+ }));
648
+ return;
649
+ }
650
+ if (request.method === 'PATCH' && id) {
651
+ setRestTraceRoute(trace, options, { resource: resource.name, operation: 'patch', id });
652
+ const body = await tracePhase(trace, 'request-body', () => readJsonBody(request, {
653
+ maxBytes: maxBodyBytes(db),
654
+ }));
655
+ const record = await tracePhase(trace, 'collection-write', () => collection.patch(id, body), {
656
+ resource: resource.name,
657
+ operation: 'patch',
658
+ });
659
+ sendJson(response, record ? 200 : 404, record ?? { error: 'Not found' });
660
+ return;
661
+ }
662
+ if (request.method === 'DELETE' && id) {
663
+ setRestTraceRoute(trace, options, { resource: resource.name, operation: 'delete', id });
664
+ const deleted = await tracePhase(trace, 'collection-write', () => collection.delete(id), {
665
+ resource: resource.name,
666
+ operation: 'delete',
667
+ });
668
+ sendJson(response, deleted ? 204 : 404, deleted ? null : { error: 'Not found' });
669
+ return;
670
+ }
671
+ setRestTraceRoute(trace, options, { resource: resource.name, operation: 'method-not-allowed' });
672
+ sendJson(response, 405, {
673
+ error: 'Method not allowed',
674
+ });
675
+ }
676
+ function idQueryRequiresJsonRoute(resource, id) {
677
+ const value = String(id ?? '');
678
+ const encoded = encodeURIComponent(value);
679
+ const route = resource.routePath ?? `/${resource.name}`;
680
+ return dbError('REST_ID_QUERY_REQUIRES_JSON_ROUTE', `The id query parameter is only supported on explicit JSON resource routes for ${resource.name}.`, {
681
+ status: 400,
682
+ hint: `Use ${route}.json?id=${encoded} or ${route}/${encoded}.`,
683
+ details: {
684
+ resource: resource.name,
685
+ id: value,
686
+ jsonRoute: `${route}.json`,
687
+ recordRoute: `${route}/{${resource.idField ?? 'id'}}`,
688
+ },
689
+ });
690
+ }
691
+ async function handleDocument(db, resource, request, response, format, options) {
692
+ const trace = options.trace;
693
+ const document = db.document(resource.name);
694
+ if (request.method === 'GET') {
695
+ setRestTraceRoute(trace, options, { resource: resource.name, operation: 'get' });
696
+ const data = await tracePhase(trace, 'document-read', () => document.all(), {
697
+ resource: resource.name,
698
+ operation: 'all',
699
+ });
700
+ await sendFormattedResource(db, response, resource, data, format, request, new URL(request.url ?? '/', 'http://db.local'), trace);
701
+ return;
702
+ }
703
+ if (request.method === 'PUT') {
704
+ setRestTraceRoute(trace, options, { resource: resource.name, operation: 'put' });
705
+ const body = await tracePhase(trace, 'request-body', () => readJsonBody(request, {
706
+ maxBytes: maxBodyBytes(db),
707
+ }));
708
+ sendJson(response, 200, await tracePhase(trace, 'document-write', () => document.put(body), {
709
+ resource: resource.name,
710
+ operation: 'put',
711
+ }));
712
+ return;
713
+ }
714
+ if (request.method === 'PATCH') {
715
+ setRestTraceRoute(trace, options, { resource: resource.name, operation: 'patch' });
716
+ const body = await tracePhase(trace, 'request-body', () => readJsonBody(request, {
717
+ maxBytes: maxBodyBytes(db),
718
+ }));
719
+ sendJson(response, 200, await tracePhase(trace, 'document-write', () => document.update(body), {
720
+ resource: resource.name,
721
+ operation: 'patch',
722
+ }));
723
+ return;
724
+ }
725
+ setRestTraceRoute(trace, options, { resource: resource.name, operation: 'method-not-allowed' });
726
+ sendJson(response, 405, {
727
+ error: 'Method not allowed',
728
+ });
729
+ }
730
+ async function sendFormattedResource(db, response, resource, data, format, request, url, trace = null) {
731
+ const effectiveFormat = format ?? negotiateRestFormat(db.config, request, 'resource');
732
+ const resolved = resolveRestFormat(db.config, effectiveFormat, 'resource');
733
+ if (!resolved) {
734
+ sendUnknownFormat(response, effectiveFormat, db.config, 'resource');
735
+ return;
736
+ }
737
+ const result = await tracePhase(trace, 'response-formatting', () => resolved.renderer({
738
+ db,
739
+ resource,
740
+ resourceName: resource.name,
741
+ data,
742
+ format: resolved.key,
743
+ request,
744
+ url,
745
+ target: 'resource',
746
+ }), {
747
+ resource: resource.name,
748
+ format: resolved.key,
749
+ target: 'resource',
750
+ });
751
+ const normalized = normalizeFormatResult(result, resolved.contentType);
752
+ sendText(response, normalized.status, normalized.body, normalized.contentType);
753
+ }
754
+ function setRestTraceRoute(trace, options, details) {
755
+ if (!trace || options.traceNested) {
756
+ return;
757
+ }
758
+ trace.setRoute({
759
+ route: trace.event.route === 'operation' ? undefined : 'rest',
760
+ ...details,
761
+ });
762
+ }
763
+ function batchItemTraceDetails(index, request) {
764
+ const item = request;
765
+ const method = String(item?.method ?? 'GET').toUpperCase();
766
+ const rawPath = String(item?.path ?? '/');
767
+ const url = rawPath.startsWith('/')
768
+ ? new URL(rawPath, 'http://db.local')
769
+ : null;
770
+ return {
771
+ index,
772
+ method,
773
+ pathname: url?.pathname,
774
+ queryKeys: url ? [...new Set([...url.searchParams.keys()])].sort() : [],
775
+ };
776
+ }
777
+ function manifestResponseFormat(url, request, routes, config) {
778
+ if (url.pathname === routes.manifestJsonPath) {
779
+ return 'json';
780
+ }
781
+ if (url.pathname === routes.manifestHtmlPath) {
782
+ return 'html';
783
+ }
784
+ if (url.pathname === routes.manifestMarkdownPath) {
785
+ return 'md';
786
+ }
787
+ if (url.pathname === routes.manifestPath) {
788
+ return negotiateRestFormat(config, request, 'manifest');
789
+ }
790
+ const parsed = splitFormatExtension(url.pathname);
791
+ return parsed.name === routes.manifestPath ? parsed.format : null;
792
+ }
793
+ function sendUnknownFormat(response, format, config, target) {
794
+ const availableFormats = availableRestFormats(config, target);
795
+ sendJson(response, 404, {
796
+ error: {
797
+ code: 'REST_UNKNOWN_FORMAT',
798
+ message: `Unknown REST format "${format}".`,
799
+ hint: `Use one of: ${listChoices(availableFormats.map((item) => `.${item}`))}.`,
800
+ details: {
801
+ format,
802
+ availableFormats,
803
+ },
804
+ },
805
+ });
806
+ }
807
+ function sendRestDisabled(response, message, details = {}) {
808
+ sendJson(response, 404, {
809
+ error: {
810
+ code: 'REST_DISABLED',
811
+ message,
812
+ hint: 'Set rest.enabled to true in db.config.mjs to enable generated REST resource routes and REST batching.',
813
+ details: {
814
+ restEnabled: false,
815
+ ...details,
816
+ },
817
+ },
818
+ });
819
+ }
820
+ function normalizeFormatResult(result, defaultContentType = 'text/plain; charset=utf-8') {
821
+ if (typeof result === 'string' || Buffer.isBuffer(result)) {
822
+ return {
823
+ status: 200,
824
+ body: result,
825
+ contentType: defaultContentType,
826
+ };
827
+ }
828
+ return {
829
+ status: result?.status ?? 200,
830
+ body: result?.body ?? '',
831
+ contentType: String(result?.contentType ?? result?.headers?.['content-type'] ?? defaultContentType),
832
+ };
833
+ }