@databehave/server 0.1.0 → 0.1.2
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/README.md +1 -1
- package/dist/admin/ui.css +1 -2
- package/dist/admin/ui.js +102 -102
- package/dist/bin.js +33 -4
- package/dist/bin.js.map +1 -1
- package/dist/json-config.js +1 -1
- package/dist/json-config.js.map +1 -1
- package/package.json +29 -27
- package/docs/admin/error-mode.md +0 -14
- package/docs/admin/headers.md +0 -63
- package/docs/admin/index.md +0 -111
- package/docs/admin/overrides.md +0 -23
- package/docs/admin/rest-api.md +0 -90
- package/docs/admin/scenarios.md +0 -28
- package/docs/cli.md +0 -76
- package/docs/config/admin.md +0 -62
- package/docs/config/cors.md +0 -72
- package/docs/config/endpoints.md +0 -162
- package/docs/config/index.md +0 -107
- package/docs/config/log.md +0 -43
- package/docs/config/mock-mode.md +0 -84
- package/docs/config/openapi.md +0 -8
- package/docs/config/schema.md +0 -34
- package/docs/config/server.md +0 -32
- package/docs/config/validation.md +0 -161
- package/docs/design.md +0 -517
- package/docs/errors.md +0 -192
- package/docs/getting-started.md +0 -146
- package/docs/index.md +0 -23
- package/docs/openapi/auto-schema.md +0 -146
- package/docs/openapi/fallback.md +0 -242
- package/docs/openapi/keywords.md +0 -78
- package/docs/recipes.md +0 -258
- package/docs/stability.md +0 -118
- package/docs/troubleshooting.md +0 -191
package/dist/admin/ui.js
CHANGED
|
@@ -1,168 +1,168 @@
|
|
|
1
|
-
(function(){
|
|
1
|
+
var R=Object.defineProperty;var F=(a,i,e)=>i in a?R(a,i,{enumerable:!0,configurable:!0,writable:!0,value:e}):a[i]=e;var l=(a,i,e)=>F(a,typeof i!="symbol"?i+"":i,e);(function(){const i=document.createElement("link").relList;if(i&&i.supports&&i.supports("modulepreload"))return;for(const s of document.querySelectorAll('link[rel="modulepreload"]'))t(s);new MutationObserver(s=>{for(const n of s)if(n.type==="childList")for(const o of n.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&t(o)}).observe(document,{childList:!0,subtree:!0});function e(s){const n={};return s.integrity&&(n.integrity=s.integrity),s.referrerPolicy&&(n.referrerPolicy=s.referrerPolicy),s.crossOrigin==="use-credentials"?n.credentials="include":s.crossOrigin==="anonymous"?n.credentials="omit":n.credentials="same-origin",n}function t(s){if(s.ep)return;s.ep=!0;const n=e(s);fetch(s.href,n)}})();const U=()=>({search:"",methods:new Set(["GET","POST","PUT","DELETE","PATCH"]),activeOnly:!1,rightPanelOpen:!1,helpOpen:!1,globalOpen:!1,expandedKey:null,focusedKey:null,routes:[],doc:null,overrides:[],scenarios:[],loading:!0}),L=(a,i)=>`${a.toUpperCase()} ${i}`,B=(...a)=>{let i=U();const e=new Set;return{get:()=>i,set:o=>{i={...i,...o};for(const r of e)r(i)},subscribe:o=>(e.add(o),()=>{e.delete(o)})}},G=(a,i)=>{const e=[];for(const t of a){const s=i?.paths?.[t.path]?.[t.method.toLowerCase()]??{},n=s.tags?.[0],o=t.path.split("/").filter(Boolean)[0]??"other",r=n??o;e.push({method:t.method.toUpperCase(),path:t.path,op:{...s,...t.summary&&!s.summary?{summary:t.summary}:{}},groupKey:r.toLowerCase(),groupLabel:r,source:t.source??"openapi"})}return e},Q=(a,i)=>{const e=new Map;for(const t of a){const s=e.get(t.groupKey)??{key:t.groupKey,label:t.groupLabel,ops:[]};s.ops.push(t),e.set(t.groupKey,s)}for(const t of e.values())t.ops.sort((s,n)=>s.path===n.path?s.method.localeCompare(n.method):s.path.localeCompare(n.path));return[...e.values()].sort((t,s)=>t.label.localeCompare(s.label))},z=a=>{const i=new Map;for(const e of a){if(e.matcher.kind!=="exact")continue;const t=L(e.matcher.method,e.matcher.path);i.set(t,(i.get(t)??0)+1)}return i},m=typeof window<"u"&&window.__DATABEHAVE_BASE__||"";let k;const v=async a=>{const i=await a.text(),e=i?JSON.parse(i):void 0;if(!a.ok){const t=typeof e=="object"&&e!==null?e.error:void 0;throw new Error(t??`HTTP ${a.status}`)}return e},f={async listOverrides(){const a=await fetch(m+"/overrides");return(await v(a)).overrides??[]},async addOverride(a){const i=await fetch(m+"/overrides",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(a)});return(await v(i)).override},async removeOverride(a){await v(await fetch(m+"/overrides/"+encodeURIComponent(a),{method:"DELETE"}))},async clearOverrides(){await v(await fetch(m+"/overrides",{method:"DELETE"}))},async listRoutes(){const a=await fetch(m+"/openapi-routes");return(await v(a)).routes??[]},async fetchOpenApi(){if(k!==void 0)return k;try{const a=await fetch(m+"/openapi.json");return a.ok?(k=await a.json(),k):(k=null,null)}catch{return k=null,null}},async listScenarios(){const a=await fetch(m+"/scenarios");return a.status===404?[]:(await v(a)).scenarios??[]},async saveScenario(a){await v(await fetch(m+"/scenarios",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({name:a})}))},async loadScenario(a){await v(await fetch(m+"/scenarios/"+encodeURIComponent(a)+"/load",{method:"POST"}))},async deleteScenario(a){await v(await fetch(m+"/scenarios/"+encodeURIComponent(a),{method:"DELETE"}))}},c=B();let J=0;const T=new Set;let g=[];const V=a=>(T.add(a),()=>{T.delete(a)}),O=()=>{for(const a of T)a(g)},p=(a,i)=>{const e=++J;g=[...g,{id:e,kind:a,message:i}],O(),setTimeout(()=>{g=g.filter(t=>t.id!==e),O()},3e3)},X=a=>{g=g.filter(i=>i.id!==a),O()},Z=()=>g,w=async()=>{try{c.set({overrides:await f.listOverrides()})}catch{}},q=async()=>{try{c.set({scenarios:await f.listScenarios()})}catch{}},D=async(a,i,e)=>{const t=await f.addOverride({matcher:a,mode:i,description:e});return p("success","Override applied"),await w(),t},E=async a=>{await f.removeOverride(a),p("success","Override removed"),await w()},W=async()=>{if(window.confirm("Clear ALL sticky overrides?"))try{await f.clearOverrides(),p("success","Cleared"),await w()}catch(a){p("error",a instanceof Error?a.message:String(a))}},Y=async a=>{try{await f.saveScenario(a),p("success","Saved: "+a),await q()}catch(i){p("error",i instanceof Error?i.message:String(i))}},ee=async a=>{try{await f.loadScenario(a),p("success","Loaded: "+a),await w()}catch(i){p("error",i instanceof Error?i.message:String(i))}},te=async a=>{try{await f.deleteScenario(a),p("success","Deleted: "+a),await q()}catch(i){p("error",i instanceof Error?i.message:String(i))}},C=()=>c.set({globalOpen:!0,rightPanelOpen:!1}),se={"&":"&","<":"<",">":">",'"':""","'":"'"},h=a=>a==null?"":String(a).replace(/[&<>"']/g,i=>se[i]??i);class ae extends HTMLElement{constructor(){super(...arguments);l(this,"_unsub");l(this,"_open",!1)}connectedCallback(){this._render(),this._unsub=c.subscribe(()=>this._render())}disconnectedCallback(){this._unsub?.()}_render(){const e=c.get().scenarios;this.innerHTML=`
|
|
2
2
|
<div style="position:relative">
|
|
3
3
|
<button class="ghost" type="button" data-toggle>Scenarios (${e.length})</button>
|
|
4
4
|
${this._open?`
|
|
5
5
|
<div style="position:absolute;top:110%;right:0;background:#fff;color:#1f2328;border:1px solid #d0d7de;border-radius:6px;padding:8px;min-width:240px;z-index:50;box-shadow:0 2px 8px rgba(0,0,0,0.1)">
|
|
6
6
|
<button class="btn" style="width:100%;margin-bottom:8px" type="button" data-save>Save current as…</button>
|
|
7
|
-
${e.map(
|
|
7
|
+
${e.map(t=>`
|
|
8
8
|
<div style="display:flex;gap:4px;align-items:center;padding:4px 0;border-top:1px solid #eaeef2">
|
|
9
|
-
<span style="flex:1;font-size:12px">${
|
|
10
|
-
<button class="btn" type="button" data-load="${
|
|
11
|
-
<button class="btn danger" type="button" data-delete="${
|
|
12
|
-
</div>`).join(
|
|
13
|
-
</div
|
|
14
|
-
</div>`,this.querySelector(
|
|
9
|
+
<span style="flex:1;font-size:12px">${h(t.name)} <small>(${t.count})</small></span>
|
|
10
|
+
<button class="btn" type="button" data-load="${h(t.name)}">Load</button>
|
|
11
|
+
<button class="btn danger" type="button" data-delete="${h(t.name)}">Delete</button>
|
|
12
|
+
</div>`).join("")}
|
|
13
|
+
</div>`:""}
|
|
14
|
+
</div>`,this.querySelector("[data-toggle]")?.addEventListener("click",()=>{this._open=!this._open,this._render()}),this.querySelector("[data-save]")?.addEventListener("click",()=>{const t=window.prompt("Scenario name (a-z, 0-9, _ -):");t&&Y(t),this._open=!1,this._render()});for(const t of this.querySelectorAll("[data-load]"))t.addEventListener("click",()=>{ee(t.dataset.load??""),this._open=!1,this._render()});for(const t of this.querySelectorAll("[data-delete]"))t.addEventListener("click",()=>{te(t.dataset.delete??"")})}}customElements.define("dbh-scenarios-menu",ae);class ne extends HTMLElement{constructor(){super(...arguments);l(this,"_unsub")}connectedCallback(){this._render(),this._unsub=c.subscribe(()=>this._render())}disconnectedCallback(){this._unsub?.()}_render(){const e=c.get(),t=e.overrides.length,s=e.overrides.filter(o=>o.matcher.kind==="global").length,n=typeof window<"u"?window.location.origin:"";this.innerHTML=`
|
|
15
15
|
<div class="top-bar">
|
|
16
16
|
<h1>Mock Injection</h1>
|
|
17
|
-
<span class="url">${
|
|
17
|
+
<span class="url">${n}</span>
|
|
18
18
|
<span class="badge">${t}</span>
|
|
19
19
|
<button class="ghost" type="button" data-clear>Clear all</button>
|
|
20
20
|
<button class="ghost global-override-btn" type="button" data-global title="Global override">
|
|
21
|
-
Global override${
|
|
21
|
+
Global override${s>0?`<span class="badge global-badge">${s}</span>`:""}
|
|
22
22
|
</button>
|
|
23
23
|
<dbh-scenarios-menu></dbh-scenarios-menu>
|
|
24
24
|
<button class="ghost" type="button" data-help title="?">?</button>
|
|
25
25
|
<button class="ghost" type="button" data-panel>Active overrides</button>
|
|
26
|
-
</div>`,this.querySelector(
|
|
27
|
-
<div class="op-row${
|
|
28
|
-
<dbh-method-badge method="${
|
|
29
|
-
<span class="path">${
|
|
30
|
-
<span class="summary">${
|
|
31
|
-
<span class="dot${
|
|
32
|
-
</div>`,this.querySelector(
|
|
26
|
+
</div>`,this.querySelector("[data-clear]")?.addEventListener("click",()=>{W()}),this.querySelector("[data-global]")?.addEventListener("click",()=>C()),this.querySelector("[data-help]")?.addEventListener("click",()=>c.set({helpOpen:!0})),this.querySelector("[data-panel]")?.addEventListener("click",()=>c.set({rightPanelOpen:!c.get().rightPanelOpen}))}}customElements.define("dbh-top-bar",ne);const ie=["GET","POST","PUT","DELETE","PATCH"];class oe extends HTMLElement{constructor(){super(...arguments);l(this,"_unsub");l(this,"_rendered",!1);l(this,"searchInput")}connectedCallback(){this._render(),this._unsub=c.subscribe(()=>this._sync())}disconnectedCallback(){this._unsub?.()}_render(){const e=c.get();this.innerHTML=`<div class="filter-bar"><input type="text" placeholder="Search path or summary…" value="${re(e.search)}" data-search />`+ie.map(t=>`<label><input type="checkbox" data-method="${t}"${e.methods.has(t)?" checked":""}/> ${t}</label>`).join("")+`<label><input type="checkbox" data-active${e.activeOnly?" checked":""}/> Active overrides only</label></div>`,this.searchInput=this.querySelector("[data-search]")??void 0,this.searchInput?.addEventListener("input",t=>{const s=t.target.value;c.set({search:s})});for(const t of this.querySelectorAll("[data-method]"))t.addEventListener("change",()=>{const s=new Set(c.get().methods),n=t.dataset.method??"";t.checked?s.add(n):s.delete(n),c.set({methods:s})});this.querySelector("[data-active]")?.addEventListener("change",t=>{c.set({activeOnly:t.target.checked})}),this._rendered=!0}_sync(){if(!this._rendered)return;const e=c.get();this.searchInput&&this.searchInput.value!==e.search&&(this.searchInput.value=e.search);for(const s of this.querySelectorAll("[data-method]"))s.checked=e.methods.has(s.dataset.method??"");const t=this.querySelector("[data-active]");t&&(t.checked=e.activeOnly)}}const re=a=>a.replace(/&/g,"&").replace(/"/g,""").replace(/</g,"<");customElements.define("dbh-filter-bar",oe);const de={GET:"#1f883d",POST:"#0969da",PUT:"#bf8700",DELETE:"#cf222e",PATCH:"#1f6feb"},M=a=>a==="http-status"||a==="business-failure"||a==="destroy"?"#cf222e":a==="delay"||a==="hang"?"#bf8700":"#0969da";class le extends HTMLElement{static get observedAttributes(){return["method"]}connectedCallback(){this._render()}attributeChangedCallback(){this.isConnected&&this._render()}_render(){const i=(this.getAttribute("method")??"").toUpperCase(),e=de[i]??"#57606a";this.innerHTML=`<span class="method-badge" style="background:${e}">${i}</span>`}}customElements.define("dbh-method-badge",le);class ce extends HTMLElement{setRow(i){const{op:e,override:t,expanded:s,focused:n}=i,o=t?M(t.mode.kind):"transparent",r=e.source==="handler";this.innerHTML=`
|
|
27
|
+
<div class="op-row${n?" focused":""}${s?" expanded":""}" data-key="${h(e.method+" "+e.path)}">
|
|
28
|
+
<dbh-method-badge method="${h(e.method)}"></dbh-method-badge>
|
|
29
|
+
<span class="path">${h(e.path)}${r?' <span class="no-oas-pill" title="No OpenAPI spec for this route">(no OAS)</span>':""}</span>
|
|
30
|
+
<span class="summary">${h(e.op.summary??"")}</span>
|
|
31
|
+
<span class="dot${t?" active":""}" style="${t?`background:${o}`:""}" title="${h(t?.mode.kind??"")}"></span>
|
|
32
|
+
</div>`,this.querySelector(".op-row")?.addEventListener("click",()=>{const b=e.method+" "+e.path,u=c.get().expandedKey;c.set({expandedKey:u===b?null:b})})}}customElements.define("dbh-operation-row",ce);const he=a=>a.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""),ue=a=>a?a.split(/\n{2,}/).map(e=>`<p>${he(e.trim()).replace(/`([^`]+)`/g,(o,r)=>`<code>${r}</code>`).replace(/\n/g,"<br/>")}</p>`).join(""):"",H={string:"string",integer:0,number:0,boolean:!0,null:null},_=(a,i,e=0,t=new Set)=>{if(!a||e>8)return null;if(a.example!==void 0)return a.example;if(a.default!==void 0)return a.default;if(a.enum&&a.enum.length>0)return a.enum[0];if(a.$ref){if(t.has(a.$ref))return null;const s=new Set(t);s.add(a.$ref);const n=a.$ref.replace(/^#\/components\/schemas\//,""),o=i?.components?.schemas?.[n];return _(o,i,e+1,s)}if(a.oneOf?.[0])return _(a.oneOf[0],i,e+1,t);if(a.anyOf?.[0])return _(a.anyOf[0],i,e+1,t);if(a.allOf&&a.allOf.length>0){const s={};for(const n of a.allOf){const o=_(n,i,e+1,t);o&&typeof o=="object"&&!Array.isArray(o)&&Object.assign(s,o)}return s}if(a.type==="object"||a.properties){const s={},n=a.properties??{};for(const[o,r]of Object.entries(n))s[o]=_(r,i,e+1,t);return s}return a.type==="array"?[_(a.items,i,e+1,t)]:typeof a.type=="string"&&a.type in H?H[a.type]:null},pe=(a,i)=>{const e=a.replace(/^#\/components\/schemas\//,"");return i?.components?.schemas?.[e]},P=a=>a?a.$ref?a.$ref.split("/").pop()??"ref":a.type==="array"?`${P(a.items)}[]`:a.type??(a.properties?"object":"any"):"any",x=(a,i,e=0,t,s)=>{if(!a)return'<span class="type">any</span>';if(e>6)return'<span class="type">…</span>';const n=a.$ref?pe(a.$ref,i)??a:a,o=t?`<span class="key">${h(t)}${s?'<span class="req">*</span>':""}: </span>`:"",r=`<span class="type">${h(P(n))}</span>`,d=e<2?" open":"";return n.type==="object"||n.properties?`<details${d}><summary>${o}${r}</summary>`+Object.entries(n.properties??{}).map(([b,u])=>x(u,i,e+1,b,n.required?.includes(b))).join("")+"</details>":n.type==="array"?`<details${d}><summary>${o}${r}</summary>`+x(n.items,i,e+1)+"</details>":`<div style="margin-left:${e?14:0}px">${o}${r}</div>`};class be extends HTMLElement{constructor(){super(...arguments);l(this,"_op",null);l(this,"_doc",null)}setOp(e,t){this._op=e,this._doc=t,this._render()}_render(){const e=this._op;if(!e){this.innerHTML="";return}if(e.source==="handler"){this.innerHTML=`
|
|
33
33
|
<div class="spec-zone">
|
|
34
34
|
<h4>Spec</h4>
|
|
35
35
|
<div class="desc" style="color:#57606a;font-style:italic">
|
|
36
36
|
No OpenAPI spec for this route. Inject controls and Try-it-out still
|
|
37
37
|
work; request/response schemas are unavailable.
|
|
38
38
|
</div>
|
|
39
|
-
</div>`;return}
|
|
39
|
+
</div>`;return}const t=this._doc,s=e.op.requestBody?.content?.["application/json"],n=s?.schema,o=s?s.example??_(n,t):null,r=o!=null?JSON.stringify(o,null,2):"",d=e.op.parameters??[],b=e.op.responses??{};this.innerHTML=`
|
|
40
40
|
<div class="spec-zone">
|
|
41
41
|
<h4>Spec</h4>
|
|
42
|
-
${e.op.description?`<div class="desc">${
|
|
43
|
-
${
|
|
42
|
+
${e.op.description?`<div class="desc">${ue(e.op.description)}</div>`:""}
|
|
43
|
+
${d.length>0?`
|
|
44
44
|
<div style="margin-top:8px">
|
|
45
45
|
<h4>Parameters</h4>
|
|
46
46
|
<table>
|
|
47
47
|
<thead><tr><th>name</th><th>in</th><th>type</th><th>required</th><th>description</th></tr></thead>
|
|
48
48
|
<tbody>
|
|
49
|
-
${
|
|
49
|
+
${d.map(u=>`
|
|
50
50
|
<tr>
|
|
51
|
-
<td><code>${
|
|
52
|
-
<td>${
|
|
53
|
-
<td>${
|
|
54
|
-
<td>${
|
|
55
|
-
<td>${
|
|
56
|
-
</tr>`).join(
|
|
51
|
+
<td><code>${h(u.name)}</code></td>
|
|
52
|
+
<td>${h(u.in)}</td>
|
|
53
|
+
<td>${h(u.schema?.type??"any")}</td>
|
|
54
|
+
<td>${u.required?"yes":""}</td>
|
|
55
|
+
<td>${h(u.description??"")}</td>
|
|
56
|
+
</tr>`).join("")}
|
|
57
57
|
</tbody>
|
|
58
58
|
</table>
|
|
59
|
-
</div
|
|
60
|
-
${
|
|
59
|
+
</div>`:""}
|
|
60
|
+
${n?`
|
|
61
61
|
<div style="margin-top:8px">
|
|
62
62
|
<h4>Request body</h4>
|
|
63
|
-
<div class="schema-tree">${
|
|
64
|
-
${
|
|
65
|
-
</div
|
|
66
|
-
${Object.keys(
|
|
63
|
+
<div class="schema-tree">${x(n,t)}</div>
|
|
64
|
+
${r?'<button class="btn ghost" style="margin-top:6px" type="button" data-copy>Copy as JSON example</button>':""}
|
|
65
|
+
</div>`:""}
|
|
66
|
+
${Object.keys(b).length>0?`
|
|
67
67
|
<div style="margin-top:8px">
|
|
68
68
|
<h4>Responses</h4>
|
|
69
|
-
${Object.entries(
|
|
69
|
+
${Object.entries(b).map(([u,y])=>{const S=y.content?.["application/json"]?.schema;return`
|
|
70
70
|
<details style="margin-top:4px">
|
|
71
|
-
<summary><strong>${
|
|
72
|
-
${
|
|
73
|
-
</details>`}).join(
|
|
74
|
-
</div
|
|
75
|
-
</div>`,this.querySelector(
|
|
71
|
+
<summary><strong>${h(u)}</strong> — ${h(y.description??"")}</summary>
|
|
72
|
+
${S?`<div class="schema-tree">${x(S,t)}</div>`:""}
|
|
73
|
+
</details>`}).join("")}
|
|
74
|
+
</div>`:""}
|
|
75
|
+
</div>`,this.querySelector("[data-copy]")?.addEventListener("click",()=>{navigator.clipboard?.writeText(r),p("info","Copied")})}}customElements.define("dbh-spec-zone",be);const K=a=>{const i=new TextEncoder().encode(a);let e="";for(const t of i)e+=String.fromCharCode(t);return btoa(e)},N=a=>{for(let i=0;i<a.length;i++)if(a.charCodeAt(i)>127)return!1;return!0},me=["http-status","business-failure","custom-body","empty-body","malformed-json","delay","hang","destroy"];class fe extends HTMLElement{constructor(){super(...arguments);l(this,"_op",null);l(this,"_existing");l(this,"_globalActive",!1);l(this,"headerOverride",null);l(this,"_scope","exact");l(this,"_kind","http-status");l(this,"_status",500);l(this,"_message","");l(this,"_body","{}");l(this,"_contentType","application/json");l(this,"_ms",1e3);l(this,"_description","")}setOp(e){const t=this._existing?.id===e.existing?.id&&this._op?.path===e.op.path&&this._op?.method===e.op.method;this._op=e.op,this._existing=e.existing,this._globalActive=e.globalActive,t||this._syncFromExisting(),this._render()}_syncFromExisting(){const e=this._existing;if(!e){this._scope="exact",this._kind="http-status",this._status=500,this._message="",this._body="{}",this._contentType="application/json",this._ms=1e3,this._description="";return}(e.matcher.kind==="exact"||e.matcher.kind==="path")&&(this._scope=e.matcher.kind),this._kind=e.mode.kind,this._description=e.description??"";const t=e.mode;t.kind==="http-status"?this._status=t.status:t.kind==="business-failure"?this._message=t.message:t.kind==="custom-body"?(this._status=t.status??200,this._body=JSON.stringify(t.body,null,2),this._contentType=t.contentType??"application/json"):t.kind==="empty-body"?this._status=t.status??204:t.kind==="delay"&&(this._ms=t.ms)}_renderFieldsHtml(){const e=this._kind;return e==="http-status"||e==="empty-body"||e==="malformed-json"?`<div class="row"><label>Status</label><input type="number" data-field="status" value="${this._status}" /></div>`:e==="business-failure"?`<div class="row"><label>Message</label><input type="text" data-field="message" value="${h(this._message)}" placeholder="Optional message returned with the failure" /></div>`:e==="custom-body"?`
|
|
76
76
|
<div class="row"><label>Status</label><input type="number" data-field="status" value="${this._status}" /></div>
|
|
77
|
-
<div class="row"><label>Content-Type</label><input type="text" data-field="contentType" value="${
|
|
78
|
-
<div class="row"><label>Body</label><textarea data-field="body">${
|
|
77
|
+
<div class="row"><label>Content-Type</label><input type="text" data-field="contentType" value="${h(this._contentType)}" /></div>
|
|
78
|
+
<div class="row"><label>Body</label><textarea data-field="body">${h(this._body)}</textarea></div>`:e==="delay"?`<div class="row"><label>Delay (ms)</label><input type="number" data-field="ms" value="${this._ms}" /></div>`:""}_render(){if(!this._op){this.innerHTML="";return}const e=this._existing;this.innerHTML=`
|
|
79
79
|
<div class="mock-zone">
|
|
80
80
|
<h4>Inject error</h4>
|
|
81
81
|
${this._globalActive?`
|
|
82
82
|
<div class="global-active-hint" role="note">
|
|
83
83
|
<span>A global override is active. Manage it from the top bar.</span>
|
|
84
84
|
<button type="button" class="btn ghost" data-open-global>Open</button>
|
|
85
|
-
</div
|
|
85
|
+
</div>`:""}
|
|
86
86
|
<div class="row">
|
|
87
87
|
<label>Scope</label>
|
|
88
88
|
<div class="radio-group">
|
|
89
|
-
<label><input type="radio" name="scope" data-scope="exact"${this._scope
|
|
90
|
-
<label><input type="radio" name="scope" data-scope="path"${this._scope
|
|
89
|
+
<label><input type="radio" name="scope" data-scope="exact"${this._scope==="exact"?" checked":""}/> Exact (METHOD + this path)</label>
|
|
90
|
+
<label><input type="radio" name="scope" data-scope="path"${this._scope==="path"?" checked":""}/> Path (any method on this path)</label>
|
|
91
91
|
</div>
|
|
92
92
|
</div>
|
|
93
93
|
<div class="row">
|
|
94
94
|
<label>Mode</label>
|
|
95
|
-
<select data-mode>${
|
|
95
|
+
<select data-mode>${me.map(t=>`<option value="${t}"${t===this._kind?" selected":""}>${t}</option>`).join("")}</select>
|
|
96
96
|
</div>
|
|
97
97
|
<div data-fields>${this._renderFieldsHtml()}</div>
|
|
98
98
|
<div class="row">
|
|
99
99
|
<label>Description</label>
|
|
100
|
-
<input type="text" data-field="description" value="${
|
|
100
|
+
<input type="text" data-field="description" value="${h(this._description)}" />
|
|
101
101
|
</div>
|
|
102
102
|
<div class="actions">
|
|
103
|
-
<button class="btn primary" type="button" data-submit>${e
|
|
104
|
-
${e?`<button class="btn danger" type="button" data-remove="${
|
|
103
|
+
<button class="btn primary" type="button" data-submit>${e?"Update":"Apply"}</button>
|
|
104
|
+
${e?`<button class="btn danger" type="button" data-remove="${h(e.id)}">Remove</button>`:""}
|
|
105
105
|
</div>
|
|
106
|
-
</div>`,this._wire()}_wire(){this.querySelector(
|
|
106
|
+
</div>`,this._wire()}_wire(){this.querySelector("[data-open-global]")?.addEventListener("click",()=>C());for(const s of this.querySelectorAll("[data-scope]"))s.addEventListener("change",()=>{s.checked&&(this._scope=s.dataset.scope)});const e=this.querySelector("[data-mode]");e?.addEventListener("change",()=>{this._kind=e.value;const s=this.querySelector("[data-fields]");s&&(s.innerHTML=this._renderFieldsHtml(),this._wireFields())}),this._wireFields(),this.querySelector("[data-submit]")?.addEventListener("click",()=>this._submit());const t=this.querySelector("[data-remove]");t?.addEventListener("click",()=>{E(t.dataset.remove??"")})}_wireFields(){const e=this.querySelector('[data-field="description"]');e?.addEventListener("input",()=>{this._description=e.value});for(const t of this.querySelectorAll("[data-fields] [data-field]"))t.addEventListener("input",()=>{const s=t.dataset.field;s==="status"?this._status=Number(t.value):s==="ms"?this._ms=Number(t.value):s==="message"?this._message=t.value:s==="body"?this._body=t.value:s==="contentType"&&(this._contentType=t.value)})}_submit(){const e=this._op;if(!e)return;const t=this._scope==="exact"?{kind:"exact",method:e.method,path:e.path}:{kind:"path",path:e.path};let s,n=null;const o=this._kind;switch(o){case"http-status":s={kind:o,status:this._status};break;case"business-failure":s={kind:o,message:this._message},N(this._message)||(n={name:"x-mock-business-failure-b64",value:K(this._message)});break;case"custom-body":{let r;try{r=JSON.parse(this._body)}catch{r=this._body}s={kind:o,status:this._status,body:r,contentType:this._contentType};break}case"empty-body":s={kind:o,status:this._status};break;case"malformed-json":s={kind:o,status:this._status};break;case"delay":s={kind:o,ms:this._ms};break;case"hang":s={kind:o};break;case"destroy":s={kind:o};break}this.headerOverride=n,this.dispatchEvent(new CustomEvent("dbh-header-override",{detail:n,bubbles:!0})),(async()=>{const r=this._existing;if(r&&t.kind===r.matcher.kind)try{await E(r.id)}catch{}try{await D(t,s,this._description||void 0)}catch(d){p("error",d instanceof Error?d.message:String(d))}})()}}customElements.define("dbh-inject-error-panel",fe);const ye=(a,i)=>{const e=a.op.requestBody?.content?.["application/json"]?.schema;return e?JSON.stringify(_(e,i),null,2):""};class ve extends HTMLElement{constructor(){super(...arguments);l(this,"_op",null);l(this,"_doc",null);l(this,"extraHeader",null);l(this,"_pvals",{});l(this,"_freeQuery",[]);l(this,"_body","");l(this,"_busy",!1)}setOp(e,t){this._op=e,this._doc=t,this._pvals={};for(const s of e.op.parameters??[])this._pvals[s.name]=String(s.schema?.default??"");this._body=ye(e,t),this._freeQuery=e.source==="handler"?[{key:"",value:""}]:[],this._render()}_render(){const e=this._op;if(!e){this.innerHTML="";return}const t=e.op.parameters??[],s=e.method!=="GET"&&e.method!=="HEAD",n=e.source==="handler";this.innerHTML=`
|
|
107
107
|
<details class="try-it">
|
|
108
108
|
<summary>Try it out</summary>
|
|
109
|
-
${t.map(
|
|
109
|
+
${t.map(r=>`
|
|
110
110
|
<div class="field">
|
|
111
|
-
<label>${
|
|
112
|
-
<input type="text" data-pname="${
|
|
113
|
-
</div>`).join(
|
|
114
|
-
${
|
|
111
|
+
<label>${h(r.name)} <small>(${h(r.in)}${r.required?", required":""})</small></label>
|
|
112
|
+
<input type="text" data-pname="${h(r.name)}" value="${h(this._pvals[r.name]??"")}" />
|
|
113
|
+
</div>`).join("")}
|
|
114
|
+
${n?`
|
|
115
115
|
<div class="field">
|
|
116
116
|
<label>query params <small>(free-form)</small></label>
|
|
117
117
|
<div data-free-query>
|
|
118
|
-
${this._freeQuery.map((
|
|
118
|
+
${this._freeQuery.map((r,d)=>`
|
|
119
119
|
<div class="free-q-row" style="display:flex;gap:6px;margin-bottom:4px">
|
|
120
|
-
<input type="text" data-fq-key="${
|
|
121
|
-
<input type="text" data-fq-val="${
|
|
122
|
-
<button class="btn ghost" type="button" data-fq-rm="${
|
|
123
|
-
</div>`).join(
|
|
120
|
+
<input type="text" data-fq-key="${d}" placeholder="name" value="${h(r.key)}" style="flex:1" />
|
|
121
|
+
<input type="text" data-fq-val="${d}" placeholder="value" value="${h(r.value)}" style="flex:2" />
|
|
122
|
+
<button class="btn ghost" type="button" data-fq-rm="${d}" title="Remove">−</button>
|
|
123
|
+
</div>`).join("")}
|
|
124
124
|
<button class="btn ghost" type="button" data-fq-add>+ Add query param</button>
|
|
125
125
|
</div>
|
|
126
|
-
</div
|
|
127
|
-
${
|
|
126
|
+
</div>`:""}
|
|
127
|
+
${s?`
|
|
128
128
|
<div class="field">
|
|
129
|
-
<label>body${
|
|
130
|
-
<textarea data-body style="width:100%;min-height:100px;font-family:ui-monospace,Menlo,monospace">${
|
|
131
|
-
</div
|
|
132
|
-
<button class="btn primary" type="button" data-send${this._busy
|
|
129
|
+
<label>body${n?" <small>(free-form)</small>":""}</label>
|
|
130
|
+
<textarea data-body style="width:100%;min-height:100px;font-family:ui-monospace,Menlo,monospace">${h(this._body)}</textarea>
|
|
131
|
+
</div>`:""}
|
|
132
|
+
<button class="btn primary" type="button" data-send${this._busy?" disabled":""}>${this._busy?"…":"Send"}</button>
|
|
133
133
|
<dbh-response-viewer></dbh-response-viewer>
|
|
134
|
-
</details>`;for(
|
|
134
|
+
</details>`;for(const r of this.querySelectorAll("[data-pname]"))r.addEventListener("input",()=>{this._pvals[r.dataset.pname??""]=r.value});for(const r of this.querySelectorAll("[data-fq-key]"))r.addEventListener("input",()=>{const d=Number(r.dataset.fqKey);this._freeQuery[d]&&(this._freeQuery[d].key=r.value)});for(const r of this.querySelectorAll("[data-fq-val]"))r.addEventListener("input",()=>{const d=Number(r.dataset.fqVal);this._freeQuery[d]&&(this._freeQuery[d].value=r.value)});for(const r of this.querySelectorAll("[data-fq-rm]"))r.addEventListener("click",()=>{const d=Number(r.dataset.fqRm);this._freeQuery.splice(d,1),this._freeQuery.length===0&&this._freeQuery.push({key:"",value:""}),this._render()});this.querySelector("[data-fq-add]")?.addEventListener("click",()=>{this._freeQuery.push({key:"",value:""}),this._render()});const o=this.querySelector("[data-body]");o?.addEventListener("input",()=>{this._body=o.value}),this.querySelector("[data-send]")?.addEventListener("click",()=>{this._send()})}_buildUrl(){const e=this._op;if(!e)return"";const t=e.op.parameters??[];let s=e.path;const n=new URLSearchParams;for(const r of t){const d=this._pvals[r.name];d&&(r.in==="path"?s=s.replace(`{${r.name}}`,encodeURIComponent(d)):r.in==="query"&&n.set(r.name,d))}for(const r of this._freeQuery)r.key&&n.append(r.key,r.value);const o=n.toString();return o?`${s}?${o}`:s}async _send(){const e=this._op;if(!e)return;this._busy=!0;const t=this.querySelector("dbh-response-viewer");t?.setResult(null);const s=this.querySelector("[data-send]");s&&(s.disabled=!0,s.textContent="…");const n=performance.now();try{const o={};for(const y of e.op.parameters??[])y.in==="header"&&this._pvals[y.name]&&(o[y.name]=this._pvals[y.name]);this.extraHeader&&(o[this.extraHeader.name]=this.extraHeader.value);const r={method:e.method,headers:o};e.method!=="GET"&&e.method!=="HEAD"&&this._body&&(o["content-type"]=o["content-type"]??"application/json",r.body=this._body);const d=await fetch(this._buildUrl(),r),b=await d.text(),u={status:d.status,headers:{},body:b,elapsedMs:Math.round(performance.now()-n)};d.headers.forEach((y,S)=>{u.headers[S]=y}),t?.setResult(u)}catch(o){t?.setResult({error:o instanceof Error?o.message:String(o)})}finally{this._busy=!1,s&&(s.disabled=!1,s.textContent="Send")}}}customElements.define("dbh-try-it-out",ve);const I=(a,i)=>{if(!a.methods||a.methods.length===0)return!0;const e=i.toUpperCase();return a.methods.some(t=>t.toUpperCase()===e)};class _e extends HTMLElement{constructor(){super(...arguments);l(this,"_spec");l(this,"_inject");l(this,"_try")}connectedCallback(){this.childElementCount===0&&(this.innerHTML=`
|
|
135
135
|
<div class="op-card">
|
|
136
136
|
<dbh-spec-zone></dbh-spec-zone>
|
|
137
137
|
<dbh-inject-error-panel></dbh-inject-error-panel>
|
|
138
138
|
<dbh-try-it-out></dbh-try-it-out>
|
|
139
|
-
</div>`,this._spec=this.querySelector(
|
|
140
|
-
<div class="group${this._open
|
|
139
|
+
</div>`,this._spec=this.querySelector("dbh-spec-zone"),this._inject=this.querySelector("dbh-inject-error-panel"),this._try=this.querySelector("dbh-try-it-out"),this._inject.addEventListener("dbh-header-override",e=>{this._try.extraHeader=e.detail}))}setData(e){this._spec||this.connectedCallback();const{op:t,doc:s,overrides:n}=e,o=n.find(d=>d.matcher.kind==="exact"&&d.matcher.method===t.method&&d.matcher.path===t.path),r=n.some(d=>d.matcher.kind==="global"&&I(d.matcher,t.method));this._spec.setOp(t,s),this._inject.setOp({op:t,existing:o,globalActive:r}),this._try.setOp(t,s)}}customElements.define("dbh-operation-card",_e);class ge extends HTMLElement{constructor(){super(...arguments);l(this,"_label","");l(this,"_ops",[]);l(this,"_doc",null);l(this,"_overrides",[]);l(this,"_expandedKey",null);l(this,"_focusedKey",null);l(this,"_open",!0)}setData(e){this._label=e.label,this._ops=e.ops,this._doc=e.doc,this._overrides=e.overrides,this._expandedKey=e.expandedKey,this._focusedKey=e.focusedKey,this._render()}_render(){const e=this._ops.reduce((s,n)=>s+(this._overrides.some(o=>o.matcher.kind==="exact"&&o.matcher.method===n.method&&o.matcher.path===n.path)?1:0),0);this.innerHTML=`
|
|
140
|
+
<div class="group${this._open?" open":""}">
|
|
141
141
|
<div class="group-header" data-header>
|
|
142
|
-
<h3>${
|
|
143
|
-
<span class="count">${this._ops.length} ops${e?` · ${e} active
|
|
144
|
-
<span>${this._open
|
|
142
|
+
<h3>${h(this._label)}</h3>
|
|
143
|
+
<span class="count">${this._ops.length} ops${e?` · ${e} active`:""}</span>
|
|
144
|
+
<span>${this._open?"−":"+"}</span>
|
|
145
145
|
</div>
|
|
146
146
|
<div class="group-body" data-body></div>
|
|
147
|
-
</div>`,this.querySelector(
|
|
148
|
-
<aside class="right-panel${e.rightPanelOpen
|
|
147
|
+
</div>`,this.querySelector("[data-header]")?.addEventListener("click",()=>{this._open=!this._open,this._render()});const t=this.querySelector("[data-body]");if(t)for(const s of this._ops){const n=`${s.method} ${s.path}`,o=this._overrides.find(d=>d.matcher.kind==="exact"&&d.matcher.method===s.method&&d.matcher.path===s.path),r=document.createElement("dbh-operation-row");if(r.setRow({op:s,override:o,expanded:this._expandedKey===n,focused:this._focusedKey===n}),t.appendChild(r),this._expandedKey===n){const d=document.createElement("dbh-operation-card");t.appendChild(d),d.setData({op:s,doc:this._doc,overrides:this._overrides})}}}}customElements.define("dbh-tag-group",ge);const ke=a=>{if(a.matcher.kind==="exact")return`${a.matcher.method} ${a.matcher.path}`;if(a.matcher.kind==="path")return`* ${a.matcher.path}`;const i=a.matcher.methods;return i&&i.length>0?`${i.join("|")} (global)`:"* (global)"},A=a=>a.matcher.kind==="global"?0:a.matcher.kind==="path"?1:2;class $e extends HTMLElement{constructor(){super(...arguments);l(this,"_unsub")}connectedCallback(){this._render(),this._unsub=c.subscribe(()=>this._render())}disconnectedCallback(){this._unsub?.()}_render(){const e=c.get(),t=[...e.overrides].sort((s,n)=>A(s)-A(n));this.innerHTML=`
|
|
148
|
+
<aside class="right-panel${e.rightPanelOpen?" open":""}">
|
|
149
149
|
<header>
|
|
150
150
|
<h2>Active overrides (${e.overrides.length})</h2>
|
|
151
151
|
<button class="btn ghost" type="button" data-close>×</button>
|
|
152
152
|
</header>
|
|
153
153
|
<div class="list">
|
|
154
|
-
${e.overrides.length===0
|
|
155
|
-
<div class="override-item${
|
|
156
|
-
<span class="dot" style="background:${
|
|
157
|
-
${
|
|
154
|
+
${e.overrides.length===0?'<p style="padding:12px;color:#57606a">No active overrides</p>':t.map(s=>`
|
|
155
|
+
<div class="override-item${s.matcher.kind==="global"?" is-global":""}" data-jump="${h(s.id)}">
|
|
156
|
+
<span class="dot" style="background:${M(s.mode.kind)}"></span>
|
|
157
|
+
${s.matcher.kind==="global"?'<span class="scope-badge scope-global">GLOBAL</span>':""}
|
|
158
158
|
<div class="info">
|
|
159
|
-
<div class="route">${
|
|
160
|
-
<small>${
|
|
159
|
+
<div class="route">${h(ke(s))}</div>
|
|
160
|
+
<small>${h(s.mode.kind)}${s.description?` — ${h(s.description)}`:""}</small>
|
|
161
161
|
</div>
|
|
162
|
-
<button class="btn danger" type="button" data-remove="${
|
|
163
|
-
</div>`).join(
|
|
162
|
+
<button class="btn danger" type="button" data-remove="${h(s.id)}">Remove</button>
|
|
163
|
+
</div>`).join("")}
|
|
164
164
|
</div>
|
|
165
|
-
</aside>`,this.querySelector(
|
|
165
|
+
</aside>`,this.querySelector("[data-close]")?.addEventListener("click",()=>c.set({rightPanelOpen:!1}));for(const s of this.querySelectorAll("[data-jump]"))s.addEventListener("click",()=>{const n=s.dataset.jump??"",o=c.get().overrides.find(d=>d.id===n);if(!o)return;if(o.matcher.kind==="global"){C();return}if(o.matcher.kind!=="exact"){c.set({rightPanelOpen:!1});return}const r=L(o.matcher.method,o.matcher.path);c.set({expandedKey:r,focusedKey:r,rightPanelOpen:!1}),setTimeout(()=>{const d=document.querySelector(`[data-key="${r.replace(/"/g,'\\"')}"]`);d&&"scrollIntoView"in d&&d.scrollIntoView({behavior:"smooth",block:"center"})},50)});for(const s of this.querySelectorAll("[data-remove]"))s.addEventListener("click",n=>{n.stopPropagation(),E(s.dataset.remove??"")})}}customElements.define("dbh-active-overrides-panel",$e);class Ee extends HTMLElement{constructor(){super(...arguments);l(this,"_unsub")}connectedCallback(){this._render(),this._unsub=c.subscribe(()=>this._render())}disconnectedCallback(){this._unsub?.()}_render(){const t=c.get().overrides.length;if(t===0){this.innerHTML="";return}this.innerHTML=`<button class="pill-float" type="button">${t} Active overrides</button>`,this.querySelector("button")?.addEventListener("click",()=>c.set({rightPanelOpen:!0}))}}customElements.define("dbh-floating-pill",Ee);class we extends HTMLElement{constructor(){super(...arguments);l(this,"_unsub")}connectedCallback(){this._render(Z()),this._unsub=V(e=>this._render(e))}disconnectedCallback(){this._unsub?.()}_render(e){this.innerHTML='<div class="toast-stack">'+e.map(t=>`<div class="toast ${h(t.kind)}" data-id="${t.id}">${h(t.message)}</div>`).join("")+"</div>";for(const t of this.querySelectorAll(".toast"))t.addEventListener("click",()=>{const s=Number(t.dataset.id);Number.isFinite(s)&&X(s)})}}customElements.define("dbh-toasts",we);class Se extends HTMLElement{constructor(){super(...arguments);l(this,"_unsub")}connectedCallback(){this._render(),this._unsub=c.subscribe(()=>this._render())}disconnectedCallback(){this._unsub?.()}_render(){if(!c.get().helpOpen){this.innerHTML="";return}this.innerHTML=`
|
|
166
166
|
<div class="modal-backdrop">
|
|
167
167
|
<div class="modal">
|
|
168
168
|
<h2>Keyboard shortcuts</h2>
|
|
@@ -179,10 +179,10 @@
|
|
|
179
179
|
<button class="btn" type="button" data-ok>OK</button>
|
|
180
180
|
</div>
|
|
181
181
|
</div>
|
|
182
|
-
</div>`;
|
|
182
|
+
</div>`;const t=()=>c.set({helpOpen:!1});this.querySelector(".modal-backdrop")?.addEventListener("click",t),this.querySelector(".modal")?.addEventListener("click",s=>s.stopPropagation()),this.querySelector("[data-ok]")?.addEventListener("click",t)}}customElements.define("dbh-help-modal",Se);const xe=["http-status","business-failure","custom-body","empty-body","malformed-json","delay","hang","destroy"],$=["GET","POST","PUT","PATCH","DELETE","HEAD","OPTIONS"];class Te extends HTMLElement{constructor(){super(...arguments);l(this,"_unsub");l(this,"_kind","http-status");l(this,"_status",503);l(this,"_message","");l(this,"_body","{}");l(this,"_contentType","application/json");l(this,"_ms",1e3);l(this,"_description","");l(this,"_methods",new Set($));l(this,"_lastExistingId")}connectedCallback(){this._render(),this._unsub=c.subscribe(()=>this._render())}disconnectedCallback(){this._unsub?.()}_globals(){return c.get().overrides.filter(e=>e.matcher.kind==="global")}_syncFromExisting(e,t){if(t&&!(e&&e.id===this._lastExistingId))if(this._lastExistingId=e?.id,e){this._kind=e.mode.kind,this._description=e.description??"";const s=e.matcher.kind==="global"?e.matcher.methods:void 0;this._methods=new Set(s&&s.length>0?s.map(o=>o.toUpperCase()):$);const n=e.mode;n.kind==="http-status"?this._status=n.status:n.kind==="business-failure"?this._message=n.message:n.kind==="custom-body"?(this._status=n.status??200,this._body=JSON.stringify(n.body,null,2),this._contentType=n.contentType??"application/json"):n.kind==="empty-body"?this._status=n.status??204:n.kind==="delay"&&(this._ms=n.ms)}else this._kind="http-status",this._status=503,this._message="",this._body="{}",this._contentType="application/json",this._ms=1e3,this._description="",this._methods=new Set($)}_renderFieldsHtml(){const e=this._kind;return e==="http-status"||e==="empty-body"||e==="malformed-json"?`<div class="row"><label>Status</label><input type="number" data-field="status" value="${this._status}" /></div>`:e==="business-failure"?`<div class="row"><label>Message</label><input type="text" data-field="message" value="${h(this._message)}" /></div>`:e==="custom-body"?`
|
|
183
183
|
<div class="row"><label>Status</label><input type="number" data-field="status" value="${this._status}" /></div>
|
|
184
|
-
<div class="row"><label>Content-Type</label><input type="text" data-field="contentType" value="${
|
|
185
|
-
<div class="row"><label>Body</label><textarea data-field="body">${
|
|
184
|
+
<div class="row"><label>Content-Type</label><input type="text" data-field="contentType" value="${h(this._contentType)}" /></div>
|
|
185
|
+
<div class="row"><label>Body</label><textarea data-field="body">${h(this._body)}</textarea></div>`:e==="delay"?`<div class="row"><label>Delay (ms)</label><input type="number" data-field="ms" value="${this._ms}" /></div>`:""}_render(){const e=c.get().globalOpen;if(!e){this.innerHTML="",this._lastExistingId=void 0;return}const t=this._globals(),s=t[0];this._syncFromExisting(s,e),this.innerHTML=`
|
|
186
186
|
<div class="modal-backdrop">
|
|
187
187
|
<div class="modal global-override-modal">
|
|
188
188
|
<header class="global-override-header">
|
|
@@ -192,42 +192,42 @@
|
|
|
192
192
|
<p class="global-override-desc">Applies to every route on this server. Use to simulate the backend being down, slow, etc.</p>
|
|
193
193
|
<div class="row">
|
|
194
194
|
<label>Mode</label>
|
|
195
|
-
<select data-mode>${
|
|
195
|
+
<select data-mode>${xe.map(n=>`<option value="${n}"${n===this._kind?" selected":""}>${n}</option>`).join("")}</select>
|
|
196
196
|
</div>
|
|
197
197
|
<div class="row global-override-methods">
|
|
198
198
|
<label>Methods</label>
|
|
199
199
|
<div class="method-checkboxes">
|
|
200
|
-
${
|
|
200
|
+
${$.map(n=>`
|
|
201
201
|
<label class="method-checkbox">
|
|
202
|
-
<input type="checkbox" data-method="${
|
|
203
|
-
<span>${
|
|
204
|
-
</label>`).join(
|
|
202
|
+
<input type="checkbox" data-method="${n}"${this._methods.has(n)?" checked":""} />
|
|
203
|
+
<span>${n}</span>
|
|
204
|
+
</label>`).join("")}
|
|
205
205
|
</div>
|
|
206
206
|
</div>
|
|
207
207
|
<div data-fields>${this._renderFieldsHtml()}</div>
|
|
208
208
|
<div class="row">
|
|
209
209
|
<label>Description</label>
|
|
210
|
-
<input type="text" data-field="description" value="${
|
|
210
|
+
<input type="text" data-field="description" value="${h(this._description)}" />
|
|
211
211
|
</div>
|
|
212
212
|
<div class="actions">
|
|
213
|
-
<button type="button" class="btn primary" data-submit>${
|
|
214
|
-
${
|
|
213
|
+
<button type="button" class="btn primary" data-submit>${s?"Update":"Apply"}</button>
|
|
214
|
+
${s?`<button type="button" class="btn danger" data-remove="${h(s.id)}">Remove</button>`:""}
|
|
215
215
|
</div>
|
|
216
216
|
${t.length>0?`
|
|
217
217
|
<div class="global-override-list">
|
|
218
218
|
<h3>Active global overrides (${t.length})</h3>
|
|
219
|
-
${t.map(
|
|
219
|
+
${t.map(n=>{const o=n.matcher.kind==="global"?n.matcher.methods:void 0,r=o&&o.length>0?`${o.join("|")} (global)`:"* (global)";return`
|
|
220
220
|
<div class="override-item">
|
|
221
|
-
<span class="dot" style="background:${
|
|
221
|
+
<span class="dot" style="background:${M(n.mode.kind)}"></span>
|
|
222
222
|
<div class="info">
|
|
223
|
-
<div class="route">${
|
|
224
|
-
<small>${
|
|
223
|
+
<div class="route">${h(r)}</div>
|
|
224
|
+
<small>${h(n.mode.kind)}${n.description?` — ${h(n.description)}`:""}</small>
|
|
225
225
|
</div>
|
|
226
|
-
<button type="button" class="btn danger" data-remove="${
|
|
227
|
-
</div>`}).join(
|
|
228
|
-
</div
|
|
226
|
+
<button type="button" class="btn danger" data-remove="${h(n.id)}">Remove</button>
|
|
227
|
+
</div>`}).join("")}
|
|
228
|
+
</div>`:""}
|
|
229
229
|
</div>
|
|
230
|
-
</div>`,this._wire()}_wire(){
|
|
230
|
+
</div>`,this._wire()}_wire(){const e=()=>c.set({globalOpen:!1});this.querySelector(".modal-backdrop")?.addEventListener("click",e),this.querySelector(".modal")?.addEventListener("click",s=>s.stopPropagation()),this.querySelector("[data-close]")?.addEventListener("click",e);const t=this.querySelector("[data-mode]");t?.addEventListener("change",()=>{this._kind=t.value;const s=this.querySelector("[data-fields]");s&&(s.innerHTML=this._renderFieldsHtml(),this._wireFields())}),this._wireFields();for(const s of this.querySelectorAll("[data-method]"))s.addEventListener("change",()=>{const n=s.dataset.method??"";s.checked?this._methods.add(n):this._methods.delete(n)});this.querySelector("[data-submit]")?.addEventListener("click",()=>this._submit());for(const s of this.querySelectorAll("[data-remove]"))s.addEventListener("click",()=>{E(s.dataset.remove??"")})}_wireFields(){const e=this.querySelector('[data-field="description"]');e?.addEventListener("input",()=>{this._description=e.value});for(const t of this.querySelectorAll("[data-fields] [data-field]"))t.addEventListener("input",()=>{const s=t.dataset.field;s==="status"?this._status=Number(t.value):s==="ms"?this._ms=Number(t.value):s==="message"?this._message=t.value:s==="body"?this._body=t.value:s==="contentType"&&(this._contentType=t.value)})}_submit(){let e;const t=this._kind;switch(t){case"http-status":e={kind:t,status:this._status};break;case"business-failure":e={kind:t,message:this._message},N(this._message)||K(this._message);break;case"custom-body":{let r;try{r=JSON.parse(this._body)}catch{r=this._body}e={kind:t,status:this._status,body:r,contentType:this._contentType};break}case"empty-body":e={kind:t,status:this._status};break;case"malformed-json":e={kind:t,status:this._status};break;case"delay":e={kind:t,ms:this._ms};break;case"hang":e={kind:t};break;case"destroy":e={kind:t};break}const s=$.length,n=$.filter(r=>this._methods.has(r));if(n.length===0){p("error","Select at least one HTTP method.");return}const o=n.length===s?{kind:"global"}:{kind:"global",methods:n};(async()=>{const r=this._globals()[0];if(r)try{await E(r.id)}catch{}try{await D(o,e,this._description||void 0)}catch(d){p("error",d instanceof Error?d.message:String(d))}})()}}customElements.define("dbh-global-override-panel",Te);const Oe=["GET","POST","PUT","DELETE","PATCH"];class Le extends HTMLElement{constructor(){super(...arguments);l(this,"_filter");l(this,"_main");l(this,"_unsub");l(this,"_debounce");l(this,"_debouncedSearch","");l(this,"_onKeyDown",e=>this._handleKey(e))}connectedCallback(){this.innerHTML=`
|
|
231
231
|
<div class="app">
|
|
232
232
|
<dbh-top-bar></dbh-top-bar>
|
|
233
233
|
<dbh-filter-bar></dbh-filter-bar>
|
|
@@ -237,4 +237,4 @@
|
|
|
237
237
|
<dbh-toasts></dbh-toasts>
|
|
238
238
|
<dbh-help-modal></dbh-help-modal>
|
|
239
239
|
<dbh-global-override-panel></dbh-global-override-panel>
|
|
240
|
-
</div>`,this._filter=this.querySelector(
|
|
240
|
+
</div>`,this._filter=this.querySelector("dbh-filter-bar"),this._main=this.querySelector("[data-main]"),this._unsub=c.subscribe(()=>this._onStateChange()),window.addEventListener("keydown",this._onKeyDown),(async()=>{const[e,t]=await Promise.all([f.listRoutes(),f.fetchOpenApi()]);c.set({routes:e,doc:t??null,loading:!1}),await w(),await q(),this._renderList()})()}disconnectedCallback(){this._unsub?.(),window.removeEventListener("keydown",this._onKeyDown),this._debounce&&clearTimeout(this._debounce)}_onStateChange(){c.get().search!==this._debouncedSearch?(this._debounce&&clearTimeout(this._debounce),this._debounce=setTimeout(()=>{this._debouncedSearch=c.get().search,this._renderList()},150)):this._renderList()}_filtered(){const e=c.get(),t=G(e.routes,e.doc),s=this._debouncedSearch.trim().toLowerCase(),n=e.methods.size>0?e.methods:new Set(Oe);return t.filter(o=>!(!n.has(o.method)||s&&!o.path.toLowerCase().includes(s)&&!o.op.summary?.toLowerCase().includes(s)||e.activeOnly&&!e.overrides.some(d=>d.matcher.kind==="exact"&&d.matcher.method===o.method&&d.matcher.path===o.path||d.matcher.kind==="path"&&d.matcher.path===o.path||d.matcher.kind==="global"&&I(d.matcher,o.method))))}_renderList(){const e=c.get(),t=this._filtered(),s=Q(t,z(e.overrides));if(this._main.innerHTML="",s.length===0){const n=document.createElement("p");n.style.color="#57606a",n.textContent="No operations match these filters.",this._main.appendChild(n);return}for(const n of s){const o=document.createElement("dbh-tag-group");this._main.appendChild(o),o.setData({label:n.label,ops:n.ops,doc:e.doc,overrides:e.overrides,expandedKey:e.expandedKey,focusedKey:e.focusedKey})}}_handleKey(e){const t=e.target,s=!!t&&(t.tagName==="INPUT"||t.tagName==="TEXTAREA"||t.tagName==="SELECT"),n=this._filter?.searchInput,o=c.get();if(e.key==="/"&&!s)e.preventDefault(),n?.focus();else if((e.ctrlKey||e.metaKey)&&e.key.toLowerCase()==="k")e.preventDefault(),n?.focus();else if(e.key==="?"&&!s)e.preventDefault(),c.set({helpOpen:!0});else if(e.key==="Escape")o.helpOpen?c.set({helpOpen:!1}):o.globalOpen?c.set({globalOpen:!1}):o.rightPanelOpen?c.set({rightPanelOpen:!1}):o.expandedKey?c.set({expandedKey:null}):n?.blur();else if((e.key==="ArrowDown"||e.key==="ArrowUp")&&document.activeElement===n){e.preventDefault();const r=this._filtered().map(u=>L(u.method,u.path));if(r.length===0)return;const d=o.focusedKey?r.indexOf(o.focusedKey):-1,b=e.key==="ArrowDown"?Math.min(r.length-1,d+1):Math.max(0,d-1);c.set({focusedKey:r[b]??null})}else e.key==="Enter"&&document.activeElement===n&&o.focusedKey&&(e.preventDefault(),c.set({expandedKey:o.expandedKey===o.focusedKey?null:o.focusedKey}))}}customElements.define("dbh-app",Le);const j=document.getElementById("root");if(j){const a=document.createElement("dbh-app");j.appendChild(a)}
|