@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.
- package/admin-build/_app/immutable/chunks/{DpVAayDG.js → 6oMK_164.js} +1 -1
- package/admin-build/_app/immutable/chunks/{B5Nwfelm.js → B2TnDKF7.js} +1 -1
- package/admin-build/_app/immutable/chunks/{DCvwWZrm.js → B6MschND.js} +1 -1
- package/admin-build/_app/immutable/chunks/{Du5vWVa2.js → B94PilAN.js} +1 -1
- package/admin-build/_app/immutable/chunks/{Dc1-6Po6.js → BEW7Ez_g.js} +1 -1
- package/admin-build/_app/immutable/chunks/{Dlty5069.js → BoOooyH6.js} +1 -1
- package/admin-build/_app/immutable/chunks/{CzSAxmuj.js → BqTb6Mxk.js} +1 -1
- package/admin-build/_app/immutable/chunks/{DCKcAiQH.js → BvHnF5tV.js} +1 -1
- package/admin-build/_app/immutable/chunks/{B-_-hJ9o.js → CaVKAiCe.js} +1 -1
- package/admin-build/_app/immutable/chunks/{DRqPU3wD.js → Cdm5zBRA.js} +1 -1
- package/admin-build/_app/immutable/chunks/{byv2rTy8.js → CrOZMmdF.js} +1 -1
- package/admin-build/_app/immutable/chunks/{DiyBpamp.js → Cw6OYcq-.js} +1 -1
- package/admin-build/_app/immutable/chunks/{A_3UuvCe.js → D2j3I1VQ.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BxoNtYHK.js → DPdQ7z0T.js} +3 -3
- package/admin-build/_app/immutable/chunks/{nZvorU8i.js → J2Gw0SMu.js} +1 -1
- package/admin-build/_app/immutable/chunks/{CZ0TVkCa.js → pUxw8jfq.js} +1 -1
- package/admin-build/_app/immutable/entry/{app.CfrmEXPD.js → app.D3flihMw.js} +2 -2
- package/admin-build/_app/immutable/entry/start.Cl6sLxnz.js +1 -0
- package/admin-build/_app/immutable/nodes/{0.Cn2BZ4da.js → 0.CdczqZLK.js} +1 -1
- package/admin-build/_app/immutable/nodes/{1.Dv4LX_Co.js → 1.DxcSsEqS.js} +1 -1
- package/admin-build/_app/immutable/nodes/{10.DPVv3kat.js → 10.DuAd4aIm.js} +1 -1
- package/admin-build/_app/immutable/nodes/{11.CiCb6Ayu.js → 11.0jgHQL92.js} +1 -1
- package/admin-build/_app/immutable/nodes/{12.CIPyeekF.js → 12.CKNPqmyy.js} +1 -1
- package/admin-build/_app/immutable/nodes/{13.Z15Lt36e.js → 13.B1p2POXS.js} +1 -1
- package/admin-build/_app/immutable/nodes/{14.s0l5bAq3.js → 14.Bb-REBND.js} +1 -1
- package/admin-build/_app/immutable/nodes/{15.UwSSNO76.js → 15.1uBFCX0X.js} +1 -1
- package/admin-build/_app/immutable/nodes/{16.qiD8i883.js → 16.BR7WwQrS.js} +1 -1
- package/admin-build/_app/immutable/nodes/{17.Dy3dcSvu.js → 17.Cm57KKXV.js} +1 -1
- package/admin-build/_app/immutable/nodes/{18.DeXyPYsO.js → 18.CoiwfAuQ.js} +1 -1
- package/admin-build/_app/immutable/nodes/{19.CAbuyS6w.js → 19.B8ZdLlXj.js} +1 -1
- package/admin-build/_app/immutable/nodes/{20.Bec0T7un.js → 20.DnHeFlTv.js} +1 -1
- package/admin-build/_app/immutable/nodes/21.CJFaf0Ia.js +1 -0
- package/admin-build/_app/immutable/nodes/{22.CdVprrv2.js → 22.CItETFzy.js} +1 -1
- package/admin-build/_app/immutable/nodes/{23.Y8RzVLoF.js → 23.CWSGMcKJ.js} +1 -1
- package/admin-build/_app/immutable/nodes/{24.CWhHYFBx.js → 24.CWbEqNMB.js} +1 -1
- package/admin-build/_app/immutable/nodes/{25.wCBplOVt.js → 25.DRkLEhKi.js} +1 -1
- package/admin-build/_app/immutable/nodes/{26.Cod_JRFK.js → 26.BRxO8AYH.js} +1 -1
- package/admin-build/_app/immutable/nodes/{27.BO2HVMu9.js → 27.BLs-nVHz.js} +1 -1
- package/admin-build/_app/immutable/nodes/{28.DxG-FBVQ.js → 28.G79qkdBK.js} +1 -1
- package/admin-build/_app/immutable/nodes/{29.CjGqWGvE.js → 29.BOcI6g0N.js} +1 -1
- package/admin-build/_app/immutable/nodes/{3.By3_OmdZ.js → 3.B6q-7qr8.js} +1 -1
- package/admin-build/_app/immutable/nodes/{30.M_H7Htpq.js → 30.DAIC7dKd.js} +1 -1
- package/admin-build/_app/immutable/nodes/{31.DEU18izM.js → 31.pl0XXjXF.js} +1 -1
- package/admin-build/_app/immutable/nodes/{4.DeYhKtzJ.js → 4.DOdvVlZj.js} +1 -1
- package/admin-build/_app/immutable/nodes/{5.9WLgxhrD.js → 5.BW_zlgye.js} +1 -1
- package/admin-build/_app/immutable/nodes/{6.BdT2i_dd.js → 6.Dxy1CAI2.js} +1 -1
- package/admin-build/_app/immutable/nodes/{7.CHq0s4K6.js → 7.BG98w_o7.js} +1 -1
- package/admin-build/_app/immutable/nodes/{8.DuvRw-XZ.js → 8.DoG5R2rG.js} +1 -1
- package/admin-build/_app/immutable/nodes/{9.C2Ub82wn.js → 9.Dmxf6zAC.js} +1 -1
- package/admin-build/_app/version.json +1 -1
- package/admin-build/index.html +7 -7
- package/package.json +3 -3
- package/src/__tests__/admin-data-routes.test.ts +29 -0
- package/src/__tests__/database-do-route-validation.test.ts +108 -0
- package/src/__tests__/database-live-route.test.ts +82 -0
- package/src/__tests__/do-router.test.ts +116 -0
- package/src/__tests__/functions-context.test.ts +84 -0
- package/src/__tests__/functions-d1-proxy.test.ts +54 -0
- package/src/__tests__/meta-route-registration.test.ts +20 -15
- package/src/__tests__/plugin-migration-routing.test.ts +32 -0
- package/src/__tests__/provider-aware-sql.test.ts +9 -3
- package/src/__tests__/room-auth-state-loss.test.ts +122 -0
- package/src/__tests__/room-handler-context.test.ts +4 -4
- package/src/__tests__/room-rate-limit-scopes.test.ts +38 -0
- package/src/__tests__/runtime-startup.test.ts +49 -0
- package/src/__tests__/scheduled.test.ts +55 -0
- package/src/__tests__/service-key-db-proxy.test.ts +122 -1
- package/src/__tests__/sql-route.test.ts +66 -0
- package/src/__tests__/table-hook-runtime.test.ts +137 -0
- package/src/durable-objects/database-do.ts +50 -45
- package/src/durable-objects/database-live-do.ts +15 -0
- package/src/durable-objects/room-runtime-base.ts +387 -129
- package/src/durable-objects/rooms-do.ts +31 -24
- package/src/index.ts +334 -282
- package/src/lib/d1-handler.ts +10 -21
- package/src/lib/do-router.ts +135 -3
- package/src/lib/functions.ts +4 -3
- package/src/lib/internal-transport.ts +28 -12
- package/src/lib/plugin-migration-routing.ts +28 -0
- package/src/lib/postgres-handler.ts +12 -20
- package/src/lib/provider-aware-sql.ts +19 -15
- package/src/lib/runtime-startup.ts +53 -0
- package/src/lib/table-hook-runtime.ts +62 -0
- package/src/routes/admin.ts +41 -41
- package/src/routes/database-live.ts +110 -12
- package/src/routes/sql.ts +22 -17
- package/src/routes/tables.ts +42 -29
- package/admin-build/_app/immutable/entry/start.l1WvHznQ.js +0 -1
- 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/
|
|
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":"
|
|
1
|
+
{"version":"1774526372532"}
|
package/admin-build/index.html
CHANGED
|
@@ -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.
|
|
9
|
-
<link href="/admin/_app/immutable/chunks/
|
|
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/
|
|
13
|
-
<link href="/admin/_app/immutable/entry/app.
|
|
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
|
-
|
|
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.
|
|
38
|
-
import("/admin/_app/immutable/entry/app.
|
|
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
|
+
"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.
|
|
38
|
-
"@edge-base/shared": "0.2.
|
|
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
|
});
|