@edge-base/server 0.1.1

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 (309) hide show
  1. package/admin-build/.gitkeep +0 -0
  2. package/admin-build/_app/env.js +1 -0
  3. package/admin-build/_app/immutable/assets/0.Bm6cF078.css +1 -0
  4. package/admin-build/_app/immutable/assets/1.BfW3pUNa.css +1 -0
  5. package/admin-build/_app/immutable/assets/11.CVmQOewb.css +1 -0
  6. package/admin-build/_app/immutable/assets/12.B1EhbRZT.css +1 -0
  7. package/admin-build/_app/immutable/assets/13.BvwYeuwE.css +1 -0
  8. package/admin-build/_app/immutable/assets/14.CdVfcO0R.css +1 -0
  9. package/admin-build/_app/immutable/assets/15.2yeZ66b-.css +1 -0
  10. package/admin-build/_app/immutable/assets/17.BVg0JEVu.css +1 -0
  11. package/admin-build/_app/immutable/assets/18.Rwnl3x_i.css +1 -0
  12. package/admin-build/_app/immutable/assets/20.DsPWA9AV.css +1 -0
  13. package/admin-build/_app/immutable/assets/21.Dz2RJ56c.css +1 -0
  14. package/admin-build/_app/immutable/assets/22.DwNLk5Ai.css +1 -0
  15. package/admin-build/_app/immutable/assets/23.CFpu0gOO.css +1 -0
  16. package/admin-build/_app/immutable/assets/24.Cy5LBeoJ.css +1 -0
  17. package/admin-build/_app/immutable/assets/25.pUyLVf-h.css +1 -0
  18. package/admin-build/_app/immutable/assets/26.DBcGrlXa.css +1 -0
  19. package/admin-build/_app/immutable/assets/27.BswYyAJD.css +1 -0
  20. package/admin-build/_app/immutable/assets/28.B4ueB1Kf.css +1 -0
  21. package/admin-build/_app/immutable/assets/29.B-qU6PdF.css +1 -0
  22. package/admin-build/_app/immutable/assets/3.Dg81Pgmd.css +1 -0
  23. package/admin-build/_app/immutable/assets/30.CsdWum94.css +1 -0
  24. package/admin-build/_app/immutable/assets/31.U6OwIp50.css +1 -0
  25. package/admin-build/_app/immutable/assets/4.CyawCCux.css +1 -0
  26. package/admin-build/_app/immutable/assets/5.C0YO2HTk.css +1 -0
  27. package/admin-build/_app/immutable/assets/8.Br5jd6kD.css +1 -0
  28. package/admin-build/_app/immutable/assets/Badge.EMYLHBxE.css +1 -0
  29. package/admin-build/_app/immutable/assets/Button.DpzMRTjK.css +1 -0
  30. package/admin-build/_app/immutable/assets/ConfirmDialog.DAnaWRRk.css +1 -0
  31. package/admin-build/_app/immutable/assets/EmptyState.CwKsu57Y.css +1 -0
  32. package/admin-build/_app/immutable/assets/Input.BDUSenmU.css +1 -0
  33. package/admin-build/_app/immutable/assets/Modal.Dm5B0Xie.css +1 -0
  34. package/admin-build/_app/immutable/assets/PageShell.CmU-Xh-b.css +1 -0
  35. package/admin-build/_app/immutable/assets/SchemaFieldEditor.g4NsCdno.css +1 -0
  36. package/admin-build/_app/immutable/assets/Select.BW4Keufm.css +1 -0
  37. package/admin-build/_app/immutable/assets/Skeleton.KWUulTKJ.css +1 -0
  38. package/admin-build/_app/immutable/assets/Tabs.CniGYb67.css +1 -0
  39. package/admin-build/_app/immutable/assets/TimeChart.BTCDAvmT.css +1 -0
  40. package/admin-build/_app/immutable/assets/Toggle.Cy_K12OM.css +1 -0
  41. package/admin-build/_app/immutable/assets/TopList.ClFzmPlA.css +1 -0
  42. package/admin-build/_app/immutable/chunks/7B47DvSx.js +1 -0
  43. package/admin-build/_app/immutable/chunks/7f08Id8e.js +1 -0
  44. package/admin-build/_app/immutable/chunks/8wJeQ7LN.js +1 -0
  45. package/admin-build/_app/immutable/chunks/B-h2afW5.js +1 -0
  46. package/admin-build/_app/immutable/chunks/B8vJP3wz.js +1 -0
  47. package/admin-build/_app/immutable/chunks/BR_fL5Yv.js +1 -0
  48. package/admin-build/_app/immutable/chunks/BY92tFS2.js +1 -0
  49. package/admin-build/_app/immutable/chunks/BcR-Rdj9.js +1 -0
  50. package/admin-build/_app/immutable/chunks/BdrwyZv8.js +1 -0
  51. package/admin-build/_app/immutable/chunks/Bh56EfQ_.js +1 -0
  52. package/admin-build/_app/immutable/chunks/BkrCkgYp.js +1 -0
  53. package/admin-build/_app/immutable/chunks/BmRjiP5k.js +1 -0
  54. package/admin-build/_app/immutable/chunks/BsokvhWC.js +1 -0
  55. package/admin-build/_app/immutable/chunks/C4D51vTW.js +1 -0
  56. package/admin-build/_app/immutable/chunks/C6puvcoR.js +2 -0
  57. package/admin-build/_app/immutable/chunks/CCKNu7m7.js +1 -0
  58. package/admin-build/_app/immutable/chunks/CWj6FrbW.js +1 -0
  59. package/admin-build/_app/immutable/chunks/Ce-ngf4p.js +5 -0
  60. package/admin-build/_app/immutable/chunks/Cs0GwzJA.js +1 -0
  61. package/admin-build/_app/immutable/chunks/CwROoZK0.js +1 -0
  62. package/admin-build/_app/immutable/chunks/CxCPv_Ut.js +1 -0
  63. package/admin-build/_app/immutable/chunks/CxbRue-5.js +1 -0
  64. package/admin-build/_app/immutable/chunks/CyqB6g-D.js +1 -0
  65. package/admin-build/_app/immutable/chunks/D5h5A1cc.js +2 -0
  66. package/admin-build/_app/immutable/chunks/DnyL7Zq-.js +1 -0
  67. package/admin-build/_app/immutable/chunks/DoPXzH7F.js +1 -0
  68. package/admin-build/_app/immutable/chunks/DrQSgw-f.js +1 -0
  69. package/admin-build/_app/immutable/chunks/DttM2zNO.js +1 -0
  70. package/admin-build/_app/immutable/chunks/DuXuUBWN.js +1 -0
  71. package/admin-build/_app/immutable/chunks/MdeqaOQx.js +10 -0
  72. package/admin-build/_app/immutable/chunks/NuUjtcO2.js +1 -0
  73. package/admin-build/_app/immutable/chunks/Q2nPFxS6.js +1 -0
  74. package/admin-build/_app/immutable/chunks/R6arueIl.js +1 -0
  75. package/admin-build/_app/immutable/chunks/UUazaC_N.js +1 -0
  76. package/admin-build/_app/immutable/chunks/cOYbrQxx.js +1 -0
  77. package/admin-build/_app/immutable/chunks/eFQHTGwA.js +1 -0
  78. package/admin-build/_app/immutable/chunks/ehbppgYb.js +1 -0
  79. package/admin-build/_app/immutable/chunks/glwixJlP.js +1 -0
  80. package/admin-build/_app/immutable/chunks/vApWTCBs.js +1 -0
  81. package/admin-build/_app/immutable/chunks/w89G9Xpi.js +1 -0
  82. package/admin-build/_app/immutable/chunks/wJsUhbfZ.js +1 -0
  83. package/admin-build/_app/immutable/chunks/zfauFM8P.js +1 -0
  84. package/admin-build/_app/immutable/entry/app.CcO-Uos3.js +2 -0
  85. package/admin-build/_app/immutable/entry/start.COebYq3I.js +1 -0
  86. package/admin-build/_app/immutable/nodes/0.CjtHKU-6.js +1 -0
  87. package/admin-build/_app/immutable/nodes/1.DEisjlM0.js +1 -0
  88. package/admin-build/_app/immutable/nodes/10.CvhdyWVB.js +1 -0
  89. package/admin-build/_app/immutable/nodes/11.DjHqcOvy.js +1 -0
  90. package/admin-build/_app/immutable/nodes/12.mQLz4Mj_.js +1 -0
  91. package/admin-build/_app/immutable/nodes/13.CBonZZyP.js +110 -0
  92. package/admin-build/_app/immutable/nodes/14.d-oiZL0j.js +3 -0
  93. package/admin-build/_app/immutable/nodes/15.CKPQsUYF.js +1 -0
  94. package/admin-build/_app/immutable/nodes/16.wPzAPQGx.js +1 -0
  95. package/admin-build/_app/immutable/nodes/17.DayhKyEZ.js +1 -0
  96. package/admin-build/_app/immutable/nodes/18.DKwS0Ir0.js +1 -0
  97. package/admin-build/_app/immutable/nodes/19.wPzAPQGx.js +1 -0
  98. package/admin-build/_app/immutable/nodes/2.BKoKrw1i.js +1 -0
  99. package/admin-build/_app/immutable/nodes/20.BvIkkkrW.js +1 -0
  100. package/admin-build/_app/immutable/nodes/21.DMaFhdHk.js +128 -0
  101. package/admin-build/_app/immutable/nodes/22.3xdgwuK1.js +1 -0
  102. package/admin-build/_app/immutable/nodes/23.8Bvgjbsl.js +112 -0
  103. package/admin-build/_app/immutable/nodes/24.DzSSzRhG.js +2 -0
  104. package/admin-build/_app/immutable/nodes/25.9KKYBnAE.js +2 -0
  105. package/admin-build/_app/immutable/nodes/26.Bhn9dfhY.js +1 -0
  106. package/admin-build/_app/immutable/nodes/27.kRLiC24G.js +1 -0
  107. package/admin-build/_app/immutable/nodes/28.BVIN1-7N.js +1 -0
  108. package/admin-build/_app/immutable/nodes/29.3yabZWj4.js +1 -0
  109. package/admin-build/_app/immutable/nodes/3.BFtSOkX7.js +2 -0
  110. package/admin-build/_app/immutable/nodes/30.CyCQlwaP.js +1 -0
  111. package/admin-build/_app/immutable/nodes/31.C4LDXjES.js +1 -0
  112. package/admin-build/_app/immutable/nodes/4.CvbiMlCa.js +1 -0
  113. package/admin-build/_app/immutable/nodes/5.C6BLv2eM.js +1 -0
  114. package/admin-build/_app/immutable/nodes/6.BcXvfl2P.js +1 -0
  115. package/admin-build/_app/immutable/nodes/7.CIuqhPiK.js +1 -0
  116. package/admin-build/_app/immutable/nodes/8.BQOR_JfO.js +1 -0
  117. package/admin-build/_app/immutable/nodes/9.NZqXQxPy.js +1 -0
  118. package/admin-build/_app/version.json +1 -0
  119. package/admin-build/favicon.svg +26 -0
  120. package/admin-build/index.html +45 -0
  121. package/openapi.json +19543 -0
  122. package/package.json +66 -0
  123. package/src/__tests__/admin-assets.test.ts +55 -0
  124. package/src/__tests__/admin-data-routes.test.ts +488 -0
  125. package/src/__tests__/admin-db-target.test.ts +103 -0
  126. package/src/__tests__/admin-routing.test.ts +31 -0
  127. package/src/__tests__/admin-user-management.test.ts +311 -0
  128. package/src/__tests__/analytics-query.test.ts +75 -0
  129. package/src/__tests__/auth-d1.test.ts +749 -0
  130. package/src/__tests__/auth-db-adapter.test.ts +73 -0
  131. package/src/__tests__/auth-jwt.test.ts +440 -0
  132. package/src/__tests__/auth-oauth.test.ts +389 -0
  133. package/src/__tests__/auth-password.test.ts +367 -0
  134. package/src/__tests__/auth-redirect.test.ts +87 -0
  135. package/src/__tests__/backup-restore.test.ts +711 -0
  136. package/src/__tests__/broadcast.test.ts +128 -0
  137. package/src/__tests__/cli.test.ts +178 -0
  138. package/src/__tests__/cloudflare-realtime.test.ts +113 -0
  139. package/src/__tests__/config.test.ts +469 -0
  140. package/src/__tests__/cors.test.ts +154 -0
  141. package/src/__tests__/cron.test.ts +302 -0
  142. package/src/__tests__/d1-handler.test.ts +402 -0
  143. package/src/__tests__/d1-sql.test.ts +120 -0
  144. package/src/__tests__/database-live-config.test.ts +42 -0
  145. package/src/__tests__/database-live-emitter.test.ts +56 -0
  146. package/src/__tests__/database-live-filters.test.ts +63 -0
  147. package/src/__tests__/database-live-route.test.ts +113 -0
  148. package/src/__tests__/db-sql.test.ts +163 -0
  149. package/src/__tests__/do-lifecycle.test.ts +263 -0
  150. package/src/__tests__/do-router.test.ts +729 -0
  151. package/src/__tests__/email-provider.test.ts +128 -0
  152. package/src/__tests__/email-templates.test.ts +528 -0
  153. package/src/__tests__/error-format.test.ts +250 -0
  154. package/src/__tests__/field-ops.test.ts +242 -0
  155. package/src/__tests__/functions-context.test.ts +334 -0
  156. package/src/__tests__/functions-d1-proxy.test.ts +229 -0
  157. package/src/__tests__/functions-registry-runtime-config.test.ts +17 -0
  158. package/src/__tests__/functions-route.test.ts +139 -0
  159. package/src/__tests__/internal-request.test.ts +77 -0
  160. package/src/__tests__/log-writer.test.ts +44 -0
  161. package/src/__tests__/logger.test.ts +58 -0
  162. package/src/__tests__/meta-admin-proxy.test.ts +48 -0
  163. package/src/__tests__/meta-export-coverage.test.ts +191 -0
  164. package/src/__tests__/meta-route-registration.test.ts +47 -0
  165. package/src/__tests__/namespace-dump.test.ts +28 -0
  166. package/src/__tests__/oauth-providers.test.ts +337 -0
  167. package/src/__tests__/openapi-coverage.test.ts +144 -0
  168. package/src/__tests__/pagination.test.ts +59 -0
  169. package/src/__tests__/password-policy.test.ts +191 -0
  170. package/src/__tests__/plugin-migrations.test.ts +379 -0
  171. package/src/__tests__/postgres-batch-compat.test.ts +133 -0
  172. package/src/__tests__/postgres-dialect.test.ts +328 -0
  173. package/src/__tests__/postgres-executor.test.ts +79 -0
  174. package/src/__tests__/postgres-field-ops-compat.test.ts +222 -0
  175. package/src/__tests__/postgres-schema-init.test.ts +105 -0
  176. package/src/__tests__/postgres-table-utils.test.ts +107 -0
  177. package/src/__tests__/presence.test.ts +199 -0
  178. package/src/__tests__/provider.test.ts +550 -0
  179. package/src/__tests__/public-user-profile.test.ts +339 -0
  180. package/src/__tests__/push-handlers.test.ts +179 -0
  181. package/src/__tests__/push-provider.test.ts +80 -0
  182. package/src/__tests__/push-token.test.ts +418 -0
  183. package/src/__tests__/query.test.ts +771 -0
  184. package/src/__tests__/rate-limit.test.ts +260 -0
  185. package/src/__tests__/room-access-policy.test.ts +101 -0
  186. package/src/__tests__/room-handler-context.test.ts +130 -0
  187. package/src/__tests__/room-monitoring.test.ts +138 -0
  188. package/src/__tests__/room-runtime-routing.test.ts +222 -0
  189. package/src/__tests__/room.test.ts +254 -0
  190. package/src/__tests__/route-parser.test.ts +490 -0
  191. package/src/__tests__/rules.test.ts +234 -0
  192. package/src/__tests__/runtime-surface-accounting.test.ts +120 -0
  193. package/src/__tests__/scheduled.test.ts +80 -0
  194. package/src/__tests__/schema.test.ts +1273 -0
  195. package/src/__tests__/security-hardening.test.ts +312 -0
  196. package/src/__tests__/server.unit.test.ts +333 -0
  197. package/src/__tests__/service-key-db-proxy.test.ts +650 -0
  198. package/src/__tests__/service-key-provider-bypass.test.ts +138 -0
  199. package/src/__tests__/service-key.test.ts +757 -0
  200. package/src/__tests__/smoke-skip-report.test.ts +72 -0
  201. package/src/__tests__/sms-provider.test.ts +39 -0
  202. package/src/__tests__/sql-route.test.ts +218 -0
  203. package/src/__tests__/storage-hook-context.test.ts +115 -0
  204. package/src/__tests__/totp.test.ts +200 -0
  205. package/src/__tests__/uuid.test.ts +144 -0
  206. package/src/__tests__/validation.test.ts +773 -0
  207. package/src/__tests__/websocket-pending.test.ts +163 -0
  208. package/src/_functions-registry.ts +51 -0
  209. package/src/bench-entry.ts +9 -0
  210. package/src/cloudflare-test.d.ts +1 -0
  211. package/src/durable-objects/auth-do.ts +49 -0
  212. package/src/durable-objects/database-do.ts +2240 -0
  213. package/src/durable-objects/database-live-do.ts +949 -0
  214. package/src/durable-objects/logs-do.ts +1200 -0
  215. package/src/durable-objects/room-runtime-base.ts +1604 -0
  216. package/src/durable-objects/rooms-do.ts +2191 -0
  217. package/src/generated-config.ts +6 -0
  218. package/src/index.ts +382 -0
  219. package/src/lib/admin-assets.ts +54 -0
  220. package/src/lib/admin-db-target.ts +301 -0
  221. package/src/lib/admin-routing.ts +35 -0
  222. package/src/lib/admin-user-management.ts +464 -0
  223. package/src/lib/analytics-adapter.ts +103 -0
  224. package/src/lib/analytics-query.ts +579 -0
  225. package/src/lib/auth-d1-service.ts +1193 -0
  226. package/src/lib/auth-d1.ts +1056 -0
  227. package/src/lib/auth-db-adapter.ts +289 -0
  228. package/src/lib/auth-redirect.ts +116 -0
  229. package/src/lib/cidr.ts +115 -0
  230. package/src/lib/client-ip.ts +51 -0
  231. package/src/lib/cloudflare-realtime.ts +251 -0
  232. package/src/lib/control-db.ts +36 -0
  233. package/src/lib/cron.ts +163 -0
  234. package/src/lib/d1-handler.ts +1425 -0
  235. package/src/lib/d1-schema-init.ts +255 -0
  236. package/src/lib/d1-sql.ts +33 -0
  237. package/src/lib/database-live-config.ts +24 -0
  238. package/src/lib/database-live-emitter.ts +111 -0
  239. package/src/lib/db-sql.ts +66 -0
  240. package/src/lib/do-retry.ts +36 -0
  241. package/src/lib/do-router.ts +270 -0
  242. package/src/lib/do-sql.ts +73 -0
  243. package/src/lib/email-provider.ts +379 -0
  244. package/src/lib/email-templates.ts +285 -0
  245. package/src/lib/email-translations.ts +422 -0
  246. package/src/lib/errors.ts +151 -0
  247. package/src/lib/functions.ts +2091 -0
  248. package/src/lib/hono.ts +56 -0
  249. package/src/lib/internal-request.ts +56 -0
  250. package/src/lib/jwt.ts +354 -0
  251. package/src/lib/log-writer.ts +272 -0
  252. package/src/lib/namespace-dump.ts +125 -0
  253. package/src/lib/oauth-providers.ts +1225 -0
  254. package/src/lib/op-parser.ts +99 -0
  255. package/src/lib/openapi.ts +146 -0
  256. package/src/lib/pagination.ts +19 -0
  257. package/src/lib/password-policy.ts +102 -0
  258. package/src/lib/password.ts +145 -0
  259. package/src/lib/plugin-migrations.ts +612 -0
  260. package/src/lib/postgres-executor.ts +203 -0
  261. package/src/lib/postgres-handler.ts +1102 -0
  262. package/src/lib/postgres-schema-init.ts +341 -0
  263. package/src/lib/postgres-table-utils.ts +87 -0
  264. package/src/lib/public-user-profile.ts +187 -0
  265. package/src/lib/push-provider.ts +409 -0
  266. package/src/lib/push-token.ts +294 -0
  267. package/src/lib/query-engine.ts +768 -0
  268. package/src/lib/room-monitoring.ts +97 -0
  269. package/src/lib/room-runtime.ts +14 -0
  270. package/src/lib/route-parser.ts +434 -0
  271. package/src/lib/schema.ts +538 -0
  272. package/src/lib/schemas.ts +152 -0
  273. package/src/lib/service-key.ts +419 -0
  274. package/src/lib/sms-provider.ts +230 -0
  275. package/src/lib/startup-config.ts +99 -0
  276. package/src/lib/totp.ts +242 -0
  277. package/src/lib/uuid.ts +87 -0
  278. package/src/lib/validation.ts +205 -0
  279. package/src/lib/version.ts +2 -0
  280. package/src/lib/websocket-pending.ts +40 -0
  281. package/src/middleware/auth.ts +169 -0
  282. package/src/middleware/captcha-verify.ts +217 -0
  283. package/src/middleware/cors.ts +159 -0
  284. package/src/middleware/error-handler.ts +54 -0
  285. package/src/middleware/internal-guard.ts +26 -0
  286. package/src/middleware/logger.ts +126 -0
  287. package/src/middleware/rate-limit.ts +283 -0
  288. package/src/middleware/rules.ts +475 -0
  289. package/src/routes/admin-auth.ts +447 -0
  290. package/src/routes/admin.ts +3501 -0
  291. package/src/routes/analytics-api.ts +290 -0
  292. package/src/routes/auth.ts +4222 -0
  293. package/src/routes/backup.ts +1466 -0
  294. package/src/routes/config.ts +53 -0
  295. package/src/routes/d1.ts +109 -0
  296. package/src/routes/database-live.ts +281 -0
  297. package/src/routes/functions.ts +155 -0
  298. package/src/routes/health.ts +32 -0
  299. package/src/routes/kv.ts +167 -0
  300. package/src/routes/oauth.ts +1055 -0
  301. package/src/routes/push.ts +1465 -0
  302. package/src/routes/room.ts +639 -0
  303. package/src/routes/schema-endpoint.ts +76 -0
  304. package/src/routes/sql.ts +176 -0
  305. package/src/routes/storage.ts +1674 -0
  306. package/src/routes/tables.ts +699 -0
  307. package/src/routes/users.ts +21 -0
  308. package/src/routes/vectorize.ts +372 -0
  309. package/src/types.ts +99 -0
