koishi-plugin-dataview-next 0.0.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/dist/index.js +1 -0
- package/dist/style.css +1 -0
- package/lib/index.cjs +352 -0
- package/lib/index.d.ts +10 -0
- package/lib/shared.d.ts +66 -0
- package/package.json +73 -0
- package/src/index.ts +429 -0
- package/src/shared.ts +95 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{send as M,message as h,icons as se}from"@koishijs/client";import{defineComponent as X,ref as _,reactive as Z,computed as j,watch as R,onBeforeUnmount as Pe,resolveComponent as v,resolveDirective as Ye,openBlock as u,createBlock as q,withCtx as o,createElementVNode as i,withDirectives as Ae,createElementBlock as d,toDisplayString as f,createTextVNode as w,createCommentVNode as U,createVNode as l,Fragment as Q,renderList as W,resolveDynamicComponent as ye,mergeProps as _e,normalizeClass as ge,h as C}from"vue";import{useRoute as Ie,useRouter as Ke}from"vue-router";const T={info:"dataview-next/info",query:"dataview-next/query",create:"dataview-next/create",update:"dataview-next/update",remove:"dataview-next/remove",drop:"dataview-next/drop"},Je={class:"header-title"},Le={key:0,class:"muted"},Ze={class:"source-list"},Qe=["onClick"],We={class:"source-name"},Xe={key:0,class:"count"},et={key:0,class:"table-list"},tt=["onClick"],at={class:"table-name"},nt={key:1,class:"table-panel"},ot={class:"table-header"},lt={key:0},st={key:1},rt={class:"table-actions"},ut={class:"query-bar"},it={key:0,class:"change-bar"},ct={class:"column-header"},dt={key:0,class:"cell-editor"},pt=["disabled","onDblclick"],mt={class:"form-grid"},vt={class:"danger-text"},ft=X({__name:"index",setup($){const ee=Ie(),H=Ke(),g=_(),S=_([]),te=_(0),b=_(false),P=_(""),z=Z({}),Y=Z({}),re=_(),we=[30,50,100,150,200,500,1e3],F=_(30),O=_(1),p=Z({}),x=Z({}),N=_(false),G=_(false),A=_("");let E,ae=false;const I=j(()=>{const e=ee.params.name;return Array.isArray(e)?e.join("/"):e||""}),r=j(()=>{var e;return I.value?(e=g.value)==null?void 0:e.tables[I.value]:void 0}),ne=j(()=>{var e;return Object.values(((e=r.value)==null?void 0:e.fields)||{})}),k=j(()=>Object.keys(p).length>0),ke=j(()=>Object.values(p).reduce((e,t)=>e+Object.keys(t).length,0)),ue=j(()=>{var e,t;return((t=(e=g.value)==null?void 0:e.display)==null?void 0:t.showGroupCount)??false}),ie=j(()=>{var e,t;return((t=(e=g.value)==null?void 0:e.display)==null?void 0:t.showGroupSize)??true}),he=j(()=>{var L;const e=new Map;for(const n of Object.values(((L=g.value)==null?void 0:L.tables)||{}))e.has(n.source.id)||e.set(n.source.id,{source:n.source,tables:[]}),e.get(n.source.id).tables.push(n);const t=[...e.values()].map(n=>({...n,size:n.tables.reduce((m,D)=>m+(D.size||0),0),tables:n.tables.sort((m,D)=>m.name.localeCompare(D.name))})),s=t.filter(n=>n.tables.length>1),y=t.filter(n=>n.tables.length===1).flatMap(n=>n.tables).sort((n,m)=>n.name.localeCompare(m.name));return y.length&&s.push({source:{id:"single-table-sources",label:"其他",type:"unknown"},size:y.reduce((n,m)=>n+(m.size||0),0),tables:y}),s.sort((n,m)=>n.source.label.localeCompare(m.source.label))});function ce(e){return e<1024?`${e} B`:e<1048576?`${(e/1024).toFixed(1)} KiB`:`${(e/1048576).toFixed(1)} MiB`}function Ce(e){return e<1048576?`${(e/1024).toFixed(1)} KiB`:`${(e/1048576).toFixed(1)} MiB`}function xe(e){const t=[];return ue.value&&t.push(String(e.tables.length)),ie.value&&t.push(Ce(e.size)),t.join(" · ")}function de(e){return!!e&&typeof e=="object"&&"__dataviewType"in e}function Ve(e){return de(e)?e.value:e}function pe(e,t){return de(e)?e.__dataviewType==="binary"?`<Binary len=${e.value}>`:String(e.value):t.type==="json"||t.type==="list"?e===void 0?"":JSON.stringify(e):e instanceof Date?e.toISOString():e==null?"":String(e)}function K(e){return e.type==="boolean"?{component:"el-switch",props:{}}:e.type==="timestamp"||e.type==="date"?{component:"el-date-picker",props:{type:e.type==="date"?"date":"datetime",valueFormat:"YYYY-MM-DDTHH:mm:ss.SSSZ"}}:e.type==="time"?{component:"el-time-picker",props:{valueFormat:"HH:mm:ss"}}:["integer","unsigned","float","double","decimal"].includes(e.type)?{component:"el-input-number",props:{controls:false}}:{component:"el-input",props:{type:e.type==="json"||e.type==="list"?"textarea":"text"}}}function je(e){Y[e]=!Y[e]}function Se(e){H.replace(`/dataview-next/${e}`)}async function J(){g.value=await M(T.info),await B()}function ze(){return Object.fromEntries(Object.entries(z).filter(([,e])=>e!==""))}async function B(){if(!r.value){S.value=[],te.value=0;return}b.value=true;try{const e=await M(T.query,r.value.name,{keyword:P.value,filters:ze(),offset:(O.value-1)*F.value,limit:F.value,sort:re.value});S.value=e.rows,te.value=e.count??r.value.count??e.rows.length}catch(e){h.error(e.message||"读取数据失败"),S.value=[]}finally{b.value=false}}function me(){k.value||(E&&clearTimeout(E),E=setTimeout(()=>{O.value!==1&&(ae=true,O.value=1),B()},300))}function Oe(e){re.value=e.order?{field:e.prop,order:e.order==="ascending"?"asc":"desc"}:void 0,B()}function Be(e,t){var s;return((s=p[e])==null?void 0:s[t])!==void 0}function De(e,t,s){if(s.type==="binary")return;p[t]||(p[t]={});const y=e[s.name];p[t][s.name]=s.type==="json"||s.type==="list"?pe(y,s):Ve(y)}function Me(e,t){delete p[e][t],Object.keys(p[e]).length||delete p[e]}function oe(){for(const e of Object.keys(p))delete p[Number(e)]}function ve(e){var t;return Object.fromEntries((((t=r.value)==null?void 0:t.primary)||[]).map(s=>[s,e[s]]))}async function Ue(){if(r.value){b.value=true;try{for(const[e,t]of Object.entries(p))await M(T.update,r.value.name,ve(S.value[Number(e)]),t);oe(),await B(),h.success("修改已应用")}catch(e){h.error(e.message||"修改数据失败")}finally{b.value=false}}}async function Te(){if(r.value){b.value=true;try{await M(T.create,r.value.name,x);for(const e of Object.keys(x))x[e]="";N.value=false,await J(),h.success("数据已创建")}catch(e){h.error(e.message||"创建数据失败")}finally{b.value=false}}}async function $e(e){if(r.value){b.value=true;try{await M(T.remove,r.value.name,ve(e)),await J(),h.success("数据已删除")}catch(t){h.error(t.message||"删除数据失败")}finally{b.value=false}}}function Fe(){A.value="",G.value=true}async function Ne(){if(r.value){b.value=true;try{g.value=await M(T.drop,r.value.name),G.value=false,await B(),h.success("数据表已删除")}catch(e){h.error(e.message||"删除数据表失败")}finally{b.value=false}}}return R(I,()=>{oe(),O.value=1,B()}),R([F,O],()=>{if(ae){ae=false;return}B()}),R(P,me),R(z,me,{deep:true}),R(ne,e=>{var t,s;for(const y of e)z[t=y.name]||(z[t]=""),x[s=y.name]||(x[s]="")},{immediate:true}),Pe(()=>{E&&clearTimeout(E)}),J(),(e,t)=>{const s=v("k-icon"),y=v("el-scrollbar"),L=v("k-empty"),n=v("el-button"),m=v("el-input"),D=v("k-button"),fe=v("el-table-column"),Ge=v("el-popconfirm"),Ee=v("el-table"),Re=v("el-pagination"),be=v("el-dialog"),qe=v("k-layout"),He=Ye("loading");return u(),q(qe,{class:"dataview-next"},{header:o(()=>{var a;return[i("span",Je,[l(s,{name:"dataview-database"}),t[9]||(t[9]=w(" 数据库 ",-1))]),(a=g.value)!=null&&a.size?(u(),d("span",Le,"("+f(ce(g.value.size))+")",1)):U("v-if",true)]}),menu:o(()=>[i("span",{class:"menu-item",onClick:J},[l(s,{class:"menu-icon",name:"dataview-refresh"})])]),left:o(()=>[l(y,null,{default:o(()=>[i("div",Ze,[(u(true),d(Q,null,W(he.value,a=>(u(),d("section",{key:a.source.id,class:"source-group"},[i("button",{class:"source-title",onClick:c=>je(a.source.id)},[l(s,{class:ge(["source-chevron",{expanded:!Y[a.source.id]}]),name:"dataview-chevron"},null,8,["class"]),i("span",We,f(a.source.label),1),ue.value||ie.value?(u(),d("span",Xe,f(xe(a)),1)):U("v-if",true)],8,Qe),Y[a.source.id]?U("v-if",true):(u(),d("div",et,[(u(true),d(Q,null,W(a.tables,c=>(u(),d("button",{key:c.name,class:ge(["table-entry",{active:c.name===I.value}]),onClick:V=>Se(c.name)},[i("span",at,f(c.name),1)],10,tt))),128))]))]))),128))])]),_:1})]),default:o(()=>[r.value?Ae((u(),d("main",nt,[i("header",ot,[i("div",null,[i("h2",null,f(r.value.name),1),i("p",null,[w(f(r.value.source.label)+" ",1),r.value.size?(u(),d("span",lt," · "+f(ce(r.value.size)),1)):U("v-if",true),r.value.physical?U("v-if",true):(u(),d("span",st," · 未初始化或无统计信息"))])]),i("div",rt,[l(n,{disabled:k.value,onClick:t[0]||(t[0]=a=>N.value=true)},{default:o(()=>[...t[11]||(t[11]=[w("新增行",-1)])]),_:1},8,["disabled"]),l(n,{type:"danger",disabled:k.value,onClick:Fe},{default:o(()=>[...t[12]||(t[12]=[w("删除表",-1)])]),_:1},8,["disabled"])])]),i("div",ut,[l(m,{modelValue:P.value,"onUpdate:modelValue":t[1]||(t[1]=a=>P.value=a),clearable:"",placeholder:"搜索文本或数字字段",disabled:k.value},{prefix:o(()=>[l(s,{name:"search"})]),_:1},8,["modelValue","disabled"])]),k.value?(u(),d("div",it,[i("span",null,"有 "+f(ke.value)+" 项修改尚未应用",1),i("div",null,[l(n,{type:"primary",onClick:Ue},{default:o(()=>[...t[13]||(t[13]=[w("应用修改",-1)])]),_:1}),l(n,{onClick:oe},{default:o(()=>[...t[14]||(t[14]=[w("取消修改",-1)])]),_:1})])])):U("v-if",true),l(Ee,{class:"data-grid",data:S.value,style:{width:"100%"},height:"100%",flexible:"",border:"",onSortChange:Oe},{default:o(()=>[(u(true),d(Q,null,W(ne.value,a=>(u(),q(fe,{key:a.name,prop:a.name,label:a.name,fixed:r.value.primary.includes(a.name),sortable:k.value?false:"custom","min-width":"100","show-overflow-tooltip":""},{header:o(()=>[i("div",ct,[i("span",null,f(a.name),1),l(m,{modelValue:z[a.name],"onUpdate:modelValue":c=>z[a.name]=c,size:"small",clearable:"",placeholder:a.type,disabled:k.value},null,8,["modelValue","onUpdate:modelValue","placeholder","disabled"])])]),default:o(({row:c,$index:V})=>[Be(V,a.name)?(u(),d("div",dt,[(u(),q(ye(K(a).component),_e({modelValue:p[V][a.name],"onUpdate:modelValue":le=>p[V][a.name]=le},{ref_for:true},K(a).props,{size:"small"}),null,16,["modelValue","onUpdate:modelValue"])),l(D,{frameless:"",type:"danger",onClick:le=>Me(V,a.name)},{default:o(()=>[l(s,{name:"times-full"})]),_:1},8,["onClick"])])):(u(),d("button",{key:1,class:"cell-value",disabled:a.type==="binary",onDblclick:le=>De(c,V,a)},f(pe(c[a.name],a)),41,pt))]),_:2},1032,["prop","label","fixed","sortable"]))),128)),l(fe,{label:"操作",width:"72",fixed:"right",align:"center"},{default:o(({row:a})=>[l(Ge,{title:"真的要删除这条数据吗?","confirm-button-text":"是","cancel-button-text":"否",onConfirm:c=>$e(a)},{reference:o(()=>[l(D,{frameless:"",type:"danger",disabled:k.value},{default:o(()=>[l(s,{name:"times-full"})]),_:1},8,["disabled"])]),_:1},8,["onConfirm"])]),_:1})]),_:1},8,["data"]),l(Re,{layout:"sizes, prev, pager, next, jumper",total:te.value,"page-sizes":we,"page-size":F.value,"onUpdate:pageSize":t[2]||(t[2]=a=>F.value=a),"current-page":O.value,"onUpdate:currentPage":t[3]||(t[3]=a=>O.value=a),disabled:k.value},null,8,["total","page-size","current-page","disabled"])])),[[He,b.value]]):(u(),q(L,{key:0,class:"dataview-empty"},{default:o(()=>[...t[10]||(t[10]=[i("div",null,"在左侧选择要访问的数据表",-1)])]),_:1})),l(be,{modelValue:N.value,"onUpdate:modelValue":t[5]||(t[5]=a=>N.value=a),title:"新增行",width:"640px"},{footer:o(()=>[l(n,{onClick:t[4]||(t[4]=a=>N.value=false)},{default:o(()=>[...t[15]||(t[15]=[w("取消",-1)])]),_:1}),l(n,{type:"primary",onClick:Te},{default:o(()=>[...t[16]||(t[16]=[w("创建",-1)])]),_:1})]),default:o(()=>[i("div",mt,[(u(true),d(Q,null,W(ne.value,a=>(u(),d("label",{key:a.name},[i("span",null,f(a.name),1),(u(),q(ye(K(a).component),_e({modelValue:x[a.name],"onUpdate:modelValue":c=>x[a.name]=c},{ref_for:true},K(a).props),null,16,["modelValue","onUpdate:modelValue"]))]))),128))])]),_:1},8,["modelValue"]),l(be,{modelValue:G.value,"onUpdate:modelValue":t[8]||(t[8]=a=>G.value=a),title:"删除数据表",width:"520px"},{footer:o(()=>{var a;return[l(n,{onClick:t[7]||(t[7]=c=>G.value=false)},{default:o(()=>[...t[17]||(t[17]=[w("取消",-1)])]),_:1}),l(n,{type:"danger",disabled:A.value!==((a=r.value)==null?void 0:a.name),onClick:Ne},{default:o(()=>[...t[18]||(t[18]=[w(" 删除表 ",-1)])]),_:1},8,["disabled"])]}),default:o(()=>{var a,c;return[i("p",vt," 这会物理删除数据表 "+f((a=r.value)==null?void 0:a.name)+"。只要插件仍在运行,模型定义仍会留在列表中。 ",1),l(m,{modelValue:A.value,"onUpdate:modelValue":t[6]||(t[6]=V=>A.value=V),placeholder:(c=r.value)==null?void 0:c.name},null,8,["modelValue","placeholder"])]}),_:1},8,["modelValue"])]),_:1})}}}),bt=($,ee)=>{const H=$.__vccOpts||$;for(const[g,S]of ee)H[g]=S;return H},yt=bt(ft,[["__scopeId","data-v-00a84af0"]]);se.register("dataview-database",X({render:()=>C("svg",{class:"k-icon",xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"1.5","stroke-linecap":"round","stroke-linejoin":"round"},[C("path",{d:"M4 6a8 3 0 1 0 16 0a8 3 0 1 0 -16 0"}),C("path",{d:"M4 6v6a8 3 0 0 0 16 0v-6"}),C("path",{d:"M4 12v6a8 3 0 0 0 16 0v-6"})])}));se.register("dataview-chevron",X({render:()=>C("svg",{class:"k-icon",xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"1.8","stroke-linecap":"round","stroke-linejoin":"round"},[C("path",{d:"M9 6l6 6l-6 6"})])}));se.register("dataview-refresh",X({render:()=>C("svg",{class:"k-icon",xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"1.8","stroke-linecap":"round","stroke-linejoin":"round"},[C("path",{d:"M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"}),C("path",{d:"M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"})])}));const kt=$=>{$.page({path:"/dataview-next/:name*",name:"数据库",icon:"dataview-database",order:410,authority:4,component:yt})};export{kt as default};
|
package/dist/style.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.dataview-next[data-v-00a84af0] .layout-header .left{display:flex;gap:4px;align-items:center;margin-left:24px;padding-left:0}.dataview-next .header-title[data-v-00a84af0]{display:inline-flex;gap:6px;align-items:center}.dataview-next .muted[data-v-00a84af0]{color:var(--fg2);font-weight:400}.source-list[data-v-00a84af0]{padding:12px 0}.source-group+.source-group[data-v-00a84af0]{margin-top:8px}.source-title[data-v-00a84af0],.table-entry[data-v-00a84af0],.cell-value[data-v-00a84af0]{border:0;background:transparent;color:inherit;font:inherit}.source-title[data-v-00a84af0]{display:grid;grid-template-columns:20px minmax(0,1fr) max-content;gap:4px;align-items:center;width:100%;min-width:0;padding:6px 16px;font-weight:600;text-align:left;cursor:pointer}.source-name[data-v-00a84af0]{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.source-chevron[data-v-00a84af0]{transition:transform .15s ease}.source-chevron.expanded[data-v-00a84af0]{transform:rotate(90deg)}.table-list[data-v-00a84af0]{display:grid;gap:2px}.table-entry[data-v-00a84af0]{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px;align-items:center;width:100%;min-width:0;padding:6px 16px 6px 40px;text-align:left;cursor:pointer}.table-entry.active[data-v-00a84af0],.table-entry[data-v-00a84af0]:hover,.source-title[data-v-00a84af0]:hover{background:var(--k-hover-bg)}.table-name[data-v-00a84af0]{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.count[data-v-00a84af0]{color:var(--fg2);font-size:12px;white-space:nowrap}.dataview-empty[data-v-00a84af0]{height:100%}.table-panel[data-v-00a84af0]{display:flex;flex-direction:column;gap:12px;height:100%;min-width:0;min-height:0;padding:24px;box-sizing:border-box}.table-header[data-v-00a84af0],.change-bar[data-v-00a84af0],.query-bar[data-v-00a84af0]{display:flex;gap:12px;align-items:center;justify-content:space-between;min-width:0}.table-header h2[data-v-00a84af0]{margin:0;font-size:22px}.table-header p[data-v-00a84af0]{margin:4px 0 0;color:var(--fg2)}.table-actions[data-v-00a84af0],.change-bar>div[data-v-00a84af0]{display:flex;gap:8px}.query-bar[data-v-00a84af0]{justify-content:flex-start}.query-bar .el-input[data-v-00a84af0]{max-width:360px}.change-bar[data-v-00a84af0]{justify-content:flex-start;padding:8px 12px;border-radius:6px}.data-grid[data-v-00a84af0]{flex:1 1 0;min-height:0;min-width:0}.data-grid[data-v-00a84af0] .el-table__body .el-table__cell{padding:4px 0}.el-pagination[data-v-00a84af0]{flex:0 0 auto;align-self:center;justify-content:center;min-width:0}.el-pagination[data-v-00a84af0] .el-select{width:112px}.column-header[data-v-00a84af0]{display:grid;gap:6px}.data-grid[data-v-00a84af0] th.is-sortable .cell{display:grid;grid-template-columns:minmax(0,max-content) 24px minmax(0,1fr);gap:6px 4px;align-items:start}.data-grid[data-v-00a84af0] th.is-sortable .column-header{display:contents}.data-grid[data-v-00a84af0] th.is-sortable .column-header .el-input{grid-column:1/-1}.data-grid[data-v-00a84af0] th.is-sortable .column-header>span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.data-grid[data-v-00a84af0] th.is-sortable .caret-wrapper{grid-column:2;grid-row:1;align-self:center}.cell-editor[data-v-00a84af0]{display:flex;gap:4px;align-items:center}.cell-editor[data-v-00a84af0]>:first-child{flex:1}.cell-value[data-v-00a84af0]{display:block;width:100%;min-height:20px;padding:0;overflow:hidden;text-align:left;text-overflow:ellipsis;white-space:nowrap;cursor:text}.cell-value[data-v-00a84af0]:disabled{cursor:not-allowed}.form-grid[data-v-00a84af0]{display:grid;gap:12px}.form-grid label[data-v-00a84af0]{display:grid;gap:6px}.danger-text[data-v-00a84af0]{color:var(--danger)}
|
package/lib/index.cjs
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name2 in all)
|
|
8
|
+
__defProp(target, name2, { get: all[name2], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
Config: () => Config,
|
|
24
|
+
apply: () => apply,
|
|
25
|
+
events: () => events,
|
|
26
|
+
inject: () => inject,
|
|
27
|
+
name: () => name
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(index_exports);
|
|
30
|
+
var import_koishi = require("koishi");
|
|
31
|
+
var import_path = require("path");
|
|
32
|
+
|
|
33
|
+
// src/shared.ts
|
|
34
|
+
var events = {
|
|
35
|
+
info: "dataview-next/info",
|
|
36
|
+
query: "dataview-next/query",
|
|
37
|
+
create: "dataview-next/create",
|
|
38
|
+
update: "dataview-next/update",
|
|
39
|
+
remove: "dataview-next/remove",
|
|
40
|
+
drop: "dataview-next/drop"
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// src/index.ts
|
|
44
|
+
var name = "dataview-next";
|
|
45
|
+
var inject = ["console", "database"];
|
|
46
|
+
var Config = import_koishi.Schema.object({
|
|
47
|
+
showGroupCount: import_koishi.Schema.boolean().default(false).description("\u5728\u5DE6\u4FA7\u5206\u7C7B\u6807\u9898\u4E2D\u663E\u793A\u8868\u6570\u91CF"),
|
|
48
|
+
showGroupSize: import_koishi.Schema.boolean().default(true).description("\u5728\u5DE6\u4FA7\u5206\u7C7B\u6807\u9898\u4E2D\u663E\u793A\u5206\u7C7B\u5927\u5C0F")
|
|
49
|
+
});
|
|
50
|
+
var textTypes = /* @__PURE__ */ new Set(["char", "string", "text"]);
|
|
51
|
+
var numberTypes = /* @__PURE__ */ new Set(["integer", "unsigned", "float", "double", "decimal"]);
|
|
52
|
+
var dateTypes = /* @__PURE__ */ new Set(["timestamp", "date", "time"]);
|
|
53
|
+
function getDatabase(ctx) {
|
|
54
|
+
return ctx.database;
|
|
55
|
+
}
|
|
56
|
+
function normalizeId(value) {
|
|
57
|
+
return value.toLowerCase().replace(/[^a-z0-9_.-]+/g, "-");
|
|
58
|
+
}
|
|
59
|
+
function sourceOf(label, type = "plugin") {
|
|
60
|
+
return { id: normalizeId(label), label, type };
|
|
61
|
+
}
|
|
62
|
+
var coreTables = /* @__PURE__ */ new Set(["user", "binding", "channel"]);
|
|
63
|
+
var databaseSources = /* @__PURE__ */ new Set(["database", "sqlite", "mysql", "mongo", "mongodb", "postgres", "database-sqlite", "database-mysql", "database-mongo", "database-postgres"]);
|
|
64
|
+
function normalizePluginName(name2) {
|
|
65
|
+
return name2.replace(/^(koishi-|@koishijs\/)plugin-/, "");
|
|
66
|
+
}
|
|
67
|
+
function isLoaderTarget(value) {
|
|
68
|
+
return typeof value === "object" && value !== null && "keyFor" in value && typeof value.keyFor === "function";
|
|
69
|
+
}
|
|
70
|
+
function getLoader(ctx) {
|
|
71
|
+
const loader = ctx?.root?.get?.("loader") || ctx?.get?.("loader");
|
|
72
|
+
return isLoaderTarget(loader) ? loader : void 0;
|
|
73
|
+
}
|
|
74
|
+
function getRuntimeName(ctx) {
|
|
75
|
+
const runtime = ctx?.scope?.runtime;
|
|
76
|
+
const loader = getLoader(ctx);
|
|
77
|
+
if (runtime?.plugin) {
|
|
78
|
+
const key = loader?.keyFor(runtime.plugin);
|
|
79
|
+
if (key) return key;
|
|
80
|
+
}
|
|
81
|
+
return runtime?.name || ctx?.scope?.name;
|
|
82
|
+
}
|
|
83
|
+
function getSource(table, ctx) {
|
|
84
|
+
if (coreTables.has(table)) {
|
|
85
|
+
return { id: "core", label: "Koishi Core", type: "core" };
|
|
86
|
+
}
|
|
87
|
+
const dot = table.indexOf(".");
|
|
88
|
+
if (dot > 0) {
|
|
89
|
+
return sourceOf(table.slice(0, dot));
|
|
90
|
+
}
|
|
91
|
+
const underscore = table.indexOf("_");
|
|
92
|
+
if (underscore > 0) {
|
|
93
|
+
return sourceOf(table.slice(0, underscore));
|
|
94
|
+
}
|
|
95
|
+
const name2 = getRuntimeName(ctx);
|
|
96
|
+
if (name2) {
|
|
97
|
+
const pluginName = normalizePluginName(name2);
|
|
98
|
+
if (pluginName !== "root" && pluginName !== "koishi" && !databaseSources.has(pluginName)) {
|
|
99
|
+
return sourceOf(pluginName);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return { id: "unknown", label: "\u672A\u77E5\u6765\u6E90", type: "unknown" };
|
|
103
|
+
}
|
|
104
|
+
function hasTypeName(value) {
|
|
105
|
+
return typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
|
|
106
|
+
}
|
|
107
|
+
function getFieldType(field) {
|
|
108
|
+
if (field.deftype) return field.deftype;
|
|
109
|
+
if (typeof field.type === "string") return field.type;
|
|
110
|
+
if (hasTypeName(field.type)) return field.type.type;
|
|
111
|
+
return "json";
|
|
112
|
+
}
|
|
113
|
+
function getFieldInfo(name2, field) {
|
|
114
|
+
return {
|
|
115
|
+
name: name2,
|
|
116
|
+
type: getFieldType(field),
|
|
117
|
+
nullable: field.nullable,
|
|
118
|
+
length: field.length
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function encodeValue(value) {
|
|
122
|
+
if (value instanceof Date) {
|
|
123
|
+
return { __dataviewType: "date", value: value.toISOString() };
|
|
124
|
+
}
|
|
125
|
+
if (typeof value === "bigint") {
|
|
126
|
+
return { __dataviewType: "bigint", value: value.toString() };
|
|
127
|
+
}
|
|
128
|
+
if (import_koishi.Binary.is(value)) {
|
|
129
|
+
return { __dataviewType: "binary", value: value.byteLength };
|
|
130
|
+
}
|
|
131
|
+
if (Array.isArray(value)) return value.map(encodeValue);
|
|
132
|
+
if (value && typeof value === "object") {
|
|
133
|
+
const result = {};
|
|
134
|
+
for (const [key, item] of Object.entries(value)) {
|
|
135
|
+
result[key] = encodeValue(item);
|
|
136
|
+
}
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
return value;
|
|
140
|
+
}
|
|
141
|
+
function isEncodedSpecial(value) {
|
|
142
|
+
return typeof value === "object" && value !== null && "__dataviewType" in value;
|
|
143
|
+
}
|
|
144
|
+
function isBlank(value) {
|
|
145
|
+
return value === void 0 || value === null || value === "";
|
|
146
|
+
}
|
|
147
|
+
function unwrapSpecial(value) {
|
|
148
|
+
return isEncodedSpecial(value) ? value.value : value;
|
|
149
|
+
}
|
|
150
|
+
function coerceValue(field, value) {
|
|
151
|
+
value = unwrapSpecial(value);
|
|
152
|
+
if (value === void 0 || value === null) return value;
|
|
153
|
+
if (numberTypes.has(field.type)) {
|
|
154
|
+
if (value === "") return void 0;
|
|
155
|
+
return Number(value);
|
|
156
|
+
}
|
|
157
|
+
if (field.type === "bigint") {
|
|
158
|
+
if (value === "") return void 0;
|
|
159
|
+
return BigInt(String(value));
|
|
160
|
+
}
|
|
161
|
+
if (field.type === "boolean") {
|
|
162
|
+
if (typeof value === "boolean") return value;
|
|
163
|
+
return value === "true";
|
|
164
|
+
}
|
|
165
|
+
if (field.type === "time") {
|
|
166
|
+
if (value === "") return void 0;
|
|
167
|
+
const [hours = 0, minutes = 0, seconds = 0] = String(value).split(":").map(Number);
|
|
168
|
+
const date = /* @__PURE__ */ new Date();
|
|
169
|
+
date.setHours(hours, minutes, seconds, 0);
|
|
170
|
+
return date;
|
|
171
|
+
}
|
|
172
|
+
if (dateTypes.has(field.type)) {
|
|
173
|
+
if (value === "") return void 0;
|
|
174
|
+
return new Date(String(value));
|
|
175
|
+
}
|
|
176
|
+
if (field.type === "json" || field.type === "list") {
|
|
177
|
+
if (typeof value !== "string") return value;
|
|
178
|
+
if (value === "") return void 0;
|
|
179
|
+
return JSON.parse(value);
|
|
180
|
+
}
|
|
181
|
+
return value;
|
|
182
|
+
}
|
|
183
|
+
function escapeRegExp(source) {
|
|
184
|
+
return source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
185
|
+
}
|
|
186
|
+
function getTable(ctx, name2) {
|
|
187
|
+
const table = ctx.model.tables[name2];
|
|
188
|
+
if (!table) throw new Error(`unknown table: ${name2}`);
|
|
189
|
+
return table;
|
|
190
|
+
}
|
|
191
|
+
async function getInfo(ctx, config) {
|
|
192
|
+
const stats = await ctx.database.stats();
|
|
193
|
+
const statTables = stats.tables || {};
|
|
194
|
+
const tables = {};
|
|
195
|
+
for (const [name2, model] of Object.entries(ctx.model.tables).sort(([a], [b]) => a.localeCompare(b))) {
|
|
196
|
+
const fields = {};
|
|
197
|
+
for (const [fieldName, field] of Object.entries(model.fields)) {
|
|
198
|
+
if (field && import_koishi.Field.available(field)) {
|
|
199
|
+
fields[fieldName] = getFieldInfo(fieldName, field);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const tableStats = statTables[name2];
|
|
203
|
+
tables[name2] = {
|
|
204
|
+
name: name2,
|
|
205
|
+
source: getSource(name2, model.ctx),
|
|
206
|
+
fields,
|
|
207
|
+
primary: (0, import_koishi.makeArray)(model.primary),
|
|
208
|
+
count: tableStats?.count,
|
|
209
|
+
size: tableStats?.size,
|
|
210
|
+
physical: Object.prototype.hasOwnProperty.call(statTables, name2)
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
size: stats.size,
|
|
215
|
+
tables,
|
|
216
|
+
display: {
|
|
217
|
+
showGroupCount: config?.showGroupCount ?? false,
|
|
218
|
+
showGroupSize: config?.showGroupSize ?? true
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function buildQuery(table, request) {
|
|
223
|
+
const clauses = [];
|
|
224
|
+
const filters = request.filters || {};
|
|
225
|
+
for (const [fieldName, value] of Object.entries(filters)) {
|
|
226
|
+
if (isBlank(value)) continue;
|
|
227
|
+
const field = table.fields[fieldName];
|
|
228
|
+
if (!field) continue;
|
|
229
|
+
clauses.push({ [fieldName]: { $regex: { source: escapeRegExp(String(value)), flags: "i" } } });
|
|
230
|
+
}
|
|
231
|
+
const keyword = request.keyword?.trim();
|
|
232
|
+
if (keyword) {
|
|
233
|
+
const source = escapeRegExp(keyword);
|
|
234
|
+
const numericKeyword = Number(keyword);
|
|
235
|
+
const keywordClauses = [];
|
|
236
|
+
for (const field of Object.values(table.fields)) {
|
|
237
|
+
if (textTypes.has(field.type)) {
|
|
238
|
+
keywordClauses.push({ [field.name]: { $regex: { source, flags: "i" } } });
|
|
239
|
+
} else if (numberTypes.has(field.type) && Number.isFinite(numericKeyword)) {
|
|
240
|
+
keywordClauses.push({ [field.name]: numericKeyword });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (keywordClauses.length) {
|
|
244
|
+
clauses.push({ $or: keywordClauses });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (!clauses.length) return {};
|
|
248
|
+
if (clauses.length === 1) return clauses[0];
|
|
249
|
+
return { $and: clauses };
|
|
250
|
+
}
|
|
251
|
+
async function queryTable(ctx, tableName, request) {
|
|
252
|
+
const info = await getInfo(ctx);
|
|
253
|
+
const table = info.tables[tableName];
|
|
254
|
+
if (!table) throw new Error(`unknown table: ${tableName}`);
|
|
255
|
+
const query = buildQuery(table, request);
|
|
256
|
+
const sort = request.sort?.field && table.fields[request.sort.field] ? { [request.sort.field]: request.sort.order } : void 0;
|
|
257
|
+
const cursor = {
|
|
258
|
+
offset: Math.max(0, request.offset || 0),
|
|
259
|
+
limit: Math.min(Math.max(1, request.limit || 30), 1e3),
|
|
260
|
+
sort
|
|
261
|
+
};
|
|
262
|
+
const database = getDatabase(ctx);
|
|
263
|
+
const rows = await database.get(tableName, query, cursor);
|
|
264
|
+
let count;
|
|
265
|
+
try {
|
|
266
|
+
const primary = table.primary.find((field) => table.fields[field]) || Object.keys(table.fields)[0];
|
|
267
|
+
if (primary) {
|
|
268
|
+
count = await database.select(tableName, query).execute((row) => import_koishi.$.count(row[primary]));
|
|
269
|
+
}
|
|
270
|
+
} catch {
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
rows: encodeValue(rows),
|
|
274
|
+
count
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
function coerceCreateRecord(table, data) {
|
|
278
|
+
const result = {};
|
|
279
|
+
for (const [fieldName, value] of Object.entries(data || {})) {
|
|
280
|
+
const field = table.fields[fieldName];
|
|
281
|
+
if (!field) continue;
|
|
282
|
+
if (isBlank(value)) continue;
|
|
283
|
+
result[fieldName] = coerceValue(field, value);
|
|
284
|
+
}
|
|
285
|
+
return result;
|
|
286
|
+
}
|
|
287
|
+
function coerceUpdateRecord(table, data) {
|
|
288
|
+
const result = {};
|
|
289
|
+
for (const [fieldName, value] of Object.entries(data || {})) {
|
|
290
|
+
const field = table.fields[fieldName];
|
|
291
|
+
if (!field) continue;
|
|
292
|
+
result[fieldName] = coerceValue(field, value);
|
|
293
|
+
}
|
|
294
|
+
return result;
|
|
295
|
+
}
|
|
296
|
+
function coercePrimary(table, data) {
|
|
297
|
+
const result = {};
|
|
298
|
+
for (const fieldName of table.primary) {
|
|
299
|
+
const field = table.fields[fieldName];
|
|
300
|
+
if (!field) continue;
|
|
301
|
+
result[fieldName] = coerceValue(field, data?.[fieldName]);
|
|
302
|
+
}
|
|
303
|
+
return result;
|
|
304
|
+
}
|
|
305
|
+
async function updateRow(ctx, tableName, primary, data) {
|
|
306
|
+
const info = await getInfo(ctx);
|
|
307
|
+
const table = info.tables[tableName];
|
|
308
|
+
if (!table) throw new Error(`unknown table: ${tableName}`);
|
|
309
|
+
await getDatabase(ctx).set(tableName, coercePrimary(table, primary), coerceUpdateRecord(table, data));
|
|
310
|
+
}
|
|
311
|
+
async function createRow(ctx, tableName, data) {
|
|
312
|
+
const info = await getInfo(ctx);
|
|
313
|
+
const table = info.tables[tableName];
|
|
314
|
+
if (!table) throw new Error(`unknown table: ${tableName}`);
|
|
315
|
+
await getDatabase(ctx).create(tableName, coerceCreateRecord(table, data));
|
|
316
|
+
}
|
|
317
|
+
async function removeRow(ctx, tableName, primary) {
|
|
318
|
+
const info = await getInfo(ctx);
|
|
319
|
+
const table = info.tables[tableName];
|
|
320
|
+
if (!table) throw new Error(`unknown table: ${tableName}`);
|
|
321
|
+
await getDatabase(ctx).remove(tableName, coercePrimary(table, primary));
|
|
322
|
+
}
|
|
323
|
+
function addListener(ctx, name2, callback) {
|
|
324
|
+
const console = ctx.console;
|
|
325
|
+
console.addListener(name2, async (...args) => encodeValue(await callback(...args)), { authority: 4 });
|
|
326
|
+
}
|
|
327
|
+
function apply(ctx, config) {
|
|
328
|
+
ctx.inject(["console", "database"], (ctx2) => {
|
|
329
|
+
ctx2.console.addEntry({
|
|
330
|
+
dev: (0, import_path.resolve)(__dirname, "../client/index.ts"),
|
|
331
|
+
prod: (0, import_path.resolve)(__dirname, "../dist")
|
|
332
|
+
});
|
|
333
|
+
addListener(ctx2, events.info, () => getInfo(ctx2, config));
|
|
334
|
+
addListener(ctx2, events.query, (table, request) => queryTable(ctx2, table, request || {}));
|
|
335
|
+
addListener(ctx2, events.create, (table, data) => createRow(ctx2, table, data));
|
|
336
|
+
addListener(ctx2, events.update, (table, primary, data) => updateRow(ctx2, table, primary, data));
|
|
337
|
+
addListener(ctx2, events.remove, (table, primary) => removeRow(ctx2, table, primary));
|
|
338
|
+
addListener(ctx2, events.drop, async (table) => {
|
|
339
|
+
getTable(ctx2, table);
|
|
340
|
+
await getDatabase(ctx2).drop(table);
|
|
341
|
+
return getInfo(ctx2);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
346
|
+
0 && (module.exports = {
|
|
347
|
+
Config,
|
|
348
|
+
apply,
|
|
349
|
+
events,
|
|
350
|
+
inject,
|
|
351
|
+
name
|
|
352
|
+
});
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Context, Schema } from 'koishi';
|
|
2
|
+
export * from './shared';
|
|
3
|
+
export declare const name = "dataview-next";
|
|
4
|
+
export declare const inject: string[];
|
|
5
|
+
export interface Config {
|
|
6
|
+
showGroupCount: boolean;
|
|
7
|
+
showGroupSize: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare const Config: Schema<Config>;
|
|
10
|
+
export declare function apply(ctx: Context, config: Config): void;
|
package/lib/shared.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export declare const events: {
|
|
2
|
+
readonly info: "dataview-next/info";
|
|
3
|
+
readonly query: "dataview-next/query";
|
|
4
|
+
readonly create: "dataview-next/create";
|
|
5
|
+
readonly update: "dataview-next/update";
|
|
6
|
+
readonly remove: "dataview-next/remove";
|
|
7
|
+
readonly drop: "dataview-next/drop";
|
|
8
|
+
};
|
|
9
|
+
export type FieldType = 'primary' | 'integer' | 'unsigned' | 'float' | 'double' | 'decimal' | 'char' | 'string' | 'text' | 'boolean' | 'timestamp' | 'date' | 'time' | 'binary' | 'bigint' | 'list' | 'json' | 'expr' | string;
|
|
10
|
+
export interface EncodedSpecial {
|
|
11
|
+
__dataviewType: 'date' | 'bigint' | 'binary';
|
|
12
|
+
value: string | number;
|
|
13
|
+
}
|
|
14
|
+
export interface FieldInfo {
|
|
15
|
+
name: string;
|
|
16
|
+
type: FieldType;
|
|
17
|
+
nullable?: boolean;
|
|
18
|
+
length?: number;
|
|
19
|
+
}
|
|
20
|
+
export interface TableSource {
|
|
21
|
+
id: string;
|
|
22
|
+
label: string;
|
|
23
|
+
type: 'core' | 'plugin' | 'unknown';
|
|
24
|
+
}
|
|
25
|
+
export interface TableInfo {
|
|
26
|
+
name: string;
|
|
27
|
+
source: TableSource;
|
|
28
|
+
fields: Record<string, FieldInfo>;
|
|
29
|
+
primary: string[];
|
|
30
|
+
count?: number;
|
|
31
|
+
size?: number;
|
|
32
|
+
physical: boolean;
|
|
33
|
+
}
|
|
34
|
+
export interface DatabaseInfo {
|
|
35
|
+
size?: number;
|
|
36
|
+
tables: Record<string, TableInfo>;
|
|
37
|
+
display: {
|
|
38
|
+
showGroupCount: boolean;
|
|
39
|
+
showGroupSize: boolean;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export interface SortState {
|
|
43
|
+
field: string;
|
|
44
|
+
order: 'asc' | 'desc';
|
|
45
|
+
}
|
|
46
|
+
export interface QueryRequest {
|
|
47
|
+
keyword?: string;
|
|
48
|
+
filters?: Record<string, unknown>;
|
|
49
|
+
offset?: number;
|
|
50
|
+
limit?: number;
|
|
51
|
+
sort?: SortState;
|
|
52
|
+
}
|
|
53
|
+
export interface QueryResult {
|
|
54
|
+
rows: Record<string, unknown>[];
|
|
55
|
+
count?: number;
|
|
56
|
+
}
|
|
57
|
+
declare module '@koishijs/plugin-console' {
|
|
58
|
+
interface Events {
|
|
59
|
+
'dataview-next/info'(): Promise<DatabaseInfo>;
|
|
60
|
+
'dataview-next/query'(table: string, request: QueryRequest): Promise<QueryResult>;
|
|
61
|
+
'dataview-next/create'(table: string, data: Record<string, unknown>): Promise<void>;
|
|
62
|
+
'dataview-next/update'(table: string, primary: Record<string, unknown>, data: Record<string, unknown>): Promise<void>;
|
|
63
|
+
'dataview-next/remove'(table: string, primary: Record<string, unknown>): Promise<void>;
|
|
64
|
+
'dataview-next/drop'(table: string): Promise<DatabaseInfo>;
|
|
65
|
+
}
|
|
66
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "koishi-plugin-dataview-next",
|
|
3
|
+
"description": "在网页控制台中分类管理数据库表,支持删除表",
|
|
4
|
+
"version": "0.0.2",
|
|
5
|
+
"main": "lib/index.cjs",
|
|
6
|
+
"types": "lib/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./lib/index.d.ts",
|
|
10
|
+
"import": "./lib/index.cjs",
|
|
11
|
+
"require": "./lib/index.cjs"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"lib",
|
|
17
|
+
"dist",
|
|
18
|
+
"src"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "rm -rf lib dist && tsc -b --force && yarn build:server && yarn build:client",
|
|
22
|
+
"build:client": "koishi-console build",
|
|
23
|
+
"build:server": "esbuild src/index.ts --bundle --platform=node --format=cjs --packages=external --outfile=lib/index.cjs",
|
|
24
|
+
"typecheck": "tsc -b"
|
|
25
|
+
},
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"homepage": "https://github.com/Sor85/koishi-plugin-dataview-next",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/Sor85/koishi-plugin-dataview-next.git"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"bot",
|
|
34
|
+
"chatbot",
|
|
35
|
+
"koishi",
|
|
36
|
+
"plugin",
|
|
37
|
+
"database"
|
|
38
|
+
],
|
|
39
|
+
"packageManager": "yarn@4.15.0",
|
|
40
|
+
"koishi": {
|
|
41
|
+
"browser": true,
|
|
42
|
+
"public": [
|
|
43
|
+
"dist"
|
|
44
|
+
],
|
|
45
|
+
"description": {
|
|
46
|
+
"en": "Manage categorized database tables in the console, with table deletion support",
|
|
47
|
+
"zh": "在网页控制台中分类管理数据库表,支持删除表"
|
|
48
|
+
},
|
|
49
|
+
"service": {
|
|
50
|
+
"required": [
|
|
51
|
+
"console",
|
|
52
|
+
"database"
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"@koishijs/plugin-console": "^5.30.11",
|
|
58
|
+
"koishi": "^4.18.11"
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@koishijs/client": "^5.30.11",
|
|
62
|
+
"@koishijs/console": "^5.30.11",
|
|
63
|
+
"@koishijs/plugin-console": "^5.30.11",
|
|
64
|
+
"@koishijs/plugin-database-sqlite": "^4.7.0",
|
|
65
|
+
"@types/node": "^20.14.6",
|
|
66
|
+
"esbuild": "^0.28.1",
|
|
67
|
+
"koishi": "^4.18.11",
|
|
68
|
+
"typescript": "^5.9.3"
|
|
69
|
+
},
|
|
70
|
+
"dependencies": {
|
|
71
|
+
"@koishijs/console": "^5.30.11"
|
|
72
|
+
}
|
|
73
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import type {} from '@koishijs/plugin-console'
|
|
2
|
+
import { $, Binary, Context, Dict, Field, makeArray, Schema } from 'koishi'
|
|
3
|
+
import { resolve } from 'path'
|
|
4
|
+
import {
|
|
5
|
+
DatabaseInfo,
|
|
6
|
+
EncodedSpecial,
|
|
7
|
+
FieldInfo,
|
|
8
|
+
QueryRequest,
|
|
9
|
+
QueryResult,
|
|
10
|
+
TableInfo,
|
|
11
|
+
TableSource,
|
|
12
|
+
events,
|
|
13
|
+
} from './shared'
|
|
14
|
+
|
|
15
|
+
export * from './shared'
|
|
16
|
+
|
|
17
|
+
export const name = 'dataview-next'
|
|
18
|
+
export const inject = ['console', 'database']
|
|
19
|
+
|
|
20
|
+
export interface Config {
|
|
21
|
+
showGroupCount: boolean
|
|
22
|
+
showGroupSize: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const Config: Schema<Config> = Schema.object({
|
|
26
|
+
showGroupCount: Schema.boolean().default(false).description('在左侧分类标题中显示表数量'),
|
|
27
|
+
showGroupSize: Schema.boolean().default(true).description('在左侧分类标题中显示分类大小'),
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const textTypes = new Set(['char', 'string', 'text'])
|
|
31
|
+
const numberTypes = new Set(['integer', 'unsigned', 'float', 'double', 'decimal'])
|
|
32
|
+
const dateTypes = new Set(['timestamp', 'date', 'time'])
|
|
33
|
+
|
|
34
|
+
type SourceContext = {
|
|
35
|
+
root?: {
|
|
36
|
+
get?(name: string): unknown
|
|
37
|
+
}
|
|
38
|
+
get?(name: string): unknown
|
|
39
|
+
scope?: {
|
|
40
|
+
name?: string
|
|
41
|
+
runtime?: {
|
|
42
|
+
name?: string
|
|
43
|
+
plugin?: unknown
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface LoaderTarget {
|
|
49
|
+
keyFor(plugin: unknown): string | undefined
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface DatabaseTarget {
|
|
53
|
+
get(table: string, query: Dict, cursor: Dict): Promise<Dict[]>
|
|
54
|
+
select(table: string, query: Dict): {
|
|
55
|
+
execute<T>(callback: (row: Dict) => unknown): Promise<T>
|
|
56
|
+
}
|
|
57
|
+
set(table: string, query: Dict, data: Dict): Promise<unknown>
|
|
58
|
+
create(table: string, data: Dict): Promise<unknown>
|
|
59
|
+
remove(table: string, query: Dict): Promise<unknown>
|
|
60
|
+
drop(table: string): Promise<void>
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getDatabase(ctx: Context): DatabaseTarget {
|
|
64
|
+
return ctx.database as unknown as DatabaseTarget
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function normalizeId(value: string) {
|
|
68
|
+
return value.toLowerCase().replace(/[^a-z0-9_.-]+/g, '-')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function sourceOf(label: string, type: TableSource['type'] = 'plugin'): TableSource {
|
|
72
|
+
return { id: normalizeId(label), label, type }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const coreTables = new Set(['user', 'binding', 'channel'])
|
|
76
|
+
const databaseSources = new Set(['database', 'sqlite', 'mysql', 'mongo', 'mongodb', 'postgres', 'database-sqlite', 'database-mysql', 'database-mongo', 'database-postgres'])
|
|
77
|
+
|
|
78
|
+
function normalizePluginName(name: string) {
|
|
79
|
+
return name.replace(/^(koishi-|@koishijs\/)plugin-/, '')
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function isLoaderTarget(value: unknown): value is LoaderTarget {
|
|
83
|
+
return typeof value === 'object'
|
|
84
|
+
&& value !== null
|
|
85
|
+
&& 'keyFor' in value
|
|
86
|
+
&& typeof value.keyFor === 'function'
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getLoader(ctx?: SourceContext) {
|
|
90
|
+
const loader = ctx?.root?.get?.('loader') || ctx?.get?.('loader')
|
|
91
|
+
return isLoaderTarget(loader) ? loader : undefined
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function getRuntimeName(ctx?: SourceContext): string | undefined {
|
|
95
|
+
const runtime = ctx?.scope?.runtime
|
|
96
|
+
const loader = getLoader(ctx)
|
|
97
|
+
if (runtime?.plugin) {
|
|
98
|
+
const key = loader?.keyFor(runtime.plugin)
|
|
99
|
+
if (key) return key
|
|
100
|
+
}
|
|
101
|
+
return runtime?.name || ctx?.scope?.name
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getSource(table: string, ctx?: SourceContext): TableSource {
|
|
105
|
+
if (coreTables.has(table)) {
|
|
106
|
+
return { id: 'core', label: 'Koishi Core', type: 'core' }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// model.ctx can point at the database driver; namespaced tables are a better owner hint.
|
|
110
|
+
const dot = table.indexOf('.')
|
|
111
|
+
if (dot > 0) {
|
|
112
|
+
return sourceOf(table.slice(0, dot))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const underscore = table.indexOf('_')
|
|
116
|
+
if (underscore > 0) {
|
|
117
|
+
return sourceOf(table.slice(0, underscore))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const name = getRuntimeName(ctx)
|
|
121
|
+
if (name) {
|
|
122
|
+
const pluginName = normalizePluginName(name)
|
|
123
|
+
if (pluginName !== 'root' && pluginName !== 'koishi' && !databaseSources.has(pluginName)) {
|
|
124
|
+
return sourceOf(pluginName)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { id: 'unknown', label: '未知来源', type: 'unknown' }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function hasTypeName(value: unknown): value is { type: string } {
|
|
132
|
+
return typeof value === 'object'
|
|
133
|
+
&& value !== null
|
|
134
|
+
&& 'type' in value
|
|
135
|
+
&& typeof value.type === 'string'
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function getFieldType(field: Field): string {
|
|
139
|
+
if (field.deftype) return field.deftype
|
|
140
|
+
if (typeof field.type === 'string') return field.type
|
|
141
|
+
if (hasTypeName(field.type)) return field.type.type
|
|
142
|
+
return 'json'
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function getFieldInfo(name: string, field: Field): FieldInfo {
|
|
146
|
+
return {
|
|
147
|
+
name,
|
|
148
|
+
type: getFieldType(field),
|
|
149
|
+
nullable: field.nullable,
|
|
150
|
+
length: field.length,
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function encodeValue(value: unknown): unknown {
|
|
155
|
+
if (value instanceof Date) {
|
|
156
|
+
return { __dataviewType: 'date', value: value.toISOString() } satisfies EncodedSpecial
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (typeof value === 'bigint') {
|
|
160
|
+
return { __dataviewType: 'bigint', value: value.toString() } satisfies EncodedSpecial
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (Binary.is(value)) {
|
|
164
|
+
return { __dataviewType: 'binary', value: value.byteLength } satisfies EncodedSpecial
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (Array.isArray(value)) return value.map(encodeValue)
|
|
168
|
+
|
|
169
|
+
if (value && typeof value === 'object') {
|
|
170
|
+
const result: Dict<unknown> = {}
|
|
171
|
+
for (const [key, item] of Object.entries(value)) {
|
|
172
|
+
result[key] = encodeValue(item)
|
|
173
|
+
}
|
|
174
|
+
return result
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return value
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function isEncodedSpecial(value: unknown): value is EncodedSpecial {
|
|
181
|
+
return typeof value === 'object'
|
|
182
|
+
&& value !== null
|
|
183
|
+
&& '__dataviewType' in value
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function isBlank(value: unknown) {
|
|
187
|
+
return value === undefined || value === null || value === ''
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function unwrapSpecial(value: unknown) {
|
|
191
|
+
return isEncodedSpecial(value) ? value.value : value
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function coerceValue(field: FieldInfo, value: unknown) {
|
|
195
|
+
value = unwrapSpecial(value)
|
|
196
|
+
if (value === undefined || value === null) return value
|
|
197
|
+
|
|
198
|
+
if (numberTypes.has(field.type)) {
|
|
199
|
+
if (value === '') return undefined
|
|
200
|
+
return Number(value)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (field.type === 'bigint') {
|
|
204
|
+
if (value === '') return undefined
|
|
205
|
+
return BigInt(String(value))
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (field.type === 'boolean') {
|
|
209
|
+
if (typeof value === 'boolean') return value
|
|
210
|
+
return value === 'true'
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (field.type === 'time') {
|
|
214
|
+
if (value === '') return undefined
|
|
215
|
+
const [hours = 0, minutes = 0, seconds = 0] = String(value).split(':').map(Number)
|
|
216
|
+
const date = new Date()
|
|
217
|
+
date.setHours(hours, minutes, seconds, 0)
|
|
218
|
+
return date
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (dateTypes.has(field.type)) {
|
|
222
|
+
if (value === '') return undefined
|
|
223
|
+
return new Date(String(value))
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (field.type === 'json' || field.type === 'list') {
|
|
227
|
+
if (typeof value !== 'string') return value
|
|
228
|
+
if (value === '') return undefined
|
|
229
|
+
return JSON.parse(value)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return value
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function escapeRegExp(source: string) {
|
|
236
|
+
return source.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function getTable(ctx: Context, name: string) {
|
|
240
|
+
const table = ctx.model.tables[name]
|
|
241
|
+
if (!table) throw new Error(`unknown table: ${name}`)
|
|
242
|
+
return table
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function getInfo(ctx: Context, config?: Config): Promise<DatabaseInfo> {
|
|
246
|
+
const stats = await ctx.database.stats()
|
|
247
|
+
const statTables = stats.tables || {}
|
|
248
|
+
const tables: Record<string, TableInfo> = {}
|
|
249
|
+
|
|
250
|
+
for (const [name, model] of Object.entries(ctx.model.tables).sort(([a], [b]) => a.localeCompare(b))) {
|
|
251
|
+
const fields: Record<string, FieldInfo> = {}
|
|
252
|
+
for (const [fieldName, field] of Object.entries(model.fields)) {
|
|
253
|
+
if (field && Field.available(field)) {
|
|
254
|
+
fields[fieldName] = getFieldInfo(fieldName, field)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const tableStats = statTables[name]
|
|
259
|
+
tables[name] = {
|
|
260
|
+
name,
|
|
261
|
+
source: getSource(name, model.ctx),
|
|
262
|
+
fields,
|
|
263
|
+
primary: makeArray(model.primary),
|
|
264
|
+
count: tableStats?.count,
|
|
265
|
+
size: tableStats?.size,
|
|
266
|
+
physical: Object.prototype.hasOwnProperty.call(statTables, name),
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
size: stats.size,
|
|
272
|
+
tables,
|
|
273
|
+
display: {
|
|
274
|
+
showGroupCount: config?.showGroupCount ?? false,
|
|
275
|
+
showGroupSize: config?.showGroupSize ?? true,
|
|
276
|
+
},
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function buildQuery(table: TableInfo, request: QueryRequest) {
|
|
281
|
+
const clauses: Dict[] = []
|
|
282
|
+
const filters = request.filters || {}
|
|
283
|
+
|
|
284
|
+
for (const [fieldName, value] of Object.entries(filters)) {
|
|
285
|
+
if (isBlank(value)) continue
|
|
286
|
+
const field = table.fields[fieldName]
|
|
287
|
+
if (!field) continue
|
|
288
|
+
clauses.push({ [fieldName]: { $regex: { source: escapeRegExp(String(value)), flags: 'i' } } })
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const keyword = request.keyword?.trim()
|
|
292
|
+
if (keyword) {
|
|
293
|
+
const source = escapeRegExp(keyword)
|
|
294
|
+
const numericKeyword = Number(keyword)
|
|
295
|
+
const keywordClauses: Dict[] = []
|
|
296
|
+
|
|
297
|
+
for (const field of Object.values(table.fields)) {
|
|
298
|
+
if (textTypes.has(field.type)) {
|
|
299
|
+
keywordClauses.push({ [field.name]: { $regex: { source, flags: 'i' } } })
|
|
300
|
+
} else if (numberTypes.has(field.type) && Number.isFinite(numericKeyword)) {
|
|
301
|
+
// 数字列不能稳定跨数据库做 regex,顶部搜索只对数字关键词做等值匹配。
|
|
302
|
+
keywordClauses.push({ [field.name]: numericKeyword })
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (keywordClauses.length) {
|
|
307
|
+
clauses.push({ $or: keywordClauses })
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (!clauses.length) return {}
|
|
312
|
+
if (clauses.length === 1) return clauses[0]
|
|
313
|
+
return { $and: clauses }
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function queryTable(ctx: Context, tableName: string, request: QueryRequest): Promise<QueryResult> {
|
|
317
|
+
const info = await getInfo(ctx)
|
|
318
|
+
const table = info.tables[tableName]
|
|
319
|
+
if (!table) throw new Error(`unknown table: ${tableName}`)
|
|
320
|
+
|
|
321
|
+
const query = buildQuery(table, request)
|
|
322
|
+
const sort = request.sort?.field && table.fields[request.sort.field]
|
|
323
|
+
? { [request.sort.field]: request.sort.order }
|
|
324
|
+
: undefined
|
|
325
|
+
const cursor = {
|
|
326
|
+
offset: Math.max(0, request.offset || 0),
|
|
327
|
+
limit: Math.min(Math.max(1, request.limit || 30), 1000),
|
|
328
|
+
sort,
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const database = getDatabase(ctx)
|
|
332
|
+
const rows = await database.get(tableName, query, cursor)
|
|
333
|
+
let count: number | undefined
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
const primary = table.primary.find(field => table.fields[field]) || Object.keys(table.fields)[0]
|
|
337
|
+
if (primary) {
|
|
338
|
+
count = await database.select(tableName, query).execute<number>((row: Dict) => $.count(row[primary]))
|
|
339
|
+
}
|
|
340
|
+
} catch {}
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
rows: encodeValue(rows) as Record<string, unknown>[],
|
|
344
|
+
count,
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function coerceCreateRecord(table: TableInfo, data: Dict) {
|
|
349
|
+
const result: Dict = {}
|
|
350
|
+
for (const [fieldName, value] of Object.entries(data || {})) {
|
|
351
|
+
const field = table.fields[fieldName]
|
|
352
|
+
if (!field) continue
|
|
353
|
+
if (isBlank(value)) continue
|
|
354
|
+
result[fieldName] = coerceValue(field, value)
|
|
355
|
+
}
|
|
356
|
+
return result
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function coerceUpdateRecord(table: TableInfo, data: Dict) {
|
|
360
|
+
const result: Dict = {}
|
|
361
|
+
for (const [fieldName, value] of Object.entries(data || {})) {
|
|
362
|
+
const field = table.fields[fieldName]
|
|
363
|
+
if (!field) continue
|
|
364
|
+
result[fieldName] = coerceValue(field, value)
|
|
365
|
+
}
|
|
366
|
+
return result
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function coercePrimary(table: TableInfo, data: Dict) {
|
|
370
|
+
const result: Dict = {}
|
|
371
|
+
for (const fieldName of table.primary) {
|
|
372
|
+
const field = table.fields[fieldName]
|
|
373
|
+
if (!field) continue
|
|
374
|
+
result[fieldName] = coerceValue(field, data?.[fieldName])
|
|
375
|
+
}
|
|
376
|
+
return result
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function updateRow(ctx: Context, tableName: string, primary: Dict, data: Dict) {
|
|
380
|
+
const info = await getInfo(ctx)
|
|
381
|
+
const table = info.tables[tableName]
|
|
382
|
+
if (!table) throw new Error(`unknown table: ${tableName}`)
|
|
383
|
+
await getDatabase(ctx).set(tableName, coercePrimary(table, primary), coerceUpdateRecord(table, data))
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async function createRow(ctx: Context, tableName: string, data: Dict) {
|
|
387
|
+
const info = await getInfo(ctx)
|
|
388
|
+
const table = info.tables[tableName]
|
|
389
|
+
if (!table) throw new Error(`unknown table: ${tableName}`)
|
|
390
|
+
await getDatabase(ctx).create(tableName, coerceCreateRecord(table, data))
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
async function removeRow(ctx: Context, tableName: string, primary: Dict) {
|
|
394
|
+
const info = await getInfo(ctx)
|
|
395
|
+
const table = info.tables[tableName]
|
|
396
|
+
if (!table) throw new Error(`unknown table: ${tableName}`)
|
|
397
|
+
await getDatabase(ctx).remove(tableName, coercePrimary(table, primary))
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
type EventName = typeof events[keyof typeof events]
|
|
401
|
+
|
|
402
|
+
interface ConsoleEventTarget {
|
|
403
|
+
addListener(name: string, callback: (...args: unknown[]) => Promise<unknown>, options: { authority: number }): void
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function addListener<T extends unknown[]>(ctx: Context, name: EventName, callback: (...args: T) => Promise<unknown>) {
|
|
407
|
+
const console = ctx.console as unknown as ConsoleEventTarget
|
|
408
|
+
console.addListener(name, async (...args) => encodeValue(await callback(...args as T)), { authority: 4 })
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export function apply(ctx: Context, config: Config) {
|
|
412
|
+
ctx.inject(['console', 'database'], (ctx) => {
|
|
413
|
+
ctx.console.addEntry({
|
|
414
|
+
dev: resolve(__dirname, '../client/index.ts'),
|
|
415
|
+
prod: resolve(__dirname, '../dist'),
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
addListener(ctx, events.info, () => getInfo(ctx, config))
|
|
419
|
+
addListener(ctx, events.query, (table: string, request: QueryRequest) => queryTable(ctx, table, request || {}))
|
|
420
|
+
addListener(ctx, events.create, (table: string, data: Dict) => createRow(ctx, table, data))
|
|
421
|
+
addListener(ctx, events.update, (table: string, primary: Dict, data: Dict) => updateRow(ctx, table, primary, data))
|
|
422
|
+
addListener(ctx, events.remove, (table: string, primary: Dict) => removeRow(ctx, table, primary))
|
|
423
|
+
addListener(ctx, events.drop, async (table: string) => {
|
|
424
|
+
getTable(ctx, table)
|
|
425
|
+
await getDatabase(ctx).drop(table)
|
|
426
|
+
return getInfo(ctx)
|
|
427
|
+
})
|
|
428
|
+
})
|
|
429
|
+
}
|
package/src/shared.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export const events = {
|
|
2
|
+
info: 'dataview-next/info',
|
|
3
|
+
query: 'dataview-next/query',
|
|
4
|
+
create: 'dataview-next/create',
|
|
5
|
+
update: 'dataview-next/update',
|
|
6
|
+
remove: 'dataview-next/remove',
|
|
7
|
+
drop: 'dataview-next/drop',
|
|
8
|
+
} as const
|
|
9
|
+
|
|
10
|
+
export type FieldType =
|
|
11
|
+
| 'primary'
|
|
12
|
+
| 'integer'
|
|
13
|
+
| 'unsigned'
|
|
14
|
+
| 'float'
|
|
15
|
+
| 'double'
|
|
16
|
+
| 'decimal'
|
|
17
|
+
| 'char'
|
|
18
|
+
| 'string'
|
|
19
|
+
| 'text'
|
|
20
|
+
| 'boolean'
|
|
21
|
+
| 'timestamp'
|
|
22
|
+
| 'date'
|
|
23
|
+
| 'time'
|
|
24
|
+
| 'binary'
|
|
25
|
+
| 'bigint'
|
|
26
|
+
| 'list'
|
|
27
|
+
| 'json'
|
|
28
|
+
| 'expr'
|
|
29
|
+
| string
|
|
30
|
+
|
|
31
|
+
export interface EncodedSpecial {
|
|
32
|
+
__dataviewType: 'date' | 'bigint' | 'binary'
|
|
33
|
+
value: string | number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface FieldInfo {
|
|
37
|
+
name: string
|
|
38
|
+
type: FieldType
|
|
39
|
+
nullable?: boolean
|
|
40
|
+
length?: number
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface TableSource {
|
|
44
|
+
id: string
|
|
45
|
+
label: string
|
|
46
|
+
type: 'core' | 'plugin' | 'unknown'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface TableInfo {
|
|
50
|
+
name: string
|
|
51
|
+
source: TableSource
|
|
52
|
+
fields: Record<string, FieldInfo>
|
|
53
|
+
primary: string[]
|
|
54
|
+
count?: number
|
|
55
|
+
size?: number
|
|
56
|
+
physical: boolean
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface DatabaseInfo {
|
|
60
|
+
size?: number
|
|
61
|
+
tables: Record<string, TableInfo>
|
|
62
|
+
display: {
|
|
63
|
+
showGroupCount: boolean
|
|
64
|
+
showGroupSize: boolean
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface SortState {
|
|
69
|
+
field: string
|
|
70
|
+
order: 'asc' | 'desc'
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface QueryRequest {
|
|
74
|
+
keyword?: string
|
|
75
|
+
filters?: Record<string, unknown>
|
|
76
|
+
offset?: number
|
|
77
|
+
limit?: number
|
|
78
|
+
sort?: SortState
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface QueryResult {
|
|
82
|
+
rows: Record<string, unknown>[]
|
|
83
|
+
count?: number
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
declare module '@koishijs/plugin-console' {
|
|
87
|
+
interface Events {
|
|
88
|
+
'dataview-next/info'(): Promise<DatabaseInfo>
|
|
89
|
+
'dataview-next/query'(table: string, request: QueryRequest): Promise<QueryResult>
|
|
90
|
+
'dataview-next/create'(table: string, data: Record<string, unknown>): Promise<void>
|
|
91
|
+
'dataview-next/update'(table: string, primary: Record<string, unknown>, data: Record<string, unknown>): Promise<void>
|
|
92
|
+
'dataview-next/remove'(table: string, primary: Record<string, unknown>): Promise<void>
|
|
93
|
+
'dataview-next/drop'(table: string): Promise<DatabaseInfo>
|
|
94
|
+
}
|
|
95
|
+
}
|