@edge-base/server 0.3.1 → 0.3.3
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/{C-zXt-cq.js → ArkGiTMv.js} +1 -1
- package/admin-build/_app/immutable/chunks/{DF40xjpj.js → B6_FvkIG.js} +1 -1
- package/admin-build/_app/immutable/chunks/{Cek51lkE.js → BM46GgYk.js} +3 -3
- package/admin-build/_app/immutable/chunks/{BSIzoJ5I.js → Bxlz62x0.js} +1 -1
- package/admin-build/_app/immutable/chunks/ByFwWOVj.js +1 -0
- package/admin-build/_app/immutable/chunks/{aPulNooa.js → CKJGIsEj.js} +1 -1
- package/admin-build/_app/immutable/chunks/{D0mNYcxN.js → CUEBmgYR.js} +1 -1
- package/admin-build/_app/immutable/chunks/{C1Mycuvd.js → Cav53qqs.js} +1 -1
- package/admin-build/_app/immutable/chunks/{DOOhHO-v.js → CtOkHpP1.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BkrC_6Ss.js → D3arvney.js} +1 -1
- package/admin-build/_app/immutable/chunks/{Dvlxm2Iq.js → DC_HyNNV.js} +1 -1
- package/admin-build/_app/immutable/chunks/{dZsKGnWZ.js → Da6ZRIF1.js} +1 -1
- package/admin-build/_app/immutable/chunks/{BeYswbvA.js → DfwnKUzy.js} +1 -1
- package/admin-build/_app/immutable/chunks/DuAuob_N.js +1 -0
- package/admin-build/_app/immutable/chunks/{BunyH6GE.js → ZQw8NgPJ.js} +1 -1
- package/admin-build/_app/immutable/chunks/{Xm1dHMTE.js → fgIr2CuT.js} +1 -1
- package/admin-build/_app/immutable/entry/{app.j1WHBlZX.js → app.BtTg2P3N.js} +2 -2
- package/admin-build/_app/immutable/entry/start.D21f0BIj.js +1 -0
- package/admin-build/_app/immutable/nodes/{0.Oo7ZpbR-.js → 0.CqnCTNhX.js} +1 -1
- package/admin-build/_app/immutable/nodes/{1.zgMnKHns.js → 1.BLn3o9jT.js} +1 -1
- package/admin-build/_app/immutable/nodes/{10.u5mkRiUe.js → 10.Bp6HrvpD.js} +1 -1
- package/admin-build/_app/immutable/nodes/{11.9ynAgaxV.js → 11.79DhgZw-.js} +1 -1
- package/admin-build/_app/immutable/nodes/{12.M_EqzJ6s.js → 12.A7_yb57H.js} +1 -1
- package/admin-build/_app/immutable/nodes/{13.D_NQ9q60.js → 13.DgMDZXO8.js} +1 -1
- package/admin-build/_app/immutable/nodes/{14.-vENIJ2w.js → 14.DycfMAIE.js} +1 -1
- package/admin-build/_app/immutable/nodes/{15.DoMCZhGv.js → 15.Bk_-RJ3j.js} +1 -1
- package/admin-build/_app/immutable/nodes/{16.6BmnzScq.js → 16.B7Vjh-Xp.js} +1 -1
- package/admin-build/_app/immutable/nodes/{17.q6QQ9N_J.js → 17.DKfB3-O7.js} +1 -1
- package/admin-build/_app/immutable/nodes/{18.DUhJ6dKj.js → 18.Bwq1YHfM.js} +1 -1
- package/admin-build/_app/immutable/nodes/{19.CvkvPlGP.js → 19.3S-h9EgK.js} +1 -1
- package/admin-build/_app/immutable/nodes/{20.cACSbRIl.js → 20.Cm2djLN8.js} +1 -1
- package/admin-build/_app/immutable/nodes/21.C3hqim4E.js +1 -0
- package/admin-build/_app/immutable/nodes/{22.BsQ6xeKr.js → 22.DltIIio7.js} +1 -1
- package/admin-build/_app/immutable/nodes/{23.CNW3mLz5.js → 23.qVqH8BFx.js} +1 -1
- package/admin-build/_app/immutable/nodes/{24.CReqtEOy.js → 24.BBgqZFZ0.js} +1 -1
- package/admin-build/_app/immutable/nodes/{25.BuxVhqlb.js → 25.2xJDygjz.js} +1 -1
- package/admin-build/_app/immutable/nodes/{26.C-LzPf62.js → 26.B7pxYnqL.js} +1 -1
- package/admin-build/_app/immutable/nodes/{27.Dl5RL_tB.js → 27.P3eodDFO.js} +1 -1
- package/admin-build/_app/immutable/nodes/{28.CWfSQgjH.js → 28.CPEn1uNw.js} +1 -1
- package/admin-build/_app/immutable/nodes/{29.wUSfh2eQ.js → 29.D7ZGYHTI.js} +1 -1
- package/admin-build/_app/immutable/nodes/{3.D_laBXeK.js → 3.UaLSRTpv.js} +1 -1
- package/admin-build/_app/immutable/nodes/{30.DcMhZyQ0.js → 30.D-iftjK2.js} +1 -1
- package/admin-build/_app/immutable/nodes/{31.CSwhNsNi.js → 31.CHSNieUr.js} +1 -1
- package/admin-build/_app/immutable/nodes/{4.Cq28vPI2.js → 4.BgPFR0hR.js} +1 -1
- package/admin-build/_app/immutable/nodes/{5.BTT9B3oI.js → 5.BRn6WaF_.js} +1 -1
- package/admin-build/_app/immutable/nodes/{6.iCmmzXZp.js → 6.Cdem1OHu.js} +1 -1
- package/admin-build/_app/immutable/nodes/{7.qY5v7qqJ.js → 7.FpFS7ceb.js} +1 -1
- package/admin-build/_app/immutable/nodes/{8.C6p4RvgK.js → 8.aCQtyAed.js} +1 -1
- package/admin-build/_app/immutable/nodes/{9.f548N7zh.js → 9.9BZCZcm_.js} +1 -1
- package/admin-build/_app/version.json +1 -1
- package/admin-build/index.html +7 -7
- package/openapi.json +193 -0
- package/package.json +3 -3
- package/src/__tests__/error-format.test.ts +21 -0
- package/src/__tests__/postgres-api-consistency.test.ts +240 -0
- package/src/__tests__/postgres-batch-compat.test.ts +33 -27
- package/src/__tests__/postgres-batch.test.ts +394 -0
- package/src/__tests__/postgres-field-ops-compat.test.ts +2 -1
- package/src/__tests__/postgres-transact.test.ts +148 -0
- package/src/__tests__/smoke-skip-report.test.ts +1 -1
- package/src/durable-objects/database-do.ts +455 -15
- package/src/durable-objects/room-runtime-base.ts +8 -1
- package/src/lib/d1-handler.ts +416 -50
- package/src/lib/errors.ts +8 -4
- package/src/lib/internal-transport.ts +10 -0
- package/src/lib/postgres-handler.ts +562 -84
- package/src/routes/auth.ts +58 -46
- package/src/routes/tables.ts +140 -0
- package/admin-build/_app/immutable/chunks/D3hdqsfp.js +0 -1
- package/admin-build/_app/immutable/chunks/DdjFeYwG.js +0 -1
- package/admin-build/_app/immutable/entry/start.BgMeGq-q.js +0 -1
- package/admin-build/_app/immutable/nodes/21.DZ7G8rMF.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/D3arvney.js";import{a as F,f as I}from"../chunks/DEELgv7K.js";import{d as Y,s as Z}from"../chunks/CjcrXziO.js";import{P as tt}from"../chunks/BkZCgsc3.js";import{a as et}from"../chunks/Q2nPFxS6.js";import{M as _,T as at}from"../chunks/CtOkHpP1.js";import{D as rt,T as nt}from"../chunks/Da6ZRIF1.js";var st=I("<button> </button>"),ot=I('<div class="range-selector"></div>'),lt=I('<div class="analytics-grid"><!> <!> <!> <!></div> <div class="analytics-chart"><!></div> <div class="analytics-bottom"><!> <!></div>',1);function yt(w,A){j(A,!0);let s=D(!0),o=D(null),p=D("24h");const M=[{value:"1h",label:"1H"},{value:"6h",label:"6H"},{value:"24h",label:"24H"},{value:"7d",label:"7D"},{value:"30d",label:"30D"}],T=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(T)()}`);y(o,e,!0)}catch(e){V(Y(e,"Failed to load functions analytics."))}finally{y(s,!1)}}function k(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`}))});tt(w,{title:"Functions Analytics",description:"Serverless function execution and performance metrics",get docsHref(){return et},actions:a=>{var c=ot();W(c,21,()=>M,X,(f,d)=>{var l=st();let v;var h=b(l,!0);g(l),J(()=>{v=Z(l,1,"range-btn",null,v,{"range-btn--active":t(p)===t(d).value}),N(h,t(d).label)}),O("click",l,()=>k(t(d).value)),F(f,l)}),g(c),F(a,c)},children:(a,c)=>{var f=lt(),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)());at(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)());rt(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)());nt(S,{get items(){return t(r)},title:"Top Functions",get loading(){return t(s)}})}g(R),F(a,f)}}),z()}K(["click"]);export{yt as component};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"
|
|
1
|
+
{"version":"1783478674771"}
|
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.D21f0BIj.js" rel="modulepreload">
|
|
9
|
+
<link href="/admin/_app/immutable/chunks/DuAuob_N.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/ByFwWOVj.js" rel="modulepreload">
|
|
13
|
+
<link href="/admin/_app/immutable/entry/app.BtTg2P3N.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_lsmon = {
|
|
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.D21f0BIj.js"),
|
|
38
|
+
import("/admin/_app/immutable/entry/app.BtTg2P3N.js")
|
|
39
39
|
]).then(([kit, app]) => {
|
|
40
40
|
kit.start(app, element);
|
|
41
41
|
});
|
package/openapi.json
CHANGED
|
@@ -6027,6 +6027,17 @@
|
|
|
6027
6027
|
"description": "Full-text search query",
|
|
6028
6028
|
"name": "search",
|
|
6029
6029
|
"in": "query"
|
|
6030
|
+
},
|
|
6031
|
+
{
|
|
6032
|
+
"schema": {
|
|
6033
|
+
"type": "string",
|
|
6034
|
+
"description": "Set to '0' or 'false' to skip the COUNT query. total is returned as null and hasMore still drives cursor pagination.",
|
|
6035
|
+
"example": "0"
|
|
6036
|
+
},
|
|
6037
|
+
"required": false,
|
|
6038
|
+
"description": "Set to '0' or 'false' to skip the COUNT query. total is returned as null and hasMore still drives cursor pagination.",
|
|
6039
|
+
"name": "includeTotal",
|
|
6040
|
+
"in": "query"
|
|
6030
6041
|
}
|
|
6031
6042
|
],
|
|
6032
6043
|
"responses": {
|
|
@@ -6548,6 +6559,86 @@
|
|
|
6548
6559
|
}
|
|
6549
6560
|
}
|
|
6550
6561
|
},
|
|
6562
|
+
"/api/db/{namespace}/transact": {
|
|
6563
|
+
"post": {
|
|
6564
|
+
"operationId": "dbSingleTransact",
|
|
6565
|
+
"tags": [
|
|
6566
|
+
"client"
|
|
6567
|
+
],
|
|
6568
|
+
"summary": "Apply multi-table write operations atomically in a single-instance database",
|
|
6569
|
+
"parameters": [
|
|
6570
|
+
{
|
|
6571
|
+
"schema": {
|
|
6572
|
+
"type": "string",
|
|
6573
|
+
"description": "Database namespace",
|
|
6574
|
+
"example": "app"
|
|
6575
|
+
},
|
|
6576
|
+
"required": true,
|
|
6577
|
+
"description": "Database namespace",
|
|
6578
|
+
"name": "namespace",
|
|
6579
|
+
"in": "path"
|
|
6580
|
+
}
|
|
6581
|
+
],
|
|
6582
|
+
"requestBody": {
|
|
6583
|
+
"required": true,
|
|
6584
|
+
"content": {
|
|
6585
|
+
"application/json": {
|
|
6586
|
+
"schema": {
|
|
6587
|
+
"type": "object",
|
|
6588
|
+
"properties": {
|
|
6589
|
+
"operations": {
|
|
6590
|
+
"type": "array",
|
|
6591
|
+
"description": "Ordered operations applied atomically in one transaction. op: insert | update | delete | expect. insert: { table, op, data }. update: { table, op, id, data }. delete: { table, op, id }. expect: { table, op, id?, where?, exists } asserts row state and aborts the transaction with 409 when unmet.",
|
|
6592
|
+
"items": {
|
|
6593
|
+
"type": "object",
|
|
6594
|
+
"additionalProperties": {}
|
|
6595
|
+
}
|
|
6596
|
+
}
|
|
6597
|
+
},
|
|
6598
|
+
"required": [
|
|
6599
|
+
"operations"
|
|
6600
|
+
],
|
|
6601
|
+
"additionalProperties": false
|
|
6602
|
+
}
|
|
6603
|
+
}
|
|
6604
|
+
}
|
|
6605
|
+
},
|
|
6606
|
+
"responses": {
|
|
6607
|
+
"200": {
|
|
6608
|
+
"description": "Ordered per-operation results",
|
|
6609
|
+
"content": {
|
|
6610
|
+
"application/json": {
|
|
6611
|
+
"schema": {
|
|
6612
|
+
"type": "object",
|
|
6613
|
+
"properties": {
|
|
6614
|
+
"results": {
|
|
6615
|
+
"type": "array",
|
|
6616
|
+
"items": {
|
|
6617
|
+
"type": "object",
|
|
6618
|
+
"additionalProperties": {}
|
|
6619
|
+
}
|
|
6620
|
+
}
|
|
6621
|
+
},
|
|
6622
|
+
"additionalProperties": {}
|
|
6623
|
+
}
|
|
6624
|
+
}
|
|
6625
|
+
}
|
|
6626
|
+
},
|
|
6627
|
+
"400": {
|
|
6628
|
+
"description": "Validation error (unknown table, malformed op, batch limit exceeded)"
|
|
6629
|
+
},
|
|
6630
|
+
"403": {
|
|
6631
|
+
"description": "An access rule rejected one of the operations"
|
|
6632
|
+
},
|
|
6633
|
+
"409": {
|
|
6634
|
+
"description": "An expect operation was not satisfied; transaction rolled back"
|
|
6635
|
+
},
|
|
6636
|
+
"501": {
|
|
6637
|
+
"description": "Provider does not support multi-table transactions (d1/postgres)"
|
|
6638
|
+
}
|
|
6639
|
+
}
|
|
6640
|
+
}
|
|
6641
|
+
},
|
|
6551
6642
|
"/api/db/{namespace}/{instanceId}/tables/{table}/count": {
|
|
6552
6643
|
"get": {
|
|
6553
6644
|
"operationId": "dbCountRecords",
|
|
@@ -7584,6 +7675,17 @@
|
|
|
7584
7675
|
"description": "Full-text search query",
|
|
7585
7676
|
"name": "search",
|
|
7586
7677
|
"in": "query"
|
|
7678
|
+
},
|
|
7679
|
+
{
|
|
7680
|
+
"schema": {
|
|
7681
|
+
"type": "string",
|
|
7682
|
+
"description": "Set to '0' or 'false' to skip the COUNT query. total is returned as null and hasMore still drives cursor pagination.",
|
|
7683
|
+
"example": "0"
|
|
7684
|
+
},
|
|
7685
|
+
"required": false,
|
|
7686
|
+
"description": "Set to '0' or 'false' to skip the COUNT query. total is returned as null and hasMore still drives cursor pagination.",
|
|
7687
|
+
"name": "includeTotal",
|
|
7688
|
+
"in": "query"
|
|
7587
7689
|
}
|
|
7588
7690
|
],
|
|
7589
7691
|
"responses": {
|
|
@@ -8138,6 +8240,97 @@
|
|
|
8138
8240
|
}
|
|
8139
8241
|
}
|
|
8140
8242
|
},
|
|
8243
|
+
"/api/db/{namespace}/{instanceId}/transact": {
|
|
8244
|
+
"post": {
|
|
8245
|
+
"operationId": "dbTransact",
|
|
8246
|
+
"tags": [
|
|
8247
|
+
"client"
|
|
8248
|
+
],
|
|
8249
|
+
"summary": "Apply multi-table write operations atomically in a dynamic database instance",
|
|
8250
|
+
"parameters": [
|
|
8251
|
+
{
|
|
8252
|
+
"schema": {
|
|
8253
|
+
"type": "string",
|
|
8254
|
+
"description": "Database namespace",
|
|
8255
|
+
"example": "app"
|
|
8256
|
+
},
|
|
8257
|
+
"required": true,
|
|
8258
|
+
"description": "Database namespace",
|
|
8259
|
+
"name": "namespace",
|
|
8260
|
+
"in": "path"
|
|
8261
|
+
},
|
|
8262
|
+
{
|
|
8263
|
+
"schema": {
|
|
8264
|
+
"type": "string",
|
|
8265
|
+
"description": "Database instance ID",
|
|
8266
|
+
"example": "ws-456"
|
|
8267
|
+
},
|
|
8268
|
+
"required": true,
|
|
8269
|
+
"description": "Database instance ID",
|
|
8270
|
+
"name": "instanceId",
|
|
8271
|
+
"in": "path"
|
|
8272
|
+
}
|
|
8273
|
+
],
|
|
8274
|
+
"requestBody": {
|
|
8275
|
+
"required": true,
|
|
8276
|
+
"content": {
|
|
8277
|
+
"application/json": {
|
|
8278
|
+
"schema": {
|
|
8279
|
+
"type": "object",
|
|
8280
|
+
"properties": {
|
|
8281
|
+
"operations": {
|
|
8282
|
+
"type": "array",
|
|
8283
|
+
"description": "Ordered operations applied atomically in one transaction. op: insert | update | delete | expect. insert: { table, op, data }. update: { table, op, id, data }. delete: { table, op, id }. expect: { table, op, id?, where?, exists } asserts row state and aborts the transaction with 409 when unmet.",
|
|
8284
|
+
"items": {
|
|
8285
|
+
"type": "object",
|
|
8286
|
+
"additionalProperties": {}
|
|
8287
|
+
}
|
|
8288
|
+
}
|
|
8289
|
+
},
|
|
8290
|
+
"required": [
|
|
8291
|
+
"operations"
|
|
8292
|
+
],
|
|
8293
|
+
"additionalProperties": false
|
|
8294
|
+
}
|
|
8295
|
+
}
|
|
8296
|
+
}
|
|
8297
|
+
},
|
|
8298
|
+
"responses": {
|
|
8299
|
+
"200": {
|
|
8300
|
+
"description": "Ordered per-operation results",
|
|
8301
|
+
"content": {
|
|
8302
|
+
"application/json": {
|
|
8303
|
+
"schema": {
|
|
8304
|
+
"type": "object",
|
|
8305
|
+
"properties": {
|
|
8306
|
+
"results": {
|
|
8307
|
+
"type": "array",
|
|
8308
|
+
"items": {
|
|
8309
|
+
"type": "object",
|
|
8310
|
+
"additionalProperties": {}
|
|
8311
|
+
}
|
|
8312
|
+
}
|
|
8313
|
+
},
|
|
8314
|
+
"additionalProperties": {}
|
|
8315
|
+
}
|
|
8316
|
+
}
|
|
8317
|
+
}
|
|
8318
|
+
},
|
|
8319
|
+
"400": {
|
|
8320
|
+
"description": "Validation error (unknown table, malformed op, batch limit exceeded)"
|
|
8321
|
+
},
|
|
8322
|
+
"403": {
|
|
8323
|
+
"description": "An access rule rejected one of the operations"
|
|
8324
|
+
},
|
|
8325
|
+
"409": {
|
|
8326
|
+
"description": "An expect operation was not satisfied; transaction rolled back"
|
|
8327
|
+
},
|
|
8328
|
+
"501": {
|
|
8329
|
+
"description": "Provider does not support multi-table transactions (d1/postgres)"
|
|
8330
|
+
}
|
|
8331
|
+
}
|
|
8332
|
+
}
|
|
8333
|
+
},
|
|
8141
8334
|
"/api/db/connect-check": {
|
|
8142
8335
|
"get": {
|
|
8143
8336
|
"operationId": "checkDatabaseSubscriptionConnection",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@edge-base/server",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
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/
|
|
38
|
-
"@edge-base/
|
|
37
|
+
"@edge-base/core": "0.3.3",
|
|
38
|
+
"@edge-base/shared": "0.3.3"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@cloudflare/vitest-pool-workers": "^0.8.71",
|
|
@@ -263,6 +263,27 @@ describe('normalizeDatabaseError', () => {
|
|
|
263
263
|
expect(err?.slug).toBe('constraint-failed');
|
|
264
264
|
});
|
|
265
265
|
|
|
266
|
+
// PostgreSQL uses different constraint phrasings than SQLite/D1; the same
|
|
267
|
+
// normalizer must map them to the same slugs/status codes for provider parity.
|
|
268
|
+
it('maps PostgreSQL foreign key violations to the same 400 slug', () => {
|
|
269
|
+
const err = normalizeDatabaseError(
|
|
270
|
+
new Error('insert or update on table "posts" violates foreign key constraint "posts_author_id_fkey"'),
|
|
271
|
+
);
|
|
272
|
+
expect(err).toBeInstanceOf(EdgeBaseError);
|
|
273
|
+
expect(err?.code).toBe(400);
|
|
274
|
+
expect(err?.slug).toBe('foreign-key-failed');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('maps PostgreSQL not-null violations to the constraint slug and extracts the column', () => {
|
|
278
|
+
const err = normalizeDatabaseError(
|
|
279
|
+
new Error('null value in column "email" of relation "users" violates not-null constraint'),
|
|
280
|
+
);
|
|
281
|
+
expect(err).toBeInstanceOf(EdgeBaseError);
|
|
282
|
+
expect(err?.code).toBe(400);
|
|
283
|
+
expect(err?.message).toContain("'email'");
|
|
284
|
+
expect(err?.slug).toBe('constraint-failed');
|
|
285
|
+
});
|
|
286
|
+
|
|
266
287
|
it('returns null for blank string inputs', () => {
|
|
267
288
|
expect(normalizeDatabaseError(' ')).toBeNull();
|
|
268
289
|
});
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostgreSQL handler ↔ D1/DO contract parity (single-record + search):
|
|
3
|
+
* - GET /search without a term → 200 { items: [] } (was 400)
|
|
4
|
+
* - PATCH {id} with an empty body → 200 existing record (was 400)
|
|
5
|
+
* - DELETE {id} → { deleted: true } (was { success: true, deleted: <row> })
|
|
6
|
+
* - validation failures carry only { code, message, data } (no legacy `errors`)
|
|
7
|
+
* - 403 rule-rejection wording matches the D1 handler
|
|
8
|
+
*
|
|
9
|
+
* Uses a mocked executor (same pattern as postgres-batch.test.ts).
|
|
10
|
+
*/
|
|
11
|
+
import { describe, expect, it, vi, afterEach } from 'vitest';
|
|
12
|
+
import { defineConfig } from '@edge-base/shared';
|
|
13
|
+
import { buildInternalHandlerContext } from '../lib/internal-request.js';
|
|
14
|
+
import type { Env } from '../types.js';
|
|
15
|
+
|
|
16
|
+
const CONFIG = defineConfig({
|
|
17
|
+
release: true,
|
|
18
|
+
databases: {
|
|
19
|
+
shared: {
|
|
20
|
+
provider: 'postgres',
|
|
21
|
+
connectionString: 'DB_POSTGRES_SHARED_URL',
|
|
22
|
+
tables: {
|
|
23
|
+
api_docs: {
|
|
24
|
+
schema: {
|
|
25
|
+
title: { type: 'string', required: true },
|
|
26
|
+
status: { type: 'string' },
|
|
27
|
+
},
|
|
28
|
+
access: {
|
|
29
|
+
read: () => true,
|
|
30
|
+
insert: () => true,
|
|
31
|
+
update: () => true,
|
|
32
|
+
delete: () => true,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
// Auto timestamps disabled so an empty update body yields a genuinely
|
|
36
|
+
// empty change set (exercises the no-op early-return path).
|
|
37
|
+
api_notes: {
|
|
38
|
+
schema: {
|
|
39
|
+
title: { type: 'string', required: true },
|
|
40
|
+
createdAt: false,
|
|
41
|
+
updatedAt: false,
|
|
42
|
+
},
|
|
43
|
+
access: {
|
|
44
|
+
read: () => true,
|
|
45
|
+
insert: () => true,
|
|
46
|
+
update: () => true,
|
|
47
|
+
delete: () => true,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
function makeEnv(): Env {
|
|
56
|
+
return {
|
|
57
|
+
EDGEBASE_CONFIG: CONFIG,
|
|
58
|
+
DB_POSTGRES_SHARED_URL: 'postgres://edgebase:test@localhost/shared',
|
|
59
|
+
} as unknown as Env;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function mockExecutorModule(
|
|
63
|
+
handler: (sql: string, params: unknown[]) => { rows: Record<string, unknown>[]; rowCount: number },
|
|
64
|
+
) {
|
|
65
|
+
const calls: Array<{ sql: string; params: unknown[] }> = [];
|
|
66
|
+
vi.doMock('../lib/postgres-executor.js', () => ({
|
|
67
|
+
executePostgresQuery: vi.fn(),
|
|
68
|
+
ensureLocalDevPostgresSchema: vi.fn().mockResolvedValue(undefined),
|
|
69
|
+
withPostgresConnection: vi.fn(async (_cs: string, fn: (q: unknown) => Promise<unknown>) =>
|
|
70
|
+
fn(async (sql: string, params: unknown[] = []) => {
|
|
71
|
+
calls.push({ sql, params });
|
|
72
|
+
return handler(sql, params);
|
|
73
|
+
})),
|
|
74
|
+
getLocalDevPostgresExecOptions: vi.fn(() => undefined),
|
|
75
|
+
getProviderBindingName: () => 'DB_POSTGRES_SHARED',
|
|
76
|
+
}));
|
|
77
|
+
vi.doMock('../lib/postgres-schema-init.js', () => ({
|
|
78
|
+
ensurePgSchema: vi.fn().mockResolvedValue(undefined),
|
|
79
|
+
}));
|
|
80
|
+
return calls;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function callPg(
|
|
84
|
+
method: string,
|
|
85
|
+
path: string,
|
|
86
|
+
{ body, search, query, table = 'api_docs' }:
|
|
87
|
+
{ body?: unknown; search?: string; query?: string; table?: string } = {},
|
|
88
|
+
) {
|
|
89
|
+
const { handlePgRequest } = await import('../lib/postgres-handler.js');
|
|
90
|
+
const qs = search !== undefined
|
|
91
|
+
? `?search=${encodeURIComponent(search)}`
|
|
92
|
+
: query !== undefined ? `?${query}` : '';
|
|
93
|
+
const url = `http://internal/api/db/shared${path}${qs}`;
|
|
94
|
+
const ctx = buildInternalHandlerContext({
|
|
95
|
+
env: makeEnv(),
|
|
96
|
+
request: new Request(url, {
|
|
97
|
+
method,
|
|
98
|
+
headers: { 'Content-Type': 'application/json', 'X-Is-Service-Key': 'true' },
|
|
99
|
+
}),
|
|
100
|
+
body: body as Record<string, unknown>,
|
|
101
|
+
});
|
|
102
|
+
return handlePgRequest(ctx as never, 'shared', table, path);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
describe('postgres API consistency (D1/DO parity)', () => {
|
|
106
|
+
afterEach(() => {
|
|
107
|
+
vi.resetModules();
|
|
108
|
+
vi.clearAllMocks();
|
|
109
|
+
vi.unstubAllGlobals();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('returns 200 { items: [] } when the search term is missing (no 400)', async () => {
|
|
113
|
+
const calls = mockExecutorModule(() => ({ rows: [], rowCount: 0 }));
|
|
114
|
+
const response = await callPg('GET', '/tables/api_docs/search');
|
|
115
|
+
expect(response.status).toBe(200);
|
|
116
|
+
expect(await response.json()).toEqual({ items: [] });
|
|
117
|
+
// Early return — no search query is issued.
|
|
118
|
+
expect(calls.some((c) => /ILIKE/i.test(c.sql))).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('runs an ILIKE search over text fields when a term is provided', async () => {
|
|
122
|
+
const hit = { id: 'doc-1', title: 'hello world', status: 'open' };
|
|
123
|
+
const calls = mockExecutorModule((sql) => {
|
|
124
|
+
if (/ILIKE/i.test(sql) && !/COUNT/i.test(sql)) return { rows: [hit], rowCount: 1 };
|
|
125
|
+
if (/COUNT/i.test(sql)) return { rows: [{ total: 1 }], rowCount: 1 };
|
|
126
|
+
return { rows: [], rowCount: 0 };
|
|
127
|
+
});
|
|
128
|
+
const response = await callPg('GET', '/tables/api_docs/search', { search: 'hello' });
|
|
129
|
+
expect(response.status).toBe(200);
|
|
130
|
+
const json = (await response.json()) as { items: unknown[]; total: number };
|
|
131
|
+
expect(json.items).toHaveLength(1);
|
|
132
|
+
expect(json.total).toBe(1);
|
|
133
|
+
// The search scans the schema's text columns (title, status), not just id.
|
|
134
|
+
const searchSql = calls.find((c) => /ILIKE/i.test(c.sql))?.sql ?? '';
|
|
135
|
+
expect(searchSql).toMatch(/"title"/);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('returns the existing record with 200 for an empty update body (no 400)', async () => {
|
|
139
|
+
const existing = { id: 'note-1', title: 'Kept' };
|
|
140
|
+
const calls = mockExecutorModule((sql) => {
|
|
141
|
+
if (sql.startsWith('SELECT')) return { rows: [existing], rowCount: 1 };
|
|
142
|
+
return { rows: [], rowCount: 0 };
|
|
143
|
+
});
|
|
144
|
+
const response = await callPg('PATCH', '/tables/api_notes/note-1', { body: {}, table: 'api_notes' });
|
|
145
|
+
expect(response.status).toBe(200);
|
|
146
|
+
const json = (await response.json()) as Record<string, unknown>;
|
|
147
|
+
expect(json.id).toBe('note-1');
|
|
148
|
+
expect(json.title).toBe('Kept');
|
|
149
|
+
// No UPDATE statement is executed for a no-op update.
|
|
150
|
+
expect(calls.some((c) => c.sql.startsWith('UPDATE'))).toBe(false);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('returns { deleted: true } from a single-record delete', async () => {
|
|
154
|
+
const existing = { id: 'doc-1', title: 'Bye', status: 'open' };
|
|
155
|
+
mockExecutorModule((sql) => {
|
|
156
|
+
if (sql.startsWith('SELECT')) return { rows: [existing], rowCount: 1 };
|
|
157
|
+
if (sql.startsWith('DELETE')) return { rows: [existing], rowCount: 1 };
|
|
158
|
+
return { rows: [], rowCount: 0 };
|
|
159
|
+
});
|
|
160
|
+
const response = await callPg('DELETE', '/tables/api_docs/doc-1');
|
|
161
|
+
expect(response.status).toBe(200);
|
|
162
|
+
expect(await response.json()).toEqual({ deleted: true });
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('omits the legacy `errors` field from validation failures', async () => {
|
|
166
|
+
mockExecutorModule(() => ({ rows: [], rowCount: 0 }));
|
|
167
|
+
// Missing required `title`.
|
|
168
|
+
const response = await callPg('POST', '/tables/api_docs', { body: { status: 'open' } });
|
|
169
|
+
expect(response.status).toBe(400);
|
|
170
|
+
const json = (await response.json()) as Record<string, unknown>;
|
|
171
|
+
expect(json.data).toBeDefined();
|
|
172
|
+
expect(json).not.toHaveProperty('errors');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('lists records with a total count', async () => {
|
|
176
|
+
mockExecutorModule((sql) => {
|
|
177
|
+
if (/COUNT/i.test(sql)) return { rows: [{ total: 2 }], rowCount: 1 };
|
|
178
|
+
if (sql.startsWith('SELECT')) {
|
|
179
|
+
return { rows: [{ id: 'a', title: 'A' }, { id: 'b', title: 'B' }], rowCount: 2 };
|
|
180
|
+
}
|
|
181
|
+
return { rows: [], rowCount: 0 };
|
|
182
|
+
});
|
|
183
|
+
const response = await callPg('GET', '/tables/api_docs');
|
|
184
|
+
expect(response.status).toBe(200);
|
|
185
|
+
const json = (await response.json()) as { items: unknown[]; total: number };
|
|
186
|
+
expect(json.items).toHaveLength(2);
|
|
187
|
+
expect(json.total).toBe(2);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('skips the COUNT query when includeTotal=0 (total is null)', async () => {
|
|
191
|
+
const calls = mockExecutorModule((sql) => {
|
|
192
|
+
if (sql.startsWith('SELECT')) return { rows: [{ id: 'a', title: 'A' }], rowCount: 1 };
|
|
193
|
+
return { rows: [], rowCount: 0 };
|
|
194
|
+
});
|
|
195
|
+
const response = await callPg('GET', '/tables/api_docs', { query: 'includeTotal=0' });
|
|
196
|
+
expect(response.status).toBe(200);
|
|
197
|
+
const json = (await response.json()) as { items: unknown[]; total: number | null };
|
|
198
|
+
expect(json.items).toHaveLength(1);
|
|
199
|
+
expect(json.total).toBeNull();
|
|
200
|
+
expect(calls.some((c) => /COUNT/i.test(c.sql))).toBe(false);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('gets a single record by id', async () => {
|
|
204
|
+
mockExecutorModule((sql) => {
|
|
205
|
+
if (sql.startsWith('SELECT')) return { rows: [{ id: 'doc-1', title: 'Hi' }], rowCount: 1 };
|
|
206
|
+
return { rows: [], rowCount: 0 };
|
|
207
|
+
});
|
|
208
|
+
const response = await callPg('GET', '/tables/api_docs/doc-1');
|
|
209
|
+
expect(response.status).toBe(200);
|
|
210
|
+
expect((await response.json() as { id: string }).id).toBe('doc-1');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('returns 404 for a missing record', async () => {
|
|
214
|
+
mockExecutorModule(() => ({ rows: [], rowCount: 0 }));
|
|
215
|
+
const response = await callPg('GET', '/tables/api_docs/nope');
|
|
216
|
+
expect(response.status).toBe(404);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('counts records', async () => {
|
|
220
|
+
mockExecutorModule((sql) => {
|
|
221
|
+
if (/COUNT/i.test(sql)) return { rows: [{ total: 7 }], rowCount: 1 };
|
|
222
|
+
return { rows: [], rowCount: 0 };
|
|
223
|
+
});
|
|
224
|
+
const response = await callPg('GET', '/tables/api_docs/count');
|
|
225
|
+
expect(response.status).toBe(200);
|
|
226
|
+
expect((await response.json() as { total: number }).total).toBe(7);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('inserts a record and returns it', async () => {
|
|
230
|
+
mockExecutorModule((sql) => {
|
|
231
|
+
if (sql.startsWith('INSERT')) {
|
|
232
|
+
return { rows: [{ id: 'new-1', title: 'Fresh', status: 'open' }], rowCount: 1 };
|
|
233
|
+
}
|
|
234
|
+
return { rows: [], rowCount: 0 };
|
|
235
|
+
});
|
|
236
|
+
const response = await callPg('POST', '/tables/api_docs', { body: { title: 'Fresh' } });
|
|
237
|
+
expect([200, 201]).toContain(response.status);
|
|
238
|
+
expect((await response.json() as { title: string }).title).toBe('Fresh');
|
|
239
|
+
});
|
|
240
|
+
});
|
|
@@ -11,31 +11,33 @@ describe('postgres batch compatibility', () => {
|
|
|
11
11
|
});
|
|
12
12
|
|
|
13
13
|
it('accepts D1-style batch inserts for upsertMany and returns inserted rows', async () => {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
rowCount: 1
|
|
38
|
-
}
|
|
14
|
+
let insertCount = 0;
|
|
15
|
+
const insertRows = [
|
|
16
|
+
{
|
|
17
|
+
id: 'probe-1',
|
|
18
|
+
probeKey: 'probe-a',
|
|
19
|
+
phase: 'batch',
|
|
20
|
+
sourceProvider: 'neon',
|
|
21
|
+
targetProvider: 'neon',
|
|
22
|
+
checksum: 'checksum-a',
|
|
23
|
+
verified: false,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'probe-2',
|
|
27
|
+
probeKey: 'probe-b',
|
|
28
|
+
phase: 'batch',
|
|
29
|
+
sourceProvider: 'neon',
|
|
30
|
+
targetProvider: 'neon',
|
|
31
|
+
checksum: 'checksum-b',
|
|
32
|
+
verified: false,
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
const executePostgresQuery = vi.fn(async (_cs: string, sql: string, _params?: unknown[]) => {
|
|
36
|
+
if (sql.startsWith('INSERT INTO')) {
|
|
37
|
+
return { rows: [insertRows[insertCount++]], rowCount: 1 };
|
|
38
|
+
}
|
|
39
|
+
return { rows: [], rowCount: 0 };
|
|
40
|
+
});
|
|
39
41
|
|
|
40
42
|
vi.doMock('../lib/postgres-executor.js', () => ({
|
|
41
43
|
executePostgresQuery,
|
|
@@ -126,8 +128,12 @@ describe('postgres batch compatibility', () => {
|
|
|
126
128
|
expect(response.status).toBe(200);
|
|
127
129
|
expect(json.inserted).toHaveLength(2);
|
|
128
130
|
expect(json.items).toHaveLength(2);
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
+
// Batch inserts are wrapped in a single BEGIN/COMMIT transaction.
|
|
132
|
+
const sqls = executePostgresQuery.mock.calls.map((call) => call[1] as string);
|
|
133
|
+
expect(sqls[0]).toBe('BEGIN');
|
|
134
|
+
expect(sqls[sqls.length - 1]).toBe('COMMIT');
|
|
135
|
+
expect(sqls.filter((sql) => sql.startsWith('INSERT INTO'))).toHaveLength(2);
|
|
136
|
+
const firstSql = sqls.find((sql) => sql.startsWith('INSERT INTO'))!;
|
|
131
137
|
expect(firstSql).toContain('ON CONFLICT ("probeKey") DO UPDATE');
|
|
132
138
|
});
|
|
133
139
|
});
|