@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,824 @@
1
+ import path from 'node:path';
2
+ import { mkdir, rm } from 'node:fs/promises';
3
+ import { loadConfig } from '../../config.js';
4
+ import { dbError, listChoices } from '../../errors.js';
5
+ import { resolveResource, resourceAliasCollisionGroups } from '../../names.js';
6
+ import { createDbOperationHandler } from '../../operations.js';
7
+ import { loadProjectSchema } from '../../schema.js';
8
+ import { syncDb } from '../../sync.js';
9
+ import { createRuntime } from '../storage/runtime.js';
10
+ import { DbCollection } from './collection.js';
11
+ import { DbDocument } from './document.js';
12
+ import { assertValidScopedName, assertValidSnapshotId, branchRegistryPath, cloneConfigResources, configRecord, forkRegistryPath, loadPersistedMigrationLocks, migrationLocksPathForScope, normalizeScopedConfig, readJsonFile, readJsonFileSync, routingPathForScope, scopedConfig, snapshotDirForScope, snapshotId, writeJsonFile, } from './scope-state.js';
13
+ class DbResourceRegistry extends Map {
14
+ migrateResource;
15
+ constructor(entries, migrateResource) {
16
+ super(entries);
17
+ this.migrateResource = migrateResource;
18
+ }
19
+ migrate(name, options) {
20
+ return this.migrateResource(name, options);
21
+ }
22
+ }
23
+ export async function openDb(options = {}) {
24
+ const rawOptions = typeof options === 'string' ? { from: options } : options;
25
+ const loadedSchema = loadedSchemaFromOptions(rawOptions);
26
+ const config = await loadConfig(openOptionsForConfig(rawOptions, loadedSchema));
27
+ const syncOnOpen = rawOptions.syncOnOpen ?? true;
28
+ const project = (syncOnOpen
29
+ ? await syncDb(config, { allowErrors: rawOptions.allowSourceErrors === true })
30
+ : await loadProjectSchema(config, { load: config.schemaLoadMode ?? 'runtime' }));
31
+ const db = new Db(config, project.resources, project.diagnostics);
32
+ if (syncOnOpen) {
33
+ await db.runtime.hydrate();
34
+ }
35
+ return db;
36
+ }
37
+ function loadedSchemaFromOptions(options) {
38
+ return isLoadedDbSchema(options?.schema) ? options.schema : null;
39
+ }
40
+ function openOptionsForConfig(options, loadedSchema) {
41
+ const next = loadedSchema
42
+ ? optionsFromLoadedSchema(options, loadedSchema)
43
+ : { ...options };
44
+ next.load ??= 'runtime';
45
+ return next;
46
+ }
47
+ function optionsFromLoadedSchema(options, loadedSchema) {
48
+ const { schema: _loadedSchema, ...overrides } = options;
49
+ const next = {
50
+ ...(loadedSchema.config ?? {}),
51
+ ...overrides,
52
+ };
53
+ next.from ??= locatorInputForLoadedSchema(loadedSchema);
54
+ return next;
55
+ }
56
+ function locatorInputForLoadedSchema(loadedSchema) {
57
+ const locator = loadedSchema.locator ?? loadedSchema.config?.schemaLocator;
58
+ if (locator?.file) {
59
+ return locator.file;
60
+ }
61
+ if (locator?.mode === 'source-dir' && locator.sourceDir) {
62
+ return locator.sourceDir;
63
+ }
64
+ return locator?.cwd ?? loadedSchema.config?.cwd;
65
+ }
66
+ function isLoadedDbSchema(value) {
67
+ const candidate = value;
68
+ return candidate?.kind === 'DbSchema' && candidate?.resources instanceof Map && Boolean(candidate?.config);
69
+ }
70
+ export class Db {
71
+ config;
72
+ resources;
73
+ diagnostics;
74
+ schemaVersion;
75
+ runtime;
76
+ events;
77
+ scope;
78
+ forks;
79
+ branches;
80
+ snapshots;
81
+ migrations;
82
+ routing;
83
+ migrationLocks;
84
+ constructor(config, resources, diagnostics = [], scope) {
85
+ const scoped = normalizeScopedConfig(config, scope);
86
+ this.config = scoped.config;
87
+ this.resources = new DbResourceRegistry(resources.map((resource) => [resource.name, resource]), (name, options) => this.migrateResource(name, options));
88
+ assertNoResourceAliasCollisions(this.resources);
89
+ this.diagnostics = diagnostics;
90
+ this.schemaVersion = Date.now();
91
+ this.scope = scoped.scope;
92
+ this.runtime = createRuntime(this.config, resources);
93
+ this.events = this.runtime.events;
94
+ this.migrationLocks = loadPersistedMigrationLocks(this.scope);
95
+ this.forks = {
96
+ create: (name, options = {}) => this.createFork(name, options),
97
+ open: (name) => this.openFork(name),
98
+ ensure: (name, options = {}) => this.ensureFork(name, options),
99
+ list: () => this.listForks(),
100
+ delete: (name) => this.deleteFork(name),
101
+ };
102
+ this.branches = {
103
+ create: (name, options = {}) => this.createBranch(name, options),
104
+ open: (name) => this.openBranch(name),
105
+ ensure: (name, options = {}) => this.ensureBranch(name, options),
106
+ list: () => this.listBranches(),
107
+ delete: (name) => this.deleteBranch(name),
108
+ };
109
+ this.snapshots = {
110
+ create: (options = {}) => this.createSnapshot(options),
111
+ restore: (id, options = {}) => this.restoreSnapshot(id, options),
112
+ };
113
+ this.migrations = {
114
+ start: (name, options) => this.startMigration(name, options),
115
+ verify: (name, options) => this.verifyMigration(name, options),
116
+ finish: (name) => this.finishMigration(name),
117
+ };
118
+ this.routing = {
119
+ set: (routes) => this.setRouting(routes),
120
+ };
121
+ }
122
+ collection(name) {
123
+ const resource = this.requireResource(name, 'collection');
124
+ return new DbCollection(this, resource);
125
+ }
126
+ document(name) {
127
+ const resource = this.requireResource(name, 'document');
128
+ return new DbDocument(this, resource);
129
+ }
130
+ async operation(template, variables = {}) {
131
+ const result = await createDbOperationHandler(this).execute(template, variables);
132
+ return result.body;
133
+ }
134
+ query(template, variables = {}) {
135
+ return this.operation(template, variables);
136
+ }
137
+ fork(name) {
138
+ assertValidScopedName(name, 'fork');
139
+ this.assertRootForkLifecycle('open fork');
140
+ this.assertForkExists(name);
141
+ this.assertBranchExists(name, 'main');
142
+ return this.scopedDb({
143
+ fork: name,
144
+ branch: 'main',
145
+ rootStateDir: this.scope.rootStateDir,
146
+ });
147
+ }
148
+ branch(name) {
149
+ assertValidScopedName(name, 'branch');
150
+ if (!this.scope.fork) {
151
+ throw dbError('DB_BRANCH_REQUIRES_FORK', `Cannot open branch "${name}" without a fork.`, {
152
+ status: 400,
153
+ hint: 'Open a fork with await db.forks.open("tenant_id"), then open a branch with await tenant.branches.open(name).',
154
+ details: {
155
+ branch: name,
156
+ },
157
+ });
158
+ }
159
+ this.assertBranchExists(this.scope.fork, name);
160
+ return this.scopedDb({
161
+ fork: this.scope.fork,
162
+ branch: name,
163
+ rootStateDir: this.scope.rootStateDir,
164
+ });
165
+ }
166
+ assertResourceWritable(resourceName) {
167
+ for (const lock of this.migrationLocks.values()) {
168
+ if (lock.mode === 'read-only' && lock.resources.includes(resourceName)) {
169
+ throw dbError('RESOURCE_MIGRATING', `Resource "${resourceName}" is read-only while migration "${lock.name}" is running.`, {
170
+ status: 423,
171
+ hint: 'Wait for the migration to finish, retry later, or write to a migration target outside the public resource API.',
172
+ details: {
173
+ resource: resourceName,
174
+ migration: lock.name,
175
+ fork: this.scope.fork,
176
+ branch: this.scope.branch,
177
+ },
178
+ });
179
+ }
180
+ }
181
+ }
182
+ requireResource(name, kind) {
183
+ const { resource, candidates } = resolveResource(this.resources, name);
184
+ if (!resource) {
185
+ throw dbError('DB_UNKNOWN_RESOURCE', `Unknown db resource "${name}".`, {
186
+ status: 404,
187
+ hint: `Use one of: ${listChoices(this.resourceNames())}.`,
188
+ details: {
189
+ resource: name,
190
+ requestedResource: name,
191
+ normalizedCandidates: candidates,
192
+ availableResources: this.resourceNames(),
193
+ },
194
+ });
195
+ }
196
+ if (resource.kind !== kind) {
197
+ throw dbError('DB_RESOURCE_KIND_MISMATCH', `Resource "${name}" is a ${resource.kind}, not a ${kind}.`, {
198
+ status: 400,
199
+ hint: resource.kind === 'collection'
200
+ ? `Use db.collection("${name}") for this resource.`
201
+ : `Use db.document("${name}") for this resource.`,
202
+ details: {
203
+ resource: name,
204
+ expectedKind: kind,
205
+ actualKind: resource.kind,
206
+ },
207
+ });
208
+ }
209
+ return resource;
210
+ }
211
+ resourceNames() {
212
+ return [...this.resources.keys()];
213
+ }
214
+ close() {
215
+ return this.runtime.close();
216
+ }
217
+ scopedDb(scope) {
218
+ const config = scopedConfig(this.config, scope);
219
+ const next = new Db(config, [...this.resources.values()], this.diagnostics, scope);
220
+ return next;
221
+ }
222
+ async createFork(name, options = {}) {
223
+ assertValidScopedName(name, 'fork');
224
+ this.assertRootForkLifecycle('create fork');
225
+ const now = new Date().toISOString();
226
+ const registryPath = forkRegistryPath(this.scope.rootStateDir);
227
+ const registry = await readJsonFile(registryPath, { forks: {} });
228
+ if (configRecord(registry.forks)[name]) {
229
+ throw dbError('DB_FORK_ALREADY_EXISTS', `Fork "${name}" already exists.`, {
230
+ status: 409,
231
+ hint: 'Open it with db.forks.open(name), or use db.forks.ensure(name, options) for idempotent setup.',
232
+ details: {
233
+ fork: name,
234
+ },
235
+ });
236
+ }
237
+ const source = await this.resolveForkSource(options.from);
238
+ const target = this.scopedDb({
239
+ fork: name,
240
+ branch: 'main',
241
+ rootStateDir: this.scope.rootStateDir,
242
+ });
243
+ await copyForkSource(source, target, this.resourceNames());
244
+ registry.forks = {
245
+ ...configRecord(registry.forks),
246
+ [name]: {
247
+ id: name,
248
+ metadata: options.metadata ?? {},
249
+ from: serializeForkSource(options.from),
250
+ createdAt: now,
251
+ },
252
+ };
253
+ await writeJsonFile(registryPath, registry);
254
+ await this.writeBranchRegistry(name, {
255
+ main: {
256
+ id: 'main',
257
+ metadata: {},
258
+ from: source.label,
259
+ createdAt: now,
260
+ },
261
+ });
262
+ return target;
263
+ }
264
+ async openFork(name) {
265
+ return this.fork(name);
266
+ }
267
+ async ensureFork(name, options = {}) {
268
+ assertValidScopedName(name, 'fork');
269
+ this.assertRootForkLifecycle('ensure fork');
270
+ const registry = await readJsonFile(forkRegistryPath(this.scope.rootStateDir), { forks: {} });
271
+ if (configRecord(registry.forks)[name]) {
272
+ return this.openFork(name);
273
+ }
274
+ return this.createFork(name, options);
275
+ }
276
+ async listForks() {
277
+ this.assertRootForkLifecycle('list forks');
278
+ const registry = await readJsonFile(forkRegistryPath(this.scope.rootStateDir), { forks: {} });
279
+ return Object.values(registry.forks ?? {});
280
+ }
281
+ async deleteFork(name) {
282
+ assertValidScopedName(name, 'fork');
283
+ this.assertRootForkLifecycle('delete fork');
284
+ const registryPath = forkRegistryPath(this.scope.rootStateDir);
285
+ const registry = await readJsonFile(registryPath, { forks: {} });
286
+ const existed = Boolean(registry.forks?.[name]);
287
+ if (registry.forks) {
288
+ delete registry.forks[name];
289
+ }
290
+ await writeJsonFile(registryPath, registry);
291
+ await rm(path.join(this.scope.rootStateDir, 'forks', name), { force: true, recursive: true });
292
+ return existed;
293
+ }
294
+ async createBranch(name, options = {}) {
295
+ assertValidScopedName(name, 'branch');
296
+ if (!this.scope.fork) {
297
+ throw dbError('DB_BRANCH_REQUIRES_FORK', `Cannot create branch "${name}" without a fork.`, {
298
+ status: 400,
299
+ hint: 'Open a fork with await db.forks.open("tenant_id"), then create a branch with await tenant.branches.create(name).',
300
+ details: {
301
+ branch: name,
302
+ },
303
+ });
304
+ }
305
+ const registryPath = branchRegistryPath(this.scope.rootStateDir, this.scope.fork);
306
+ const registry = await readJsonFile(registryPath, { branches: {} });
307
+ if (configRecord(registry.branches)[name]) {
308
+ throw dbError('DB_BRANCH_ALREADY_EXISTS', `Branch "${name}" already exists in fork "${this.scope.fork}".`, {
309
+ status: 409,
310
+ hint: 'Open it with tenant.branches.open(branch), or use tenant.branches.ensure(branch, options) for idempotent setup.',
311
+ details: {
312
+ fork: this.scope.fork,
313
+ branch: name,
314
+ },
315
+ });
316
+ }
317
+ const source = this.branch(options.from ?? this.scope.branch ?? 'main');
318
+ const target = this.scopedDb({
319
+ fork: this.scope.fork,
320
+ branch: name,
321
+ rootStateDir: this.scope.rootStateDir,
322
+ });
323
+ await copyResources(source, target, this.resourceNames());
324
+ registry.branches = {
325
+ ...configRecord(registry.branches),
326
+ [name]: {
327
+ id: name,
328
+ metadata: options.metadata ?? {},
329
+ from: options.from ?? this.scope.branch ?? 'main',
330
+ createdAt: new Date().toISOString(),
331
+ },
332
+ };
333
+ await writeJsonFile(registryPath, registry);
334
+ return target;
335
+ }
336
+ async openBranch(name) {
337
+ return this.branch(name);
338
+ }
339
+ async ensureBranch(name, options = {}) {
340
+ assertValidScopedName(name, 'branch');
341
+ if (!this.scope.fork) {
342
+ throw dbError('DB_BRANCH_REQUIRES_FORK', `Cannot ensure branch "${name}" without a fork.`, {
343
+ status: 400,
344
+ hint: 'Open or create a fork before ensuring a branch.',
345
+ details: {
346
+ branch: name,
347
+ },
348
+ });
349
+ }
350
+ const registry = await readJsonFile(branchRegistryPath(this.scope.rootStateDir, this.scope.fork), { branches: {} });
351
+ if (configRecord(registry.branches)[name]) {
352
+ return this.openBranch(name);
353
+ }
354
+ return this.createBranch(name, options);
355
+ }
356
+ async listBranches() {
357
+ if (!this.scope.fork) {
358
+ throw dbError('DB_BRANCH_REQUIRES_FORK', 'Cannot list branches without a fork.', {
359
+ status: 400,
360
+ hint: 'Open a fork before listing its branches.',
361
+ details: {},
362
+ });
363
+ }
364
+ const registry = await readJsonFile(branchRegistryPath(this.scope.rootStateDir, this.scope.fork), { branches: {} });
365
+ return Object.values(registry.branches ?? {});
366
+ }
367
+ async deleteBranch(name) {
368
+ assertValidScopedName(name, 'branch');
369
+ if (!this.scope.fork) {
370
+ throw dbError('DB_BRANCH_REQUIRES_FORK', `Cannot delete branch "${name}" without a fork.`, {
371
+ status: 400,
372
+ hint: 'Open a fork before deleting one of its branches.',
373
+ details: {
374
+ branch: name,
375
+ },
376
+ });
377
+ }
378
+ if (name === 'main') {
379
+ throw dbError('DB_BRANCH_MAIN_DELETE_FORBIDDEN', 'Cannot delete the main branch.', {
380
+ status: 400,
381
+ hint: 'Delete the entire fork with db.forks.delete(name), or delete a non-main branch.',
382
+ details: {
383
+ fork: this.scope.fork,
384
+ branch: name,
385
+ },
386
+ });
387
+ }
388
+ const registryPath = branchRegistryPath(this.scope.rootStateDir, this.scope.fork);
389
+ const registry = await readJsonFile(registryPath, { branches: {} });
390
+ const existed = Boolean(configRecord(registry.branches)[name]);
391
+ registry.branches = { ...configRecord(registry.branches) };
392
+ delete registry.branches[name];
393
+ await writeJsonFile(registryPath, registry);
394
+ await rm(path.join(this.scope.rootStateDir, 'forks', this.scope.fork, 'branches', name), {
395
+ force: true,
396
+ recursive: true,
397
+ });
398
+ return existed;
399
+ }
400
+ async createSnapshot(options = {}) {
401
+ const resources = options.resources ?? this.resourceNames();
402
+ const id = snapshotId(options.label);
403
+ const snapshotDir = snapshotDirForScope(this.scope, id);
404
+ const resourcesDir = path.join(snapshotDir, 'resources');
405
+ await mkdir(resourcesDir, { recursive: true });
406
+ for (const resourceName of resources) {
407
+ const resource = this.resourceForName(resourceName);
408
+ const value = await readResourceValue(this, resource);
409
+ await writeJsonFile(path.join(resourcesDir, `${resource.name}.json`), value);
410
+ }
411
+ const manifest = {
412
+ id,
413
+ label: options.label,
414
+ fork: this.scope.fork,
415
+ branch: this.scope.branch,
416
+ resources,
417
+ createdAt: new Date().toISOString(),
418
+ };
419
+ await writeJsonFile(path.join(snapshotDir, 'manifest.json'), manifest);
420
+ return {
421
+ id,
422
+ label: options.label,
423
+ fork: this.scope.fork,
424
+ branch: this.scope.branch,
425
+ resources,
426
+ path: snapshotDir,
427
+ };
428
+ }
429
+ async restoreSnapshot(id, options = {}) {
430
+ assertValidSnapshotId(id);
431
+ const snapshotDir = snapshotDirForScope(this.scope, id);
432
+ const manifest = await readJsonFile(path.join(snapshotDir, 'manifest.json'), null);
433
+ if (!manifest) {
434
+ throw dbError('DB_SNAPSHOT_NOT_FOUND', `Snapshot "${id}" was not found.`, {
435
+ status: 404,
436
+ hint: 'Create a snapshot before restoring it, or check the snapshot id.',
437
+ details: {
438
+ snapshot: id,
439
+ fork: this.scope.fork,
440
+ },
441
+ });
442
+ }
443
+ const resources = options.resources ?? manifest.resources ?? this.resourceNames();
444
+ for (const resourceName of resources) {
445
+ const resource = this.resourceForName(resourceName);
446
+ const value = await readJsonFile(path.join(snapshotDir, 'resources', `${resource.name}.json`), undefined);
447
+ if (value !== undefined) {
448
+ await writeResourceValue(this, resource, value);
449
+ }
450
+ }
451
+ }
452
+ async startMigration(name, options) {
453
+ assertValidScopedName(name, 'migration');
454
+ const lock = {
455
+ name,
456
+ resources: options.resources.map((resourceName) => this.resourceForName(resourceName).name),
457
+ mode: options.mode ?? 'read-only',
458
+ startedAt: new Date().toISOString(),
459
+ };
460
+ this.migrationLocks.set(name, lock);
461
+ await this.persistMigrationLocks();
462
+ return lock;
463
+ }
464
+ async migrateResource(name, options) {
465
+ const resource = this.resourceForName(name);
466
+ const fromAdapter = this.runtime.adapterForStore(resource, options.from);
467
+ const toAdapter = this.runtime.adapterForStore(resource, options.to);
468
+ const value = await fromAdapter.readResource?.(resource, fallbackForResource(resource));
469
+ if (toAdapter.withResourceWrite) {
470
+ await toAdapter.withResourceWrite(resource, async () => {
471
+ await toAdapter.writeResource?.(resource, cloneJson(value));
472
+ });
473
+ }
474
+ else {
475
+ await toAdapter.writeResource?.(resource, cloneJson(value));
476
+ }
477
+ const migrationName = this.activeMigrationForResource(resource.name);
478
+ if (migrationName) {
479
+ await this.recordMigrationCopy(migrationName, resource.name, {
480
+ resource: resource.name,
481
+ from: options.from,
482
+ to: options.to,
483
+ copiedAt: new Date().toISOString(),
484
+ });
485
+ }
486
+ }
487
+ async verifyMigration(name, options) {
488
+ const checks = options.checks ?? ['count', 'schema', 'checksum'];
489
+ const lock = this.migrationLocks.get(name);
490
+ for (const resourceName of options.resources) {
491
+ const resource = this.resourceForName(resourceName);
492
+ const copy = lock?.copies?.[resource.name];
493
+ if (!copy) {
494
+ throw dbError('MIGRATION_VERIFY_FAILED', `Cannot verify resource "${resource.name}" because it was not migrated in "${name}".`, {
495
+ status: 409,
496
+ hint: 'Call resources.migrate(resource, { from, to }) before verifying the migration.',
497
+ details: {
498
+ migration: name,
499
+ resource: resource.name,
500
+ },
501
+ });
502
+ }
503
+ const fromValue = await this.runtime.adapterForStore(resource, copy.from).readResource?.(resource, fallbackForResource(resource));
504
+ const toValue = await this.runtime.adapterForStore(resource, copy.to).readResource?.(resource, fallbackForResource(resource));
505
+ if (checks.includes('count') && countValue(fromValue) !== countValue(toValue)) {
506
+ throw migrationVerifyError(name, resource.name, 'count');
507
+ }
508
+ if (checks.includes('checksum') && stableJson(fromValue) !== stableJson(toValue)) {
509
+ throw migrationVerifyError(name, resource.name, 'checksum');
510
+ }
511
+ if (checks.includes('schema')) {
512
+ // Runtime writes already validate through resource APIs. Store-to-store
513
+ // migrations preserve JSON values, so schema verification is a no-op in
514
+ // this dependency-light v1.
515
+ }
516
+ }
517
+ }
518
+ async finishMigration(name) {
519
+ this.migrationLocks.delete(name);
520
+ await this.persistMigrationLocks();
521
+ }
522
+ async setRouting(routes) {
523
+ const persistedRoutes = await readJsonFile(this.routingPath(), {});
524
+ const nextRoutes = {
525
+ ...persistedRoutes,
526
+ ...routes,
527
+ };
528
+ const resourcesConfig = cloneConfigResources(this.config.resources) ?? {};
529
+ for (const [resourceName, storeName] of Object.entries(nextRoutes)) {
530
+ const resource = this.resourceForName(resourceName);
531
+ const existing = configRecord(resourcesConfig[resource.name]);
532
+ resourcesConfig[resource.name] = {
533
+ ...existing,
534
+ store: storeName,
535
+ };
536
+ }
537
+ this.config.resources = resourcesConfig;
538
+ await writeJsonFile(this.routingPath(), nextRoutes);
539
+ return nextRoutes;
540
+ }
541
+ activeMigrationForResource(resourceName) {
542
+ for (const lock of this.migrationLocks.values()) {
543
+ if (lock.resources.includes(resourceName)) {
544
+ return lock.name;
545
+ }
546
+ }
547
+ return null;
548
+ }
549
+ assertRootForkLifecycle(action) {
550
+ if (!this.scope.fork) {
551
+ return;
552
+ }
553
+ throw dbError('DB_FORK_REQUIRES_ROOT', `Cannot ${action} from fork "${this.scope.fork}".`, {
554
+ status: 400,
555
+ hint: 'Use the root db handle for fork lifecycle methods. Pass an explicit from source when creating a fork from another fork or branch.',
556
+ details: {
557
+ fork: this.scope.fork,
558
+ branch: this.scope.branch,
559
+ action,
560
+ },
561
+ });
562
+ }
563
+ assertForkExists(name) {
564
+ const registry = readJsonFileSync(forkRegistryPath(this.scope.rootStateDir), { forks: {} });
565
+ if (configRecord(registry.forks)[name]) {
566
+ return;
567
+ }
568
+ throw dbError('DB_FORK_NOT_FOUND', `Fork "${name}" was not found.`, {
569
+ status: 404,
570
+ hint: 'Create the fork with db.forks.create(name) before opening it, or check the fork id.',
571
+ details: {
572
+ fork: name,
573
+ },
574
+ });
575
+ }
576
+ assertBranchExists(fork, name) {
577
+ const registry = readJsonFileSync(branchRegistryPath(this.scope.rootStateDir, fork), { branches: {} });
578
+ if (configRecord(registry.branches)[name]) {
579
+ return;
580
+ }
581
+ throw dbError('DB_BRANCH_NOT_FOUND', `Branch "${name}" was not found in fork "${fork}".`, {
582
+ status: 404,
583
+ hint: 'Create the branch with tenant.branches.create(branch) before opening it, or check the branch id.',
584
+ details: {
585
+ fork,
586
+ branch: name,
587
+ },
588
+ });
589
+ }
590
+ async resolveForkSource(from) {
591
+ if (from === undefined || from === 'main') {
592
+ return {
593
+ kind: 'db',
594
+ label: 'main',
595
+ db: this,
596
+ };
597
+ }
598
+ if (typeof from === 'string') {
599
+ throw dbError('DB_FORK_SOURCE_UNSUPPORTED', `Unsupported fork source "${from}".`, {
600
+ status: 400,
601
+ hint: 'Use from: "main", from: { fork, branch }, or from: { fork, snapshot } so the source is unambiguous.',
602
+ details: {
603
+ from,
604
+ },
605
+ });
606
+ }
607
+ if ('snapshot' in from) {
608
+ const sourceScope = this.sourceScope(from.fork ?? this.scope.fork ?? null, this.scope.branch);
609
+ const snapshot = String(from.snapshot);
610
+ assertValidSnapshotId(snapshot);
611
+ const snapshotDir = snapshotDirForScope(sourceScope, snapshot);
612
+ const manifest = await readJsonFile(path.join(snapshotDir, 'manifest.json'), null);
613
+ if (!manifest) {
614
+ throw dbError('DB_SNAPSHOT_NOT_FOUND', `Snapshot "${snapshot}" was not found.`, {
615
+ status: 404,
616
+ hint: 'Create the snapshot before using it as a fork source, or pass the fork that owns the snapshot.',
617
+ details: {
618
+ snapshot,
619
+ fork: sourceScope.fork,
620
+ },
621
+ });
622
+ }
623
+ const manifestRecord = configRecord(manifest);
624
+ return {
625
+ kind: 'snapshot',
626
+ label: `snapshot:${snapshot}`,
627
+ snapshotDir,
628
+ resources: Array.isArray(manifestRecord.resources)
629
+ ? manifestRecord.resources.map(String)
630
+ : this.resourceNames(),
631
+ };
632
+ }
633
+ const sourceBranch = from.branch;
634
+ if (typeof sourceBranch !== 'string') {
635
+ throw dbError('DB_FORK_SOURCE_UNSUPPORTED', 'Fork branch sources must include a branch name.', {
636
+ status: 400,
637
+ hint: 'Use from: { fork: "tenant_id", branch: "main" } when copying from another fork branch.',
638
+ details: {
639
+ from,
640
+ },
641
+ });
642
+ }
643
+ const sourceScope = this.sourceScope(from.fork ?? this.scope.fork ?? null, sourceBranch);
644
+ if (sourceScope.fork) {
645
+ this.assertForkExists(sourceScope.fork);
646
+ this.assertBranchExists(sourceScope.fork, sourceScope.branch);
647
+ }
648
+ else if (sourceScope.branch !== 'main') {
649
+ throw dbError('DB_BRANCH_REQUIRES_FORK', `Cannot use root branch "${sourceScope.branch}" as a fork source.`, {
650
+ status: 400,
651
+ hint: 'Only root main can be used without a fork. Use from: { fork, branch } for branch sources.',
652
+ details: {
653
+ branch: sourceScope.branch,
654
+ },
655
+ });
656
+ }
657
+ return {
658
+ kind: 'db',
659
+ label: sourceScope.fork
660
+ ? `fork:${sourceScope.fork}/branch:${sourceScope.branch}`
661
+ : sourceScope.branch,
662
+ db: this.scopedDb(sourceScope),
663
+ };
664
+ }
665
+ sourceScope(fork, branch = 'main') {
666
+ if (fork) {
667
+ assertValidScopedName(fork, 'fork');
668
+ }
669
+ assertValidScopedName(branch, 'branch');
670
+ return {
671
+ fork,
672
+ branch,
673
+ rootStateDir: this.scope.rootStateDir,
674
+ };
675
+ }
676
+ async writeBranchRegistry(fork, branches) {
677
+ const registryPath = branchRegistryPath(this.scope.rootStateDir, fork);
678
+ const registry = await readJsonFile(registryPath, { branches: {} });
679
+ registry.branches = {
680
+ ...configRecord(registry.branches),
681
+ ...branches,
682
+ };
683
+ await writeJsonFile(registryPath, registry);
684
+ }
685
+ async recordMigrationCopy(migrationName, resourceName, copy) {
686
+ const lock = this.migrationLocks.get(migrationName);
687
+ if (!lock) {
688
+ return;
689
+ }
690
+ lock.copies = {
691
+ ...(lock.copies ?? {}),
692
+ [resourceName]: copy,
693
+ };
694
+ this.migrationLocks.set(migrationName, lock);
695
+ await this.persistMigrationLocks();
696
+ }
697
+ async persistMigrationLocks() {
698
+ await writeJsonFile(this.migrationLocksPath(), {
699
+ locks: Object.fromEntries(this.migrationLocks),
700
+ });
701
+ }
702
+ resourceForName(name) {
703
+ const { resource, candidates } = resolveResource(this.resources, name);
704
+ if (resource) {
705
+ return resource;
706
+ }
707
+ throw dbError('DB_UNKNOWN_RESOURCE', `Unknown db resource "${name}".`, {
708
+ status: 404,
709
+ hint: `Use one of: ${listChoices(this.resourceNames())}.`,
710
+ details: {
711
+ resource: name,
712
+ requestedResource: name,
713
+ normalizedCandidates: candidates,
714
+ availableResources: this.resourceNames(),
715
+ },
716
+ });
717
+ }
718
+ migrationLocksPath() {
719
+ return migrationLocksPathForScope(this.scope);
720
+ }
721
+ routingPath() {
722
+ return routingPathForScope(this.scope);
723
+ }
724
+ }
725
+ export function stateFileForDebug(db, resourceName) {
726
+ return path.join(db.config.stateDir, 'state', `${resourceName}.json`);
727
+ }
728
+ async function copyResources(source, target, resourceNames) {
729
+ for (const resourceName of resourceNames) {
730
+ const resource = source.resourceForName(resourceName);
731
+ const value = await readResourceValue(source, resource);
732
+ await writeResourceValue(target, resource, value);
733
+ }
734
+ }
735
+ async function copyForkSource(source, target, resourceNames) {
736
+ if (source.kind === 'db') {
737
+ await copyResources(source.db, target, resourceNames);
738
+ return;
739
+ }
740
+ for (const resourceName of source.resources) {
741
+ const resource = target.resourceForName(resourceName);
742
+ const value = await readJsonFile(path.join(source.snapshotDir, 'resources', `${resource.name}.json`), undefined);
743
+ if (value !== undefined) {
744
+ await writeResourceValue(target, resource, value);
745
+ }
746
+ }
747
+ }
748
+ async function readResourceValue(db, resource) {
749
+ return db.runtime.adapterFor(resource).readResource?.(resource, fallbackForResource(resource));
750
+ }
751
+ async function writeResourceValue(db, resource, value) {
752
+ const adapter = db.runtime.adapterFor(resource);
753
+ if (adapter.withResourceWrite) {
754
+ await adapter.withResourceWrite(resource, async () => {
755
+ await adapter.writeResource?.(resource, cloneJson(value));
756
+ });
757
+ return;
758
+ }
759
+ await adapter.writeResource?.(resource, cloneJson(value));
760
+ }
761
+ function fallbackForResource(resource) {
762
+ return resource.kind === 'collection' ? [] : {};
763
+ }
764
+ function cloneJson(value) {
765
+ if (value === undefined) {
766
+ return value;
767
+ }
768
+ return JSON.parse(JSON.stringify(value));
769
+ }
770
+ function countValue(value) {
771
+ if (Array.isArray(value)) {
772
+ return value.length;
773
+ }
774
+ if (value && typeof value === 'object') {
775
+ return Object.keys(value).length;
776
+ }
777
+ return value === undefined || value === null ? 0 : 1;
778
+ }
779
+ function stableJson(value) {
780
+ return JSON.stringify(sortJson(value));
781
+ }
782
+ function serializeForkSource(from) {
783
+ return from ?? 'main';
784
+ }
785
+ function sortJson(value) {
786
+ if (Array.isArray(value)) {
787
+ return value.map(sortJson);
788
+ }
789
+ if (value && typeof value === 'object') {
790
+ return Object.fromEntries(Object.entries(value)
791
+ .sort(([left], [right]) => left.localeCompare(right))
792
+ .map(([key, child]) => [key, sortJson(child)]));
793
+ }
794
+ return value;
795
+ }
796
+ function migrationVerifyError(name, resourceName, check) {
797
+ return dbError('MIGRATION_VERIFY_FAILED', `Migration "${name}" failed ${check} verification for resource "${resourceName}".`, {
798
+ status: 409,
799
+ hint: 'Keep the resource read-only, inspect the migration target, and rerun the resource migration before switching routing.',
800
+ details: {
801
+ migration: name,
802
+ resource: resourceName,
803
+ check,
804
+ },
805
+ });
806
+ }
807
+ function assertNoResourceAliasCollisions(resources) {
808
+ const collisions = resourceAliasCollisionGroups(resources);
809
+ if (collisions.length === 0) {
810
+ return;
811
+ }
812
+ const collision = collisions[0];
813
+ throw dbError('DB_RESOURCE_ALIAS_COLLISION', `Resource aliases are ambiguous for "${collision.alias}".`, {
814
+ status: 400,
815
+ hint: 'Rename one resource so its camelCase and kebab-case aliases are unique.',
816
+ details: {
817
+ alias: collision.alias,
818
+ aliases: collision.aliases,
819
+ resources: collision.resources,
820
+ candidates: collision.candidates,
821
+ collisions,
822
+ },
823
+ });
824
+ }