@edge-base/server 0.2.3 → 0.2.5

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 (89) hide show
  1. package/admin-build/_app/immutable/chunks/{DpVAayDG.js → 6oMK_164.js} +1 -1
  2. package/admin-build/_app/immutable/chunks/{B5Nwfelm.js → B2TnDKF7.js} +1 -1
  3. package/admin-build/_app/immutable/chunks/{DCvwWZrm.js → B6MschND.js} +1 -1
  4. package/admin-build/_app/immutable/chunks/{Du5vWVa2.js → B94PilAN.js} +1 -1
  5. package/admin-build/_app/immutable/chunks/{Dc1-6Po6.js → BEW7Ez_g.js} +1 -1
  6. package/admin-build/_app/immutable/chunks/{Dlty5069.js → BoOooyH6.js} +1 -1
  7. package/admin-build/_app/immutable/chunks/{CzSAxmuj.js → BqTb6Mxk.js} +1 -1
  8. package/admin-build/_app/immutable/chunks/{DCKcAiQH.js → BvHnF5tV.js} +1 -1
  9. package/admin-build/_app/immutable/chunks/{B-_-hJ9o.js → CaVKAiCe.js} +1 -1
  10. package/admin-build/_app/immutable/chunks/{DRqPU3wD.js → Cdm5zBRA.js} +1 -1
  11. package/admin-build/_app/immutable/chunks/{byv2rTy8.js → CrOZMmdF.js} +1 -1
  12. package/admin-build/_app/immutable/chunks/{DiyBpamp.js → Cw6OYcq-.js} +1 -1
  13. package/admin-build/_app/immutable/chunks/{A_3UuvCe.js → D2j3I1VQ.js} +1 -1
  14. package/admin-build/_app/immutable/chunks/{BxoNtYHK.js → DPdQ7z0T.js} +3 -3
  15. package/admin-build/_app/immutable/chunks/{nZvorU8i.js → J2Gw0SMu.js} +1 -1
  16. package/admin-build/_app/immutable/chunks/{CZ0TVkCa.js → pUxw8jfq.js} +1 -1
  17. package/admin-build/_app/immutable/entry/{app.CfrmEXPD.js → app.D3flihMw.js} +2 -2
  18. package/admin-build/_app/immutable/entry/start.Cl6sLxnz.js +1 -0
  19. package/admin-build/_app/immutable/nodes/{0.Cn2BZ4da.js → 0.CdczqZLK.js} +1 -1
  20. package/admin-build/_app/immutable/nodes/{1.Dv4LX_Co.js → 1.DxcSsEqS.js} +1 -1
  21. package/admin-build/_app/immutable/nodes/{10.DPVv3kat.js → 10.DuAd4aIm.js} +1 -1
  22. package/admin-build/_app/immutable/nodes/{11.CiCb6Ayu.js → 11.0jgHQL92.js} +1 -1
  23. package/admin-build/_app/immutable/nodes/{12.CIPyeekF.js → 12.CKNPqmyy.js} +1 -1
  24. package/admin-build/_app/immutable/nodes/{13.Z15Lt36e.js → 13.B1p2POXS.js} +1 -1
  25. package/admin-build/_app/immutable/nodes/{14.s0l5bAq3.js → 14.Bb-REBND.js} +1 -1
  26. package/admin-build/_app/immutable/nodes/{15.UwSSNO76.js → 15.1uBFCX0X.js} +1 -1
  27. package/admin-build/_app/immutable/nodes/{16.qiD8i883.js → 16.BR7WwQrS.js} +1 -1
  28. package/admin-build/_app/immutable/nodes/{17.Dy3dcSvu.js → 17.Cm57KKXV.js} +1 -1
  29. package/admin-build/_app/immutable/nodes/{18.DeXyPYsO.js → 18.CoiwfAuQ.js} +1 -1
  30. package/admin-build/_app/immutable/nodes/{19.CAbuyS6w.js → 19.B8ZdLlXj.js} +1 -1
  31. package/admin-build/_app/immutable/nodes/{20.Bec0T7un.js → 20.DnHeFlTv.js} +1 -1
  32. package/admin-build/_app/immutable/nodes/21.CJFaf0Ia.js +1 -0
  33. package/admin-build/_app/immutable/nodes/{22.CdVprrv2.js → 22.CItETFzy.js} +1 -1
  34. package/admin-build/_app/immutable/nodes/{23.Y8RzVLoF.js → 23.CWSGMcKJ.js} +1 -1
  35. package/admin-build/_app/immutable/nodes/{24.CWhHYFBx.js → 24.CWbEqNMB.js} +1 -1
  36. package/admin-build/_app/immutable/nodes/{25.wCBplOVt.js → 25.DRkLEhKi.js} +1 -1
  37. package/admin-build/_app/immutable/nodes/{26.Cod_JRFK.js → 26.BRxO8AYH.js} +1 -1
  38. package/admin-build/_app/immutable/nodes/{27.BO2HVMu9.js → 27.BLs-nVHz.js} +1 -1
  39. package/admin-build/_app/immutable/nodes/{28.DxG-FBVQ.js → 28.G79qkdBK.js} +1 -1
  40. package/admin-build/_app/immutable/nodes/{29.CjGqWGvE.js → 29.BOcI6g0N.js} +1 -1
  41. package/admin-build/_app/immutable/nodes/{3.By3_OmdZ.js → 3.B6q-7qr8.js} +1 -1
  42. package/admin-build/_app/immutable/nodes/{30.M_H7Htpq.js → 30.DAIC7dKd.js} +1 -1
  43. package/admin-build/_app/immutable/nodes/{31.DEU18izM.js → 31.pl0XXjXF.js} +1 -1
  44. package/admin-build/_app/immutable/nodes/{4.DeYhKtzJ.js → 4.DOdvVlZj.js} +1 -1
  45. package/admin-build/_app/immutable/nodes/{5.9WLgxhrD.js → 5.BW_zlgye.js} +1 -1
  46. package/admin-build/_app/immutable/nodes/{6.BdT2i_dd.js → 6.Dxy1CAI2.js} +1 -1
  47. package/admin-build/_app/immutable/nodes/{7.CHq0s4K6.js → 7.BG98w_o7.js} +1 -1
  48. package/admin-build/_app/immutable/nodes/{8.DuvRw-XZ.js → 8.DoG5R2rG.js} +1 -1
  49. package/admin-build/_app/immutable/nodes/{9.C2Ub82wn.js → 9.Dmxf6zAC.js} +1 -1
  50. package/admin-build/_app/version.json +1 -1
  51. package/admin-build/index.html +7 -7
  52. package/package.json +3 -3
  53. package/src/__tests__/admin-data-routes.test.ts +29 -0
  54. package/src/__tests__/database-do-route-validation.test.ts +108 -0
  55. package/src/__tests__/database-live-route.test.ts +82 -0
  56. package/src/__tests__/do-router.test.ts +116 -0
  57. package/src/__tests__/functions-context.test.ts +84 -0
  58. package/src/__tests__/functions-d1-proxy.test.ts +54 -0
  59. package/src/__tests__/meta-route-registration.test.ts +20 -15
  60. package/src/__tests__/plugin-migration-routing.test.ts +32 -0
  61. package/src/__tests__/provider-aware-sql.test.ts +9 -3
  62. package/src/__tests__/room-auth-state-loss.test.ts +122 -0
  63. package/src/__tests__/room-handler-context.test.ts +4 -4
  64. package/src/__tests__/room-rate-limit-scopes.test.ts +38 -0
  65. package/src/__tests__/runtime-startup.test.ts +49 -0
  66. package/src/__tests__/scheduled.test.ts +55 -0
  67. package/src/__tests__/service-key-db-proxy.test.ts +122 -1
  68. package/src/__tests__/sql-route.test.ts +66 -0
  69. package/src/__tests__/table-hook-runtime.test.ts +137 -0
  70. package/src/durable-objects/database-do.ts +50 -45
  71. package/src/durable-objects/database-live-do.ts +15 -0
  72. package/src/durable-objects/room-runtime-base.ts +387 -129
  73. package/src/durable-objects/rooms-do.ts +31 -24
  74. package/src/index.ts +334 -282
  75. package/src/lib/d1-handler.ts +10 -21
  76. package/src/lib/do-router.ts +135 -3
  77. package/src/lib/functions.ts +4 -3
  78. package/src/lib/internal-transport.ts +28 -12
  79. package/src/lib/plugin-migration-routing.ts +28 -0
  80. package/src/lib/postgres-handler.ts +12 -20
  81. package/src/lib/provider-aware-sql.ts +19 -15
  82. package/src/lib/runtime-startup.ts +53 -0
  83. package/src/lib/table-hook-runtime.ts +62 -0
  84. package/src/routes/admin.ts +41 -41
  85. package/src/routes/database-live.ts +110 -12
  86. package/src/routes/sql.ts +22 -17
  87. package/src/routes/tables.ts +42 -29
  88. package/admin-build/_app/immutable/entry/start.l1WvHznQ.js +0 -1
  89. package/admin-build/_app/immutable/nodes/21.DuDYelMY.js +0 -1