@@ -0,0 +1,699 @@
1
+ /**
2
+ * Worker-level database table routes — §7.
3
+ *
4
+ * REST URL patterns:
5
+ * Single-instance DB: /api/db/{namespace}/tables/{table}[/{id}][/action]
6
+ * Dynamic DB: /api/db/{namespace}/{instanceId}/tables/{table}[/{id}][/action]
7
+ *
8
+ * Worker acts as a pure proxy — all data operations happen inside DatabaseDOs.
9
+ * DO routing: getDbDoName(namespace, instanceId?) → 'shared' | 'workspace:ws-456' etc.
10
+ *
11
+ * All 18 routes (9 static + 9 dynamic) are explicitly defined via createRoute()
12
+ * for OpenAPI spec generation. SDK codegen reads these to generate type-safe
13
+ * client methods — NO hardcoded paths allowed in SDKs.
14
+ *
15
+ * 9 operations per DB type:
16
+ * GET list, get, count, search
17
+ * POST insert, batch, batchByFilter
18
+ * PATCH update
19
+ * DELETE delete
20
+ *
21
+ * Reference: /api/collections/* completely removed (§7). Now uses /api/db/*.
22
+ */
23
+ import { OpenAPIHono, createRoute, z, type HonoEnv } from '../lib/hono.js';
24
+ import type { Context } from 'hono';
25
+ import { getDbDoName, parseConfig, shouldRouteToD1 } from '../lib/do-router.js';
26
+ import { fetchDOWithRetry } from '../lib/do-retry.js';
27
+ import {
28
+ queryParamsSchema, listResponseSchema, recordResponseSchema,
29
+ jsonResponseSchema, errorResponseSchema, zodDefaultHook,
30
+ } from '../lib/schemas.js';
31
+ import type { AuthContext } from '@edge-base/shared';
32
+ import { handlePgRequest } from '../lib/postgres-handler.js';
33
+ import { handleD1Request } from '../lib/d1-handler.js';
34
+
35
+
36
+ export const tablesRoute = new OpenAPIHono<HonoEnv>({ defaultHook: zodDefaultHook });
37
+
38
+ // ─── Shared param schemas ─────────────────────────────────────────────
39
+
40
+ const singleInstanceTableParams = z.object({
41
+ namespace: z.string().openapi({ description: 'Database namespace', example: 'app' }),
42
+ table: z.string().openapi({ description: 'Table name', example: 'posts' }),
43
+ });
44
+
45
+ const singleInstanceTableIdParams = z.object({
46
+ namespace: z.string().openapi({ description: 'Database namespace', example: 'app' }),
47
+ table: z.string().openapi({ description: 'Table name', example: 'posts' }),
48
+ id: z.string().openapi({ description: 'Record ID' }),
49
+ });
50
+
51
+ const dynamicTableParams = z.object({
52
+ namespace: z.string().openapi({ description: 'Database namespace', example: 'workspace' }),
53
+ instanceId: z.string().openapi({ description: 'Instance ID', example: 'ws-456' }),
54
+ table: z.string().openapi({ description: 'Table name', example: 'posts' }),
55
+ });
56
+
57
+ const dynamicTableIdParams = z.object({
58
+ namespace: z.string().openapi({ description: 'Database namespace', example: 'workspace' }),
59
+ instanceId: z.string().openapi({ description: 'Instance ID', example: 'ws-456' }),
60
+ table: z.string().openapi({ description: 'Table name', example: 'posts' }),
61
+ id: z.string().openapi({ description: 'Record ID' }),
62
+ });
63
+
64
+ /** Query params for insert/batch — supports ?upsert=true&conflictTarget=email. */
65
+ const insertQuerySchema = z.object({
66
+ upsert: z.string().optional().openapi({ description: 'Set to "true" for upsert mode' }),
67
+ conflictTarget: z.string().optional().openapi({ description: 'Column to use for conflict detection in upsert mode' }),
68
+ });
69
+
70
+ // ======================================================================
71
+ // SINGLE-INSTANCE DB: /{namespace}/tables/*
72
+ // Must be registered BEFORE dynamic /:namespace/:instanceId routes.
73
+ // Within GET routes: /count and /search BEFORE /{id} to avoid shadowing.
74
+ // ======================================================================
75
+
76
+ // ─── GET /{namespace}/tables/{table}/count ────────────────────────────
77
+
78
+ const dbSingleCountRecords = createRoute({
79
+ operationId: 'dbSingleCountRecords',
80
+ method: 'get',
81
+ path: '/{namespace}/tables/{table}/count',
82
+ tags: ['client'],
83
+ summary: 'Count records in a single-instance table',
84
+ request: {
85
+ params: singleInstanceTableParams,
86
+ query: queryParamsSchema,
87
+ },
88
+ responses: {
89
+ 200: { description: 'Count result', content: { 'application/json': { schema: jsonResponseSchema } } },
90
+ 400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
91
+ 403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
92
+ },
93
+ });
94
+
95
+ tablesRoute.openapi(dbSingleCountRecords, async (c) => {
96
+ const namespace = c.req.param('namespace')!;
97
+ const table = c.req.param('table')!;
98
+ return routeToDO(c as unknown as Context<HonoEnv>, namespace, undefined, table, `/tables/${table}/count`);
99
+ });
100
+
101
+ // ─── GET /{namespace}/tables/{table}/search ───────────────────────────
102
+
103
+ const dbSingleSearchRecords = createRoute({
104
+ operationId: 'dbSingleSearchRecords',
105
+ method: 'get',
106
+ path: '/{namespace}/tables/{table}/search',
107
+ tags: ['client'],
108
+ summary: 'Search records in a single-instance table',
109
+ request: {
110
+ params: singleInstanceTableParams,
111
+ query: queryParamsSchema,
112
+ },
113
+ responses: {
114
+ 200: { description: 'Search results', content: { 'application/json': { schema: listResponseSchema } } },
115
+ 400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
116
+ 403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
117
+ },
118
+ });
119
+
120
+ tablesRoute.openapi(dbSingleSearchRecords, async (c) => {
121
+ const namespace = c.req.param('namespace')!;
122
+ const table = c.req.param('table')!;
123
+ return routeToDO(c as unknown as Context<HonoEnv>, namespace, undefined, table, `/tables/${table}/search`);
124
+ });
125
+
126
+ // ─── GET /{namespace}/tables/{table}/{id} ─────────────────────────────
127
+
128
+ const dbSingleGetRecord = createRoute({
129
+ operationId: 'dbSingleGetRecord',
130
+ method: 'get',
131
+ path: '/{namespace}/tables/{table}/{id}',
132
+ tags: ['client'],
133
+ summary: 'Get a single record from a single-instance table',
134
+ request: {
135
+ params: singleInstanceTableIdParams,
136
+ query: z.object({
137
+ fields: z.string().optional().openapi({ description: 'Comma-separated field names to return' }),
138
+ }),
139
+ },
140
+ responses: {
141
+ 200: { description: 'Record found', content: { 'application/json': { schema: recordResponseSchema } } },
142
+ 404: { description: 'Not found', content: { 'application/json': { schema: errorResponseSchema } } },
143
+ 403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
144
+ },
145
+ });
146
+
147
+ tablesRoute.openapi(dbSingleGetRecord, async (c) => {
148
+ const namespace = c.req.param('namespace')!;
149
+ const table = c.req.param('table')!;
150
+ const id = c.req.param('id')!;
151
+ return routeToDO(c as unknown as Context<HonoEnv>, namespace, undefined, table, `/tables/${table}/${id}`);
152
+ });
153
+
154
+ // ─── GET /{namespace}/tables/{table} ──────────────────────────────────
155
+
156
+ const dbSingleListRecords = createRoute({
157
+ operationId: 'dbSingleListRecords',
158
+ method: 'get',
159
+ path: '/{namespace}/tables/{table}',
160
+ tags: ['client'],
161
+ summary: 'List records from a single-instance table',
162
+ request: {
163
+ params: singleInstanceTableParams,
164
+ query: queryParamsSchema,
165
+ },
166
+ responses: {
167
+ 200: { description: 'Success', content: { 'application/json': { schema: listResponseSchema } } },
168
+ 400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
169
+ 403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
170
+ },
171
+ });
172
+
173
+ tablesRoute.openapi(dbSingleListRecords, async (c) => {
174
+ const namespace = c.req.param('namespace')!;
175
+ const table = c.req.param('table')!;
176
+ return routeToDO(c as unknown as Context<HonoEnv>, namespace, undefined, table, `/tables/${table}`);
177
+ });
178
+
179
+ // ─── POST /{namespace}/tables/{table}/batch ───────────────────────────
180
+
181
+ const dbSingleBatchRecords = createRoute({
182
+ operationId: 'dbSingleBatchRecords',
183
+ method: 'post',
184
+ path: '/{namespace}/tables/{table}/batch',
185
+ tags: ['client'],
186
+ summary: 'Batch insert records into a single-instance table',
187
+ request: {
188
+ params: singleInstanceTableParams,
189
+ query: insertQuerySchema,
190
+ body: { content: { 'application/json': { schema: z.record(z.string(), z.unknown()) } }, required: true },
191
+ },
192
+ responses: {
193
+ 200: { description: 'Batch result', content: { 'application/json': { schema: jsonResponseSchema } } },
194
+ 400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
195
+ 403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
196
+ },
197
+ });
198
+
199
+ tablesRoute.openapi(dbSingleBatchRecords, async (c) => {
200
+ const namespace = c.req.param('namespace')!;
201
+ const table = c.req.param('table')!;
202
+ return routeToDO(c as unknown as Context<HonoEnv>, namespace, undefined, table, `/tables/${table}/batch`);
203
+ });
204
+
205
+ // ─── POST /{namespace}/tables/{table}/batch-by-filter ─────────────────
206
+
207
+ const dbSingleBatchByFilter = createRoute({
208
+ operationId: 'dbSingleBatchByFilter',
209
+ method: 'post',
210
+ path: '/{namespace}/tables/{table}/batch-by-filter',
211
+ tags: ['client'],
212
+ summary: 'Batch update/delete records by filter in a single-instance table',
213
+ request: {
214
+ params: singleInstanceTableParams,
215
+ query: insertQuerySchema,
216
+ body: { content: { 'application/json': { schema: z.record(z.string(), z.unknown()) } }, required: true },
217
+ },
218
+ responses: {
219
+ 200: { description: 'Batch result', content: { 'application/json': { schema: jsonResponseSchema } } },
220
+ 400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
221
+ 403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
222
+ },
223
+ });
224
+
225
+ tablesRoute.openapi(dbSingleBatchByFilter, async (c) => {
226
+ const namespace = c.req.param('namespace')!;
227
+ const table = c.req.param('table')!;
228
+ return routeToDO(c as unknown as Context<HonoEnv>, namespace, undefined, table, `/tables/${table}/batch-by-filter`);
229
+ });
230
+
231
+ // ─── POST /{namespace}/tables/{table} (insert) ────────────────────────
232
+
233
+ const dbSingleInsertRecord = createRoute({
234
+ operationId: 'dbSingleInsertRecord',
235
+ method: 'post',
236
+ path: '/{namespace}/tables/{table}',
237
+ tags: ['client'],
238
+ summary: 'Insert a record into a single-instance table',
239
+ request: {
240
+ params: singleInstanceTableParams,
241
+ query: insertQuerySchema,
242
+ body: { content: { 'application/json': { schema: z.record(z.string(), z.unknown()) } }, required: true },
243
+ },
244
+ responses: {
245
+ 201: { description: 'Created', content: { 'application/json': { schema: recordResponseSchema } } },
246
+ 200: { description: 'Updated (upsert)', content: { 'application/json': { schema: recordResponseSchema } } },
247
+ 400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
248
+ 403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
249
+ },
250
+ });
251
+
252
+ tablesRoute.openapi(dbSingleInsertRecord, async (c) => {
253
+ const namespace = c.req.param('namespace')!;
254
+ const table = c.req.param('table')!;
255
+ return routeToDO(c as unknown as Context<HonoEnv>, namespace, undefined, table, `/tables/${table}`);
256
+ });
257
+
258
+ // ─── PATCH /{namespace}/tables/{table}/{id} ───────────────────────────
259
+
260
+ const dbSingleUpdateRecord = createRoute({
261
+ operationId: 'dbSingleUpdateRecord',
262
+ method: 'patch',
263
+ path: '/{namespace}/tables/{table}/{id}',
264
+ tags: ['client'],
265
+ summary: 'Update a record in a single-instance table',
266
+ request: {
267
+ params: singleInstanceTableIdParams,
268
+ body: { content: { 'application/json': { schema: z.record(z.string(), z.unknown()) } }, required: true },
269
+ },
270
+ responses: {
271
+ 200: { description: 'Updated', content: { 'application/json': { schema: recordResponseSchema } } },
272
+ 404: { description: 'Not found', content: { 'application/json': { schema: errorResponseSchema } } },
273
+ 403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
274
+ },
275
+ });
276
+
277
+ tablesRoute.openapi(dbSingleUpdateRecord, async (c) => {
278
+ const namespace = c.req.param('namespace')!;
279
+ const table = c.req.param('table')!;
280
+ const id = c.req.param('id')!;
281
+ return routeToDO(c as unknown as Context<HonoEnv>, namespace, undefined, table, `/tables/${table}/${id}`);
282
+ });
283
+
284
+ // ─── DELETE /{namespace}/tables/{table}/{id} ──────────────────────────
285
+
286
+ const dbSingleDeleteRecord = createRoute({
287
+ operationId: 'dbSingleDeleteRecord',
288
+ method: 'delete',
289
+ path: '/{namespace}/tables/{table}/{id}',
290
+ tags: ['client'],
291
+ summary: 'Delete a record from a single-instance table',
292
+ request: {
293
+ params: singleInstanceTableIdParams,
294
+ },
295
+ responses: {
296
+ 200: { description: 'Deleted', content: { 'application/json': { schema: jsonResponseSchema } } },
297
+ 404: { description: 'Not found', content: { 'application/json': { schema: errorResponseSchema } } },
298
+ 403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
299
+ },
300
+ });
301
+
302
+ tablesRoute.openapi(dbSingleDeleteRecord, async (c) => {
303
+ const namespace = c.req.param('namespace')!;
304
+ const table = c.req.param('table')!;
305
+ const id = c.req.param('id')!;
306
+ return routeToDO(c as unknown as Context<HonoEnv>, namespace, undefined, table, `/tables/${table}/${id}`);
307
+ });
308
+
309
+ // ======================================================================
310
+ // DYNAMIC DB: /{namespace}/{instanceId}/tables/*
311
+ // Same 9 operations with additional namespace + instanceId params.
312
+ // ======================================================================
313
+
314
+ // ─── GET /{namespace}/{instanceId}/tables/{table}/count ───────────────
315
+
316
+ const dbCountRecords = createRoute({
317
+ operationId: 'dbCountRecords',
318
+ method: 'get',
319
+ path: '/{namespace}/{instanceId}/tables/{table}/count',
320
+ tags: ['client'],
321
+ summary: 'Count records in dynamic table',
322
+ request: {
323
+ params: dynamicTableParams,
324
+ query: queryParamsSchema,
325
+ },
326
+ responses: {
327
+ 200: { description: 'Count result', content: { 'application/json': { schema: jsonResponseSchema } } },
328
+ 400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
329
+ 403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
330
+ },
331
+ });
332
+
333
+ tablesRoute.openapi(dbCountRecords, async (c) => {
334
+ const namespace = c.req.param('namespace')!;
335
+ const instanceId = c.req.param('instanceId')!;
336
+ const table = c.req.param('table')!;
337
+ return routeToDO(c as unknown as Context<HonoEnv>, namespace, instanceId, table, `/tables/${table}/count`);
338
+ });
339
+
340
+ // ─── GET /{namespace}/{instanceId}/tables/{table}/search ──────────────
341
+
342
+ const dbSearchRecords = createRoute({
343
+ operationId: 'dbSearchRecords',
344
+ method: 'get',
345
+ path: '/{namespace}/{instanceId}/tables/{table}/search',
346
+ tags: ['client'],
347
+ summary: 'Search records in dynamic table',
348
+ request: {
349
+ params: dynamicTableParams,
350
+ query: queryParamsSchema,
351
+ },
352
+ responses: {
353
+ 200: { description: 'Search results', content: { 'application/json': { schema: listResponseSchema } } },
354
+ 400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
355
+ 403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
356
+ },
357
+ });
358
+
359
+ tablesRoute.openapi(dbSearchRecords, async (c) => {
360
+ const namespace = c.req.param('namespace')!;
361
+ const instanceId = c.req.param('instanceId')!;
362
+ const table = c.req.param('table')!;
363
+ return routeToDO(c as unknown as Context<HonoEnv>, namespace, instanceId, table, `/tables/${table}/search`);
364
+ });
365
+
366
+ // ─── GET /{namespace}/{instanceId}/tables/{table}/{id} ────────────────
367
+
368
+ const dbGetRecord = createRoute({
369
+ operationId: 'dbGetRecord',
370
+ method: 'get',
371
+ path: '/{namespace}/{instanceId}/tables/{table}/{id}',
372
+ tags: ['client'],
373
+ summary: 'Get single record from dynamic table',
374
+ request: {
375
+ params: dynamicTableIdParams,
376
+ query: z.object({
377
+ fields: z.string().optional().openapi({ description: 'Comma-separated field names to return' }),
378
+ }),
379
+ },
380
+ responses: {
381
+ 200: { description: 'Record found', content: { 'application/json': { schema: recordResponseSchema } } },
382
+ 404: { description: 'Not found', content: { 'application/json': { schema: errorResponseSchema } } },
383
+ 403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
384
+ },
385
+ });
386
+
387
+ tablesRoute.openapi(dbGetRecord, async (c) => {
388
+ const namespace = c.req.param('namespace')!;
389
+ const instanceId = c.req.param('instanceId')!;
390
+ const table = c.req.param('table')!;
391
+ const id = c.req.param('id')!;
392
+ return routeToDO(c as unknown as Context<HonoEnv>, namespace, instanceId, table, `/tables/${table}/${id}`);
393
+ });
394
+
395
+ // ─── GET /{namespace}/{instanceId}/tables/{table} ─────────────────────
396
+
397
+ const dbListRecords = createRoute({
398
+ operationId: 'dbListRecords',
399
+ method: 'get',
400
+ path: '/{namespace}/{instanceId}/tables/{table}',
401
+ tags: ['client'],
402
+ summary: 'List records from dynamic table',
403
+ request: {
404
+ params: dynamicTableParams,
405
+ query: queryParamsSchema,
406
+ },
407
+ responses: {
408
+ 200: { description: 'Success', content: { 'application/json': { schema: listResponseSchema } } },
409
+ 400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
410
+ 403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
411
+ },
412
+ });
413
+
414
+ tablesRoute.openapi(dbListRecords, async (c) => {
415
+ const namespace = c.req.param('namespace')!;
416
+ const instanceId = c.req.param('instanceId')!;
417
+ const table = c.req.param('table')!;
418
+ return routeToDO(c as unknown as Context<HonoEnv>, namespace, instanceId, table, `/tables/${table}`);
419
+ });
420
+
421
+ // ─── POST /{namespace}/{instanceId}/tables/{table}/batch ──────────────
422
+
423
+ const dbBatchRecords = createRoute({
424
+ operationId: 'dbBatchRecords',
425
+ method: 'post',
426
+ path: '/{namespace}/{instanceId}/tables/{table}/batch',
427
+ tags: ['client'],
428
+ summary: 'Batch insert records into dynamic table',
429
+ request: {
430
+ params: dynamicTableParams,
431
+ query: insertQuerySchema,
432
+ body: { content: { 'application/json': { schema: z.record(z.string(), z.unknown()) } }, required: true },
433
+ },
434
+ responses: {
435
+ 200: { description: 'Batch result', content: { 'application/json': { schema: jsonResponseSchema } } },
436
+ 400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
437
+ 403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
438
+ },
439
+ });
440
+
441
+ tablesRoute.openapi(dbBatchRecords, async (c) => {
442
+ const namespace = c.req.param('namespace')!;
443
+ const instanceId = c.req.param('instanceId')!;
444
+ const table = c.req.param('table')!;
445
+ return routeToDO(c as unknown as Context<HonoEnv>, namespace, instanceId, table, `/tables/${table}/batch`);
446
+ });
447
+
448
+ // ─── POST /{namespace}/{instanceId}/tables/{table}/batch-by-filter ────
449
+
450
+ const dbBatchByFilter = createRoute({
451
+ operationId: 'dbBatchByFilter',
452
+ method: 'post',
453
+ path: '/{namespace}/{instanceId}/tables/{table}/batch-by-filter',
454
+ tags: ['client'],
455
+ summary: 'Batch update/delete records by filter in dynamic table',
456
+ request: {
457
+ params: dynamicTableParams,
458
+ query: insertQuerySchema,
459
+ body: { content: { 'application/json': { schema: z.record(z.string(), z.unknown()) } }, required: true },
460
+ },
461
+ responses: {
462
+ 200: { description: 'Batch result', content: { 'application/json': { schema: jsonResponseSchema } } },
463
+ 400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
464
+ 403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
465
+ },
466
+ });
467
+
468
+ tablesRoute.openapi(dbBatchByFilter, async (c) => {
469
+ const namespace = c.req.param('namespace')!;
470
+ const instanceId = c.req.param('instanceId')!;
471
+ const table = c.req.param('table')!;
472
+ return routeToDO(c as unknown as Context<HonoEnv>, namespace, instanceId, table, `/tables/${table}/batch-by-filter`);
473
+ });
474
+
475
+ // ─── POST /{namespace}/{instanceId}/tables/{table} (insert) ───────────
476
+
477
+ const dbInsertRecord = createRoute({
478
+ operationId: 'dbInsertRecord',
479
+ method: 'post',
480
+ path: '/{namespace}/{instanceId}/tables/{table}',
481
+ tags: ['client'],
482
+ summary: 'Insert record into dynamic table',
483
+ request: {
484
+ params: dynamicTableParams,
485
+ query: insertQuerySchema,
486
+ body: { content: { 'application/json': { schema: z.record(z.string(), z.unknown()) } }, required: true },
487
+ },
488
+ responses: {
489
+ 201: { description: 'Created', content: { 'application/json': { schema: recordResponseSchema } } },
490
+ 200: { description: 'Updated (upsert)', content: { 'application/json': { schema: recordResponseSchema } } },
491
+ 400: { description: 'Bad request', content: { 'application/json': { schema: errorResponseSchema } } },
492
+ 403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
493
+ },
494
+ });
495
+
496
+ tablesRoute.openapi(dbInsertRecord, async (c) => {
497
+ const namespace = c.req.param('namespace')!;
498
+ const instanceId = c.req.param('instanceId')!;
499
+ const table = c.req.param('table')!;
500
+ return routeToDO(c as unknown as Context<HonoEnv>, namespace, instanceId, table, `/tables/${table}`);
501
+ });
502
+
503
+ // ─── PATCH /{namespace}/{instanceId}/tables/{table}/{id} ──────────────
504
+
505
+ const dbUpdateRecord = createRoute({
506
+ operationId: 'dbUpdateRecord',
507
+ method: 'patch',
508
+ path: '/{namespace}/{instanceId}/tables/{table}/{id}',
509
+ tags: ['client'],
510
+ summary: 'Update record in dynamic table',
511
+ request: {
512
+ params: dynamicTableIdParams,
513
+ body: { content: { 'application/json': { schema: z.record(z.string(), z.unknown()) } }, required: true },
514
+ },
515
+ responses: {
516
+ 200: { description: 'Updated', content: { 'application/json': { schema: recordResponseSchema } } },
517
+ 404: { description: 'Not found', content: { 'application/json': { schema: errorResponseSchema } } },
518
+ 403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
519
+ },
520
+ });
521
+
522
+ tablesRoute.openapi(dbUpdateRecord, async (c) => {
523
+ const namespace = c.req.param('namespace')!;
524
+ const instanceId = c.req.param('instanceId')!;
525
+ const table = c.req.param('table')!;
526
+ const id = c.req.param('id')!;
527
+ return routeToDO(c as unknown as Context<HonoEnv>, namespace, instanceId, table, `/tables/${table}/${id}`);
528
+ });
529
+
530
+ // ─── DELETE /{namespace}/{instanceId}/tables/{table}/{id} ─────────────
531
+
532
+ const dbDeleteRecord = createRoute({
533
+ operationId: 'dbDeleteRecord',
534
+ method: 'delete',
535
+ path: '/{namespace}/{instanceId}/tables/{table}/{id}',
536
+ tags: ['client'],
537
+ summary: 'Delete record from dynamic table',
538
+ request: {
539
+ params: dynamicTableIdParams,
540
+ },
541
+ responses: {
542
+ 200: { description: 'Deleted', content: { 'application/json': { schema: jsonResponseSchema } } },
543
+ 404: { description: 'Not found', content: { 'application/json': { schema: errorResponseSchema } } },
544
+ 403: { description: 'Forbidden', content: { 'application/json': { schema: errorResponseSchema } } },
545
+ },
546
+ });
547
+
548
+ tablesRoute.openapi(dbDeleteRecord, async (c) => {
549
+ const namespace = c.req.param('namespace')!;
550
+ const instanceId = c.req.param('instanceId')!;
551
+ const table = c.req.param('table')!;
552
+ const id = c.req.param('id')!;
553
+ return routeToDO(c as unknown as Context<HonoEnv>, namespace, instanceId, table, `/tables/${table}/${id}`);
554
+ });
555
+
556
+ // ======================================================================
557
+ // Shared DO proxy logic
558
+ // ======================================================================
559
+
560
+ /**
561
+ * Route a request to the correct backend based on provider config.
562
+ * - provider='do' (default): forwards to DatabaseDO instance
563
+ * - provider='neon'|'postgres': handles in Worker via postgres-handler
564
+ *
565
+ * Handles §36 canCreate 2-RTT flow for dynamic DOs.
566
+ */
567
+ async function routeToDO(
568
+ c: Context<HonoEnv>,
569
+ namespace: string,
570
+ instanceId: string | undefined,
571
+ _tableName: string,
572
+ doPath: string,
573
+ ): Promise<Response> {
574
+ const tableName = decodeURIComponent(_tableName);
575
+ // Check provider — route to D1 or PostgreSQL handler if not DO
576
+ const config = parseConfig(c.env);
577
+ const dbBlock = config.databases?.[namespace];
578
+
579
+ if (!dbBlock) {
580
+ return c.json({ code: 404, message: `Database '${namespace}' not found in config.` }, 404);
581
+ }
582
+
583
+ // D1 route: single-instance namespaces without dynamic instanceId
584
+ if (!instanceId && shouldRouteToD1(namespace, config)) {
585
+ return handleD1Request(c as unknown as Context<HonoEnv>, namespace, tableName, doPath);
586
+ }
587
+
588
+ // PostgreSQL route
589
+ const provider = dbBlock.provider;
590
+ if (provider === 'neon' || provider === 'postgres') {
591
+ return handlePgRequest(c as unknown as Context<HonoEnv>, namespace, tableName, doPath);
592
+ }
593
+
594
+ // Build DO name: 'shared' | 'workspace:ws-456' (§2)
595
+ const doName = getDbDoName(namespace, instanceId);
596
+
597
+ const doId = c.env.DATABASE.idFromName(doName);
598
+ const stub = c.env.DATABASE.get(doId);
599
+
600
+ // Build forwarded request
601
+ const url = new URL(c.req.raw.url);
602
+ const doUrl = `http://do${doPath}${url.search}`;
603
+
604
+ const headers = new Headers(c.req.raw.headers);
605
+ headers.delete('X-EdgeBase-Internal');
606
+ headers.delete('X-Is-Service-Key');
607
+ headers.delete('X-DO-Create-Authorized');
608
+ headers.set('X-DO-Name', doName);
609
+
610
+ // Forward auth context to DO for hooks (#133 §6)
611
+ const auth = c.get('auth') as AuthContext | null | undefined;
612
+ if (auth !== undefined) {
613
+ headers.set('X-Auth-Context', JSON.stringify(auth));
614
+ }
615
+
616
+ // The trusted upstream rules middleware validates scoped/constrained Service Keys
617
+ // and pins the bypass decision on the request context. Downstream handlers and DOs
618
+ // must consume that decision instead of re-running a wildcard scope check.
619
+ const isServiceKey = c.get('isServiceKey' as never) === true;
620
+ if (isServiceKey) {
621
+ headers.set('X-Is-Service-Key', 'true');
622
+ }
623
+
624
+ const init: RequestInit = { method: c.req.raw.method, headers };
625
+ // Always pre-read body as text for non-GET/HEAD methods.
626
+ // ReadableStream is consumed by zod-validator middleware, so we
627
+ // reconstruct from Hono's cached parsed body (c.req.json()).
628
+ let bodyText: string | null = null;
629
+ if (c.req.raw.method !== 'GET' && c.req.raw.method !== 'HEAD') {
630
+ try {
631
+ const json = await c.req.json();
632
+ bodyText = JSON.stringify(json);
633
+ } catch {
634
+ // Body might be empty or non-JSON — default to empty object
635
+ // so DO doesn't crash with "Unexpected end of JSON input"
636
+ bodyText = '{}';
637
+ }
638
+ init.body = bodyText;
639
+ }
640
+
641
+ // Retry on DO reset only for idempotent (read) methods.
642
+ // Writes are not retried to avoid duplicate side-effects (hooks, triggers,
643
+ // database-live events) and non-idempotent ops ($op: increment).
644
+ const safeToRetry = c.req.raw.method === 'GET' || c.req.raw.method === 'HEAD';
645
+ const res = await fetchDOWithRetry(stub, doUrl, {
646
+ method: c.req.raw.method,
647
+ headers,
648
+ body: bodyText,
649
+ }, { safeToRetry });
650
+
651
+ // §36: Handle needsCreate 2-RTT flow for dynamic DOs (not 'shared' or static)
652
+ if (res.status === 201 && instanceId) {
653
+ const body = await res.clone().json().catch(() => null) as
654
+ | { needsCreate?: boolean; namespace?: string; id?: string }
655
+ | null;
656
+ if (body?.needsCreate) {
657
+ // Evaluate DbLevelRules.canCreate(auth, id) in Worker (#133 §36)
658
+ const config = parseConfig(c.env);
659
+ const dbBlock = config.databases?.[namespace];
660
+ const canCreateFn = dbBlock?.access?.canCreate;
661
+
662
+ // Internal/admin DB proxy calls already bypass row-level rules.
663
+ // Dynamic DB bootstrap must honor that bypass too, otherwise
664
+ // context.admin.db(namespace, id).table(...).insert() fails on first write.
665
+ let allowed = isServiceKey;
666
+ if (!allowed && canCreateFn) {
667
+ try {
668
+ allowed = await Promise.resolve(canCreateFn(auth ?? null, body.id ?? instanceId));
669
+ } catch {
670
+ allowed = false; // fail-closed
671
+ }
672
+ }
673
+
674
+ if (!allowed) {
675
+ return c.json(
676
+ { code: 403, message: 'DB creation not allowed.', error: 'CANNOT_CREATE_DB' },
677
+ 403,
678
+ );
679
+ }
680
+
681
+ // Authorized — retry with X-DO-Create-Authorized header
682
+ const retryHeaders = new Headers(headers);
683
+ retryHeaders.set('X-DO-Create-Authorized', '1');
684
+ const retryInit: RequestInit = { method: c.req.raw.method, headers: retryHeaders };
685
+ // BUG-008 fix: use pre-read body text (stream already consumed above)
686
+ if (c.req.raw.method !== 'GET' && c.req.raw.method !== 'HEAD') {
687
+ retryInit.body = bodyText ?? null;
688
+ }
689
+ // needsCreate 2-RTT: DO is empty at this point, safe to retry
690
+ return fetchDOWithRetry(stub, doUrl, {
691
+ method: c.req.raw.method,
692
+ headers: retryHeaders,
693
+ body: (c.req.raw.method !== 'GET' && c.req.raw.method !== 'HEAD') ? (bodyText ?? null) : null,
694
+ }, { safeToRetry: true });
695
+ }
696
+ }
697
+
698
+ return res;
699
+ }