@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,1015 @@
1
+ export function renderDbViewer(options = {}) {
2
+ const graphqlPath = options.graphqlPath ?? '/graphql';
3
+ const schemaPath = options.schemaPath ?? '/__db/schema';
4
+ const manifestPath = options.manifestPath ?? '/__db/manifest.json';
5
+ const eventsPath = options.eventsPath ?? '/__db/events';
6
+ const importPath = options.importPath ?? '/__db/import';
7
+ const restBatchPath = options.restBatchPath ?? '/__db/batch';
8
+ const restBasePath = options.restBasePath ?? '';
9
+ const sourceDirLabel = options.sourceDirLabel ?? 'db/';
10
+ const buttonClass = 'inline-flex min-h-10 items-center justify-center gap-2 rounded-md border border-slate-300 bg-white px-3 py-2 text-sm font-semibold text-slate-800 shadow-sm transition hover:border-emerald-700 hover:bg-emerald-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 active:translate-y-px';
11
+ const primaryButtonClass = 'inline-flex min-h-10 items-center justify-center gap-2 rounded-md border border-emerald-700 bg-emerald-700 px-3 py-2 text-sm font-semibold text-white shadow-sm transition hover:bg-emerald-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 active:translate-y-px';
12
+ const tabClass = 'inline-flex min-h-10 items-center justify-center rounded-md border border-slate-300 bg-white px-3 py-2 text-sm font-semibold text-slate-700 shadow-sm transition hover:border-emerald-700 hover:bg-emerald-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500';
13
+ const activeTabClass = 'inline-flex min-h-10 items-center justify-center rounded-md border border-emerald-700 bg-emerald-50 px-3 py-2 text-sm font-semibold text-emerald-800 shadow-sm transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500';
14
+ const resourceButtonClass = 'inline-grid w-full gap-1 rounded-md border border-slate-300 bg-white px-3 py-3 text-left shadow-sm transition hover:border-emerald-700 hover:bg-emerald-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500';
15
+ const activeResourceButtonClass = 'inline-grid w-full gap-1 rounded-md border border-emerald-700 bg-emerald-50 px-3 py-3 text-left shadow-sm ring-1 ring-emerald-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500';
16
+ const panelClass = 'min-w-0 rounded-lg border border-slate-200 bg-white shadow-sm';
17
+ const panelHeadClass = 'flex items-center justify-between gap-3 border-b border-slate-200 px-4 py-3';
18
+ const panelBodyClass = 'p-4';
19
+ const stackClass = 'grid gap-3';
20
+ const rowClass = 'flex flex-wrap items-center gap-2';
21
+ const mutedClass = 'text-sm text-slate-500';
22
+ const codeClass = 'min-h-12 overflow-auto whitespace-pre-wrap break-words rounded-md bg-slate-950 p-3 font-mono text-xs leading-5 text-slate-100';
23
+ const textareaClass = 'min-h-40 w-full resize-y rounded-md border border-slate-300 bg-white p-3 font-mono text-sm text-slate-950 shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500';
24
+ const inputClass = 'min-h-10 w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-950 shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500';
25
+ const selectClass = 'min-h-10 rounded-md border border-slate-300 bg-white px-3 py-2 text-sm font-semibold text-slate-800 shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500';
26
+ const viewerGridClass = 'grid gap-4 xl:grid-cols-[minmax(0,1.25fr)_minmax(320px,0.75fr)]';
27
+ const tableWrapClass = 'overflow-auto rounded-md border border-slate-200';
28
+ const tableClass = 'w-full min-w-[480px] border-collapse bg-white';
29
+ const thClass = 'sticky top-0 border-b border-slate-200 bg-slate-100 px-3 py-2 text-left text-xs font-semibold text-slate-600';
30
+ const tdClass = 'max-w-[360px] border-b border-slate-200 px-3 py-2 align-top font-mono text-xs text-slate-800 break-words';
31
+ const exampleClass = 'grid gap-2 rounded-lg border border-slate-200 bg-white p-3 shadow-sm';
32
+ const exampleHeadClass = 'flex flex-wrap items-center justify-between gap-2';
33
+ const pillClass = 'inline-flex items-center rounded-full border border-slate-200 bg-slate-100 px-2.5 py-1 text-xs font-medium text-slate-600';
34
+ const warningPillClass = 'inline-flex items-center rounded-full border border-amber-200 bg-amber-50 px-2.5 py-1 text-xs font-medium text-amber-700';
35
+ const errorPillClass = 'inline-flex items-center rounded-full border border-red-200 bg-red-50 px-2.5 py-1 text-xs font-medium text-red-700';
36
+ const importDropClass = 'mt-4 rounded-lg border-2 border-dashed border-slate-300 bg-white p-4 text-sm text-slate-600 shadow-sm transition';
37
+ const importDropActiveClass = 'mt-4 rounded-lg border-2 border-dashed border-emerald-500 bg-emerald-50 p-4 text-sm text-emerald-800 shadow-sm transition';
38
+ return `<!doctype html>
39
+ <html lang="en">
40
+ <head>
41
+ <meta charset="utf-8">
42
+ <meta name="viewport" content="width=device-width, initial-scale=1">
43
+ <title>db viewer</title>
44
+ <script src="https://cdn.tailwindcss.com"></script>
45
+ <script src="https://unpkg.com/htmx.org@2.0.4"></script>
46
+ </head>
47
+ <body class="bg-slate-50 text-slate-950 antialiased">
48
+ <div class="grid min-h-screen grid-rows-[auto_1fr]">
49
+ <header class="flex flex-col gap-3 border-b border-slate-200 bg-white px-5 py-4 shadow-sm sm:flex-row sm:items-center sm:justify-between">
50
+ <div>
51
+ <h1 class="text-lg font-bold tracking-normal text-slate-950">db viewer</h1>
52
+ <div class="${mutedClass}" id="subtitle">Loading local fixture database</div>
53
+ </div>
54
+ <div class="${rowClass} sm:justify-end" id="status"></div>
55
+ </header>
56
+ <div class="grid min-h-0 lg:grid-cols-[minmax(220px,280px)_minmax(0,1fr)]">
57
+ <aside class="max-h-72 overflow-auto border-b border-slate-200 bg-slate-50 p-4 lg:max-h-none lg:border-b-0 lg:border-r">
58
+ <div class="mb-4 flex flex-wrap items-center justify-between gap-3">
59
+ <h2 class="text-base font-bold tracking-normal text-slate-950">Resources</h2>
60
+ <button type="button" id="refresh" class="${buttonClass}">Refresh</button>
61
+ </div>
62
+ <div id="resource-list" class="grid gap-2"></div>
63
+ <div id="csv-drop" class="${importDropClass}">
64
+ <div class="font-semibold text-slate-800">Import CSV</div>
65
+ <p class="mb-3 mt-1 text-xs text-slate-500">Drop a CSV file here to copy it into ${escapeHtml(sourceDirLabel)}, sync the mirror, and open the new resource.</p>
66
+ <button type="button" id="csv-pick" class="${buttonClass}">Choose CSV</button>
67
+ <input id="csv-file" type="file" accept=".csv,text/csv" class="hidden">
68
+ <div id="csv-import-status" class="mt-3 text-xs text-slate-500"></div>
69
+ </div>
70
+ </aside>
71
+ <main class="min-w-0 overflow-auto p-5">
72
+ <div id="diagnostics-view" class="mb-4 hidden"></div>
73
+ <div class="mb-4 flex flex-wrap items-center justify-between gap-3">
74
+ <div>
75
+ <h2 id="resource-title" class="text-base font-bold tracking-normal text-slate-950">Select a resource</h2>
76
+ <div class="${mutedClass}" id="resource-detail"></div>
77
+ </div>
78
+ <div class="${rowClass}" role="tablist" aria-label="db viewer sections">
79
+ <button type="button" class="${activeTabClass}" data-tab="data">Data</button>
80
+ <button type="button" class="${tabClass}" data-tab="rest">REST</button>
81
+ <button type="button" class="${tabClass}" data-tab="graphql">GraphQL</button>
82
+ <button type="button" class="${tabClass}" data-tab="schema">Schema</button>
83
+ </div>
84
+ </div>
85
+
86
+ <section id="tab-data" data-tab-panel>
87
+ <div class="${viewerGridClass}">
88
+ <div class="${panelClass}">
89
+ <div class="${panelHeadClass}">
90
+ <h3 class="text-sm font-bold tracking-normal text-slate-950">Data</h3>
91
+ <button type="button" id="reload-data" class="${buttonClass}">Reload</button>
92
+ </div>
93
+ <div class="${panelBodyClass}" id="data-view"></div>
94
+ </div>
95
+ <div class="${panelClass}">
96
+ <div class="${panelHeadClass}">
97
+ <h3 class="text-sm font-bold tracking-normal text-slate-950">Selected JSON</h3>
98
+ <button type="button" data-copy-target="json-output" class="${buttonClass}">Copy</button>
99
+ </div>
100
+ <div class="${panelBodyClass}">
101
+ <pre id="json-output" class="${codeClass}">{}</pre>
102
+ </div>
103
+ </div>
104
+ </div>
105
+ </section>
106
+
107
+ <section id="tab-rest" data-tab-panel class="hidden">
108
+ <div class="${viewerGridClass}">
109
+ <div class="${panelClass}">
110
+ <div class="${panelHeadClass}">
111
+ <h3 class="text-sm font-bold tracking-normal text-slate-950">REST Specs</h3>
112
+ </div>
113
+ <div class="${panelBodyClass} ${stackClass}">
114
+ <p class="m-0 text-sm text-slate-600">Batch requests run sequentially. Earlier successful writes stay committed if a later item fails.</p>
115
+ <div class="${stackClass}" id="rest-examples"></div>
116
+ </div>
117
+ </div>
118
+ <div class="${panelClass}">
119
+ <div class="${panelHeadClass}">
120
+ <h3 class="text-sm font-bold tracking-normal text-slate-950">REST Runner</h3>
121
+ </div>
122
+ <div class="${panelBodyClass} ${stackClass}">
123
+ <div class="grid items-center gap-2 sm:grid-cols-[auto_minmax(180px,1fr)_auto]">
124
+ <select id="rest-method" aria-label="REST method" class="${selectClass}">
125
+ <option>GET</option>
126
+ <option>POST</option>
127
+ <option>PATCH</option>
128
+ <option>PUT</option>
129
+ <option>DELETE</option>
130
+ </select>
131
+ <input id="rest-path" class="${inputClass}" aria-label="REST path" value="/">
132
+ <button type="button" class="${primaryButtonClass}" id="run-rest">Run</button>
133
+ </div>
134
+ <textarea id="rest-body" class="${textareaClass}" aria-label="REST request body">{}</textarea>
135
+ <pre id="rest-output" class="${codeClass}">{}</pre>
136
+ </div>
137
+ </div>
138
+ </div>
139
+ </section>
140
+
141
+ <section id="tab-graphql" data-tab-panel class="hidden">
142
+ <div class="${viewerGridClass}">
143
+ <div class="${panelClass}">
144
+ <div class="${panelHeadClass}">
145
+ <h3 class="text-sm font-bold tracking-normal text-slate-950">GraphQL Examples</h3>
146
+ </div>
147
+ <div class="${panelBodyClass} ${stackClass}" id="graphql-examples"></div>
148
+ </div>
149
+ <div class="${panelClass}">
150
+ <div class="${panelHeadClass}">
151
+ <h3 class="text-sm font-bold tracking-normal text-slate-950">GraphQL Runner</h3>
152
+ <button type="button" data-copy-target="graphql-query" class="${buttonClass}">Copy Query</button>
153
+ </div>
154
+ <div class="${panelBodyClass} ${stackClass}">
155
+ <textarea id="graphql-query" class="${textareaClass}" aria-label="GraphQL query"></textarea>
156
+ <textarea id="graphql-variables" class="${textareaClass}" aria-label="GraphQL variables">{}</textarea>
157
+ <div class="${rowClass}">
158
+ <button type="button" class="${primaryButtonClass}" id="run-graphql">Run GraphQL</button>
159
+ <button type="button" id="load-sdl" class="${buttonClass}">Load SDL</button>
160
+ </div>
161
+ <pre id="graphql-output" class="${codeClass}">{}</pre>
162
+ </div>
163
+ </div>
164
+ </div>
165
+ </section>
166
+
167
+ <section id="tab-schema" data-tab-panel class="hidden">
168
+ <div class="${viewerGridClass}">
169
+ <div class="${panelClass}">
170
+ <div class="${panelHeadClass}">
171
+ <h3 class="text-sm font-bold tracking-normal text-slate-950">Fields</h3>
172
+ </div>
173
+ <div class="${panelBodyClass}" id="field-view"></div>
174
+ </div>
175
+ <div class="${panelClass}">
176
+ <div class="${panelHeadClass}">
177
+ <h3 class="text-sm font-bold tracking-normal text-slate-950">Generated Schema</h3>
178
+ <button type="button" data-copy-target="schema-output" class="${buttonClass}">Copy</button>
179
+ </div>
180
+ <div class="${panelBodyClass}">
181
+ <pre id="schema-output" class="${codeClass}">{}</pre>
182
+ </div>
183
+ </div>
184
+ </div>
185
+ </section>
186
+ </main>
187
+ </div>
188
+ </div>
189
+
190
+ <script>
191
+ const GRAPHQL_PATH = ${JSON.stringify(graphqlPath)};
192
+ const SCHEMA_PATH = ${JSON.stringify(schemaPath)};
193
+ const MANIFEST_PATH = ${JSON.stringify(manifestPath)};
194
+ const EVENTS_PATH = ${JSON.stringify(eventsPath)};
195
+ const IMPORT_PATH = ${JSON.stringify(importPath)};
196
+ const REST_BATCH_PATH = ${JSON.stringify(restBatchPath)};
197
+ const REST_BASE_PATH = ${JSON.stringify(restBasePath)};
198
+ const BUTTON_CLASS = ${JSON.stringify(buttonClass)};
199
+ const TAB_CLASS = ${JSON.stringify(tabClass)};
200
+ const ACTIVE_TAB_CLASS = ${JSON.stringify(activeTabClass)};
201
+ const RESOURCE_BUTTON_CLASS = ${JSON.stringify(resourceButtonClass)};
202
+ const ACTIVE_RESOURCE_BUTTON_CLASS = ${JSON.stringify(activeResourceButtonClass)};
203
+ const PILL_CLASS = ${JSON.stringify(pillClass)};
204
+ const WARNING_PILL_CLASS = ${JSON.stringify(warningPillClass)};
205
+ const ERROR_PILL_CLASS = ${JSON.stringify(errorPillClass)};
206
+ const IMPORT_DROP_CLASS = ${JSON.stringify(importDropClass)};
207
+ const IMPORT_DROP_ACTIVE_CLASS = ${JSON.stringify(importDropActiveClass)};
208
+ const CODE_CLASS = ${JSON.stringify(codeClass)};
209
+ const MUTED_CLASS = ${JSON.stringify(mutedClass)};
210
+ const TABLE_WRAP_CLASS = ${JSON.stringify(tableWrapClass)};
211
+ const TABLE_CLASS = ${JSON.stringify(tableClass)};
212
+ const TH_CLASS = ${JSON.stringify(thClass)};
213
+ const TD_CLASS = ${JSON.stringify(tdClass)};
214
+ const EXAMPLE_CLASS = ${JSON.stringify(exampleClass)};
215
+ const EXAMPLE_HEAD_CLASS = ${JSON.stringify(exampleHeadClass)};
216
+ const ROW_CLASS = ${JSON.stringify(rowClass)};
217
+ const state = {
218
+ manifest: null,
219
+ schema: null,
220
+ resources: [],
221
+ selected: null,
222
+ selectedData: null,
223
+ };
224
+
225
+ const els = {
226
+ subtitle: document.getElementById('subtitle'),
227
+ status: document.getElementById('status'),
228
+ resources: document.getElementById('resource-list'),
229
+ refresh: document.getElementById('refresh'),
230
+ reloadData: document.getElementById('reload-data'),
231
+ resourceTitle: document.getElementById('resource-title'),
232
+ resourceDetail: document.getElementById('resource-detail'),
233
+ dataView: document.getElementById('data-view'),
234
+ jsonOutput: document.getElementById('json-output'),
235
+ restExamples: document.getElementById('rest-examples'),
236
+ restMethod: document.getElementById('rest-method'),
237
+ restPath: document.getElementById('rest-path'),
238
+ restBody: document.getElementById('rest-body'),
239
+ restOutput: document.getElementById('rest-output'),
240
+ graphqlExamples: document.getElementById('graphql-examples'),
241
+ graphqlQuery: document.getElementById('graphql-query'),
242
+ graphqlVariables: document.getElementById('graphql-variables'),
243
+ graphqlOutput: document.getElementById('graphql-output'),
244
+ loadSdl: document.getElementById('load-sdl'),
245
+ fieldView: document.getElementById('field-view'),
246
+ schemaOutput: document.getElementById('schema-output'),
247
+ diagnosticsView: document.getElementById('diagnostics-view'),
248
+ csvDrop: document.getElementById('csv-drop'),
249
+ csvPick: document.getElementById('csv-pick'),
250
+ csvFile: document.getElementById('csv-file'),
251
+ csvImportStatus: document.getElementById('csv-import-status'),
252
+ };
253
+
254
+ document.addEventListener('click', async (event) => {
255
+ const copyButton = event.target.closest('[data-copy-target]');
256
+ if (copyButton) {
257
+ await copyText(document.getElementById(copyButton.dataset.copyTarget).textContent);
258
+ }
259
+
260
+ const exampleButton = event.target.closest('[data-load-example]');
261
+ if (exampleButton) {
262
+ loadExample(JSON.parse(exampleButton.dataset.loadExample));
263
+ }
264
+
265
+ const resourceButton = event.target.closest('[data-resource]');
266
+ if (resourceButton) {
267
+ await selectResource(resourceButton.dataset.resource);
268
+ }
269
+ });
270
+
271
+ document.querySelectorAll('[data-tab]').forEach((button) => {
272
+ button.addEventListener('click', () => showTab(button.dataset.tab));
273
+ });
274
+
275
+ els.refresh.addEventListener('click', boot);
276
+ els.reloadData.addEventListener('click', () => loadSelectedData());
277
+ document.getElementById('run-rest').addEventListener('click', runRest);
278
+ document.getElementById('run-graphql').addEventListener('click', runGraphql);
279
+ els.loadSdl.addEventListener('click', loadGraphqlSdl);
280
+ els.csvPick.addEventListener('click', () => els.csvFile.click());
281
+ els.csvFile.addEventListener('change', () => importCsvFile(els.csvFile.files[0]));
282
+ for (const eventName of ['dragenter', 'dragover']) {
283
+ els.csvDrop.addEventListener(eventName, (event) => {
284
+ event.preventDefault();
285
+ els.csvDrop.className = IMPORT_DROP_ACTIVE_CLASS;
286
+ });
287
+ }
288
+ for (const eventName of ['dragleave', 'drop']) {
289
+ els.csvDrop.addEventListener(eventName, (event) => {
290
+ event.preventDefault();
291
+ els.csvDrop.className = IMPORT_DROP_CLASS;
292
+ });
293
+ }
294
+ els.csvDrop.addEventListener('drop', (event) => {
295
+ importCsvFile(event.dataTransfer?.files?.[0]);
296
+ });
297
+
298
+ boot().catch(showFatal);
299
+ connectLiveReload();
300
+
301
+ async function boot(preferredResourceName) {
302
+ const [manifest, schema] = await Promise.all([
303
+ fetchJson(MANIFEST_PATH),
304
+ fetchJson(SCHEMA_PATH),
305
+ ]);
306
+ state.manifest = manifest;
307
+ state.schema = schema;
308
+ state.resources = [
309
+ ...Object.entries(manifest.collections || {}).map(([name, resource]) => ({ name, ...resource })),
310
+ ...Object.entries(manifest.documents || {}).map(([name, resource]) => ({ name, ...resource })),
311
+ ];
312
+ renderStatus();
313
+ renderDiagnostics();
314
+ renderResourceList();
315
+ els.subtitle.textContent = state.resources.length + ' resources loaded';
316
+ const resourceName = resolveInitialResourceName(preferredResourceName);
317
+ if (resourceName) {
318
+ await selectResource(resourceName);
319
+ }
320
+ }
321
+
322
+ async function selectResource(name) {
323
+ const resourceName = resolveResourceName(name);
324
+ state.selected = state.resources.find((resource) => resource.name === resourceName);
325
+ if (!state.selected) {
326
+ return;
327
+ }
328
+ rememberResource(resourceName);
329
+
330
+ document.querySelectorAll('[data-resource]').forEach((button) => {
331
+ button.className = button.dataset.resource === resourceName ? ACTIVE_RESOURCE_BUTTON_CLASS : RESOURCE_BUTTON_CLASS;
332
+ });
333
+
334
+ els.resourceTitle.textContent = state.selected.name;
335
+ els.resourceDetail.textContent = state.selected.kind + ' · ' + state.selected.typeName + routeText(state.selected);
336
+ renderFields();
337
+ els.schemaOutput.textContent = pretty(state.schema?.resources?.[state.selected.name] || state.selected);
338
+ await loadSelectedData();
339
+ renderRestExamples();
340
+ renderGraphqlExamples();
341
+ }
342
+
343
+ async function loadSelectedData() {
344
+ if (!state.selected) {
345
+ return;
346
+ }
347
+
348
+ const response = await fetch(resourcePath(state.selected));
349
+ if (!response.ok) {
350
+ throw new Error('Could not load ' + state.selected.name + ': ' + response.status + ' ' + response.statusText);
351
+ }
352
+ state.selectedData = await response.json();
353
+ els.jsonOutput.textContent = pretty(state.selectedData);
354
+ renderData();
355
+ }
356
+
357
+ function renderStatus() {
358
+ const diagnostics = state.manifest.diagnostics || [];
359
+ const errors = diagnostics.filter((item) => item.severity === 'error').length;
360
+ const warnings = diagnostics.filter((item) => item.severity === 'warn').length;
361
+ els.status.innerHTML = '';
362
+ els.status.append(
363
+ pill(state.resources.length + ' resources'),
364
+ pill('REST ready'),
365
+ pill(state.manifest.capabilities?.graphql === false ? 'GraphQL off' : 'GraphQL ready'),
366
+ pill(errors + ' errors', errors > 0 ? 'error' : ''),
367
+ pill(warnings + ' warnings', warnings > 0 ? 'warning' : ''),
368
+ );
369
+ }
370
+
371
+ function renderDiagnostics() {
372
+ const diagnostics = state.manifest.diagnostics || [];
373
+ if (diagnostics.length === 0) {
374
+ els.diagnosticsView.className = 'mb-4 hidden';
375
+ els.diagnosticsView.innerHTML = '';
376
+ return;
377
+ }
378
+
379
+ els.diagnosticsView.className = 'mb-4 grid gap-2 rounded-lg border border-amber-200 bg-amber-50 p-4 text-sm text-amber-950 shadow-sm';
380
+ els.diagnosticsView.innerHTML = '';
381
+ const heading = document.createElement('div');
382
+ heading.className = 'font-bold';
383
+ heading.textContent = 'Source diagnostics';
384
+ els.diagnosticsView.append(heading);
385
+
386
+ for (const diagnostic of diagnostics) {
387
+ const item = document.createElement('div');
388
+ item.className = diagnostic.severity === 'error'
389
+ ? 'rounded-md border border-red-200 bg-white p-3 text-red-900'
390
+ : 'rounded-md border border-amber-200 bg-white p-3 text-amber-900';
391
+ const fileText = diagnostic.file ? diagnostic.file + ': ' : '';
392
+ item.textContent = fileText + diagnostic.message + (diagnostic.hint ? ' ' + diagnostic.hint : '');
393
+ els.diagnosticsView.append(item);
394
+ }
395
+ }
396
+
397
+ function renderResourceList() {
398
+ els.resources.innerHTML = '';
399
+ for (const resource of state.resources) {
400
+ const button = document.createElement('button');
401
+ button.type = 'button';
402
+ button.className = RESOURCE_BUTTON_CLASS;
403
+ button.dataset.resource = resource.name;
404
+ button.innerHTML = '<span data-resource-name class="font-semibold text-slate-950"></span><span data-resource-meta class="text-xs text-slate-500"></span>';
405
+ button.querySelector('[data-resource-name]').textContent = resource.name;
406
+ button.querySelector('[data-resource-meta]').textContent = resource.kind + ' · ' + Object.keys(resource.fields || {}).length + ' fields';
407
+ els.resources.append(button);
408
+ }
409
+ }
410
+
411
+ function renderData() {
412
+ const data = state.selectedData;
413
+ if (Array.isArray(data)) {
414
+ els.dataView.innerHTML = renderTable(data, state.selected);
415
+ return;
416
+ }
417
+
418
+ els.dataView.innerHTML = '<pre class="' + CODE_CLASS + '"></pre>';
419
+ els.dataView.querySelector('pre').textContent = pretty(data);
420
+ }
421
+
422
+ function renderTable(records, resource) {
423
+ if (records.length === 0) {
424
+ return '<div class="' + MUTED_CLASS + '">[]</div>';
425
+ }
426
+
427
+ const columns = Array.from(records.reduce((set, record) => {
428
+ Object.keys(record || {}).forEach((key) => set.add(key));
429
+ return set;
430
+ }, new Set()));
431
+
432
+ const head = columns.map((column) => '<th class="' + TH_CLASS + '">' + escapeHtml(column) + '</th>').join('');
433
+ const rows = records.map((record) => '<tr>' + columns.map((column) => '<td class="' + TD_CLASS + '">' + cellHtml(resource, record, column) + '</td>').join('') + '</tr>').join('');
434
+ return '<div class="' + TABLE_WRAP_CLASS + '"><table class="' + TABLE_CLASS + '"><thead><tr>' + head + '</tr></thead><tbody>' + rows + '</tbody></table></div>';
435
+ }
436
+
437
+ function renderRestExamples() {
438
+ const examples = restExamplesFor(state.selected);
439
+ els.restExamples.innerHTML = '';
440
+ for (const example of examples) {
441
+ els.restExamples.append(exampleView(example, 'rest'));
442
+ }
443
+ if (examples[0]) {
444
+ loadExample({ kind: 'rest', ...examples[0] });
445
+ }
446
+ }
447
+
448
+ function renderGraphqlExamples() {
449
+ const examples = graphqlExamplesFor(state.selected);
450
+ els.graphqlExamples.innerHTML = '';
451
+ for (const example of examples) {
452
+ els.graphqlExamples.append(exampleView(example, 'graphql'));
453
+ }
454
+ if (examples[0]) {
455
+ loadExample({ kind: 'graphql', ...examples[0] });
456
+ }
457
+ }
458
+
459
+ function renderFields() {
460
+ const rows = Object.entries(state.selected.fields || {}).map(([name, field]) => {
461
+ return '<tr><td class="' + TD_CLASS + '">' + escapeHtml(name) + '</td><td class="' + TD_CLASS + '">' + escapeHtml(fieldType(field)) + '</td><td class="' + TD_CLASS + '">' + escapeHtml(field.required ? 'yes' : 'no') + '</td><td class="' + TD_CLASS + '">' + escapeHtml(relationTextForField(name)) + '</td><td class="' + TD_CLASS + '">' + escapeHtml(field.description || '') + '</td></tr>';
462
+ }).join('');
463
+ els.fieldView.innerHTML = relationSummary(state.selected) + '<div class="' + TABLE_WRAP_CLASS + '"><table class="' + TABLE_CLASS + '"><thead><tr><th class="' + TH_CLASS + '">Field</th><th class="' + TH_CLASS + '">Type</th><th class="' + TH_CLASS + '">Required</th><th class="' + TH_CLASS + '">Relation</th><th class="' + TH_CLASS + '">Description</th></tr></thead><tbody>' + rows + '</tbody></table></div>';
464
+ }
465
+
466
+ function exampleView(example, kind) {
467
+ const element = document.createElement('div');
468
+ element.className = EXAMPLE_CLASS;
469
+ const copyText = kind === 'rest' ? restCopyText(example) : example.query;
470
+ const payload = JSON.stringify({ kind, ...example });
471
+ element.innerHTML = '<div class="' + EXAMPLE_HEAD_CLASS + '"><div><strong class="text-sm font-semibold text-slate-950"></strong><div data-example-meta class="' + MUTED_CLASS + '"></div></div><div class="' + ROW_CLASS + '"><button type="button" data-load-example="">Load</button><button type="button" data-copy-example>Copy</button></div></div><pre class="' + CODE_CLASS + '"></pre>';
472
+ element.querySelector('strong').textContent = example.name;
473
+ element.querySelector('[data-example-meta]').textContent = kind === 'rest' ? example.method + ' ' + example.path : 'GraphQL';
474
+ element.querySelector('[data-load-example]').dataset.loadExample = payload;
475
+ element.querySelectorAll('button').forEach((button) => {
476
+ button.className = BUTTON_CLASS;
477
+ });
478
+ element.querySelector('[data-copy-example]').addEventListener('click', () => copyTextToClipboard(copyText));
479
+ element.querySelector('pre').textContent = copyText;
480
+ return element;
481
+ }
482
+
483
+ function loadExample(example) {
484
+ if (example.kind === 'rest') {
485
+ els.restMethod.value = example.method;
486
+ els.restPath.value = example.path;
487
+ els.restBody.value = example.body === undefined ? '{}' : pretty(example.body);
488
+ } else {
489
+ els.graphqlQuery.value = example.query;
490
+ els.graphqlVariables.value = pretty(example.variables || {});
491
+ }
492
+ }
493
+
494
+ async function runRest() {
495
+ const method = els.restMethod.value;
496
+ const options = { method, headers: { 'content-type': 'application/json' } };
497
+ if (!['GET', 'DELETE'].includes(method)) {
498
+ options.body = els.restBody.value.trim() || '{}';
499
+ }
500
+ const response = await fetch(els.restPath.value, options);
501
+ const text = await response.text();
502
+ els.restOutput.textContent = response.status + ' ' + response.statusText + '\\n' + formatJsonText(text);
503
+ await loadSelectedData();
504
+ }
505
+
506
+ async function runGraphql() {
507
+ const response = await fetch(GRAPHQL_PATH, {
508
+ method: 'POST',
509
+ headers: { 'content-type': 'application/json' },
510
+ body: JSON.stringify({
511
+ query: els.graphqlQuery.value,
512
+ variables: parseJson(els.graphqlVariables.value, {}),
513
+ }),
514
+ });
515
+ const json = await response.json();
516
+ els.graphqlOutput.textContent = pretty(json);
517
+ await loadSelectedData();
518
+ }
519
+
520
+ async function loadGraphqlSdl() {
521
+ const response = await fetch(GRAPHQL_PATH);
522
+ els.graphqlOutput.textContent = await response.text();
523
+ }
524
+
525
+ function restExamplesFor(resource) {
526
+ const path = resourcePath(resource);
527
+ if (resource.kind === 'document') {
528
+ return [
529
+ { name: 'Read document', method: 'GET', path },
530
+ { name: 'Replace document', method: 'PUT', path, body: sampleDocument(resource) },
531
+ { name: 'Patch document', method: 'PATCH', path, body: samplePatch(resource) },
532
+ ];
533
+ }
534
+
535
+ const id = sampleId(resource);
536
+ const examples = [
537
+ { name: 'List records', method: 'GET', path },
538
+ { name: 'List selected fields', method: 'GET', path: path + '?select=' + encodeURIComponent(selectExampleFields(resource).join(',')) },
539
+ { name: 'List page', method: 'GET', path: path + '?offset=0&limit=20' },
540
+ { name: 'Read record', method: 'GET', path: path + '/' + encodeURIComponent(id) },
541
+ { name: 'Create record', method: 'POST', path, body: sampleRecord(resource, { id: nextRecordId(resource) }) },
542
+ { name: 'Patch record', method: 'PATCH', path: path + '/' + encodeURIComponent(id), body: samplePatch(resource) },
543
+ { name: 'Delete record', method: 'DELETE', path: path + '/' + encodeURIComponent(id) },
544
+ { name: 'Batch list and schema', method: 'POST', path: REST_BATCH_PATH, body: [{ method: 'GET', path }, { method: 'GET', path: SCHEMA_PATH }] },
545
+ ];
546
+ examples.splice(4, 0, ...relationRestExamples(resource, id));
547
+ return examples;
548
+ }
549
+
550
+ function graphqlExamplesFor(resource) {
551
+ const fields = selectionFields(resource);
552
+ if (resource.kind === 'document') {
553
+ return [
554
+ { name: 'Read document', query: '{\\n ' + resource.name + ' {\\n' + fields + '\\n }\\n}' },
555
+ { name: 'Patch document', query: 'mutation {\\n update' + resource.typeName + '(patch: ' + inlineObject(samplePatch(resource)) + ') {\\n' + fields + '\\n }\\n}' },
556
+ { name: 'Set value', query: 'mutation {\\n set' + resource.typeName + '(path: "/theme", value: "dark") {\\n' + fields + '\\n }\\n}' },
557
+ ];
558
+ }
559
+
560
+ const singular = lowerFirst(resource.typeName);
561
+ return [
562
+ { name: 'List records', query: '{\\n ' + resource.name + ' {\\n' + fields + '\\n }\\n}' },
563
+ { name: 'Read record', query: 'query Get' + resource.typeName + '($id: ID!) {\\n ' + singular + '(id: $id) {\\n' + fields + '\\n }\\n}', variables: { id: sampleId(resource) } },
564
+ { name: 'Create record', query: 'mutation Create' + resource.typeName + '($input: JSON!) {\\n create' + resource.typeName + '(input: $input) {\\n' + fields + '\\n }\\n}', variables: { input: sampleRecord(resource, { id: nextRecordId(resource) }) } },
565
+ { name: 'Patch record', query: 'mutation {\\n update' + resource.typeName + '(id: "' + sampleId(resource) + '", patch: ' + inlineObject(samplePatch(resource)) + ') {\\n' + fields + '\\n }\\n}' },
566
+ { name: 'Delete record', query: 'mutation {\\n delete' + resource.typeName + '(id: "' + sampleId(resource) + '")\\n}' },
567
+ ];
568
+ }
569
+
570
+ function sampleRecord(resource, options = {}) {
571
+ const record = {};
572
+ for (const [name, field] of Object.entries(resource.fields || {})) {
573
+ record[name] = name === resource.idField && options.id !== undefined
574
+ ? options.id
575
+ : sampleValue(name, field, resource);
576
+ }
577
+ return record;
578
+ }
579
+
580
+ function sampleDocument(resource) {
581
+ return sampleRecord(resource);
582
+ }
583
+
584
+ function samplePatch(resource) {
585
+ const entries = Object.entries(resource.fields || {}).filter(([name]) => name !== resource.idField);
586
+ if (entries.length === 0) {
587
+ return {};
588
+ }
589
+ const [name, field] = entries[0];
590
+ return { [name]: sampleValue(name, field, resource) };
591
+ }
592
+
593
+ function sampleValue(name, field, resource) {
594
+ if (name === resource.idField) {
595
+ return sampleId(resource);
596
+ }
597
+ if ('default' in field) {
598
+ return field.default;
599
+ }
600
+ if (field.type === 'enum') {
601
+ return (field.values || [])[0] || 'value';
602
+ }
603
+ if (field.type === 'number') {
604
+ return 1;
605
+ }
606
+ if (field.type === 'boolean') {
607
+ return true;
608
+ }
609
+ if (field.type === 'array') {
610
+ return [];
611
+ }
612
+ if (field.type === 'object') {
613
+ return {};
614
+ }
615
+ return sampleString(name);
616
+ }
617
+
618
+ function sampleString(name) {
619
+ if (name.toLowerCase().includes('email')) {
620
+ return 'user@example.com';
621
+ }
622
+ if (name.toLowerCase().endsWith('at')) {
623
+ return new Date().toISOString();
624
+ }
625
+ return name + '-value';
626
+ }
627
+
628
+ function sampleId(resource) {
629
+ const data = state.selected?.name === resource.name ? state.selectedData : null;
630
+ if (Array.isArray(data) && data[0] && data[0][resource.idField] !== undefined) {
631
+ return data[0][resource.idField];
632
+ }
633
+ return resource.name + '_1';
634
+ }
635
+
636
+ function selectExampleFields(resource) {
637
+ const names = Object.keys(resource.fields || {}).slice(0, 3);
638
+ return names.length > 0 ? names : [resource.idField || 'id'];
639
+ }
640
+
641
+ function relationRestExamples(resource, id) {
642
+ const relation = (resource.relations || [])[0];
643
+ if (!relation) {
644
+ return [];
645
+ }
646
+
647
+ const target = state.resources.find((candidate) => candidate.name === relation.targetResource);
648
+ const targetField = Object.keys(target?.fields || {}).find((name) => name !== target.idField) || relation.targetField;
649
+ const baseFields = selectExampleFields(resource).filter((name) => !name.includes('.')).slice(0, 2);
650
+ return [
651
+ { name: 'List with ' + relation.name, method: 'GET', path: resourcePath(resource) + '?expand=' + encodeURIComponent(relation.name) },
652
+ { name: 'Read selected ' + relation.name, method: 'GET', path: resourcePath(resource) + '/' + encodeURIComponent(id) + '?expand=' + encodeURIComponent(relation.name) + '&select=' + encodeURIComponent([...baseFields, relation.name + '.' + targetField].join(',')) },
653
+ ];
654
+ }
655
+
656
+ function nextRecordId(resource) {
657
+ const data = state.selected?.name === resource.name && Array.isArray(state.selectedData)
658
+ ? state.selectedData
659
+ : [];
660
+ const ids = data
661
+ .map((record) => record?.[resource.idField])
662
+ .filter((id) => id !== undefined && id !== null && id !== '')
663
+ .map((id) => String(id));
664
+ const sample = ids[0];
665
+ const match = sample?.match(/^(.*?)(\\d+)$/);
666
+
667
+ if (match) {
668
+ const prefix = match[1];
669
+ const next = ids.reduce((max, id) => {
670
+ const current = id.match(/^(.*?)(\\d+)$/);
671
+ return current && current[1] === prefix ? Math.max(max, Number(current[2])) : max;
672
+ }, Number(match[2])) + 1;
673
+ return prefix + next;
674
+ }
675
+
676
+ return String(ids.length + 1);
677
+ }
678
+
679
+ function selectionFields(resource) {
680
+ const fieldNames = Object.keys(resource.fields || {}).slice(0, 6);
681
+ if (fieldNames.length === 0) {
682
+ return ' __typename';
683
+ }
684
+ return fieldNames.map((name) => ' ' + name).join('\\n');
685
+ }
686
+
687
+ function resourcePath(resource) {
688
+ if (resource.api?.list) {
689
+ return resource.api.list;
690
+ }
691
+ if (resource.api?.read) {
692
+ return resource.api.read;
693
+ }
694
+ return joinPaths(REST_BASE_PATH, resource.routePath || '/' + resource.name);
695
+ }
696
+
697
+ function relationForField(resource, fieldName) {
698
+ return (resource?.relations || []).find((relation) => relation.sourceField === fieldName) || null;
699
+ }
700
+
701
+ function relationTextForField(fieldName) {
702
+ const relation = relationForField(state.selected, fieldName);
703
+ return relation ? relation.name + ' -> ' + relation.targetResource + '.' + relation.targetField : '';
704
+ }
705
+
706
+ function relationSummary(resource) {
707
+ const relations = resource?.relations || [];
708
+ if (relations.length === 0) {
709
+ return '<div class="mb-3 rounded-md border border-slate-200 bg-slate-50 p-3 text-sm text-slate-600"><strong class="text-slate-900">Relations</strong><div>No explicit relations declared.</div></div>';
710
+ }
711
+
712
+ const rows = relations.map((relation) => '<tr><td class="' + TD_CLASS + '">' + escapeHtml(relation.name) + '</td><td class="' + TD_CLASS + '">' + escapeHtml(relation.sourceField) + '</td><td class="' + TD_CLASS + '">' + escapeHtml(relation.targetResource + '.' + relation.targetField) + '</td><td class="' + TD_CLASS + '"><code>expand=' + escapeHtml(relation.name) + '</code></td></tr>').join('');
713
+ return '<div class="mb-3"><h4 class="mb-2 text-sm font-bold tracking-normal text-slate-950">Relations</h4><div class="' + TABLE_WRAP_CLASS + '"><table class="' + TABLE_CLASS + '"><thead><tr><th class="' + TH_CLASS + '">Name</th><th class="' + TH_CLASS + '">Source</th><th class="' + TH_CLASS + '">Target</th><th class="' + TH_CLASS + '">REST</th></tr></thead><tbody>' + rows + '</tbody></table></div></div>';
714
+ }
715
+
716
+ function cellHtml(resource, record, column) {
717
+ const relation = relationForField(resource, column);
718
+ const value = record?.[column];
719
+ if (!relation || value === undefined || value === null || value === '') {
720
+ return escapeHtml(formatCell(value));
721
+ }
722
+
723
+ const target = state.resources.find((candidate) => candidate.name === relation.targetResource);
724
+ const targetPath = target ? resourcePath(target) : joinPaths(REST_BASE_PATH, '/' + relation.targetResource);
725
+ const href = targetPath + '/' + encodeURIComponent(value);
726
+ return '<a data-relation-link href="' + escapeHtml(href) + '" class="font-semibold text-emerald-700 hover:underline">' + escapeHtml(formatCell(value)) + '</a>';
727
+ }
728
+
729
+ function routeText(resource) {
730
+ return ' · ' + resourcePath(resource);
731
+ }
732
+
733
+ function restCopyText(example) {
734
+ const lines = [example.method + ' ' + example.path];
735
+ if (example.body !== undefined) {
736
+ lines.push('', pretty(example.body));
737
+ }
738
+ return lines.join('\\n');
739
+ }
740
+
741
+ function resolveInitialResourceName(preferredResourceName) {
742
+ const preferred = resolveResourceName(preferredResourceName);
743
+ if (preferred) {
744
+ return preferred;
745
+ }
746
+
747
+ const params = new URLSearchParams(window.location.search);
748
+ const queryResource = params.get('resource');
749
+ if (queryResource) {
750
+ const resolvedQueryResource = resolveResourceName(queryResource);
751
+ if (resolvedQueryResource) {
752
+ return resolvedQueryResource;
753
+ }
754
+ clearRememberedResource(true);
755
+ return state.resources[0]?.name;
756
+ }
757
+
758
+ const storedResource = localStorage.getItem('db:selectedResource');
759
+ if (storedResource) {
760
+ const resolvedStoredResource = resolveResourceName(storedResource);
761
+ if (resolvedStoredResource) {
762
+ return resolvedStoredResource;
763
+ }
764
+ clearRememberedResource(false);
765
+ }
766
+
767
+ if (state.selected?.name && hasResource(state.selected.name)) {
768
+ return state.selected.name;
769
+ }
770
+
771
+ return state.resources[0]?.name;
772
+ }
773
+
774
+ function hasResource(name) {
775
+ return Boolean(resolveResourceName(name));
776
+ }
777
+
778
+ function resolveResourceName(name) {
779
+ if (!name) {
780
+ return null;
781
+ }
782
+ for (const candidate of resourceNameCandidates(name)) {
783
+ if (state.resources.some((resource) => resource.name === candidate)) {
784
+ return candidate;
785
+ }
786
+ }
787
+ return null;
788
+ }
789
+
790
+ function resourceNameCandidates(value) {
791
+ const exact = String(value);
792
+ return [...new Set([exact, camelCase(exact), kebabCase(exact)])];
793
+ }
794
+
795
+ function camelCase(value) {
796
+ return words(value).map((word, index) => (
797
+ index === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1)
798
+ )).join('');
799
+ }
800
+
801
+ function kebabCase(value) {
802
+ return words(value).join('-');
803
+ }
804
+
805
+ function words(value) {
806
+ return String(value)
807
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
808
+ .split(/[^A-Za-z0-9]+/)
809
+ .filter(Boolean)
810
+ .map((part) => part.toLowerCase());
811
+ }
812
+
813
+ function rememberResource(name) {
814
+ localStorage.setItem('db:selectedResource', name);
815
+ const url = new URL(window.location.href);
816
+ url.searchParams.set('resource', name);
817
+ window.history.replaceState({}, '', url);
818
+ }
819
+
820
+ function clearRememberedResource(clearQuery) {
821
+ localStorage.removeItem('db:selectedResource');
822
+ if (clearQuery) {
823
+ const url = new URL(window.location.href);
824
+ url.searchParams.delete('resource');
825
+ window.history.replaceState({}, '', url);
826
+ }
827
+ }
828
+
829
+ function connectLiveReload() {
830
+ if (!window.EventSource) {
831
+ return;
832
+ }
833
+
834
+ const events = new EventSource(EVENTS_PATH);
835
+ events.addEventListener('db', (event) => {
836
+ const payload = JSON.parse(event.data);
837
+ if (payload.type === 'connected') {
838
+ return;
839
+ }
840
+
841
+ const selectedName = state.selected?.name;
842
+ els.subtitle.textContent = payload.type === 'synced-with-errors'
843
+ ? 'Files changed; reloaded with source errors'
844
+ : 'Files changed; reloaded';
845
+ boot(selectedName).catch(showFatal);
846
+ });
847
+ }
848
+
849
+ async function importCsvFile(file) {
850
+ if (!file) {
851
+ return;
852
+ }
853
+
854
+ if (!file.name.toLowerCase().endsWith('.csv')) {
855
+ setImportStatus('Choose a .csv file.', 'error');
856
+ return;
857
+ }
858
+
859
+ setImportStatus('Importing ' + file.name + '...', 'loading');
860
+ try {
861
+ const response = await fetch(IMPORT_PATH, {
862
+ method: 'POST',
863
+ headers: {
864
+ 'content-type': 'text/csv; charset=utf-8',
865
+ 'x-db-file-name': file.name,
866
+ },
867
+ body: file,
868
+ });
869
+ const result = await response.json();
870
+ if (!response.ok) {
871
+ throw new Error(result.error?.message || 'CSV import failed.');
872
+ }
873
+
874
+ setImportStatus('Imported ' + result.dataPath + ' and opened ' + result.resource + '.', 'success');
875
+ await boot(result.resource);
876
+ showTab('data');
877
+ } catch (error) {
878
+ setImportStatus(error.message, 'error');
879
+ } finally {
880
+ els.csvFile.value = '';
881
+ }
882
+ }
883
+
884
+ function setImportStatus(message, kind) {
885
+ els.csvImportStatus.textContent = message;
886
+ els.csvImportStatus.className = kind === 'error'
887
+ ? 'mt-3 text-xs font-medium text-red-700'
888
+ : kind === 'success'
889
+ ? 'mt-3 text-xs font-medium text-emerald-700'
890
+ : 'mt-3 text-xs text-slate-500';
891
+ }
892
+
893
+ function showTab(name) {
894
+ document.querySelectorAll('[data-tab]').forEach((button) => {
895
+ button.className = button.dataset.tab === name ? ACTIVE_TAB_CLASS : TAB_CLASS;
896
+ });
897
+ document.querySelectorAll('[data-tab-panel]').forEach((panel) => {
898
+ panel.classList.toggle('hidden', panel.id !== 'tab-' + name);
899
+ });
900
+ }
901
+
902
+ function fieldType(field) {
903
+ const suffix = field.nullable ? ' | null' : '';
904
+ if (field.type === 'enum') {
905
+ return 'enum(' + (field.values || []).join(', ') + ')' + suffix;
906
+ }
907
+ if (field.type === 'array') {
908
+ return 'array<' + fieldType(field.items || { type: 'unknown' }) + '>' + suffix;
909
+ }
910
+ return (field.type || 'unknown') + suffix;
911
+ }
912
+
913
+ function inlineObject(value) {
914
+ return JSON.stringify(value).replace(/"([^"]+)":/g, '$1:');
915
+ }
916
+
917
+ function lowerFirst(value) {
918
+ return value.charAt(0).toLowerCase() + value.slice(1);
919
+ }
920
+
921
+ function parseJson(text, fallback) {
922
+ try {
923
+ return text.trim() ? JSON.parse(text) : fallback;
924
+ } catch (error) {
925
+ return fallback;
926
+ }
927
+ }
928
+
929
+ function formatJsonText(text) {
930
+ try {
931
+ return pretty(JSON.parse(text));
932
+ } catch {
933
+ return text;
934
+ }
935
+ }
936
+
937
+ function formatCell(value) {
938
+ if (value === null || value === undefined) {
939
+ return '';
940
+ }
941
+ if (typeof value === 'object') {
942
+ return JSON.stringify(value);
943
+ }
944
+ return String(value);
945
+ }
946
+
947
+ function pretty(value) {
948
+ return JSON.stringify(value, null, 2);
949
+ }
950
+
951
+ function pill(text, className) {
952
+ const element = document.createElement('span');
953
+ element.className = className === 'error'
954
+ ? ERROR_PILL_CLASS
955
+ : className === 'warning'
956
+ ? WARNING_PILL_CLASS
957
+ : PILL_CLASS;
958
+ element.textContent = text;
959
+ return element;
960
+ }
961
+
962
+ async function fetchJson(path) {
963
+ const response = await fetch(path);
964
+ if (!response.ok) {
965
+ throw new Error('Request failed: ' + response.status + ' ' + path);
966
+ }
967
+ return response.json();
968
+ }
969
+
970
+ function joinPaths(basePath, routePath) {
971
+ if (!basePath) {
972
+ return routePath;
973
+ }
974
+
975
+ const base = '/' + String(basePath).replace(/^\\/+/, '').replace(/\\/+$/, '');
976
+ const route = '/' + String(routePath || '/').replace(/^\\/+/, '');
977
+ return base + (route === '/' ? '' : route);
978
+ }
979
+
980
+ async function copyText(text) {
981
+ await copyTextToClipboard(text);
982
+ }
983
+
984
+ async function copyTextToClipboard(text) {
985
+ if (navigator.clipboard) {
986
+ await navigator.clipboard.writeText(text);
987
+ }
988
+ }
989
+
990
+ function escapeHtml(value) {
991
+ return String(value)
992
+ .replaceAll('&', '&amp;')
993
+ .replaceAll('<', '&lt;')
994
+ .replaceAll('>', '&gt;')
995
+ .replaceAll('"', '&quot;')
996
+ .replaceAll("'", '&#039;');
997
+ }
998
+
999
+ function showFatal(error) {
1000
+ els.subtitle.textContent = 'Unable to load db viewer';
1001
+ els.dataView.innerHTML = '<pre class="' + CODE_CLASS + '"></pre>';
1002
+ els.dataView.querySelector('pre').textContent = error.stack || error.message;
1003
+ }
1004
+ </script>
1005
+ </body>
1006
+ </html>`;
1007
+ }
1008
+ function escapeHtml(value) {
1009
+ return String(value)
1010
+ .replaceAll('&', '&amp;')
1011
+ .replaceAll('<', '&lt;')
1012
+ .replaceAll('>', '&gt;')
1013
+ .replaceAll('"', '&quot;')
1014
+ .replaceAll("'", '&#39;');
1015
+ }