@@ -1 +1 @@
1
- import"../chunks/CWj6FrbW.js";import{o as U}from"../chunks/Bn2NtlTj.js";import{p as j,a as z,f as G,c as b,g as t,s as m,r as g,u as n,b as y,d as D,t as J}from"../chunks/BdTBlfLy.js";import{d as K,s as N,a as O}from"../chunks/DtZk82gG.js";import{a as Q,t as V,e as W,i as X}from"../chunks/Dc1-6Po6.js";import{a as F,f as I}from"../chunks/DEELgv7K.js";import{s as Y}from"../chunks/CoI6jjbg.js";import{P as Z}from"../chunks/B8s_s9QY.js";import{a as tt}from"../chunks/Q2nPFxS6.js";import{M as _,T as et}from"../chunks/CzSAxmuj.js";import{D as at,T as rt}from"../chunks/A_3UuvCe.js";var nt=I("<button> </button>"),st=I('<div class="range-selector"></div>'),ot=I('<div class="analytics-grid"><!> <!> <!> <!></div> <div class="analytics-chart"><!></div> <div class="analytics-bottom"><!> <!></div>',1);function bt(w,M){j(M,!0);let s=D(!0),o=D(null),p=D("24h");const T=[{value:"1h",label:"1H"},{value:"6h",label:"6H"},{value:"24h",label:"24H"},{value:"7d",label:"7D"},{value:"30d",label:"30D"}],k=n(()=>()=>{switch(t(p)){case"1h":return"minute";case"6h":case"24h":return"hour";default:return"day"}});async function $(){try{const e=await Q.fetch(`data/analytics?range=${t(p)}&category=function&metric=overview&groupBy=${t(k)()}`);y(o,e,!0)}catch(e){V(e instanceof Error?e.message:"Failed to load functions analytics")}finally{y(s,!1)}}function A(e){y(p,e,!0),y(s,!0),$()}U(()=>{$();const e=setInterval($,3e4);return()=>clearInterval(e)});const H=n(()=>()=>{var c;if(!((c=t(o))!=null&&c.summary))return 0;const{totalRequests:e,totalErrors:a}=t(o).summary;return e>0?a/e*100:0}),B=n(()=>()=>{var e;return(((e=t(o))==null?void 0:e.timeSeries)||[]).map(a=>({timestamp:a.timestamp,value:a.requests??a.value??0}))}),C=n(()=>()=>{var e;return(((e=t(o))==null?void 0:e.breakdown)||[]).map(a=>({label:a.label||"other",value:a.count}))}),E=n(()=>()=>{var e;return(((e=t(o))==null?void 0:e.topItems)||[]).map(a=>({label:a.label,count:a.count,secondary:`${Math.round(a.avgLatency)}ms · ${a.errorRate.toFixed(1)}% 5xx`}))});Z(w,{title:"Functions Analytics",description:"Serverless function execution and performance metrics",get docsHref(){return tt},actions:a=>{var c=st();W(c,21,()=>T,X,(f,d)=>{var l=nt();let v;var h=b(l,!0);g(l),J(()=>{v=Y(l,1,"range-btn",null,v,{"range-btn--active":t(p)===t(d).value}),N(h,t(d).label)}),O("click",l,()=>A(t(d).value)),F(f,l)}),g(c),F(a,c)},children:(a,c)=>{var f=ot(),d=G(f),l=b(d);{let r=n(()=>{var i,u;return((u=(i=t(o))==null?void 0:i.summary)==null?void 0:u.totalRequests)??0});_(l,{label:"Invocations",get value(){return t(r)},get loading(){return t(s)}})}var v=m(l,2);{let r=n(()=>{var i,u;return((u=(i=t(o))==null?void 0:i.summary)==null?void 0:u.uniqueUsers)??0});_(v,{label:"Unique Users",get value(){return t(r)},get loading(){return t(s)}})}var h=m(v,2);{let r=n(()=>t(H)().toFixed(1));_(h,{label:"5xx Rate",get value(){return t(r)},unit:"%",get loading(){return t(s)}})}var L=m(h,2);{let r=n(()=>{var i,u;return Math.round(((u=(i=t(o))==null?void 0:i.summary)==null?void 0:u.avgLatency)??0)});_(L,{label:"Avg Duration",get value(){return t(r)},unit:"ms",get loading(){return t(s)}})}g(d);var x=m(d,2),P=b(x);{let r=n(()=>t(B)());et(P,{get data(){return t(r)},type:"bar",height:240,label:"Function invocations over time",get loading(){return t(s)},color:"#dc2626"})}g(x);var R=m(x,2),q=b(R);{let r=n(()=>t(C)());at(q,{get items(){return t(r)},title:"Functions by invocation count",get loading(){return t(s)}})}var S=m(q,2);{let r=n(()=>t(E)());rt(S,{get items(){return t(r)},title:"Top Functions",get loading(){return t(s)}})}g(R),F(a,f)}}),z()}K(["click"]);export{bt as component};
1
+ import"../chunks/CWj6FrbW.js";import{o as U}from"../chunks/Bn2NtlTj.js";import{p as j,a as z,f as G,c as b,g as t,s as m,r as g,u as n,b as y,d as D,t as J}from"../chunks/BdTBlfLy.js";import{d as K,s as N,a as O}from"../chunks/DtZk82gG.js";import{a as Q,t as V,e as W,i as X}from"../chunks/BEW7Ez_g.js";import{a as F,f as I}from"../chunks/DEELgv7K.js";import{s as Y}from"../chunks/CoI6jjbg.js";import{P as Z}from"../chunks/B8s_s9QY.js";import{a as tt}from"../chunks/Q2nPFxS6.js";import{M as _,T as et}from"../chunks/BqTb6Mxk.js";import{D as at,T as rt}from"../chunks/D2j3I1VQ.js";var nt=I("<button> </button>"),st=I('<div class="range-selector"></div>'),ot=I('<div class="analytics-grid"><!> <!> <!> <!></div> <div class="analytics-chart"><!></div> <div class="analytics-bottom"><!> <!></div>',1);function bt(w,M){j(M,!0);let s=D(!0),o=D(null),p=D("24h");const T=[{value:"1h",label:"1H"},{value:"6h",label:"6H"},{value:"24h",label:"24H"},{value:"7d",label:"7D"},{value:"30d",label:"30D"}],k=n(()=>()=>{switch(t(p)){case"1h":return"minute";case"6h":case"24h":return"hour";default:return"day"}});async function $(){try{const e=await Q.fetch(`data/analytics?range=${t(p)}&category=function&metric=overview&groupBy=${t(k)()}`);y(o,e,!0)}catch(e){V(e instanceof Error?e.message:"Failed to load functions analytics")}finally{y(s,!1)}}function A(e){y(p,e,!0),y(s,!0),$()}U(()=>{$();const e=setInterval($,3e4);return()=>clearInterval(e)});const H=n(()=>()=>{var c;if(!((c=t(o))!=null&&c.summary))return 0;const{totalRequests:e,totalErrors:a}=t(o).summary;return e>0?a/e*100:0}),B=n(()=>()=>{var e;return(((e=t(o))==null?void 0:e.timeSeries)||[]).map(a=>({timestamp:a.timestamp,value:a.requests??a.value??0}))}),C=n(()=>()=>{var e;return(((e=t(o))==null?void 0:e.breakdown)||[]).map(a=>({label:a.label||"other",value:a.count}))}),E=n(()=>()=>{var e;return(((e=t(o))==null?void 0:e.topItems)||[]).map(a=>({label:a.label,count:a.count,secondary:`${Math.round(a.avgLatency)}ms · ${a.errorRate.toFixed(1)}% 5xx`}))});Z(w,{title:"Functions Analytics",description:"Serverless function execution and performance metrics",get docsHref(){return tt},actions:a=>{var c=st();W(c,21,()=>T,X,(f,d)=>{var l=nt();let v;var h=b(l,!0);g(l),J(()=>{v=Y(l,1,"range-btn",null,v,{"range-btn--active":t(p)===t(d).value}),N(h,t(d).label)}),O("click",l,()=>A(t(d).value)),F(f,l)}),g(c),F(a,c)},children:(a,c)=>{var f=ot(),d=G(f),l=b(d);{let r=n(()=>{var i,u;return((u=(i=t(o))==null?void 0:i.summary)==null?void 0:u.totalRequests)??0});_(l,{label:"Invocations",get value(){return t(r)},get loading(){return t(s)}})}var v=m(l,2);{let r=n(()=>{var i,u;return((u=(i=t(o))==null?void 0:i.summary)==null?void 0:u.uniqueUsers)??0});_(v,{label:"Unique Users",get value(){return t(r)},get loading(){return t(s)}})}var h=m(v,2);{let r=n(()=>t(H)().toFixed(1));_(h,{label:"5xx Rate",get value(){return t(r)},unit:"%",get loading(){return t(s)}})}var L=m(h,2);{let r=n(()=>{var i,u;return Math.round(((u=(i=t(o))==null?void 0:i.summary)==null?void 0:u.avgLatency)??0)});_(L,{label:"Avg Duration",get value(){return t(r)},unit:"ms",get loading(){return t(s)}})}g(d);var x=m(d,2),P=b(x);{let r=n(()=>t(B)());et(P,{get data(){return t(r)},type:"bar",height:240,label:"Function invocations over time",get loading(){return t(s)},color:"#dc2626"})}g(x);var R=m(x,2),q=b(R);{let r=n(()=>t(C)());at(q,{get items(){return t(r)},title:"Functions by invocation count",get loading(){return t(s)}})}var S=m(q,2);{let r=n(()=>t(E)());rt(S,{get items(){return t(r)},title:"Top Functions",get loading(){return t(s)}})}g(R),F(a,f)}}),z()}K(["click"]);export{bt as component};
@@ -1 +1 @@
1
- {"version":"1774317332252"}
1
+ {"version":"1774526372532"}
@@ -5,12 +5,12 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <title>EdgeBase Admin</title>
7
7
  <link rel="icon" href="/admin/favicon.svg" type="image/svg+xml" />
