@opengis/cms 0.0.10 → 0.0.11
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/assets/form-template-GUuvWUFU.js +1 -0
- package/dist/assets/icon-arrow-left-D8pa2IlJ.js +1 -0
- package/dist/assets/icon-check-CFkLh1zV.js +1 -0
- package/dist/assets/icon-chewron-right-GWQXirqc.js +1 -0
- package/dist/assets/icon-point-CS93fUI3.js +1 -0
- package/dist/assets/index-CAtBxNzW.js +10 -0
- package/dist/assets/index-qmYrZ9-m.js +2067 -0
- package/dist/assets/index-x9a62HSi.css +1 -0
- package/dist/assets/index-xsH4HHeE.js +6 -0
- package/dist/assets/vs-builder-UaCKb4IF.js +1 -0
- package/dist/assets/vs-builder-content-BCwIOG9K.js +1 -0
- package/dist/assets/vs-layout-De-zAhR4.js +1 -0
- package/dist/assets/vs-manager-collection-B_UcyvVB.js +1 -0
- package/dist/assets/vs-manager-collection-content-Dj40xY0r.js +1 -0
- package/dist/assets/vs-manager-collection-item-DkdSSkyR.js +1 -0
- package/dist/assets/vs-manager-collection-list-BIDZJyl4.js +1 -0
- package/dist/assets/vs-manager-single-content-DbVORsMN.js +1 -0
- package/dist/assets/vs-manager-single-vDpAZKAT.js +1 -0
- package/dist/assets/vs-media-DKX7riRI.js +1 -0
- package/dist/assets/vs-menu-tree-C4HFWvZd.js +1 -0
- package/dist/assets/vs-menu-tree-CF06sHhM.css +1 -0
- package/dist/assets/vs-not-data-B6bl0fjq.js +1 -0
- package/dist/assets/vs-permissions-CLUiqUH_.js +1 -0
- package/dist/index.html +15 -0
- package/package.json +5 -5
- package/server/app.js +5 -2
- package/server/migrations/site.sql +428 -0
- package/server/routes/site/controllers/deleteMedia.js +47 -0
- package/server/routes/site/controllers/downloadMedia.js +48 -0
- package/server/routes/site/controllers/getPermissions.js +16 -0
- package/server/routes/site/controllers/listMedia.js +60 -0
- package/server/routes/site/controllers/metadataMedia.js +37 -0
- package/server/routes/site/controllers/setPermissions.js +50 -0
- package/server/routes/site/controllers/uploadMedia.js +66 -0
- package/server/routes/site/index.mjs +34 -0
- package/dist/cms.js +0 -5900
- package/dist/cms.umd.cjs +0 -19
- package/server/migrations/media.sql +0 -30
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{_ as d,c as o,a as r,o as n,F as D,r as P,e as a,g as f,s as m,n as w,t as b,b as H,d as u,x as F,y as N,f as I,w as S,A as T,z as L}from"./index-qmYrZ9-m.js";import{I as z}from"./icon-chewron-right-GWQXirqc.js";import{a as x}from"./index-xsH4HHeE.js";import{V as O}from"./vs-not-data-B6bl0fjq.js";const R={},U={xmlns:"http://www.w3.org/2000/svg",width:"20",height:"20",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"};function q(s,e){return n(),o("svg",U,e[0]||(e[0]=[r("path",{d:"m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"},null,-1),r("polyline",{points:"9 22 9 12 15 12 15 22"},null,-1)]))}const Q=d(R,[["render",q]]),Z={components:{IconHome:Q,IconChewronRight:z},props:{initialPath:{type:String,default:""},root:{type:String,default:"Root"}},data(){return{breadcrumbs:[]}},watch:{initialPath:{immediate:!0,handler(s){const e=s.split("/").filter(Boolean);this.breadcrumbs=[this.root,...e]}}},methods:{navigateTo(s){const e=s===0?"":this.breadcrumbs.slice(1,s+1).join("/");this.$emit("update-path",e)}}},E={"aria-label":"Breadcrumb",class:"mb-4"},G={class:"flex items-start justify-between"},J={class:"flex flex-wrap items-center gap-[5px] min-h-8"},K={class:"flex items-center"},W=["onClick"],X={key:1};function Y(s,e,c,h,i,l){const g=a("IconHome"),y=a("IconChewronRight");return n(),o("nav",E,[r("div",G,[r("ol",J,[(n(!0),o(D,null,P(i.breadcrumbs,(_,p)=>(n(),o("li",{key:p,class:"whitespace-nowrap"},[r("div",K,[r("button",{onClick:v=>l.navigateTo(p),class:w([["hover:text-blue-500 focus:outline-none flex items-center",p===i.breadcrumbs.length-1?"font-medium underline text-blue-500":"text-gray-500"],"h-8 flex items-center"])},[p===0?(n(),f(g,{key:0,class:"w-4 h-4"})):(n(),o("span",X,b(_),1))],10,W),p<i.breadcrumbs.length-1?(n(),f(y,{key:0,class:"text-gray-400 w-3 h-3 ml-[10px]"})):m("",!0)])]))),128))])])])}const ee=d(Z,[["render",Y]]),te={},se={width:"14",height:"14",viewBox:"0 0 14 14",fill:"none",xmlns:"http://www.w3.org/2000/svg"};function re(s,e){return n(),o("svg",se,e[0]||(e[0]=[r("path",{d:"M6.41667 11.0833C8.994 11.0833 11.0833 8.994 11.0833 6.41667C11.0833 3.83934 8.994 1.75 6.41667 1.75C3.83934 1.75 1.75 3.83934 1.75 6.41667C1.75 8.994 3.83934 11.0833 6.41667 11.0833Z",stroke:"#6B7280","stroke-linecap":"round","stroke-linejoin":"round"},null,-1),r("path",{d:"M12.25 12.25L9.7417 9.7417",stroke:"#6B7280","stroke-linecap":"round","stroke-linejoin":"round"},null,-1)]))}const ne=d(te,[["render",re]]),oe={},ie={"aria-hidden":"true",focusable:"false",role:"img",viewBox:"0 0 16 16",width:"16",height:"16",fill:"currentColor"};function le(s,e){return n(),o("svg",ie,e[0]||(e[0]=[r("path",{d:"M.513 1.513A1.75 1.75 0 0 1 1.75 1h3.5c.55 0 1.07.26 1.4.7l.9 1.2a.25.25 0 0 0 .2.1H 13a1 1 0 0 1 1 1v.5H2.75a.75.75 0 0 0 0 1.5h11.978a1 1 0 0 1 .994 1.117L15 13.25A1.75 1.75 0 0 1 13.25 15H1.75A1.75 1.75 0 0 1 0 13.25V2.75c0-.464.184-.91.513-1.237Z"},null,-1),r("path",{d:"M1.75 1A1.75 1.75 0 0 0 0 2.75v10.5C0 14.216.784 15 1.75 15h12.5A1.75 1.75 0 0 0 16 13.25v-8.5A1.75 1.75 0 0 0 14.25 3H7.5a.25.25 0 0 1-.2-.1l-.9-1.2C6.07 1.26 5.55 1 5 1H1.75Z"},null,-1)]))}const ae=d(oe,[["render",le]]),ce={},de={width:"22",height:"22",viewBox:"0 0 24 24",fill:"none",xmlns:"http://www.w3.org/2000/svg"};function ue(s,e){return n(),o("svg",de,e[0]||(e[0]=[r("path",{d:"M3 8.2C3 7.07989 3 6.51984 3.21799 6.09202C3.40973 5.71569 3.71569 5.40973 4.09202 5.21799C4.51984 5 5.0799 5 6.2 5H9.67452C10.1637 5 10.4083 5 10.6385 5.05526C10.8425 5.10425 11.0376 5.18506 11.2166 5.29472C11.4184 5.4184 11.5914 5.59135 11.9373 5.93726L12.0627 6.06274C12.4086 6.40865 12.5816 6.5816 12.7834 6.70528C12.9624 6.81494 13.1575 6.89575 13.3615 6.94474C13.5917 7 13.8363 7 14.3255 7H17.8C18.9201 7 19.4802 7 19.908 7.21799C20.2843 7.40973 20.5903 7.71569 20.782 8.09202C21 8.51984 21 9.0799 21 10.2V15.8C21 16.9201 21 17.4802 20.782 17.908C20.5903 18.2843 20.2843 18.5903 19.908 18.782C19.4802 19 18.9201 19 17.8 19H6.2C5.07989 19 4.51984 19 4.09202 18.782C3.71569 18.5903 3.40973 18.2843 3.21799 17.908C3 17.4802 3 16.9201 3 15.8V8.2Z",stroke:"#000000","stroke-width":"1","stroke-linecap":"round","stroke-linejoin":"round"},null,-1)]))}const he=d(ce,[["render",ue]]),pe={},me={width:"24",height:"24",viewBox:"0 0 24 24",fill:"none",xmlns:"http://www.w3.org/2000/svg"};function fe(s,e){return n(),o("svg",me,e[0]||(e[0]=[r("path",{d:"M18 6L6 18",stroke:"#6B7280","stroke-linecap":"round","stroke-linejoin":"round"},null,-1),r("path",{d:"M6 6L18 18",stroke:"#6B7280","stroke-linecap":"round","stroke-linejoin":"round"},null,-1)]))}const ge=d(pe,[["render",fe]]),ye={},_e={class:"flex-shrink-0 size-3.5",xmlns:"http://www.w3.org/2000/svg",width:"22",height:"22",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round"};function xe(s,e){return n(),o("svg",_e,e[0]||(e[0]=[H('<line x1="21" x2="14" y1="4" y2="4"></line><line x1="10" x2="3" y1="4" y2="4"></line><line x1="21" x2="12" y1="12" y2="12"></line><line x1="8" x2="3" y1="12" y2="12"></line><line x1="21" x2="16" y1="20" y2="20"></line><line x1="12" x2="3" y1="20" y2="20"></line><line x1="14" x2="14" y1="2" y2="6"></line><line x1="8" x2="8" y1="10" y2="14"></line><line x1="16" x2="16" y1="18" y2="22"></line>',9)]))}const we=d(ye,[["render",xe]]),be={},ve={"aria-hidden":"true",focusable:"false",role:"img",viewBox:"0 0 16 16",width:"16",height:"16",fill:"currentColor"};function Ce(s,e){return n(),o("svg",ve,e[0]||(e[0]=[r("path",{d:"M.513 1.513A1.75 1.75 0 0 1 1.75 1h3.5c.55 0 1.07.26 1.4.7l.9 1.2a.25.25 0 0 0 .2.1H 13a1 1 0 0 1 1 1v.5H2.75a.75.75 0 0 0 0 1.5h11.978a1 1 0 0 1 .994 1.117L15 13.25A1.75 1.75 0 0 1 13.25 15H1.75A1.75 1.75 0 0 1 0 13.25V2.75c0-.464.184-.91.513-1.237Z"},null,-1),r("path",{d:"M1.75 1A1.75 1.75 0 0 0 0 2.75v10.5C0 14.216.784 15 1.75 15h12.5A1.75 1.75 0 0 0 16 13.25v-8.5A1.75 1.75 0 0 0 14.25 3H7.5a.25.25 0 0 1-.2-.1l-.9-1.2C6.07 1.26 5.55 1 5 1H1.75Z"},null,-1)]))}const ke=d(be,[["render",Ce]]),Ie={components:{Breadcrumb:ee,IconSearch:ne,IconFolder:ae,IconFolder2:he,IconClose:ge,IconImage:ke,IconActions:we,VsNotData:O},props:{type:{type:String,default:"icon"}},data(){return{icons:[],currentPage:parseInt(this.$route.query.page,10)||1,iconsPerPage:15,token:"",searchQuery:"",selectedFile:null,isAddDirModalOpen:!1,newDirName:""}},watch:{isAddDirModalOpen(s){s||(this.newDirName="")}},computed:{currentDir(){return this.$route.query.dir||""},currentTab(){return this.$route.query.tab||""},filteredIcons(){return this.searchQuery?this.icons.filter(s=>s.name.toLowerCase().includes(this.searchQuery.toLowerCase())):this.icons},sortedIcons(){return[...this.filteredIcons].sort((s,e)=>s.type==="dir"&&e.type!=="dir"?-1:s.type!=="dir"&&e.type==="dir"?1:s.name.localeCompare(e.name))},currentIcons(){const s=(this.currentPage-1)*this.iconsPerPage,e=s+this.iconsPerPage;return this.sortedIcons.slice(s,e)}},methods:{async fetchIcons(s){try{const{data:e}=await x.get("/api/cms-media",{params:{dir:s}});this.icons=(e==null?void 0:e.data)||[],this.token=e==null?void 0:e.token}catch(e){console.error("Error fetching icons:",e)}},async addDir(){var s,e;if(!this.newDirName.trim()){this.$notify({type:"error",title:"Помилка",message:"Назва папки не може бути порожньою."});return}try{const c=await x.post(`/api/cms-media/${this.token}`,{name:this.newDirName});this.$notify({type:"success",title:"Успіх!",message:`Папку "${this.newDirName}" успішно створено.`}),this.fetchIcons(this.currentDir),this.handeAddFolderClose(),this.newDirName=""}catch(c){console.error("Error creating directory:",c),this.$notify({type:"error",title:"Помилка",message:((e=(s=c.response)==null?void 0:s.data)==null?void 0:e.message)||"Не вдалося створити папку."})}},handeAddFolderClose(){this.isAddDirModalOpen=!1},navigateToDir(s){const e=`${this.currentDir}/${s}`.replace(/\/+/g,"/");this.updateURL(e)},updatePath(s){this.updateURL(s)},updateURL(s){this.$router.push({query:{...this.$route.query,dir:s,page:this.currentPage}})},async handleFileChange(s){const e=s.target.files[0];if(!e)return;const c=new FormData;c.append("file",e);try{const h=await x.post(`/api/cms-media/${this.token}`,c,{headers:{"Content-Type":"multipart/form-data"}});this.$notify({type:"success",title:"Успіх!",message:"Файл успішно додано!"}),this.currentPage=1}catch(h){console.error("Error uploading file:",h),this.$notify({type:"error",title:"Помилка",message:h.response.statustext||h.message||h})}finally{await this.fetchIcons(this.currentDir)}},clearSearch(){this.searchQuery=""},copyToClipboard(s){try{const e=document.createElement("textarea");e.value=s,document.body.appendChild(e),e.select(),e.setSelectionRange(0,s.length);const c=document.execCommand("copy");document.body.removeChild(e),c?this.$notify({type:"success",title:"Скопійовано!",message:"Назву іконки успішно скопійовано."}):this.$notify({type:"error",title:"Помилка",message:"Не вдалося скопіювати шлях."})}catch(e){this.$notify({type:"error",title:"Помилка",message:"Не вдалося скопіювати координати"}),console.error("Copy failed",e)}}},watch:{currentDir:{immediate:!0,handler(s){this.fetchIcons(s)}},currentTab:{handler(){this.currentPage=1}},currentPage(s){this.$emit("update-url",{query:{...this.$route.query,page:s}})}},async mounted(){await this.fetchIcons(this.currentDir)}},De={class:"w-full p-[20px] bg-gray-100 flex justify-center h-[calc(100vh-60px)]"},Pe={class:"p-[20px] w-full max-w-[1640px]"},$e={class:"flex items-start justify-between"},Ae={class:"flex gap-2 relative"},Ve={class:"flex items-center justify-between bg-white py-2 px-3 rounded-lg border border-gray-300 focus:outline-none focus:border-blue-500 text-sm w-[250px]"},Be={class:"flex items-center gap-[12px]"},je={for:"file-upload",class:"py-2 px-3 inline-flex justify-center items-center gap-x-2 text-sm font-medium rounded-lg border border-transparent bg-blue-600 text-white hover:bg-blue-700 focus:outline-none focus:bg-blue-700 cursor-pointer"},Me={key:0,class:"flex flex-col items-center justify-center h-64 bg-gray-50 rounded-lg border text-center"},He={key:1},Fe={class:"grid grid-cols-2 lg:grid-cols-5 gap-3 xl:gap-5"},Ne=["onClick"],Se={class:"relative group"},Te={class:"w-full h-36 sm:h-[170px] object-cover rounded-t-xl flex items-center justify-center"},Le={key:0,class:"text-[#54aeff]"},ze=["src","alt"],Oe={class:"p-3 flex items-center gap-x-3 border-t"},Re={key:0,class:"flex shrink-0 justify-center items-center w-10 h-10 bg-white border border-solid text-gray-500 rounded-lg"},Ue=["title"],qe=["onClick"],Qe={class:"block truncate text-xs text-gray-500"},Ze={class:"p-4"},Ee={class:"flex justify-end"};function Ge(s,e,c,h,i,l){var C;const g=a("Breadcrumb"),y=a("IconSearch"),_=a("IconClose"),p=a("IconFolder2"),v=a("VsNotData"),$=a("IconFolder"),A=a("IconImage"),V=a("VsPagination"),B=a("VsText"),j=a("VsDialog");return n(),o("div",De,[r("div",Pe,[r("div",null,[r("div",$e,[u(g,{initialPath:l.currentDir,onUpdatePath:l.updatePath},null,8,["initialPath","onUpdatePath"]),r("div",Ae,[r("div",Ve,[r("div",Be,[u(y),F(r("input",{"onUpdate:modelValue":e[0]||(e[0]=t=>i.searchQuery=t),type:"text",placeholder:"Пошук за назвою...",class:"focus:outline-none"},null,512),[[N,i.searchQuery]])]),i.searchQuery?(n(),f(_,{key:0,class:"w-4 h-4 cursor-pointer",onClick:l.clearSearch},null,8,["onClick"])):m("",!0)]),r("button",{ref:"dropdownButton",onClick:e[1]||(e[1]=t=>i.isAddDirModalOpen=!0),class:"py-2 px-3 inline-flex items-center gap-x-2 text-sm font-medium border border-gray-300 rounded-lg text-gray-800 hover:bg-gray-100 bg-white"},[u(p),e[7]||(e[7]=I(" Додати папку "))],512),r("label",je,[e[8]||(e[8]=I(" Завантажити ")),r("input",{id:"file-upload",type:"file",class:"hidden",onChange:e[2]||(e[2]=(...t)=>l.handleFileChange&&l.handleFileChange(...t))},null,32)])])]),(C=l.filteredIcons)!=null&&C.length?(n(),o("div",He,[r("div",Fe,[(n(!0),o(D,null,P(l.currentIcons,t=>{var k;return n(),o("div",{key:t==null?void 0:t.id,class:w(["flex flex-col bg-white border rounded-xl",{"cursor-pointer hover:border-blue-500":t.type==="dir","cursor-default":t.type!=="dir"}]),onClick:M=>t.type==="dir"?l.navigateToDir(t.name):null},[r("div",Se,[r("div",Te,[(t==null?void 0:t.type)==="dir"?(n(),o("div",Le,[u($)])):(n(),o("img",{key:1,class:"object-contain max-h-[40px]",src:`/api/cms-media/${t==null?void 0:t.token}`,alt:t==null?void 0:t.name,style:T((k=t==null?void 0:t.path)!=null&&k.includes(".svg")?"width:250px;max-height:100px":"")},null,12,ze))])]),r("div",Oe,[t.type!=="dir"?(n(),o("span",Re,[u(A)])):m("",!0),r("div",{class:"grow truncate",title:t==null?void 0:t.name},[r("p",{class:w(["block truncate text-sm font-semibold text-gray-800",{"cursor-pointer":t.type!=="dir"}]),onClick:L(M=>t.type!=="dir"&&l.copyToClipboard(t==null?void 0:t.name),["stop"])},b(t==null?void 0:t.name),11,qe),r("p",Qe,b(t==null?void 0:t.size),1)],8,Ue)])],10,Ne)}),128))]),l.filteredIcons.length>i.iconsPerPage?(n(),f(V,{key:0,defaultPage:i.currentPage,class:"mt-6 focus-visible:outline-blue-500 flex !justify-center",total:l.filteredIcons.length,pageSize:i.iconsPerPage,maxPages:3,goTo:!1,onPageChange:e[3]||(e[3]=t=>i.currentPage=t)},null,8,["defaultPage","total","pageSize"])):m("",!0)])):(n(),o("div",Me,[u(v,{text:"",class:"![&>div]:min-w-[100px] !w-auto"})]))]),u(j,{visible:i.isAddDirModalOpen,"onUpdate:visible":e[6]||(e[6]=t=>i.isAddDirModalOpen=t),title:"Додати папку",size:"small",closeClickBack:!0,onOnClose:l.handeAddFolderClose},{default:S(()=>[r("div",Ze,[u(B,{modelValue:i.newDirName,"onUpdate:modelValue":e[4]||(e[4]=t=>i.newDirName=t),placeholder:"Назва нової папки.."},null,8,["modelValue"]),r("div",Ee,[r("button",{onClick:e[5]||(e[5]=(...t)=>l.addDir&&l.addDir(...t)),class:"mt-4 py-2 px-4 text-white bg-blue-600 hover:bg-blue-700 rounded-lg"}," Створити ")])])]),_:1},8,["visible","onOnClose"])])])}const Ye=d(Ie,[["render",Ge]]);export{Ye as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{_ as B,j as h,m as P,e as C,c as m,o as d,a as i,s as $,f as J,g as O,t as U,n as E,z as p,F as q,r as A,d as N,w as M}from"./index-qmYrZ9-m.js";import{I as z}from"./icon-chewron-right-GWQXirqc.js";const G={class:"ml-5"},H={key:0,class:"ml-1"},K={__name:"vs-menu-tree-node",props:{item:{type:Object,required:!0}},emits:["item-dropped"],setup(b,{emit:u}){const a=b,s=u,o=h(!1),v=h(!1),g=h(!1),c=P(()=>a.item.children&&a.item.children.length>0),D=()=>{c.value&&(o.value=!o.value)},x=e=>{g.value=!0,e.dataTransfer.setData("application/json",JSON.stringify({id:a.item.id,title:a.item.title,url:a.item.url,children:a.item.children||[]})),e.dataTransfer.effectAllowed="move"},_=e=>{e.preventDefault(),e.dataTransfer.dropEffect="move"},y=e=>{v.value=!1,g.value=!1;try{const t=JSON.parse(e.dataTransfer.getData("application/json"));t.id!==a.item.id&&s("item-dropped",{draggedItemId:t.id,targetItemId:a.item.id,draggedItemData:t})}catch(t){console.error("Error parsing drag data:",t)}},w=e=>{v.value=!0},I=e=>{v.value=!1},T=()=>{v.value=!1,g.value=!1},r=e=>{s("item-dropped",e)};return(e,t)=>{const n=C("vs-menu-tree-node",!0);return d(),m("div",G,[i("div",{class:E(["draggable-item w-full",{"drag-over":v.value,dragging:g.value}]),draggable:"true",onDragstart:x,onDragover:p(_,["prevent"]),onDrop:p(y,["prevent"]),onDragenter:p(w,["prevent"]),onDragleave:p(I,["prevent"]),onDragend:T},[i("button",{onClick:D,class:"text-inherit flex items-center gap-1 w-full hover:text-blue-600 p-1 px-3 rounded-lg hover:bg-gray-100"},[J(U(b.item.title)+" ",1),c.value?(d(),O(z,{key:0,class:E(["h-[8px] w-[8px] text-gray-500",{"rotate-90":o.value}])},null,8,["class"])):$("",!0)])],34),c.value&&o.value?(d(),m("div",H,[(d(!0),m(q,null,A(b.item.children,l=>(d(),O(n,{key:l.id,item:l,onItemDropped:r},null,8,["item"]))),128))])):$("",!0)])}}},Q=B(K,[["__scopeId","data-v-eefd185d"]]),W={class:"flex justify-end p-[20px] gap-[10px] border-t w-full"},X={__name:"vs-menu-tree-component",setup(b){const u=h(!1),a=h(!1),s=h(),o=h([{id:1,title:"Главная",url:"/",children:[]},{id:2,title:"Блог",url:"/blog",children:[{id:3,title:"Последние посты",url:"/blog/latest",children:[{id:4,title:"Новости",url:"/blog/latest/news",children:[]},{id:5,title:"Статьи",url:"/blog/latest/articles",children:[{id:6,title:"Технологии",url:"/blog/latest/articles/tech",children:[]},{id:7,title:"Бизнес",url:"/blog/latest/articles/business",children:[]}]}]},{id:8,title:"Архив",url:"/blog/archive",children:[]}]},{id:9,title:"О нас",url:"/about",children:[{id:10,title:"Команда",url:"/about/team",children:[]},{id:11,title:"Контакты",url:"/about/contacts",children:[{id:12,title:"Офисы",url:"/about/contacts/offices",children:[]},{id:13,title:"Поддержка",url:"/about/contacts/support",children:[]}]}]},{id:14,title:"Услуги",url:"/services",children:[{id:15,title:"Веб-разработка",url:"/services/web",children:[]},{id:16,title:"Мобильные приложения",url:"/services/mobile",children:[]}]}]),v=P(()=>({id:{type:"Text",validators:["required"],label:"ID"},title:{type:"Text",validators:["required"],label:"Назва"},url:{type:"Text",validators:["required"],label:"URL"},parentId:{type:"Select",label:"Родитель",options:g(o.value)}})),g=r=>{const e=[];return r.forEach(t=>{var n;e.push({text:t.title,id:t.id}),t!=null&&t.children&&((n=t==null?void 0:t.children)==null?void 0:n.length)>0&&e.push(...g(t.children))}),e},c=(r,e,t=null)=>{for(let n=0;n<r.length;n++){if(r[n].id===e)return{item:r[n],parent:t,index:n};if(r[n].children&&r[n].children.length>0){const l=c(r[n].children,e,r[n]);if(l)return l}}return null},D=({draggedItemId:r,targetItemId:e,draggedItemData:t})=>{if(r===e)return;const n=c(o.value,r),l=c(o.value,e);if(!n||!l)return;const{parent:f,index:S}=n,{item:V}=l;if(f&&f.id===e)return;const j=(k,R)=>k.id===R?!0:k.children?k.children.some(L=>j(L,R)):!1;if(j(t,e))return;const F={id:t.id,title:t.title,url:t.url,children:t.children||[]};f?f.children.splice(S,1):o.value.splice(S,1),V.children||(V.children=[]),V.children.push(F)},x=(r,e)=>{for(const t of r){if(t.id===e)return t;if(t.children&&t.children.length>0){const n=x(t.children,e);if(n)return n}}return null},_=async()=>{const r={id:String(Math.random()),title:s.value.title,url:s.value.url,children:[]};if(s.value.parentId){const e=x(o.value,s.value.parentId);e&&e.children.push(r)}else o.value.push(r);u.value=!1,s.value={}},y=r=>{r.preventDefault(),r.dataTransfer.dropEffect="move"},w=r=>{a.value=!1;try{const e=JSON.parse(r.dataTransfer.getData("application/json")),t=c(o.value,e.id);if(!t)return;const{parent:n,index:l}=t,f={id:e.id,title:e.title,url:e.url,children:e.children||[]};n?n.children.splice(l,1):o.value.splice(l,1),o.value.push(f)}catch(e){console.error("Error parsing drag data:",e)}},I=r=>{a.value=!0},T=r=>{a.value=!1};return(r,e)=>{const t=C("VsForm"),n=C("VsDialog");return d(),m("div",null,[i("div",{class:E(["p-2 rounded-lg border border-dashed border-gray-200 mb-1",{"root-drag-over":a.value}]),onDragover:p(y,["prevent"]),onDrop:p(w,["prevent"]),onDragenter:p(I,["prevent"]),onDragleave:p(T,["prevent"])},e[4]||(e[4]=[i("div",{class:"text-center text-gray-500 min-w-[300px]"}," Перемістити в корінь ",-1)]),34),(d(!0),m(q,null,A(o.value,(l,f)=>(d(),O(Q,{key:l.id,item:l,onItemDropped:D},null,8,["item"]))),128)),i("button",{class:"px-3 text-blue-500 hover:text-blue-700 duration-300 hover:underline hover:underline-2",onClick:e[0]||(e[0]=l=>u.value=!0)}," Добавить "),N(n,{visible:u.value,"onUpdate:visible":e[3]||(e[3]=l=>u.value=l),title:"Додати елемент меню"},{footer:M(()=>[i("div",W,[i("button",{class:"py-2 px-3 inline-flex items-center gap-x-2 text-sm whitespace-nowrap text-black border rounded-lg border-gray-200 hover:bg-gray-100 duration-300",onClick:e[2]||(e[2]=l=>u.value=!1)}," Скасувати"),i("button",{class:"py-2 px-3 inline-flex items-center gap-x-2 text-sm whitespace-nowrap text-white bg-blue-500 rounded-lg !border-gray-200 hover:bg-blue-700 duration-300",onClick:_}," Зберегти ")])]),default:M(()=>[N(t,{scheme:v.value,modelValue:s.value,"onUpdate:modelValue":e[1]||(e[1]=l=>s.value=l)},null,8,["scheme","modelValue"])]),_:1},8,["visible"])])}}},Y=B(X,[["__scopeId","data-v-84b80de2"]]),Z={class:"w-full flex justify-center bg-gray-100 h-[calc(100vh-60px)]"},ee={class:"p-[20px] w-full max-w-[1440px]"},te={class:"w-full bg-white rounded-lg p-[20px] border mt-[20px]"},le={__name:"vs-menu-tree",setup(b){return(u,a)=>(d(),m("section",Z,[i("div",ee,[a[0]||(a[0]=i("h1",null,"Дерево меню",-1)),i("div",te,[N(Y)])])]))}};export{le as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.draggable-item[data-v-eefd185d]{cursor:move;-webkit-user-select:none;user-select:none;padding:4px;transition:all .2s ease;border-radius:6px;border:2px solid transparent;position:relative;z-index:1}.draggable-item[data-v-eefd185d]:hover{background-color:#f3f4f6}.draggable-item.drag-over[data-v-eefd185d]{background-color:#e0f2fe;border-color:#3b82f6;box-shadow:0 0 0 2px #3b82f633;transform:scale(1.02)}.draggable-item.dragging[data-v-eefd185d]{opacity:.5;background-color:#dbeafe;border-color:#3b82f6}.draggable-item button[data-v-eefd185d]{position:relative;z-index:2}.root-drag-over[data-v-84b80de2]{border-color:#3b82f6;background-color:#dbeafe;box-shadow:0 0 0 4px #3b82f633;transform:scale(1.01)}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{_ as i,c as a,a as r,b as s,t as l,o}from"./index-qmYrZ9-m.js";const c={props:{title:{type:String,default:()=>"Дані для відображення відсутні"},text:{type:String,default:()=>"Змініть параметри пошуку або спробуйте пізніше"}}},f={class:"w-full"},n={class:"p-5 min-h-[500px] flex flex-col justify-center items-center text-center"},d={class:"max-w-sm mx-auto mt-6"},h={class:"font-medium text-gray-800 dark:text-neutral-200"},u={class:"mt-2 text-sm text-gray-500 dark:text-neutral-500"};function x(g,t,e,y,p,k){return o(),a("div",f,[r("div",n,[t[0]||(t[0]=s('<svg class="w-48 mx-auto" viewBox="0 0 178 90" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="27" y="50.5" width="124" height="39" rx="7.5" fill="currentColor" class="fill-white dark:fill-neutral-800"></rect><rect x="27" y="50.5" width="124" height="39" rx="7.5" stroke="currentColor" class="stroke-gray-50 dark:stroke-neutral-700/10"></rect><rect x="34.5" y="58" width="24" height="24" rx="4" fill="currentColor" class="fill-gray-50 dark:fill-neutral-700/30"></rect><rect x="66.5" y="61" width="60" height="6" rx="3" fill="currentColor" class="fill-gray-50 dark:fill-neutral-700/30"></rect><rect x="66.5" y="73" width="77" height="6" rx="3" fill="currentColor" class="fill-gray-50 dark:fill-neutral-700/30"></rect><rect x="19.5" y="28.5" width="139" height="39" rx="7.5" fill="currentColor" class="fill-white dark:fill-neutral-800"></rect><rect x="19.5" y="28.5" width="139" height="39" rx="7.5" stroke="currentColor" class="stroke-gray-100 dark:stroke-neutral-700/30"></rect><rect x="27" y="36" width="24" height="24" rx="4" fill="currentColor" class="fill-gray-100 dark:fill-neutral-700/70"></rect><rect x="59" y="39" width="60" height="6" rx="3" fill="currentColor" class="fill-gray-100 dark:fill-neutral-700/70"></rect><rect x="59" y="51" width="92" height="6" rx="3" fill="currentColor" class="fill-gray-100 dark:fill-neutral-700/70"></rect><g filter="url(#filter1)"><rect x="12" y="6" width="154" height="40" rx="8" fill="currentColor" class="fill-white dark:fill-neutral-800" shape-rendering="crispEdges"></rect><rect x="12.5" y="6.5" width="153" height="39" rx="7.5" stroke="currentColor" class="stroke-gray-100 dark:stroke-neutral-700/60" shape-rendering="crispEdges"></rect><rect x="20" y="14" width="24" height="24" rx="4" fill="currentColor" class="fill-gray-200 dark:fill-neutral-700"></rect><rect x="52" y="17" width="60" height="6" rx="3" fill="currentColor" class="fill-gray-200 dark:fill-neutral-700"></rect><rect x="52" y="29" width="106" height="6" rx="3" fill="currentColor" class="fill-gray-200 dark:fill-neutral-700"></rect></g><defs><filter id="filter1" x="0" y="0" width="178" height="64" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"><feFlood flood-opacity="0" result="BackgroundImageFix"></feFlood><feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"></feColorMatrix><feOffset dy="6"></feOffset><feGaussianBlur stdDeviation="6"></feGaussianBlur><feComposite in2="hardAlpha" operator="out"></feComposite><feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.03 0"></feColorMatrix><feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1187_14810"></feBlend><feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1187_14810" result="shape"></feBlend></filter></defs></svg>',1)),r("div",d,[r("p",h,l(e.title),1),r("p",u,l(e.text),1)])])])}const m=i(c,[["render",x]]);export{m as V};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{c as x,o as i,F as y,r as m,a as t,n as B,g as I,s as $,j as P,u as E,m as V,x as U,y as A,t as j,d as L,i as M}from"./index-qmYrZ9-m.js";import{a as C}from"./index-xsH4HHeE.js";import{I as N}from"./icon-check-CFkLh1zV.js";const D={class:"flex gap-2 w-full items-center justify-between"},F=["checked","onChange"],O={__name:"vs-permissions-checkbox",props:{modelValue:{type:Array,default:()=>[]}},emits:["update:modelValue"],setup(v,{emit:_}){const r=["view","add","edit","del"],k=v,b=_,d=(c,g)=>{let u=[...k.modelValue];g?u.includes(c)||u.push(c):u=u.filter(w=>w!==c),b("update:modelValue",u)};return(c,g)=>(i(),x("div",D,[(i(),x(y,null,m(r,u=>{var w,n,e;return t("div",{class:"w-[120px] flex items-center justify-center",key:u},[t("label",{class:B(["flex h-[20px] w-[20px] items-center justify-center border text-white rounded cursor-pointer",{"bg-blue-500 border-blue-500":(w=v.modelValue)==null?void 0:w.includes(u)}])},[t("input",{type:"checkbox",class:"hidden",checked:(n=v.modelValue)==null?void 0:n.includes(u),onChange:s=>d(u,s.target.checked)},null,40,F),(e=v.modelValue)!=null&&e.includes(u)?(i(),I(N,{key:0})):$("",!0)],2)])}),64))]))}},S={class:"flex items-center gap-2 mb-2"},q={class:"w-full flex flex-col gap-2"},z={class:"flex gap-2 items-center w-full p-3 md:gap-x-6"},G={class:"flex justify-between w-full"},H={class:"text-xs font-medium text-start w-[80px]"},Q={class:"flex gap-2 items-center w-full p-3 bg-gray-100 rounded-lg md:gap-x-6 md:p-5"},J={class:"font-semibold text-gray-800 dark:text-neutral-200 w-full max-w-[300px]"},K={class:"flex justify-between w-full"},R=["onClick"],T={class:"flex flex-col gap-2 p-3"},W={class:"w-full max-w-[300px]"},X={class:"text-sm font-medium text-gray-800 dark:text-neutral-200"},Y={__name:"vs-premissions-component",props:{permissions:{},permissionsModifiers:{}},emits:["update:permissions"],setup(v){const _=P(""),r=E(v,"permissions"),k=n=>V(()=>{var s;const e=(s=r.value)==null?void 0:s.find(o=>(o==null?void 0:o.user_id)===n);return(e==null?void 0:e.actions)||[]}),b=(n,e)=>{r.value||(r.value=[]);const s=r.value.findIndex(o=>(o==null?void 0:o.user_id)===n);s===-1?r.value.push({user_id:n,actions:e}):r.value[s].actions=e},d=(n,e)=>{const s=c.value[n];if(!s)return;const o=[...r.value||[]],l=s.every(a=>{var f;const h=o.find(p=>(p==null?void 0:p.user_id)===(a==null?void 0:a.user_id));return(f=h==null?void 0:h.actions)==null?void 0:f.includes(e)});s.forEach(a=>{const h=o.findIndex(p=>(p==null?void 0:p.user_id)===(a==null?void 0:a.user_id));let f=[];if(h!==-1&&(f=[...o[h].actions]),l){const p=f.indexOf(e);p>-1&&f.splice(p,1)}else f.includes(e)||f.push(e);h===-1?o.push({user_id:a.user_id,actions:f}):o[h].actions=f}),r.value=o},c=V(()=>{if(!r.value)return{};const n={};return r.value.forEach(e=>{const s=e==null?void 0:e.content_type_id;s&&(n[s]||(n[s]=[]),n[s].push({user_id:e.user_id,subject:e.subject,content_type_id:s,actions:e.actions||[]}))}),n}),g=()=>{},u=()=>{var n;(n=r.value)==null||n.forEach(e=>{e.actions=[]}),_.value=""},w=V(()=>{const n=_.value.toLowerCase().trim();if(!n)return c.value;const e={};return Object.entries(c.value).forEach(([s,o])=>{const l=o.filter(a=>[a==null?void 0:a.subject,a==null?void 0:a.content_type_id,s].filter(Boolean).join(" ").toLowerCase().includes(n));l.length>0&&(e[s]=l)}),e});return(n,e)=>(i(),x("div",null,[t("div",S,[U(t("input",{type:"text",placeholder:"Пошук за назвою",class:"w-full h-[36px] border focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring rounded-md px-2","onUpdate:modelValue":e[0]||(e[0]=s=>_.value=s),onInput:g},null,544),[[A,_.value]]),t("button",{class:"text-sm border h-[36px] rounded-md px-2 hover:bg-gray-100 transition-all duration-300",onClick:u}," Скинути ")]),t("div",q,[t("div",z,[e[1]||(e[1]=t("h3",{class:"font-semibold text-gray-800 dark:text-neutral-200 w-full max-w-[300px]"}," Групи ",-1)),t("div",G,[(i(),x(y,null,m(["Перегляд","Додавання","Редагування","Видалення"],s=>t("p",H,j(s),1)),64))])]),(i(!0),x(y,null,m(w.value,(s,o)=>(i(),x("div",{key:o},[t("div",Q,[t("h3",J,j(o),1),t("div",K,[(i(),x(y,null,m(["view","add","edit","del"],l=>t("button",{key:l,class:"text-xs font-medium text-blue-600 text-start w-[80px] decoration-2 hover:underline focus:outline-none focus:underline",onClick:a=>d(o,l)}," Вибрати всі ",8,R)),64))])]),t("div",T,[(i(!0),x(y,null,m(s,l=>(i(),x("div",{key:l.id,class:"flex gap-2 items-center w-full"},[t("div",W,[t("p",X,j(l==null?void 0:l.subject),1)]),L(O,{"model-value":k(l==null?void 0:l.user_id).value,"onUpdate:modelValue":a=>b(l==null?void 0:l.user_id,a),"item-id":l==null?void 0:l.user_id},null,8,["model-value","onUpdate:modelValue","item-id"])]))),128))])]))),128))])]))}},Z={class:"w-full flex justify-center bg-gray-100 h-[calc(100vh-60px)]"},ee={class:"w-full max-w-[1440px]"},se={class:"mt-4 bg-white rounded-lg p-4 border"},oe={__name:"vs-permissions",setup(v){const _=M().proxy.$notify,r=P([]);(async()=>{try{const{data:d}=await C.get("/api/site-permissions/1");r.value=d==null?void 0:d.permissions}catch{}})();const b=async()=>{try{await C.post("/api/site-permissions/1",{permissions:r.value}),_({title:"Успішно",message:"Права збережено",type:"success"})}catch{_({title:"Помилка",text:"Помилка при збереженні прав",type:"error"})}};return(d,c)=>{var g;return i(),x("section",Z,[t("div",ee,[t("div",{class:"flex items-center justify-between mt-4"},[c[1]||(c[1]=t("h1",{class:"text-xl font-bold"},"Permissions",-1)),t("button",{onClick:b,class:"p-2 bg-blue-500 text-white rounded-md"}," Зберегти ")]),t("div",se,[(g=r.value)!=null&&g.length?(i(),I(Y,{key:0,permissions:r.value,"onUpdate:permissions":c[0]||(c[0]=u=>r.value=u)},null,8,["permissions"])):$("",!0)])])])}}};export{oe as default};
|
package/dist/index.html
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<title>User Profile</title>
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-qmYrZ9-m.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-x9a62HSi.css">
|
|
10
|
+
</head>
|
|
11
|
+
<body>
|
|
12
|
+
<div id="app"></div>
|
|
13
|
+
<div id="modal"></div>
|
|
14
|
+
</body>
|
|
15
|
+
</html>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengis/cms",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.11",
|
|
4
4
|
"description": "cms",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "Softpro",
|
|
@@ -23,10 +23,10 @@
|
|
|
23
23
|
"docs:preview": "vitepress preview docs"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@opengis/fastify-auth": "
|
|
27
|
-
"@opengis/fastify-file": "
|
|
28
|
-
"@opengis/fastify-table": "
|
|
29
|
-
"@opengis/v3-core": "^0.3.
|
|
26
|
+
"@opengis/fastify-auth": "1.0.85",
|
|
27
|
+
"@opengis/fastify-file": "1.0.76",
|
|
28
|
+
"@opengis/fastify-table": "1.3.42",
|
|
29
|
+
"@opengis/v3-core": "^0.3.165",
|
|
30
30
|
"@opengis/v3-filter": "^0.0.74",
|
|
31
31
|
"@vitejs/plugin-vue": "^5.0.4",
|
|
32
32
|
"cross-env": "^7.0.3",
|
package/server/app.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'url';
|
|
3
|
+
|
|
2
4
|
import { config, execMigrations } from '@opengis/fastify-table/utils.js';
|
|
3
5
|
|
|
4
6
|
config.prefix = config.prefix || '/api';
|
|
5
7
|
const { prefix } = config;
|
|
6
8
|
|
|
7
|
-
const
|
|
9
|
+
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
10
|
|
|
9
11
|
export default async function (fastify) {
|
|
10
12
|
// core
|
|
@@ -21,5 +23,6 @@ export default async function (fastify) {
|
|
|
21
23
|
fastify.register(import('./routes/category/index.mjs'), { prefix });
|
|
22
24
|
fastify.register(import('./routes/manager/index.mjs'), { prefix });
|
|
23
25
|
fastify.register(import('./routes/media/index.mjs'), { prefix });
|
|
24
|
-
|
|
26
|
+
fastify.register(import('./routes/site/index.mjs'), { prefix });
|
|
27
|
+
execMigrations(path.join(dirname, 'migrations')).catch(err => console.log(err));
|
|
25
28
|
}
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
create schema if not exists admin;
|
|
2
|
+
create table if not exists admin.users (uid text primary key default next_id());
|
|
3
|
+
|
|
4
|
+
create schema if not exists site;
|
|
5
|
+
create table if not exists site.categories (category_id text primary key default next_id());
|
|
6
|
+
|
|
7
|
+
-- drop table if exists site.articles cascade;
|
|
8
|
+
CREATE TABLE if not exists site.articles (
|
|
9
|
+
article_id text PRIMARY KEY default next_id(),
|
|
10
|
+
title VARCHAR (100) NOT NULL,
|
|
11
|
+
slug VARCHAR (50) UNIQUE,
|
|
12
|
+
content TEXT,
|
|
13
|
+
excerpt TEXT,
|
|
14
|
+
status VARCHAR not null DEFAULT 'draft' CHECK (status::text = ANY (ARRAY['draft', 'published', 'archived']::text[])),
|
|
15
|
+
published_at TIMESTAMP,
|
|
16
|
+
is_visible BOOLEAN not null DEFAULT TRUE,
|
|
17
|
+
meta_title VARCHAR,
|
|
18
|
+
meta_description TEXT,
|
|
19
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
20
|
+
updated_at TIMESTAMP DEFAULT NOW(),
|
|
21
|
+
created_by text REFERENCES admin.users(uid),
|
|
22
|
+
updated_by text REFERENCES admin.users(uid),
|
|
23
|
+
author_id text REFERENCES admin.users(uid) ON DELETE SET NULL,
|
|
24
|
+
category_id text REFERENCES site.categories(category_id) ON DELETE SET NULL
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
CREATE INDEX if not exists idx_published_articles ON site.articles(published_at) WHERE status = 'published';
|
|
28
|
+
CREATE INDEX if not exists idx_articles_status ON site.articles(status);
|
|
29
|
+
CREATE INDEX if not exists idx_articles_category_id ON site.articles(category_id);
|
|
30
|
+
CREATE INDEX if not exists idx_articles_author_id ON site.articles(author_id);
|
|
31
|
+
CREATE INDEX if not exists idx_articles_is_visible ON site.articles(is_visible);
|
|
32
|
+
CREATE INDEX if not exists idx_articles_published_at ON site.articles(published_at);
|
|
33
|
+
|
|
34
|
+
COMMENT ON TABLE site.articles IS 'Articles for the site, including content, metadata, status, and authorship.';
|
|
35
|
+
COMMENT ON COLUMN site.articles.article_id IS 'Primary key for the article, generated via next_id().';
|
|
36
|
+
COMMENT ON COLUMN site.articles.title IS 'Title of the article.';
|
|
37
|
+
COMMENT ON COLUMN site.articles.slug IS 'URL-friendly unique identifier for the article.';
|
|
38
|
+
COMMENT ON COLUMN site.articles.content IS 'Full body content of the article.';
|
|
39
|
+
COMMENT ON COLUMN site.articles.excerpt IS 'Optional summary or teaser text.';
|
|
40
|
+
COMMENT ON COLUMN site.articles.status IS 'Publication status: draft, published, or archived.';
|
|
41
|
+
COMMENT ON COLUMN site.articles.published_at IS 'Timestamp when the article was published.';
|
|
42
|
+
COMMENT ON COLUMN site.articles.is_visible IS 'Boolean flag indicating if the article is visible on the site.';
|
|
43
|
+
COMMENT ON COLUMN site.articles.meta_title IS 'SEO meta title for the article.';
|
|
44
|
+
COMMENT ON COLUMN site.articles.meta_description IS 'SEO meta description for the article.';
|
|
45
|
+
COMMENT ON COLUMN site.articles.created_at IS 'Timestamp when the article was created.';
|
|
46
|
+
COMMENT ON COLUMN site.articles.updated_at IS 'Timestamp when the article was last updated.';
|
|
47
|
+
COMMENT ON COLUMN site.articles.created_by IS 'User ID of the admin who created the article.';
|
|
48
|
+
COMMENT ON COLUMN site.articles.updated_by IS 'User ID of the admin who last updated the article.';
|
|
49
|
+
COMMENT ON COLUMN site.articles.author_id IS 'User ID of the article''s author.';
|
|
50
|
+
COMMENT ON COLUMN site.articles.category_id IS 'Category ID to which the article belongs.';
|
|
51
|
+
|
|
52
|
+
-- drop table if exists site.media cascade;
|
|
53
|
+
CREATE TABLE if not exists site.media (
|
|
54
|
+
media_id text PRIMARY KEY default next_id(),
|
|
55
|
+
filename TEXT NOT NULL,
|
|
56
|
+
filetype TEXT NOT NULL,
|
|
57
|
+
filesize INTEGER NOT NULL,
|
|
58
|
+
url TEXT,
|
|
59
|
+
description TEXT,
|
|
60
|
+
alt text,
|
|
61
|
+
mime VARCHAR(100),
|
|
62
|
+
preview_url TEXT,
|
|
63
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
64
|
+
updated_at TIMESTAMP DEFAULT NOW(),
|
|
65
|
+
created_by text REFERENCES admin.users(uid),
|
|
66
|
+
updated_by text REFERENCES admin.users(uid)
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
COMMENT ON TABLE site.media IS 'Stores uploaded media files with metadata, URLs, and audit info.';
|
|
70
|
+
COMMENT ON COLUMN site.media.media_id IS 'Unique ID for the media item.';
|
|
71
|
+
COMMENT ON COLUMN site.media.filename IS 'Original name of the uploaded file.';
|
|
72
|
+
COMMENT ON COLUMN site.media.filetype IS 'Logical type of file (e.g., image, video, document).';
|
|
73
|
+
COMMENT ON COLUMN site.media.filesize IS 'Size of the file in bytes.';
|
|
74
|
+
COMMENT ON COLUMN site.media.url IS 'URL where the file can be accessed.';
|
|
75
|
+
COMMENT ON COLUMN site.media.description IS 'Optional description or caption.';
|
|
76
|
+
COMMENT ON COLUMN site.media.alt IS 'Alternative text for screen readers or image fallbacks.';
|
|
77
|
+
COMMENT ON COLUMN site.media.mime IS 'MIME type indicating the file format (e.g., image/png).';
|
|
78
|
+
COMMENT ON COLUMN site.media.preview_url IS 'Optional preview or thumbnail URL.';
|
|
79
|
+
COMMENT ON COLUMN site.media.created_at IS 'Timestamp of when the media record was created.';
|
|
80
|
+
COMMENT ON COLUMN site.media.updated_at IS 'Timestamp of the last update to the media record.';
|
|
81
|
+
COMMENT ON COLUMN site.media.created_by IS 'User ID who uploaded the media.';
|
|
82
|
+
COMMENT ON COLUMN site.media.updated_by IS 'User ID who last updated the media.';
|
|
83
|
+
|
|
84
|
+
CREATE INDEX if not exists idx_media_filetype ON site.media(filetype);
|
|
85
|
+
CREATE INDEX if not exists idx_media_filesize ON site.media(filesize);
|
|
86
|
+
CREATE INDEX if not exists idx_media_mime ON site.media(mime);
|
|
87
|
+
CREATE INDEX if not exists idx_media_created_at ON site.media(created_at);
|
|
88
|
+
CREATE INDEX if not exists idx_media_created_by ON site.media(created_by);
|
|
89
|
+
CREATE INDEX if not exists idx_media_updated_at ON site.media(updated_at);
|
|
90
|
+
CREATE INDEX if not exists idx_media_updated_by ON site.media(updated_by);
|
|
91
|
+
|
|
92
|
+
-- drop table if exists site.article_media cascade;
|
|
93
|
+
CREATE TABLE if not exists site.article_media (
|
|
94
|
+
article_id text not null REFERENCES site.articles(article_id) ON DELETE CASCADE,
|
|
95
|
+
media_id text not null REFERENCES site.media(media_id) ON DELETE CASCADE,
|
|
96
|
+
related_id text NOT NULL,
|
|
97
|
+
related_type VARCHAR(50) NOT NULL,
|
|
98
|
+
field VARCHAR(50) NOT NULL,
|
|
99
|
+
order_index INTEGER DEFAULT 0,
|
|
100
|
+
PRIMARY KEY (article_id, media_id, field)
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
COMMENT ON TABLE site.article_media IS 'Join table for associating media with articles, supporting multiple fields and order.';
|
|
104
|
+
COMMENT ON COLUMN site.article_media.article_id IS 'Foreign key to the article that uses the media.';
|
|
105
|
+
COMMENT ON COLUMN site.article_media.media_id IS 'Foreign key to the media file linked to the article.';
|
|
106
|
+
COMMENT ON COLUMN site.article_media.related_id IS 'ID of the related entity (for polymorphic associations).';
|
|
107
|
+
COMMENT ON COLUMN site.article_media.related_type IS 'Type of the related entity (e.g., article, gallery).';
|
|
108
|
+
COMMENT ON COLUMN site.article_media.field IS 'Field name that defines the role of media (e.g., cover, inline).';
|
|
109
|
+
COMMENT ON COLUMN site.article_media.order_index IS 'Ordering index for multiple media in the same field.';
|
|
110
|
+
|
|
111
|
+
CREATE INDEX if not exists idx_article_media_related ON site.article_media(related_type, related_id);
|
|
112
|
+
CREATE INDEX if not exists idx_article_media_ordering ON site.article_media(article_id, field, order_index);
|
|
113
|
+
|
|
114
|
+
-- drop table if exists site.content_types cascade;
|
|
115
|
+
CREATE TABLE if not exists site.content_types (
|
|
116
|
+
content_type_id text PRIMARY KEY default next_id(),
|
|
117
|
+
name VARCHAR (50) NOT NULL UNIQUE,
|
|
118
|
+
display_name VARCHAR (50) NOT NULL,
|
|
119
|
+
table_name VARCHAR(50) NOT NULL,
|
|
120
|
+
status VARCHAR (20) DEFAULT 'draft' CHECK (status::text = ANY (ARRAY['draft', 'published', 'archived']::text[])),
|
|
121
|
+
visible BOOLEAN DEFAULT TRUE,
|
|
122
|
+
localized BOOLEAN DEFAULT FALSE,
|
|
123
|
+
type VARCHAR(20) NOT NULL DEFAULT 'collection' CHECK (type::text = ANY (ARRAY['collection', 'single']::text[])),
|
|
124
|
+
schema JSONB,
|
|
125
|
+
description TEXT,
|
|
126
|
+
icon VARCHAR (50),
|
|
127
|
+
color VARCHAR (20),
|
|
128
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
129
|
+
updated_at TIMESTAMP DEFAULT NOW(),
|
|
130
|
+
created_by text REFERENCES admin.users(uid),
|
|
131
|
+
updated_by text REFERENCES admin.users(uid)
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
COMMENT ON TABLE site.content_types IS 'Defines reusable content structures for dynamic content management.';
|
|
135
|
+
COMMENT ON COLUMN site.content_types.content_type_id IS 'Unique identifier for the content type.';
|
|
136
|
+
COMMENT ON COLUMN site.content_types.name IS 'Internal name used in the system (must be unique).';
|
|
137
|
+
COMMENT ON COLUMN site.content_types.display_name IS 'Human-readable label shown in UI.';
|
|
138
|
+
COMMENT ON COLUMN site.content_types.table_name IS 'Physical table where entries of this type are stored.';
|
|
139
|
+
COMMENT ON COLUMN site.content_types.status IS 'Status of the content type (draft, published, archived).';
|
|
140
|
+
COMMENT ON COLUMN site.content_types.visible IS 'Controls whether this content type appears in admin UI.';
|
|
141
|
+
COMMENT ON COLUMN site.content_types.localized IS 'Indicates whether the content type supports localization.';
|
|
142
|
+
COMMENT ON COLUMN site.content_types.type IS 'Whether this content type is a collection or a single instance.';
|
|
143
|
+
COMMENT ON COLUMN site.content_types.schema IS 'JSON schema defining the structure and fields of this content type.';
|
|
144
|
+
COMMENT ON COLUMN site.content_types.description IS 'Optional description of the content type.';
|
|
145
|
+
COMMENT ON COLUMN site.content_types.icon IS 'UI icon identifier for this type.';
|
|
146
|
+
COMMENT ON COLUMN site.content_types.color IS 'UI color code or name for this type.';
|
|
147
|
+
COMMENT ON COLUMN site.content_types.created_at IS 'Timestamp when the content type was created.';
|
|
148
|
+
COMMENT ON COLUMN site.content_types.updated_at IS 'Timestamp when the content type was last modified.';
|
|
149
|
+
COMMENT ON COLUMN site.content_types.created_by IS 'Admin user who created the content type.';
|
|
150
|
+
COMMENT ON COLUMN site.content_types.updated_by IS 'Admin user who last updated the content type.';
|
|
151
|
+
|
|
152
|
+
CREATE INDEX if not exists idx_content_types_status ON site.content_types(status);
|
|
153
|
+
CREATE INDEX if not exists idx_content_types_visible ON site.content_types(visible);
|
|
154
|
+
CREATE INDEX if not exists idx_content_types_type ON site.content_types(type);
|
|
155
|
+
CREATE INDEX if not exists idx_content_types_table_name ON site.content_types(table_name);
|
|
156
|
+
|
|
157
|
+
-- drop table if exists site.content_attributes cascade;
|
|
158
|
+
CREATE TABLE if not exists site.content_attributes (
|
|
159
|
+
content_attribute_id text PRIMARY KEY default next_id(),
|
|
160
|
+
content_type_id text REFERENCES site.content_types(content_type_id) ON DELETE CASCADE,
|
|
161
|
+
name VARCHAR NOT NULL,
|
|
162
|
+
type VARCHAR NOT NULL,
|
|
163
|
+
is_required BOOLEAN DEFAULT FALSE,
|
|
164
|
+
is_unique BOOLEAN DEFAULT FALSE,
|
|
165
|
+
minLength numeric,
|
|
166
|
+
maxLength numeric,
|
|
167
|
+
private BOOLEAN DEFAULT FALSE,
|
|
168
|
+
min numeric,
|
|
169
|
+
max numeric,
|
|
170
|
+
relation_type VARCHAR (20),
|
|
171
|
+
related_to VARCHAR(50),
|
|
172
|
+
default_value TEXT,
|
|
173
|
+
options JSONB,
|
|
174
|
+
enum text[],
|
|
175
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
176
|
+
updated_at TIMESTAMP DEFAULT NOW(),
|
|
177
|
+
created_by text REFERENCES admin.users(uid),
|
|
178
|
+
updated_by text REFERENCES admin.users(uid)
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
COMMENT ON TABLE site.content_attributes IS 'Defines fields (attributes) for a dynamic content type schema.';
|
|
182
|
+
COMMENT ON COLUMN site.content_attributes.content_attribute_id IS 'Primary key for the content attribute.';
|
|
183
|
+
COMMENT ON COLUMN site.content_attributes.content_type_id IS 'Foreign key to the content type this attribute belongs to.';
|
|
184
|
+
COMMENT ON COLUMN site.content_attributes.name IS 'Machine name of the attribute.';
|
|
185
|
+
COMMENT ON COLUMN site.content_attributes.type IS 'Data type of the attribute (e.g., text, number, boolean).';
|
|
186
|
+
COMMENT ON COLUMN site.content_attributes.is_required IS 'Whether this field must be filled.';
|
|
187
|
+
COMMENT ON COLUMN site.content_attributes.is_unique IS 'Whether values for this field must be unique.';
|
|
188
|
+
COMMENT ON COLUMN site.content_attributes.minLength IS 'Minimum length constraint (for strings).';
|
|
189
|
+
COMMENT ON COLUMN site.content_attributes.maxLength IS 'Maximum length constraint (for strings).';
|
|
190
|
+
COMMENT ON COLUMN site.content_attributes.private IS 'Whether this field is hidden in public APIs.';
|
|
191
|
+
COMMENT ON COLUMN site.content_attributes.min IS 'Minimum numeric value (if applicable).';
|
|
192
|
+
COMMENT ON COLUMN site.content_attributes.max IS 'Maximum numeric value (if applicable).';
|
|
193
|
+
COMMENT ON COLUMN site.content_attributes.relation_type IS 'Type of relation, if any (e.g., one-to-many).';
|
|
194
|
+
COMMENT ON COLUMN site.content_attributes.related_to IS 'Target entity/content type this field relates to.';
|
|
195
|
+
COMMENT ON COLUMN site.content_attributes.default_value IS 'Default value assigned to this field.';
|
|
196
|
+
COMMENT ON COLUMN site.content_attributes.options IS 'Custom configuration or metadata in JSON format.';
|
|
197
|
+
COMMENT ON COLUMN site.content_attributes.enum IS 'List of allowed enum values for this field.';
|
|
198
|
+
COMMENT ON COLUMN site.content_attributes.created_at IS 'Timestamp when this attribute was created.';
|
|
199
|
+
COMMENT ON COLUMN site.content_attributes.updated_at IS 'Timestamp when this attribute was last updated.';
|
|
200
|
+
COMMENT ON COLUMN site.content_attributes.created_by IS 'Admin user who created the content type.';
|
|
201
|
+
COMMENT ON COLUMN site.content_attributes.updated_by IS 'Admin user who last updated the content type.';
|
|
202
|
+
|
|
203
|
+
CREATE INDEX if not exists idx_attributes_by_type ON site.content_attributes(content_type_id);
|
|
204
|
+
CREATE INDEX if not exists idx_attributes_name ON site.content_attributes(name);
|
|
205
|
+
CREATE INDEX if not exists idx_attributes_type ON site.content_attributes(type);
|
|
206
|
+
|
|
207
|
+
-- drop table if exists site.article_translations cascade;
|
|
208
|
+
CREATE TABLE if not exists site.article_translations (
|
|
209
|
+
article_translation_id text PRIMARY KEY default next_id(),
|
|
210
|
+
article_id text REFERENCES site.articles(article_id) ON DELETE CASCADE,
|
|
211
|
+
locale VARCHAR(10) NOT NULL,
|
|
212
|
+
title VARCHAR (100) NOT NULL,
|
|
213
|
+
content TEXT,
|
|
214
|
+
excerpt TEXT,
|
|
215
|
+
meta_title VARCHAR (100),
|
|
216
|
+
meta_description TEXT,
|
|
217
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
218
|
+
updated_at TIMESTAMP DEFAULT NOW(),
|
|
219
|
+
created_by text REFERENCES admin.users(uid),
|
|
220
|
+
updated_by text REFERENCES admin.users(uid),
|
|
221
|
+
UNIQUE (article_id, locale)
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
COMMENT ON TABLE site.article_translations IS 'Stores translated content for articles across different locales.';
|
|
225
|
+
COMMENT ON COLUMN site.article_translations.article_translation_id IS 'Primary key for the article translation.';
|
|
226
|
+
COMMENT ON COLUMN site.article_translations.article_id IS 'Foreign key linking to the main article.';
|
|
227
|
+
COMMENT ON COLUMN site.article_translations.locale IS 'Locale code for the translation (e.g., en, fr).';
|
|
228
|
+
COMMENT ON COLUMN site.article_translations.title IS 'Translated title of the article.';
|
|
229
|
+
COMMENT ON COLUMN site.article_translations.content IS 'Translated content body.';
|
|
230
|
+
COMMENT ON COLUMN site.article_translations.excerpt IS 'Translated excerpt or teaser text.';
|
|
231
|
+
COMMENT ON COLUMN site.article_translations.meta_title IS 'Translated SEO meta title.';
|
|
232
|
+
COMMENT ON COLUMN site.article_translations.meta_description IS 'Translated SEO meta description.';
|
|
233
|
+
COMMENT ON COLUMN site.article_translations.created_at IS 'Timestamp of when this translation was created.';
|
|
234
|
+
COMMENT ON COLUMN site.article_translations.updated_at IS 'Timestamp of when this translation was last updated.';
|
|
235
|
+
COMMENT ON COLUMN site.article_translations.created_by IS 'Admin user who created this translation.';
|
|
236
|
+
COMMENT ON COLUMN site.article_translations.updated_by IS 'Admin user who last updated this translation.';
|
|
237
|
+
|
|
238
|
+
CREATE INDEX if not exists idx_article_translations_article_id ON site.article_translations(article_id);
|
|
239
|
+
CREATE INDEX if not exists idx_article_translations_locale ON site.article_translations(locale);
|
|
240
|
+
|
|
241
|
+
-- drop table if exists site.single_type_values cascade;
|
|
242
|
+
CREATE TABLE if not exists site.single_type_values (
|
|
243
|
+
single_type_value_id text PRIMARY KEY default next_id(),
|
|
244
|
+
type_name VARCHAR(100) NOT NULL,
|
|
245
|
+
key VARCHAR(255) NOT NULL,
|
|
246
|
+
value TEXT,
|
|
247
|
+
value_type VARCHAR(50),
|
|
248
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
249
|
+
updated_at TIMESTAMP DEFAULT NOW(),
|
|
250
|
+
created_by text REFERENCES admin.users(uid),
|
|
251
|
+
updated_by text REFERENCES admin.users(uid),
|
|
252
|
+
UNIQUE(type_name, key)
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
COMMENT ON TABLE site.single_type_values IS 'Stores key-value pairs for single-type settings or configurations.';
|
|
256
|
+
COMMENT ON COLUMN site.single_type_values.single_type_value_id IS 'Primary key for the single type value entry.';
|
|
257
|
+
COMMENT ON COLUMN site.single_type_values.type_name IS 'Type of the setting or configuration.';
|
|
258
|
+
COMMENT ON COLUMN site.single_type_values.key IS 'Unique key for the setting or configuration value.';
|
|
259
|
+
COMMENT ON COLUMN site.single_type_values.value IS 'The value associated with the key.';
|
|
260
|
+
COMMENT ON COLUMN site.single_type_values.value_type IS 'Type of value (e.g., string, integer, boolean).';
|
|
261
|
+
COMMENT ON COLUMN site.single_type_values.created_at IS 'Timestamp when the value entry was created.';
|
|
262
|
+
COMMENT ON COLUMN site.single_type_values.updated_at IS 'Timestamp when the value entry was last updated.';
|
|
263
|
+
COMMENT ON COLUMN site.single_type_values.created_by IS 'User who created the value entry.';
|
|
264
|
+
COMMENT ON COLUMN site.single_type_values.updated_by IS 'User who last updated the value entry.';
|
|
265
|
+
|
|
266
|
+
CREATE INDEX if not exists idx_single_type_values_type_name ON site.single_type_values(type_name);
|
|
267
|
+
CREATE INDEX if not exists idx_single_type_values_key ON site.single_type_values(key);
|
|
268
|
+
|
|
269
|
+
-- drop table if exists site.settings cascade;
|
|
270
|
+
CREATE TABLE if not exists site.settings (
|
|
271
|
+
id serial PRIMARY KEY CHECK (id = 1),
|
|
272
|
+
key VARCHAR (50) NOT NULL UNIQUE,
|
|
273
|
+
value TEXT NOT NULL,
|
|
274
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
275
|
+
updated_at TIMESTAMP DEFAULT NOW(),
|
|
276
|
+
created_by text REFERENCES admin.users(uid),
|
|
277
|
+
updated_by text REFERENCES admin.users(uid)
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
COMMENT ON TABLE site.settings IS 'Stores global settings for the site, with only one record allowed.';
|
|
281
|
+
COMMENT ON COLUMN site.settings.id IS 'Unique ID for the settings table (always 1).';
|
|
282
|
+
COMMENT ON COLUMN site.settings.key IS 'Unique key for the setting (e.g., "site_title").';
|
|
283
|
+
COMMENT ON COLUMN site.settings.value IS 'The value associated with the key (e.g., "My Site Title").';
|
|
284
|
+
COMMENT ON COLUMN site.settings.created_at IS 'Timestamp when the setting was created.';
|
|
285
|
+
COMMENT ON COLUMN site.settings.updated_at IS 'Timestamp when the setting was last updated.';
|
|
286
|
+
COMMENT ON COLUMN site.settings.created_by IS 'User who created the setting record.';
|
|
287
|
+
COMMENT ON COLUMN site.settings.updated_by IS 'User who last updated the setting record.';
|
|
288
|
+
|
|
289
|
+
CREATE INDEX if not exists idx_settings_key ON site.settings(key);
|
|
290
|
+
|
|
291
|
+
-- drop table if exists site.menus cascade;
|
|
292
|
+
CREATE TABLE if not exists site.menus (
|
|
293
|
+
menu_id text PRIMARY KEY default next_id(),
|
|
294
|
+
name VARCHAR(100) UNIQUE NOT NULL,
|
|
295
|
+
slug VARCHAR(100) UNIQUE NOT NULL,
|
|
296
|
+
description TEXT,
|
|
297
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
298
|
+
updated_at TIMESTAMP DEFAULT NOW(),
|
|
299
|
+
created_by text REFERENCES admin.users(uid),
|
|
300
|
+
updated_by text REFERENCES admin.users(uid)
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
COMMENT ON TABLE site.menus IS 'Stores menu information with a name, slug, description, and audit fields.';
|
|
304
|
+
COMMENT ON COLUMN site.menus.menu_id IS 'Primary key for the menu (auto-generated).';
|
|
305
|
+
COMMENT ON COLUMN site.menus.name IS 'Unique name for the menu.';
|
|
306
|
+
COMMENT ON COLUMN site.menus.slug IS 'Unique slug for the menu, used for URLs or other references.';
|
|
307
|
+
COMMENT ON COLUMN site.menus.description IS 'Optional description of the menu.';
|
|
308
|
+
COMMENT ON COLUMN site.menus.created_at IS 'Timestamp when the menu was created.';
|
|
309
|
+
COMMENT ON COLUMN site.menus.updated_at IS 'Timestamp when the menu was last updated.';
|
|
310
|
+
COMMENT ON COLUMN site.menus.created_by IS 'Admin user who created the menu.';
|
|
311
|
+
COMMENT ON COLUMN site.menus.updated_by IS 'Admin user who last updated the menu.';
|
|
312
|
+
|
|
313
|
+
CREATE INDEX if not exists idx_menus_name ON site.menus(name);
|
|
314
|
+
CREATE INDEX if not exists idx_menus_slug ON site.menus(slug);
|
|
315
|
+
|
|
316
|
+
-- drop table if exists site.menu_items cascade;
|
|
317
|
+
CREATE TABLE if not exists site.menu_items (
|
|
318
|
+
menu_item_id text PRIMARY KEY default next_id(),
|
|
319
|
+
menu_id text NOT NULL REFERENCES site.menus(menu_id) ON DELETE CASCADE,
|
|
320
|
+
parent_id text REFERENCES site.menu_items(menu_item_id) ON DELETE CASCADE,
|
|
321
|
+
title VARCHAR(255) NOT NULL,
|
|
322
|
+
url TEXT,
|
|
323
|
+
order_index INTEGER DEFAULT 0,
|
|
324
|
+
target VARCHAR(20),
|
|
325
|
+
icon VARCHAR(100),
|
|
326
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
327
|
+
updated_at TIMESTAMP DEFAULT NOW(),
|
|
328
|
+
created_by text REFERENCES admin.users(uid),
|
|
329
|
+
updated_by text REFERENCES admin.users(uid)
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
COMMENT ON TABLE site.menu_items IS 'Stores items within menus, supporting hierarchical (nested) structures.';
|
|
333
|
+
COMMENT ON COLUMN site.menu_items.menu_item_id IS 'Primary key for the menu item (auto-generated).';
|
|
334
|
+
COMMENT ON COLUMN site.menu_items.menu_id IS 'Foreign key linking to the associated menu.';
|
|
335
|
+
COMMENT ON COLUMN site.menu_items.parent_id IS 'Foreign key linking to the parent menu item (for nested structure).';
|
|
336
|
+
COMMENT ON COLUMN site.menu_items.title IS 'Title of the menu item.';
|
|
337
|
+
COMMENT ON COLUMN site.menu_items.url IS 'URL or path for the menu item.';
|
|
338
|
+
COMMENT ON COLUMN site.menu_items.order_index IS 'Index used for ordering the menu items.';
|
|
339
|
+
COMMENT ON COLUMN site.menu_items.target IS 'Target attribute for the menu link (e.g., "_blank" for a new tab).';
|
|
340
|
+
COMMENT ON COLUMN site.menu_items.icon IS 'Icon associated with the menu item.';
|
|
341
|
+
COMMENT ON COLUMN site.menu_items.created_at IS 'Timestamp when the menu item was created.';
|
|
342
|
+
COMMENT ON COLUMN site.menu_items.updated_at IS 'Timestamp when the menu item was last updated.';
|
|
343
|
+
COMMENT ON COLUMN site.menu_items.created_by IS 'User who created the menu item.';
|
|
344
|
+
COMMENT ON COLUMN site.menu_items.updated_by IS 'User who last updated the menu item.';
|
|
345
|
+
|
|
346
|
+
CREATE INDEX if not exists idx_menu_items_menu_id ON site.menu_items(menu_id);
|
|
347
|
+
CREATE INDEX if not exists idx_menu_items_parent_id ON site.menu_items(parent_id);
|
|
348
|
+
CREATE INDEX if not exists idx_menu_items_order_index ON site.menu_items(order_index);
|
|
349
|
+
|
|
350
|
+
-- drop table if exists site.roles cascade;
|
|
351
|
+
CREATE TABLE if not exists site.roles (
|
|
352
|
+
role_id text PRIMARY KEY default next_id(),
|
|
353
|
+
name VARCHAR(100) UNIQUE NOT NULL,
|
|
354
|
+
description TEXT,
|
|
355
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
356
|
+
updated_at TIMESTAMP DEFAULT NOW(),
|
|
357
|
+
created_by text REFERENCES admin.users(uid),
|
|
358
|
+
updated_by text REFERENCES admin.users(uid)
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
COMMENT ON TABLE site.roles IS 'Stores information about user roles, which can be used for access control and permissions.';
|
|
362
|
+
COMMENT ON COLUMN site.roles.role_id IS 'Primary key for the role (auto-generated).';
|
|
363
|
+
COMMENT ON COLUMN site.roles.name IS 'Unique name of the role (e.g., "admin", "editor").';
|
|
364
|
+
COMMENT ON COLUMN site.roles.description IS 'Description of the role and its permissions.';
|
|
365
|
+
COMMENT ON COLUMN site.roles.created_at IS 'Timestamp when the role was created.';
|
|
366
|
+
COMMENT ON COLUMN site.roles.updated_at IS 'Timestamp when the role was last updated.';
|
|
367
|
+
COMMENT ON COLUMN site.roles.created_by IS 'User who created the role.';
|
|
368
|
+
COMMENT ON COLUMN site.roles.updated_by IS 'User who last updated the role.';
|
|
369
|
+
|
|
370
|
+
CREATE INDEX if not exists idx_roles_name ON site.roles(name);
|
|
371
|
+
|
|
372
|
+
-- drop table if exists site.permissions cascade;
|
|
373
|
+
CREATE TABLE if not exists site.permissions (
|
|
374
|
+
permission_id text PRIMARY KEY default next_id(),
|
|
375
|
+
role_id text references site.roles(role_id),
|
|
376
|
+
user_id text references admin.users(uid),
|
|
377
|
+
content_type_id text references site.content_types(content_type_id),
|
|
378
|
+
subject VARCHAR(100) NOT NULL,
|
|
379
|
+
actions TEXT[] NOT NULL check (actions && (ARRAY['read', 'create', 'delete', 'edit']::text[])),
|
|
380
|
+
description TEXT,
|
|
381
|
+
created_at TIMESTAMP DEFAULT NOW(),
|
|
382
|
+
updated_at TIMESTAMP DEFAULT NOW(),
|
|
383
|
+
created_by text REFERENCES admin.users(uid),
|
|
384
|
+
updated_by text REFERENCES admin.users(uid)
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
COMMENT ON TABLE site.permissions IS 'Stores permissions assigned to roles or users, defining actions for specific subjects and content types.';
|
|
389
|
+
COMMENT ON COLUMN site.permissions.permission_id IS 'Primary key for the permission (auto-generated).';
|
|
390
|
+
COMMENT ON COLUMN site.permissions.role_id IS 'Foreign key linking to the role the permission is assigned to.';
|
|
391
|
+
COMMENT ON COLUMN site.permissions.user_id IS 'Foreign key linking to the user who has the permission.';
|
|
392
|
+
COMMENT ON COLUMN site.permissions.content_type_id IS 'Foreign key linking to the content type for which the permission applies.';
|
|
393
|
+
COMMENT ON COLUMN site.permissions.subject IS 'Subject (e.g., "post", "comment") of the permission.';
|
|
394
|
+
COMMENT ON COLUMN site.permissions.actions IS 'Array of actions allowed for the permission (e.g., ["create", "update"]).';
|
|
395
|
+
COMMENT ON COLUMN site.permissions.description IS 'Description of the permission.';
|
|
396
|
+
COMMENT ON COLUMN site.permissions.created_at IS 'Timestamp when the permission was created.';
|
|
397
|
+
COMMENT ON COLUMN site.permissions.updated_at IS 'Timestamp when the permission was last updated.';
|
|
398
|
+
COMMENT ON COLUMN site.permissions.created_by IS 'User who created the permission record.';
|
|
399
|
+
COMMENT ON COLUMN site.permissions.updated_by IS 'User who last updated the permission record.';
|
|
400
|
+
|
|
401
|
+
CREATE INDEX if not exists idx_permissions_role_id ON site.permissions(role_id);
|
|
402
|
+
CREATE INDEX if not exists idx_permissions_user_id ON site.permissions(user_id);
|
|
403
|
+
CREATE INDEX if not exists idx_permissions_content_type_id ON site.permissions(content_type_id);
|
|
404
|
+
CREATE INDEX if not exists idx_permissions_subject ON site.permissions(subject);
|
|
405
|
+
|
|
406
|
+
-- DROP FUNCTION site.getmenu(text);
|
|
407
|
+
CREATE OR REPLACE FUNCTION site.getMenu(_name text)
|
|
408
|
+
RETURNS json AS
|
|
409
|
+
$BODY$
|
|
410
|
+
DECLARE
|
|
411
|
+
|
|
412
|
+
BEGIN
|
|
413
|
+
|
|
414
|
+
return (
|
|
415
|
+
WITH RECURSIVE menu_tree AS (
|
|
416
|
+
SELECT menu_item_id as id, parent_id, title, url, 0 AS level
|
|
417
|
+
FROM site.menu_items
|
|
418
|
+
WHERE parent_id IS NULL AND title = _name
|
|
419
|
+
UNION ALL
|
|
420
|
+
SELECT mi.menu_item_id, mi.parent_id, mi.title, mi.url, level + 1
|
|
421
|
+
FROM site.menu_items mi
|
|
422
|
+
INNER JOIN menu_tree mt ON mi.parent_id = mt.id
|
|
423
|
+
)
|
|
424
|
+
SELECT json_agg(row_to_json(menu_tree.*)) from menu_tree --ORDER BY level, id
|
|
425
|
+
);
|
|
426
|
+
END;
|
|
427
|
+
$BODY$
|
|
428
|
+
LANGUAGE plpgsql VOLATILE COST 100;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { rm } from 'node:fs/promises';
|
|
4
|
+
|
|
5
|
+
import { config, dataDelete, getFolder, pgClients } from "@opengis/fastify-table/utils.js";
|
|
6
|
+
|
|
7
|
+
const rootDir = getFolder(config, 'local');
|
|
8
|
+
|
|
9
|
+
export default async function deleteMedia({
|
|
10
|
+
pg = pgClients.client, params = {}, user = {}, method = 'DELETE',
|
|
11
|
+
}, reply) {
|
|
12
|
+
if (!config.debug && method !== 'DELETE') {
|
|
13
|
+
return reply.status(403).send('access restricted');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (!params?.id) {
|
|
17
|
+
return reply.status(400).send('not enough params: id');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!pg.pk?.['site.media']) {
|
|
21
|
+
return reply.status(404).send('table not found');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const { url: relpath, id } = await pg.query(
|
|
25
|
+
'select media_id as id, url from site.media where media_id = $1 and url is not null',
|
|
26
|
+
[params.id],
|
|
27
|
+
).then(el => el.rows?.[0] || {});
|
|
28
|
+
|
|
29
|
+
if (!id) {
|
|
30
|
+
return reply.status(404).send('media not found: ' + params.id);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const res = await dataDelete({
|
|
34
|
+
pg,
|
|
35
|
+
id,
|
|
36
|
+
table: 'site.media',
|
|
37
|
+
uid: user?.uid || 0,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const filepath = path.join(rootDir, relpath);
|
|
41
|
+
|
|
42
|
+
if (existsSync(filepath)) {
|
|
43
|
+
await rm(filepath, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { id, ...res || {} };
|
|
47
|
+
}
|