8
- <link href="/admin/_app/immutable/entry/start.l1WvHznQ.js" rel="modulepreload">
9
- <link href="/admin/_app/immutable/chunks/byv2rTy8.js" rel="modulepreload">
8
+ <link href="/admin/_app/immutable/entry/start.Cl6sLxnz.js" rel="modulepreload">
9
+ <link href="/admin/_app/immutable/chunks/CrOZMmdF.js" rel="modulepreload">
10
10
  <link href="/admin/_app/immutable/chunks/BdTBlfLy.js" rel="modulepreload">
11
11
  <link href="/admin/_app/immutable/chunks/Bn2NtlTj.js" rel="modulepreload">
12
- <link href="/admin/_app/immutable/chunks/DiyBpamp.js" rel="modulepreload">
13
- <link href="/admin/_app/immutable/entry/app.CfrmEXPD.js" rel="modulepreload">
12
+ <link href="/admin/_app/immutable/chunks/Cw6OYcq-.js" rel="modulepreload">
13
+ <link href="/admin/_app/immutable/entry/app.D3flihMw.js" rel="modulepreload">
14
14
  <link href="/admin/_app/immutable/chunks/B2bEC_Hm.js" rel="modulepreload">
15
15
  <link href="/admin/_app/immutable/chunks/Bb0e0sAP.js" rel="modulepreload">
16
16
  <link href="/admin/_app/immutable/chunks/DtZk82gG.js" rel="modulepreload">
@@ -26,7 +26,7 @@
26
26
  <div style="display: contents">
27
27
  <script>
28
28
  {
29
- __sveltekit_1thdx8y = {
29
+ __sveltekit_1k0lbsj = {
30
30
  base: "/admin",
31
31
  assets: "/admin"
32
32
  };
@@ -34,8 +34,8 @@
34
34
  const element = document.currentScript.parentElement;
35
35
 
36
36
  Promise.all([
37
- import("/admin/_app/immutable/entry/start.l1WvHznQ.js"),
38
- import("/admin/_app/immutable/entry/app.CfrmEXPD.js")
37
+ import("/admin/_app/immutable/entry/start.Cl6sLxnz.js"),
38
+ import("/admin/_app/immutable/entry/app.D3flihMw.js")
39
39
  ]).then(([kit, app]) => {
40
40
  kit.start(app, element);
41
41
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edge-base/server",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "EdgeBase runtime assets consumed by the EdgeBase CLI",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -34,8 +34,8 @@
34
34
  "jose": "^6.0.0",
35
35
  "pg": "^8.16.3",
36
36
  "zod": "^4.3.6",
37
- "@edge-base/core": "0.2.3",
38
- "@edge-base/shared": "0.2.3"
37
+ "@edge-base/core": "0.2.5",
38
+ "@edge-base/shared": "0.2.5"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@cloudflare/vitest-pool-workers": "^0.8.71",
@@ -165,6 +165,35 @@ describe('admin data routes', () => {
165
165
  });
166
166
  });
167
167
 
168
+ it('rejects instanceId query params for single-instance record browsing', async () => {
169
+ setConfig(
170
+ createConfig({
171
+ shared: {
172
+ tables: {
173
+ posts: { schema: { title: { type: 'string' } } },
174
+ },
175
+ },
176
+ }),
177
+ );
178
+
179
+ const app = createApp();
180
+ const response = await app.request(
181
+ '/admin/api/data/tables/posts/records?instanceId=shadow',
182
+ {
183
+ headers: {
184
+ 'X-EdgeBase-Service-Key': 'sk-root',
185
+ },
186
+ },
187
+ {} as Env,
188
+ );
189
+
190
+ expect(response.status).toBe(400);
191
+ await expect(response.json()).resolves.toMatchObject({
192
+ code: 400,
193
+ message: "instanceId is not allowed for single-instance namespace 'shared'",
194
+ });
195
+ });
196
+
168
197
  it('routes dynamic record browsing to the scoped DO when instanceId is provided', async () => {
169
198
  setConfig(
170
199
  createConfig({
@@ -0,0 +1,108 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import { defineConfig } from '@edge-base/shared';
3
+ import { setConfig } from '../lib/do-router.js';
4
+
5
+ vi.mock('cloudflare:workers', () => ({
6
+ DurableObject: class DurableObject {
7
+ ctx: unknown;
8
+ env: unknown;
9
+
10
+ constructor(ctx: unknown, env: unknown) {
11
+ this.ctx = ctx;
12
+ this.env = env;
13
+ }
14
+ },
15
+ }));
16
+
17
+ function createCtx() {
18
+ return {
19
+ storage: {
20
+ sql: {
21
+ exec: vi.fn(),
22
+ },
23
+ },
24
+ waitUntil: vi.fn(),
25
+ } as unknown as DurableObjectState;
26
+ }
27
+
28
+ function createEnv(config?: unknown) {
29
+ return {
30
+ DATABASE_LIVE: {} as DurableObjectNamespace,
31
+ DATABASE: {} as DurableObjectNamespace,
32
+ AUTH: {} as DurableObjectNamespace,
33
+ EDGEBASE_CONFIG: config,
34
+ };
35
+ }
36
+
37
+ describe('DatabaseDO route validation', () => {
38
+ afterEach(() => {
39
+ setConfig({});
40
+ });
41
+
42
+ it('rejects id-suffixed doNames for single-instance namespaces', async () => {
43
+ const config = defineConfig({
44
+ release: true,
45
+ databases: {
46
+ app: {
47
+ provider: 'do',
48
+ tables: {
49
+ posts: {},
50
+ },
51
+ },
52
+ },
53
+ });
54
+ setConfig(config);
55
+
56
+ const { DatabaseDO } = await import('../durable-objects/database-do.js');
57
+ const ctx = createCtx();
58
+ const databaseDo = new DatabaseDO(ctx, createEnv(config) as never);
59
+
60
+ const response = await databaseDo.fetch(new Request('http://do/tables/posts', {
61
+ headers: {
62
+ 'X-DO-Name': 'app:shadow',
63
+ },
64
+ }));
65
+
66
+ expect(response.status).toBe(400);
67
+ await expect(response.json()).resolves.toMatchObject({
68
+ code: 400,
69
+ message: "instanceId is not allowed for single-instance namespace 'app'",
70
+ error: 'INVALID_DB_INSTANCE_ID',
71
+ });
72
+ expect((ctx.storage.sql.exec as unknown as ReturnType<typeof vi.fn>)).not.toHaveBeenCalled();
73
+ });
74
+
75
+ it('rejects missing instance ids for dynamic namespaces', async () => {
76
+ const config = defineConfig({
77
+ release: true,
78
+ databases: {
79
+ workspace: {
80
+ provider: 'do',
81
+ instance: true,
82
+ tables: {
83
+ users: {},
84
+ },
85
+ },
86
+ },
87
+ });
88
+ setConfig(config);
89
+
90
+ const { DatabaseDO } = await import('../durable-objects/database-do.js');
91
+ const ctx = createCtx();
92
+ const databaseDo = new DatabaseDO(ctx, createEnv(config) as never);
93
+
94
+ const response = await databaseDo.fetch(new Request('http://do/tables/users', {
95
+ headers: {
96
+ 'X-DO-Name': 'workspace',
97
+ },
98
+ }));
99
+
100
+ expect(response.status).toBe(400);
101
+ await expect(response.json()).resolves.toMatchObject({
102
+ code: 400,
103
+ message: "instanceId is required for dynamic namespace 'workspace'",
104
+ error: 'INVALID_DB_INSTANCE_ID',
105
+ });
106
+ expect((ctx.storage.sql.exec as unknown as ReturnType<typeof vi.fn>)).not.toHaveBeenCalled();
107
+ });
108
+ });
@@ -2,6 +2,7 @@ import { afterEach, describe, expect, it } from 'vitest';
2
2
  import { OpenAPIHono, type HonoEnv } from '../lib/hono.js';
3
3
  import { setConfig } from '../lib/do-router.js';
4
4
  import { databaseLiveRoute } from '../routes/database-live.js';
5
+ import { defineConfig } from '@edge-base/shared';
5
6
  import type { Env } from '../types.js';
6
7
 
7
8
  interface MockKVStore {
@@ -52,6 +53,16 @@ describe('database live subscription route', () => {
52
53
  });
53
54
 
54
55
  it('reports connect-check readiness for explicit database params', async () => {
56
+ setConfig(defineConfig({
57
+ databases: {
58
+ shared: {
59
+ tables: {
60
+ posts: { schema: { title: { type: 'string' } } },
61
+ },
62
+ },
63
+ },
64
+ }));
65
+
55
66
  const kv = createMockKV();
56
67
  const app = createApp();
57
68
 
@@ -90,7 +101,78 @@ describe('database live subscription route', () => {
90
101
  });
91
102
  });
92
103
 
104
+ it('rejects structured targets that omit instanceId for dynamic namespaces', async () => {
105
+ setConfig(defineConfig({
106
+ databases: {
107
+ workspace: {
108
+ instance: true,
109
+ tables: {
110
+ posts: { schema: { title: { type: 'string' } } },
111
+ },
112
+ },
113
+ },
114
+ }));
115
+
116
+ const kv = createMockKV();
117
+ const app = createApp();
118
+
119
+ const response = await app.request('/api/db/connect-check?namespace=workspace&table=posts', {
120
+ method: 'GET',
121
+ headers: {
122
+ 'CF-Connecting-IP': '127.0.0.1',
123
+ },
124
+ }, createMockEnv(kv));
125
+
126
+ expect(response.status).toBe(400);
127
+ await expect(response.json()).resolves.toMatchObject({
128
+ ok: false,
129
+ type: 'db_connect_invalid_request',
130
+ message: "instanceId is required for dynamic namespace 'workspace'",
131
+ });
132
+ });
133
+
134
+ it('rejects legacy channels that target dynamic namespaces without an instanceId', async () => {
135
+ setConfig(defineConfig({
136
+ databases: {
137
+ workspace: {
138
+ instance: true,
139
+ tables: {
140
+ posts: { schema: { title: { type: 'string' } } },
141
+ },
142
+ },
143
+ },
144
+ }));
145
+
146
+ const kv = createMockKV();
147
+ const app = createApp();
148
+
149
+ const response = await app.request('/api/db/connect-check?channel=dblive:workspace:posts', {
150
+ method: 'GET',
151
+ headers: {
152
+ 'CF-Connecting-IP': '127.0.0.1',
153
+ },
154
+ }, createMockEnv(kv));
155
+
156
+ expect(response.status).toBe(400);
157
+ await expect(response.json()).resolves.toMatchObject({
158
+ ok: false,
159
+ type: 'db_connect_invalid_request',
160
+ message: "instanceId is required for dynamic namespace 'workspace'",
161
+ });
162
+ });
163
+
93
164
  it('proxies websocket subscribe requests through the database-owned path and releases pending slots', async () => {
165
+ setConfig(defineConfig({
166
+ databases: {
167
+ workspace: {
168
+ instance: true,
169
+ tables: {
170
+ posts: { schema: { title: { type: 'string' } } },
171
+ },
172
+ },
173
+ },
174
+ }));
175
+
94
176
  const kv = createMockKV();
95
177
  const app = createApp();
96
178
  let forwardedUrl = '';
@@ -18,8 +18,12 @@ import {
18
18
  parseConfig,
19
19
  getTablesInNamespace,
20
20
  findTableNamespace,
21
+ formatDbTargetValidationIssue,
21
22
  callDO,
22
23
  callDOByHexId,
24
+ isDynamicDbBlock,
25
+ normalizeDbInstanceId,
26
+ resolveDbTarget,
23
27
  shouldRouteToD1,
24
28
  getD1BindingName,
25
29
  } from '../lib/do-router.js';
@@ -634,6 +638,118 @@ describe('callDOByHexId', () => {
634
638
 
635
639
  // ─── J. shouldRouteToD1 ───────────────────────────────────────────────────────
636
640
 
641
+ describe('isDynamicDbBlock', () => {
642
+ it('returns false for undefined db blocks', () => {
643
+ expect(isDynamicDbBlock()).toBe(false);
644
+ });
645
+
646
+ it('returns false for single-instance db blocks without create/access semantics', () => {
647
+ expect(isDynamicDbBlock({ tables: { posts: {} } } as any)).toBe(false);
648
+ });
649
+
650
+ it('returns true for db blocks with instance routing', () => {
651
+ expect(isDynamicDbBlock({ instance: true, tables: {} } as any)).toBe(true);
652
+ });
653
+
654
+ it('returns true for db blocks with canCreate rules', () => {
655
+ expect(isDynamicDbBlock({
656
+ access: {
657
+ canCreate: () => true,
658
+ },
659
+ tables: {},
660
+ } as any)).toBe(true);
661
+ });
662
+
663
+ it('returns true for db blocks with access rules', () => {
664
+ expect(isDynamicDbBlock({
665
+ access: {
666
+ access: () => true,
667
+ },
668
+ tables: {},
669
+ } as any)).toBe(true);
670
+ });
671
+ });
672
+
673
+ describe('normalizeDbInstanceId', () => {
674
+ it('preserves non-empty ids verbatim', () => {
675
+ expect(normalizeDbInstanceId(' ws-1 ')).toBe(' ws-1 ');
676
+ });
677
+
678
+ it('keeps blank string inputs distinguishable from missing ids', () => {
679
+ expect(normalizeDbInstanceId(' ')).toBe(' ');
680
+ expect(normalizeDbInstanceId(undefined)).toBeUndefined();
681
+ expect(normalizeDbInstanceId(null)).toBeUndefined();
682
+ });
683
+ });
684
+
685
+ describe('resolveDbTarget', () => {
686
+ const config = {
687
+ databases: {
688
+ shared: { tables: { posts: {} } },
689
+ workspace: { instance: true, tables: { members: {} } },
690
+ },
691
+ } as any;
692
+
693
+ it('resolves single-instance namespaces without instance ids', () => {
694
+ expect(resolveDbTarget(config, 'shared', undefined)).toEqual({
695
+ ok: true,
696
+ value: {
697
+ namespace: 'shared',
698
+ instanceId: undefined,
699
+ dbBlock: config.databases.shared,
700
+ dynamic: false,
701
+ },
702
+ });
703
+ });
704
+
705
+ it('rejects instance ids on single-instance namespaces', () => {
706
+ const result = resolveDbTarget(config, 'shared', 'shadow');
707
+ expect(result.ok).toBe(false);
708
+ if (result.ok) return;
709
+ expect(result.issue).toBe('instance_id_not_allowed');
710
+ expect(formatDbTargetValidationIssue(result.issue, 'shared')).toBe(
711
+ "instanceId is not allowed for single-instance namespace 'shared'",
712
+ );
713
+ });
714
+
715
+ it('requires instance ids on dynamic namespaces', () => {
716
+ const result = resolveDbTarget(config, 'workspace', undefined);
717
+ expect(result.ok).toBe(false);
718
+ if (result.ok) return;
719
+ expect(result.issue).toBe('instance_id_required');
720
+ });
721
+
722
+ it('rejects empty instance ids without rewriting non-empty ones', () => {
723
+ const empty = resolveDbTarget(config, 'workspace', ' ');
724
+ expect(empty.ok).toBe(false);
725
+ if (empty.ok) return;
726
+ expect(empty.issue).toBe('instance_id_empty');
727
+ expect(formatDbTargetValidationIssue(empty.issue, 'workspace')).toBe(
728
+ 'instanceId must not be empty',
729
+ );
730
+
731
+ const preserved = resolveDbTarget(config, 'workspace', ' ws-1 ');
732
+ expect(preserved.ok).toBe(true);
733
+ if (!preserved.ok) return;
734
+ expect(preserved.value.instanceId).toBe(' ws-1 ');
735
+ });
736
+
737
+ it('rejects ids containing colons', () => {
738
+ const result = resolveDbTarget(config, 'workspace', 'ws:1');
739
+ expect(result.ok).toBe(false);
740
+ if (result.ok) return;
741
+ expect(result.issue).toBe('instance_id_invalid');
742
+ });
743
+
744
+ it('returns not-found when the namespace is missing', () => {
745
+ const result = resolveDbTarget(config, 'missing', undefined);
746
+ expect(result.ok).toBe(false);
747
+ if (result.ok) return;
748
+ expect(result.status).toBe(404);
749
+ expect(result.issue).toBe('namespace_not_found');
750
+ });
751
+ });
752
+
637
753
  describe('shouldRouteToD1', () => {
638
754
  it('namespace not in config → false', () => {
639
755
  expect(shouldRouteToD1('unknown', {})).toBe(false);
@@ -52,6 +52,48 @@ describe('buildFunctionContext admin.db', () => {
52
52
  );
53
53
  });
54
54
 
55
+ it('preserves significant whitespace in dynamic admin.db instance ids', async () => {
56
+ const databaseFetch = vi.fn().mockResolvedValue(
57
+ new Response(JSON.stringify({ items: [{ id: 'm1' }] }), {
58
+ status: 200,
59
+ headers: { 'Content-Type': 'application/json' },
60
+ }),
61
+ );
62
+ const databaseNamespace = {
63
+ idFromName: vi.fn(() => 'workspace-id'),
64
+ get: vi.fn(() => ({ fetch: databaseFetch })),
65
+ } as unknown as DurableObjectNamespace;
66
+
67
+ const ctx = buildFunctionContext({
68
+ request: new Request('http://localhost/api/functions/feed-summary'),
69
+ auth: null,
70
+ databaseNamespace,
71
+ authNamespace: {} as DurableObjectNamespace,
72
+ d1Database: {} as D1Database,
73
+ config: {
74
+ databases: {
75
+ workspace: {
76
+ instance: true,
77
+ tables: {
78
+ members: { schema: { role: { type: 'string' } } },
79
+ },
80
+ },
81
+ },
82
+ },
83
+ });
84
+
85
+ await ctx.admin.db('workspace', ' ws-1 ').table('members').getList();
86
+
87
+ expect(databaseNamespace.idFromName).toHaveBeenCalledWith('workspace: ws-1 ');
88
+ expect(databaseFetch.mock.calls[0]?.[1]).toEqual(
89
+ expect.objectContaining({
90
+ headers: expect.objectContaining({
91
+ 'X-DO-Name': 'workspace: ws-1 ',
92
+ }),
93
+ }),
94
+ );
95
+ });
96
+
55
97
  it('routes upsert calls through the worker with upsert query params', async () => {
56
98
  const fetchMock = vi.fn().mockResolvedValue(
57
99
  new Response(JSON.stringify({ id: 'p1', title: 'Upserted', action: 'inserted' }), {
@@ -155,6 +197,46 @@ describe('buildFunctionContext admin.db', () => {
155
197
  );
156
198
  });
157
199
 
200
+ it('rejects instance ids for single-instance admin.sqlProviderAware calls before touching direct backends', async () => {
201
+ const fetchMock = vi.fn();
202
+ vi.stubGlobal('fetch', fetchMock);
203
+
204
+ const databaseNamespace = {
205
+ idFromName: vi.fn(),
206
+ get: vi.fn(),
207
+ } as unknown as DurableObjectNamespace;
208
+
209
+ const ctx = buildFunctionContext({
210
+ request: new Request('http://localhost/api/functions/feed-summary'),
211
+ auth: null,
212
+ databaseNamespace,
213
+ authNamespace: {} as DurableObjectNamespace,
214
+ d1Database: {} as D1Database,
215
+ config: {
216
+ databases: {
217
+ shared: {
218
+ tables: {
219
+ posts: { schema: { title: { type: 'string' } } },
220
+ },
221
+ },
222
+ },
223
+ },
224
+ env: {
225
+ DB_D1_SHARED: {
226
+ prepare: vi.fn(),
227
+ },
228
+ } as never,
229
+ });
230
+
231
+ await expect(
232
+ ctx.admin.sqlProviderAware('shared', 'shadow', 'SELECT COUNT(*) AS total FROM posts'),
233
+ ).rejects.toThrow("instanceId is not allowed for single-instance namespace 'shared'");
234
+ expect(
235
+ (databaseNamespace as unknown as { get: ReturnType<typeof vi.fn> }).get,
236
+ ).not.toHaveBeenCalled();
237
+ expect(fetchMock).not.toHaveBeenCalled();
238
+ });
239
+
158
240
  it('routes admin.sqlWithDirectD1Access through the database DO when env is available', async () => {
159
241
  const fetchMock = vi.fn();
160
242
  vi.stubGlobal('fetch', fetchMock);
@@ -203,6 +285,7 @@ describe('buildFunctionContext admin.db', () => {
203
285
  config: {
204
286
  databases: {
205
287
  workspace: {
288
+ instance: true,
206
289
  tables: {
207
290
  members: { schema: { userId: { type: 'string' } } },
208
291
  },
@@ -331,6 +414,7 @@ describe('buildFunctionContext admin.db', () => {
331
414
  config: {
332
415
  databases: {
333
416
  workspace: {
417
+ instance: true,
334
418
  tables: {
335
419
  members: { schema: { userId: { type: 'string' } } },
336
420
  },
@@ -226,4 +226,58 @@ describe('buildFunctionContext admin.db D1 routing', () => {
226
226
  expect(workerFetch).not.toHaveBeenCalled();
227
227
  expect(databaseFetch).not.toHaveBeenCalled();
228
228
  });
229
+
230
+ it('rejects instance ids for single-instance namespaces before touching D1 handlers', async () => {
231
+ const handleD1Request = vi.fn();
232
+ vi.doMock('../lib/d1-handler.js', () => ({
233
+ handleD1Request,
234
+ }));
235
+
236
+ const workerFetch = vi.fn();
237
+ vi.stubGlobal('fetch', workerFetch);
238
+
239
+ const databaseFetch = vi.fn();
240
+ const { buildFunctionContext } = await import('../lib/functions.js');
241
+
242
+ const ctx = buildFunctionContext({
243
+ request: new Request('http://localhost/api/functions/save-room-signal'),
244
+ auth: null,
245
+ databaseNamespace: {
246
+ idFromName: vi.fn(() => 'shared-id'),
247
+ get: vi.fn(() => ({ fetch: databaseFetch })),
248
+ } as unknown as DurableObjectNamespace,
249
+ authNamespace: {
250
+ idFromName: vi.fn(() => 'auth-id'),
251
+ get: vi.fn(() => ({ fetch: vi.fn() })),
252
+ } as unknown as DurableObjectNamespace,
253
+ d1Database: {} as D1Database,
254
+ env: {
255
+ DATABASE: {} as DurableObjectNamespace,
256
+ AUTH: {} as DurableObjectNamespace,
257
+ AUTH_DB: {} as D1Database,
258
+ DB_D1_SHARED: {} as D1Database,
259
+ } as never,
260
+ executionCtx: { waitUntil: vi.fn() } as unknown as ExecutionContext,
261
+ config: {
262
+ databases: {
263
+ shared: {
264
+ tables: {
265
+ signals: {
266
+ schema: {
267
+ title: { type: 'string', required: true },
268
+ },
269
+ },
270
+ },
271
+ },
272
+ },
273
+ },
274
+ });
275
+
276
+ await expect(
277
+ ctx.admin.db('shared', 'shadow').table('signals').getList(),
278
+ ).rejects.toThrow("instanceId is not allowed for single-instance namespace 'shared'");
279
+ expect(handleD1Request).not.toHaveBeenCalled();
280
+ expect(workerFetch).not.toHaveBeenCalled();
281
+ expect(databaseFetch).not.toHaveBeenCalled();
282
+ });
229
283
  });