kempo 4.2.15 → 4.2.16
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/app-public/account/split2.template.html +1 -0
- package/app-public/test.page.html +1 -0
- package/dist/kempo/api/fragments/lock/PUT.js +2 -0
- package/dist/kempo/api/fragments/lock/PUT.js.map +1 -0
- package/dist/kempo/api/globals/lock/PUT.js +2 -0
- package/dist/kempo/api/globals/lock/PUT.js.map +1 -0
- package/dist/kempo/api/pages/lock/PUT.js +2 -0
- package/dist/kempo/api/pages/lock/PUT.js.map +1 -0
- package/dist/kempo/api/pages/search/GET.js +2 -0
- package/dist/kempo/api/pages/search/GET.js.map +1 -0
- package/dist/kempo/api/templates/lock/PUT.js +2 -0
- package/dist/kempo/api/templates/lock/PUT.js.map +1 -0
- package/package.json +2 -2
- package/server/sdk.js +5 -0
- package/server/utils/fragments/createFragment.js +5 -1
- package/server/utils/fragments/deleteFragment.js +2 -3
- package/server/utils/fragments/disableFragment.js +0 -3
- package/server/utils/fragments/getFragmentMetadata.js +32 -0
- package/server/utils/fragments/moveFragment.js +0 -3
- package/server/utils/fragments/setFragmentLocked.js +48 -0
- package/server/utils/fragments/updateFragment.js +3 -3
- package/server/utils/fs/isResourceLocked.js +22 -0
- package/server/utils/global-content/createGlobalContent.js +3 -0
- package/server/utils/global-content/deleteGlobalContent.js +3 -3
- package/server/utils/global-content/getGlobalContentMetadata.js +37 -0
- package/server/utils/global-content/setGlobalContentLocked.js +44 -0
- package/server/utils/global-content/updateGlobalContent.js +3 -4
- package/server/utils/pages/createPage.js +19 -7
- package/server/utils/pages/deletePage.js +2 -3
- package/server/utils/pages/disablePage.js +0 -3
- package/server/utils/pages/getPage.js +7 -0
- package/server/utils/pages/getPageMetadata.js +32 -0
- package/server/utils/pages/listPages.js +11 -3
- package/server/utils/pages/movePage.js +0 -3
- package/server/utils/pages/searchByMetadata.js +61 -0
- package/server/utils/pages/setPageLocked.js +48 -0
- package/server/utils/pages/updatePage.js +19 -3
- package/server/utils/templates/createTemplate.js +5 -1
- package/server/utils/templates/deleteTemplate.js +2 -3
- package/server/utils/templates/disableTemplate.js +0 -3
- package/server/utils/templates/getTemplateMetadata.js +32 -0
- package/server/utils/templates/moveTemplate.js +0 -3
- package/server/utils/templates/setTemplateLocked.js +48 -0
- package/server/utils/templates/updateTemplate.js +3 -3
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{resolve}from"path";import currentUserHasPermission from"../../../../../server/utils/permissions/currentUserHasPermission.js";import setFragmentLocked from"../../../../../server/utils/fragments/setFragmentLocked.js";const rootDir=resolve(import.meta.dirname,"../../../../../app-public");export default async(r,e)=>{const s=r.cookies.session_token,[o,t]=await currentUserHasPermission(s,"system:fragments:update");if(o)return e.status(o.code).json({error:o.msg});if(!t)return e.status(403).json({error:"Insufficient permissions"});const{file:i,locked:n}=r.body;if(!i)return e.status(400).json({error:"File path is required"});if("boolean"!=typeof n)return e.status(400).json({error:"locked must be a boolean"});const[a,m]=await setFragmentLocked({rootDir:rootDir,file:i,locked:n});if(a)return e.status(a.code).json({error:a.msg});e.json(m)};
|
|
2
|
+
//# sourceMappingURL=PUT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src/kempo/api/fragments/lock/PUT.js","names":["resolve","currentUserHasPermission","setFragmentLocked","rootDir","dirname","async","request","response","token","cookies","session_token","permError","hasPermission","status","code","json","error","msg","file","locked","body","data"],"sources":["0"],"sourcesContent":["import { resolve } from 'path';\nimport currentUserHasPermission from '../../../../../server/utils/permissions/currentUserHasPermission.js';\nimport setFragmentLocked from '../../../../../server/utils/fragments/setFragmentLocked.js';\n\nconst rootDir = resolve(import.meta.dirname, '../../../../../app-public');\n\nexport default async (request, response) => {\n\tconst token = request.cookies.session_token;\n\tconst [permError, hasPermission] = await currentUserHasPermission(token, 'system:fragments:update');\n\n\tif(permError){\n\t\treturn response.status(permError.code).json({ error: permError.msg });\n\t}\n\n\tif(!hasPermission){\n\t\treturn response.status(403).json({ error: 'Insufficient permissions' });\n\t}\n\n\tconst { file, locked } = request.body;\n\n\tif(!file){\n\t\treturn response.status(400).json({ error: 'File path is required' });\n\t}\n\n\tif(typeof locked !== 'boolean'){\n\t\treturn response.status(400).json({ error: 'locked must be a boolean' });\n\t}\n\n\tconst [error, data] = await setFragmentLocked({ rootDir, file, locked });\n\n\tif(error){\n\t\treturn response.status(error.code).json({ error: error.msg });\n\t}\n\n\tresponse.json(data);\n};\n"],"mappings":"OAASA,YAAe,cACjBC,6BAA8B,6EAC9BC,sBAAuB,6DAE9B,MAAMC,QAAUH,oBAAoBI,QAAS,4CAE9BC,MAAOC,EAASC,KAC9B,MAAMC,EAAQF,EAAQG,QAAQC,eACvBC,EAAWC,SAAuBX,yBAAyBO,EAAO,2BAEzE,GAAGG,EACF,OAAOJ,EAASM,OAAOF,EAAUG,MAAMC,KAAK,CAAEC,MAAOL,EAAUM,MAGhE,IAAIL,EACH,OAAOL,EAASM,OAAO,KAAKE,KAAK,CAAEC,MAAO,6BAG3C,MAAME,KAAEA,EAAIC,OAAEA,GAAWb,EAAQc,KAEjC,IAAIF,EACH,OAAOX,EAASM,OAAO,KAAKE,KAAK,CAAEC,MAAO,0BAG3C,GAAqB,kBAAXG,EACT,OAAOZ,EAASM,OAAO,KAAKE,KAAK,CAAEC,MAAO,6BAG3C,MAAOA,EAAOK,SAAcnB,kBAAkB,CAAEC,gBAASe,OAAMC,WAE/D,GAAGH,EACF,OAAOT,EAASM,OAAOG,EAAMF,MAAMC,KAAK,CAAEC,MAAOA,EAAMC,MAGxDV,EAASQ,KAAKM","ignoreList":[]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{resolve}from"path";import currentUserHasPermission from"../../../../../server/utils/permissions/currentUserHasPermission.js";import setGlobalContentLocked from"../../../../../server/utils/global-content/setGlobalContentLocked.js";const rootDir=resolve(import.meta.dirname,"../../../../../app-public");export default async(o,r)=>{const s=o.cookies.session_token,[e,t]=await currentUserHasPermission(s,"system:globals:update");if(e)return r.status(e.code).json({error:e.msg});if(!t)return r.status(403).json({error:"Insufficient permissions"});const{id:n,locked:i}=o.body;if(!n)return r.status(400).json({error:"Global content ID is required"});if("boolean"!=typeof i)return r.status(400).json({error:"locked must be a boolean"});const[a,l]=await setGlobalContentLocked({rootDir:rootDir,id:n,locked:i});if(a)return r.status(a.code).json({error:a.msg});r.json(l)};
|
|
2
|
+
//# sourceMappingURL=PUT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src/kempo/api/globals/lock/PUT.js","names":["resolve","currentUserHasPermission","setGlobalContentLocked","rootDir","dirname","async","request","response","token","cookies","session_token","permError","hasPermission","status","code","json","error","msg","id","locked","body","data"],"sources":["0"],"sourcesContent":["import { resolve } from 'path';\nimport currentUserHasPermission from '../../../../../server/utils/permissions/currentUserHasPermission.js';\nimport setGlobalContentLocked from '../../../../../server/utils/global-content/setGlobalContentLocked.js';\n\nconst rootDir = resolve(import.meta.dirname, '../../../../../app-public');\n\nexport default async (request, response) => {\n\tconst token = request.cookies.session_token;\n\tconst [permError, hasPermission] = await currentUserHasPermission(token, 'system:globals:update');\n\n\tif(permError){\n\t\treturn response.status(permError.code).json({ error: permError.msg });\n\t}\n\n\tif(!hasPermission){\n\t\treturn response.status(403).json({ error: 'Insufficient permissions' });\n\t}\n\n\tconst { id, locked } = request.body;\n\n\tif(!id){\n\t\treturn response.status(400).json({ error: 'Global content ID is required' });\n\t}\n\n\tif(typeof locked !== 'boolean'){\n\t\treturn response.status(400).json({ error: 'locked must be a boolean' });\n\t}\n\n\tconst [error, data] = await setGlobalContentLocked({ rootDir, id, locked });\n\n\tif(error){\n\t\treturn response.status(error.code).json({ error: error.msg });\n\t}\n\n\tresponse.json(data);\n};\n"],"mappings":"OAASA,YAAe,cACjBC,6BAA8B,6EAC9BC,2BAA4B,uEAEnC,MAAMC,QAAUH,oBAAoBI,QAAS,4CAE9BC,MAAOC,EAASC,KAC9B,MAAMC,EAAQF,EAAQG,QAAQC,eACvBC,EAAWC,SAAuBX,yBAAyBO,EAAO,yBAEzE,GAAGG,EACF,OAAOJ,EAASM,OAAOF,EAAUG,MAAMC,KAAK,CAAEC,MAAOL,EAAUM,MAGhE,IAAIL,EACH,OAAOL,EAASM,OAAO,KAAKE,KAAK,CAAEC,MAAO,6BAG3C,MAAME,GAAEA,EAAEC,OAAEA,GAAWb,EAAQc,KAE/B,IAAIF,EACH,OAAOX,EAASM,OAAO,KAAKE,KAAK,CAAEC,MAAO,kCAG3C,GAAqB,kBAAXG,EACT,OAAOZ,EAASM,OAAO,KAAKE,KAAK,CAAEC,MAAO,6BAG3C,MAAOA,EAAOK,SAAcnB,uBAAuB,CAAEC,gBAASe,KAAIC,WAElE,GAAGH,EACF,OAAOT,EAASM,OAAOG,EAAMF,MAAMC,KAAK,CAAEC,MAAOA,EAAMC,MAGxDV,EAASQ,KAAKM","ignoreList":[]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{resolve}from"path";import currentUserHasPermission from"../../../../../server/utils/permissions/currentUserHasPermission.js";import setPageLocked from"../../../../../server/utils/pages/setPageLocked.js";const rootDir=resolve(import.meta.dirname,"../../../../../app-public");export default async(e,s)=>{const r=e.cookies.session_token,[o,t]=await currentUserHasPermission(r,"system:pages:update");if(o)return s.status(o.code).json({error:o.msg});if(!t)return s.status(403).json({error:"Insufficient permissions"});const{file:i,locked:n}=e.body;if(!i)return s.status(400).json({error:"File path is required"});if("boolean"!=typeof n)return s.status(400).json({error:"locked must be a boolean"});const[a,u]=await setPageLocked({rootDir:rootDir,file:i,locked:n});if(a)return s.status(a.code).json({error:a.msg});s.json(u)};
|
|
2
|
+
//# sourceMappingURL=PUT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src/kempo/api/pages/lock/PUT.js","names":["resolve","currentUserHasPermission","setPageLocked","rootDir","dirname","async","request","response","token","cookies","session_token","permError","hasPermission","status","code","json","error","msg","file","locked","body","data"],"sources":["0"],"sourcesContent":["import { resolve } from 'path';\nimport currentUserHasPermission from '../../../../../server/utils/permissions/currentUserHasPermission.js';\nimport setPageLocked from '../../../../../server/utils/pages/setPageLocked.js';\n\nconst rootDir = resolve(import.meta.dirname, '../../../../../app-public');\n\nexport default async (request, response) => {\n\tconst token = request.cookies.session_token;\n\tconst [permError, hasPermission] = await currentUserHasPermission(token, 'system:pages:update');\n\n\tif(permError){\n\t\treturn response.status(permError.code).json({ error: permError.msg });\n\t}\n\n\tif(!hasPermission){\n\t\treturn response.status(403).json({ error: 'Insufficient permissions' });\n\t}\n\n\tconst { file, locked } = request.body;\n\n\tif(!file){\n\t\treturn response.status(400).json({ error: 'File path is required' });\n\t}\n\n\tif(typeof locked !== 'boolean'){\n\t\treturn response.status(400).json({ error: 'locked must be a boolean' });\n\t}\n\n\tconst [error, data] = await setPageLocked({ rootDir, file, locked });\n\n\tif(error){\n\t\treturn response.status(error.code).json({ error: error.msg });\n\t}\n\n\tresponse.json(data);\n};\n"],"mappings":"OAASA,YAAe,cACjBC,6BAA8B,6EAC9BC,kBAAmB,qDAE1B,MAAMC,QAAUH,oBAAoBI,QAAS,4CAE9BC,MAAOC,EAASC,KAC9B,MAAMC,EAAQF,EAAQG,QAAQC,eACvBC,EAAWC,SAAuBX,yBAAyBO,EAAO,uBAEzE,GAAGG,EACF,OAAOJ,EAASM,OAAOF,EAAUG,MAAMC,KAAK,CAAEC,MAAOL,EAAUM,MAGhE,IAAIL,EACH,OAAOL,EAASM,OAAO,KAAKE,KAAK,CAAEC,MAAO,6BAG3C,MAAME,KAAEA,EAAIC,OAAEA,GAAWb,EAAQc,KAEjC,IAAIF,EACH,OAAOX,EAASM,OAAO,KAAKE,KAAK,CAAEC,MAAO,0BAG3C,GAAqB,kBAAXG,EACT,OAAOZ,EAASM,OAAO,KAAKE,KAAK,CAAEC,MAAO,6BAG3C,MAAOA,EAAOK,SAAcnB,cAAc,CAAEC,gBAASe,OAAMC,WAE3D,GAAGH,EACF,OAAOT,EAASM,OAAOG,EAAMF,MAAMC,KAAK,CAAEC,MAAOA,EAAMC,MAGxDV,EAASQ,KAAKM","ignoreList":[]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{resolve}from"path";import currentUserHasPermission from"../../../../server/utils/permissions/currentUserHasPermission.js";import searchByMetadata from"../../../../server/utils/pages/searchByMetadata.js";const rootDir=resolve(import.meta.dirname,"../../../../app-public");export default async(r,s)=>{const e=r.cookies.session_token,[t,o]=await currentUserHasPermission(e,"system:pages:read");if(t)return s.status(t.code).json({error:t.msg});if(!o)return s.status(403).json({error:"Insufficient permissions"});const{query:a}=r.query;if(!a)return s.status(400).json({error:"query parameter is required"});let i;try{i=JSON.parse(a)}catch{return s.status(400).json({error:"query must be valid JSON"})}if("object"!=typeof i||Array.isArray(i)||!i)return s.status(400).json({error:"query must be a JSON object"});const[n,u]=await searchByMetadata({rootDir:rootDir,query:i});if(n)return s.status(n.code).json({error:n.msg});s.json(u)};
|
|
2
|
+
//# sourceMappingURL=GET.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src/kempo/api/pages/search/GET.js","names":["resolve","currentUserHasPermission","searchByMetadata","rootDir","dirname","async","request","response","token","cookies","session_token","permError","hasPermission","status","code","json","error","msg","query","parsedQuery","JSON","parse","Array","isArray","data"],"sources":["0"],"sourcesContent":["import { resolve } from 'path';\nimport currentUserHasPermission from '../../../../server/utils/permissions/currentUserHasPermission.js';\nimport searchByMetadata from '../../../../server/utils/pages/searchByMetadata.js';\n\nconst rootDir = resolve(import.meta.dirname, '../../../../app-public');\n\nexport default async (request, response) => {\n\tconst token = request.cookies.session_token;\n\tconst [permError, hasPermission] = await currentUserHasPermission(token, 'system:pages:read');\n\n\tif(permError){\n\t\treturn response.status(permError.code).json({ error: permError.msg });\n\t}\n\n\tif(!hasPermission){\n\t\treturn response.status(403).json({ error: 'Insufficient permissions' });\n\t}\n\n\tconst { query } = request.query;\n\n\tif(!query){\n\t\treturn response.status(400).json({ error: 'query parameter is required' });\n\t}\n\n\tlet parsedQuery;\n\ttry {\n\t\tparsedQuery = JSON.parse(query);\n\t} catch {\n\t\treturn response.status(400).json({ error: 'query must be valid JSON' });\n\t}\n\n\tif(typeof parsedQuery !== 'object' || Array.isArray(parsedQuery) || !parsedQuery){\n\t\treturn response.status(400).json({ error: 'query must be a JSON object' });\n\t}\n\n\tconst [error, data] = await searchByMetadata({ rootDir, query: parsedQuery });\n\n\tif(error){\n\t\treturn response.status(error.code).json({ error: error.msg });\n\t}\n\n\tresponse.json(data);\n};\n"],"mappings":"OAASA,YAAe,cACjBC,6BAA8B,0EAC9BC,qBAAsB,qDAE7B,MAAMC,QAAUH,oBAAoBI,QAAS,yCAE9BC,MAAOC,EAASC,KAC9B,MAAMC,EAAQF,EAAQG,QAAQC,eACvBC,EAAWC,SAAuBX,yBAAyBO,EAAO,qBAEzE,GAAGG,EACF,OAAOJ,EAASM,OAAOF,EAAUG,MAAMC,KAAK,CAAEC,MAAOL,EAAUM,MAGhE,IAAIL,EACH,OAAOL,EAASM,OAAO,KAAKE,KAAK,CAAEC,MAAO,6BAG3C,MAAME,MAAEA,GAAUZ,EAAQY,MAE1B,IAAIA,EACH,OAAOX,EAASM,OAAO,KAAKE,KAAK,CAAEC,MAAO,gCAG3C,IAAIG,EACJ,IACCA,EAAcC,KAAKC,MAAMH,EAC1B,CAAE,MACD,OAAOX,EAASM,OAAO,KAAKE,KAAK,CAAEC,MAAO,4BAC3C,CAEA,GAA0B,iBAAhBG,GAA4BG,MAAMC,QAAQJ,KAAiBA,EACpE,OAAOZ,EAASM,OAAO,KAAKE,KAAK,CAAEC,MAAO,gCAG3C,MAAOA,EAAOQ,SAActB,iBAAiB,CAAEC,gBAASe,MAAOC,IAE/D,GAAGH,EACF,OAAOT,EAASM,OAAOG,EAAMF,MAAMC,KAAK,CAAEC,MAAOA,EAAMC,MAGxDV,EAASQ,KAAKS","ignoreList":[]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{resolve}from"path";import currentUserHasPermission from"../../../../../server/utils/permissions/currentUserHasPermission.js";import setTemplateLocked from"../../../../../server/utils/templates/setTemplateLocked.js";const rootDir=resolve(import.meta.dirname,"../../../../../app-public");export default async(e,s)=>{const r=e.cookies.session_token,[o,t]=await currentUserHasPermission(r,"system:pages:update");if(o)return s.status(o.code).json({error:o.msg});if(!t)return s.status(403).json({error:"Insufficient permissions"});const{file:i,locked:n}=e.body;if(!i)return s.status(400).json({error:"File path is required"});if("boolean"!=typeof n)return s.status(400).json({error:"locked must be a boolean"});const[a,m]=await setTemplateLocked({rootDir:rootDir,file:i,locked:n});if(a)return s.status(a.code).json({error:a.msg});s.json(m)};
|
|
2
|
+
//# sourceMappingURL=PUT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"src/kempo/api/templates/lock/PUT.js","names":["resolve","currentUserHasPermission","setTemplateLocked","rootDir","dirname","async","request","response","token","cookies","session_token","permError","hasPermission","status","code","json","error","msg","file","locked","body","data"],"sources":["0"],"sourcesContent":["import { resolve } from 'path';\nimport currentUserHasPermission from '../../../../../server/utils/permissions/currentUserHasPermission.js';\nimport setTemplateLocked from '../../../../../server/utils/templates/setTemplateLocked.js';\n\nconst rootDir = resolve(import.meta.dirname, '../../../../../app-public');\n\nexport default async (request, response) => {\n\tconst token = request.cookies.session_token;\n\tconst [permError, hasPermission] = await currentUserHasPermission(token, 'system:pages:update');\n\n\tif(permError){\n\t\treturn response.status(permError.code).json({ error: permError.msg });\n\t}\n\n\tif(!hasPermission){\n\t\treturn response.status(403).json({ error: 'Insufficient permissions' });\n\t}\n\n\tconst { file, locked } = request.body;\n\n\tif(!file){\n\t\treturn response.status(400).json({ error: 'File path is required' });\n\t}\n\n\tif(typeof locked !== 'boolean'){\n\t\treturn response.status(400).json({ error: 'locked must be a boolean' });\n\t}\n\n\tconst [error, data] = await setTemplateLocked({ rootDir, file, locked });\n\n\tif(error){\n\t\treturn response.status(error.code).json({ error: error.msg });\n\t}\n\n\tresponse.json(data);\n};\n"],"mappings":"OAASA,YAAe,cACjBC,6BAA8B,6EAC9BC,sBAAuB,6DAE9B,MAAMC,QAAUH,oBAAoBI,QAAS,4CAE9BC,MAAOC,EAASC,KAC9B,MAAMC,EAAQF,EAAQG,QAAQC,eACvBC,EAAWC,SAAuBX,yBAAyBO,EAAO,uBAEzE,GAAGG,EACF,OAAOJ,EAASM,OAAOF,EAAUG,MAAMC,KAAK,CAAEC,MAAOL,EAAUM,MAGhE,IAAIL,EACH,OAAOL,EAASM,OAAO,KAAKE,KAAK,CAAEC,MAAO,6BAG3C,MAAME,KAAEA,EAAIC,OAAEA,GAAWb,EAAQc,KAEjC,IAAIF,EACH,OAAOX,EAASM,OAAO,KAAKE,KAAK,CAAEC,MAAO,0BAG3C,GAAqB,kBAAXG,EACT,OAAOZ,EAASM,OAAO,KAAKE,KAAK,CAAEC,MAAO,6BAG3C,MAAOA,EAAOK,SAAcnB,kBAAkB,CAAEC,gBAASe,OAAMC,WAE/D,GAAGH,EACF,OAAOT,EAASM,OAAOG,EAAMF,MAAMC,KAAK,CAAEC,MAAOA,EAAMC,MAGxDV,EAASQ,KAAKM","ignoreList":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kempo",
|
|
3
|
-
"version": "4.2.
|
|
3
|
+
"version": "4.2.16",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A fullstack CMS framework for creating websites with user authentication",
|
|
6
6
|
"scripts": {
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"kempo": "^4.2.4",
|
|
50
50
|
"kempo-server": "^3.1.0",
|
|
51
51
|
"kempo-testing-framework": "^1.4.6",
|
|
52
|
-
"kempo-ui": "^0.
|
|
52
|
+
"kempo-ui": "^0.4.6",
|
|
53
53
|
"lexical": "^0.42.0",
|
|
54
54
|
"monaco-editor": "^0.55.1",
|
|
55
55
|
"postgres": "^3.4.7",
|
package/server/sdk.js
CHANGED
|
@@ -103,6 +103,8 @@ export { default as listDirectories } from './utils/pages/listDirectories.js';
|
|
|
103
103
|
export { default as listPages } from './utils/pages/listPages.js';
|
|
104
104
|
export { default as listTemplates } from './utils/pages/listTemplates.js';
|
|
105
105
|
export { default as movePage } from './utils/pages/movePage.js';
|
|
106
|
+
export { default as searchByMetadata } from './utils/pages/searchByMetadata.js';
|
|
107
|
+
export { default as setPageLocked } from './utils/pages/setPageLocked.js';
|
|
106
108
|
export { default as updatePage } from './utils/pages/updatePage.js';
|
|
107
109
|
|
|
108
110
|
/*
|
|
@@ -114,6 +116,7 @@ export { default as deleteTemplate } from './utils/templates/deleteTemplate.js';
|
|
|
114
116
|
export { default as disableTemplate } from './utils/templates/disableTemplate.js';
|
|
115
117
|
export { default as enableTemplate } from './utils/templates/enableTemplate.js';
|
|
116
118
|
export { default as getTemplate } from './utils/templates/getTemplate.js';
|
|
119
|
+
export { default as setTemplateLocked } from './utils/templates/setTemplateLocked.js';
|
|
117
120
|
export { default as updateTemplate } from './utils/templates/updateTemplate.js';
|
|
118
121
|
|
|
119
122
|
/*
|
|
@@ -126,6 +129,7 @@ export { default as disableFragment } from './utils/fragments/disableFragment.js
|
|
|
126
129
|
export { default as enableFragment } from './utils/fragments/enableFragment.js';
|
|
127
130
|
export { default as getFragment } from './utils/fragments/getFragment.js';
|
|
128
131
|
export { default as listFragments } from './utils/fragments/listFragments.js';
|
|
132
|
+
export { default as setFragmentLocked } from './utils/fragments/setFragmentLocked.js';
|
|
129
133
|
export { default as updateFragment } from './utils/fragments/updateFragment.js';
|
|
130
134
|
|
|
131
135
|
/*
|
|
@@ -138,6 +142,7 @@ export { default as disableGlobalContent } from './utils/global-content/disableG
|
|
|
138
142
|
export { default as enableGlobalContent } from './utils/global-content/enableGlobalContent.js';
|
|
139
143
|
export { default as getGlobalContent } from './utils/global-content/getGlobalContent.js';
|
|
140
144
|
export { default as listGlobalContent } from './utils/global-content/listGlobalContent.js';
|
|
145
|
+
export { default as setGlobalContentLocked } from './utils/global-content/setGlobalContentLocked.js';
|
|
141
146
|
export { default as updateGlobalContent } from './utils/global-content/updateGlobalContent.js';
|
|
142
147
|
|
|
143
148
|
/*
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { writeFile, mkdir } from 'fs/promises';
|
|
2
2
|
import { join, dirname } from 'path';
|
|
3
3
|
import { existsSync } from 'fs';
|
|
4
|
+
import triggerHook from '../hooks/triggerHook.js';
|
|
4
5
|
|
|
5
6
|
const slugify = name => name
|
|
6
7
|
.toLowerCase()
|
|
@@ -10,7 +11,7 @@ const slugify = name => name
|
|
|
10
11
|
.replace(/-+/g, '-')
|
|
11
12
|
.replace(/^-|-$/g, '');
|
|
12
13
|
|
|
13
|
-
export default async ({ rootDir, directory, name, author }) => {
|
|
14
|
+
export default async ({ rootDir, directory, name, author, locked = false }) => {
|
|
14
15
|
if(!rootDir){
|
|
15
16
|
return [{ code: 400, msg: 'Root directory is required' }, null];
|
|
16
17
|
}
|
|
@@ -42,6 +43,7 @@ export default async ({ rootDir, directory, name, author }) => {
|
|
|
42
43
|
` author: ${author || ''}`,
|
|
43
44
|
` created: ${now}`,
|
|
44
45
|
` updated: ${now}`,
|
|
46
|
+
...(locked ? [' locked: true'] : []),
|
|
45
47
|
'-->'
|
|
46
48
|
].join('\n');
|
|
47
49
|
|
|
@@ -53,5 +55,7 @@ export default async ({ rootDir, directory, name, author }) => {
|
|
|
53
55
|
|
|
54
56
|
const relPath = dir ? `${dir}/${slug}` : slug;
|
|
55
57
|
|
|
58
|
+
await triggerHook('fragment:created', { file: `${relPath}.fragment.html`, name, slug });
|
|
59
|
+
|
|
56
60
|
return [null, { file: `${relPath}.fragment.html`, name, slug }];
|
|
57
61
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFile, unlink } from 'fs/promises';
|
|
2
2
|
import { join, normalize, sep } from 'path';
|
|
3
3
|
import parseFrontmatter from '../fs/parseFrontmatter.js';
|
|
4
|
+
import triggerHook from '../hooks/triggerHook.js';
|
|
4
5
|
|
|
5
6
|
export default async ({ rootDir, files }) => {
|
|
6
7
|
if(!rootDir) return [{ code: 400, msg: 'Root directory is required' }, null];
|
|
@@ -21,14 +22,12 @@ export default async ({ rootDir, files }) => {
|
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
const meta = parseFrontmatter(content);
|
|
24
|
-
if(meta.locked === 'true'){
|
|
25
|
-
return [{ code: 403, msg: `Cannot delete locked fragment: ${file}` }, null];
|
|
26
|
-
}
|
|
27
25
|
if(meta.owner && meta.owner !== 'custom'){
|
|
28
26
|
return [{ code: 403, msg: `Cannot delete fragment: ${file}` }, null];
|
|
29
27
|
}
|
|
30
28
|
|
|
31
29
|
await unlink(fullPath);
|
|
30
|
+
await triggerHook('fragment:deleted', { file });
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
return [null, { deleted: files.length }];
|
|
@@ -23,9 +23,6 @@ export default async ({ rootDir, file }) => {
|
|
|
23
23
|
|
|
24
24
|
const content = await readFile(fullPath, 'utf-8');
|
|
25
25
|
const meta = parseFrontmatter(content);
|
|
26
|
-
if(meta.locked === 'true'){
|
|
27
|
-
return [{ code: 403, msg: 'Cannot disable a locked fragment' }, null];
|
|
28
|
-
}
|
|
29
26
|
|
|
30
27
|
const disabledPath = fullPath.replace(/\.fragment\.html$/, '.fragment-disabled.html');
|
|
31
28
|
if(existsSync(disabledPath)){
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { readFile } from 'fs/promises';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import parseFrontmatter from '../fs/parseFrontmatter.js';
|
|
5
|
+
|
|
6
|
+
export default async ({ rootDir, file }) => {
|
|
7
|
+
if(!rootDir){
|
|
8
|
+
return [{ code: 400, msg: 'Root directory is required' }, null];
|
|
9
|
+
}
|
|
10
|
+
if(!file){
|
|
11
|
+
return [{ code: 400, msg: 'File path is required' }, null];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const safePath = file.replace(/\.\./g, '').replace(/^\//, '');
|
|
15
|
+
const fullPath = join(rootDir, safePath);
|
|
16
|
+
|
|
17
|
+
if(!existsSync(fullPath)){
|
|
18
|
+
return [{ code: 404, msg: 'Fragment not found' }, null];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const raw = await readFile(fullPath, 'utf-8');
|
|
22
|
+
const meta = parseFrontmatter(raw);
|
|
23
|
+
|
|
24
|
+
return [null, {
|
|
25
|
+
name: meta.name,
|
|
26
|
+
owner: meta.owner,
|
|
27
|
+
locked: meta.locked === 'true',
|
|
28
|
+
author: meta.author,
|
|
29
|
+
created: meta.created,
|
|
30
|
+
updated: meta.updated
|
|
31
|
+
}];
|
|
32
|
+
};
|
|
@@ -28,9 +28,6 @@ export default async ({ rootDir, file, newFile }) => {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
const meta = parseFrontmatter(content);
|
|
31
|
-
if(meta.locked === 'true'){
|
|
32
|
-
return [{ code: 403, msg: 'Cannot move a locked fragment' }, null];
|
|
33
|
-
}
|
|
34
31
|
if(meta.owner && meta.owner !== 'custom'){
|
|
35
32
|
return [{ code: 403, msg: 'Cannot move system fragments' }, null];
|
|
36
33
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'fs/promises';
|
|
2
|
+
import { join, normalize, sep } from 'path';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
|
|
5
|
+
export default async ({ rootDir, file, locked }) => {
|
|
6
|
+
if(!rootDir) return [{ code: 400, msg: 'Root directory is required' }, null];
|
|
7
|
+
if(!file) return [{ code: 400, msg: 'File path is required' }, null];
|
|
8
|
+
if(typeof locked !== 'boolean') return [{ code: 400, msg: 'locked must be a boolean' }, null];
|
|
9
|
+
|
|
10
|
+
const normalized = normalize(file);
|
|
11
|
+
if(normalized.startsWith('..') || normalized.includes('..' + sep)){
|
|
12
|
+
return [{ code: 400, msg: 'Invalid file path' }, null];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const fullPath = join(rootDir, normalized);
|
|
16
|
+
if(!existsSync(fullPath)){
|
|
17
|
+
return [{ code: 404, msg: 'Fragment not found' }, null];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const raw = await readFile(fullPath, 'utf-8');
|
|
21
|
+
const frontmatterMatch = raw.match(/^<!--\s*\n([\s\S]*?)\n\s*-->/);
|
|
22
|
+
const existingMeta = {};
|
|
23
|
+
|
|
24
|
+
if(frontmatterMatch){
|
|
25
|
+
for(const line of frontmatterMatch[1].split('\n')){
|
|
26
|
+
const idx = line.indexOf(':');
|
|
27
|
+
if(idx === -1) continue;
|
|
28
|
+
const key = line.slice(0, idx).trim();
|
|
29
|
+
const value = line.slice(idx + 1).trim();
|
|
30
|
+
if(key) existingMeta[key] = value;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if(locked){
|
|
35
|
+
existingMeta.locked = 'true';
|
|
36
|
+
} else {
|
|
37
|
+
delete existingMeta.locked;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const frontmatter = '<!--\n' +
|
|
41
|
+
Object.entries(existingMeta).map(([k, v]) => ` ${k}: ${v}`).join('\n') +
|
|
42
|
+
'\n-->';
|
|
43
|
+
|
|
44
|
+
const body = frontmatterMatch ? raw.slice(frontmatterMatch[0].length) : '\n' + raw;
|
|
45
|
+
await writeFile(fullPath, frontmatter + body, 'utf-8');
|
|
46
|
+
|
|
47
|
+
return [null, { file: normalized, locked }];
|
|
48
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFile, writeFile } from 'fs/promises';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { existsSync } from 'fs';
|
|
4
|
+
import triggerHook from '../hooks/triggerHook.js';
|
|
4
5
|
|
|
5
6
|
export default async ({ rootDir, file, author, markup }) => {
|
|
6
7
|
if(!rootDir){
|
|
@@ -19,9 +20,6 @@ export default async ({ rootDir, file, author, markup }) => {
|
|
|
19
20
|
|
|
20
21
|
const raw = await readFile(fullPath, 'utf-8');
|
|
21
22
|
|
|
22
|
-
const lockedMatch = raw.match(/^<!--[\s\S]*?locked:\s*true[\s\S]*?-->/);
|
|
23
|
-
if(lockedMatch) return [{ code: 403, msg: 'This fragment is locked and cannot be edited' }, null];
|
|
24
|
-
|
|
25
23
|
const frontmatterMatch = raw.match(/^<!--\s*\n([\s\S]*?)\n\s*-->/);
|
|
26
24
|
const existingMeta = {};
|
|
27
25
|
if(frontmatterMatch){
|
|
@@ -54,5 +52,7 @@ export default async ({ rootDir, file, author, markup }) => {
|
|
|
54
52
|
|
|
55
53
|
await writeFile(fullPath, newContent, 'utf-8');
|
|
56
54
|
|
|
55
|
+
await triggerHook('fragment:updated', { file: safePath, updatedAt: now });
|
|
56
|
+
|
|
57
57
|
return [null, { file: safePath, updatedAt: now }];
|
|
58
58
|
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { readFile } from 'fs/promises';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import parseFrontmatter from './parseFrontmatter.js';
|
|
5
|
+
|
|
6
|
+
export default async ({ rootDir, file, resourceType = 'file' }) => {
|
|
7
|
+
if(!rootDir || !file){
|
|
8
|
+
return [{ code: 400, msg: 'Root directory and file path are required' }, null];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const safePath = file.replace(/\.\./g, '').replace(/^\//, '');
|
|
12
|
+
const fullPath = join(rootDir, safePath);
|
|
13
|
+
|
|
14
|
+
if(!existsSync(fullPath)){
|
|
15
|
+
return [{ code: 404, msg: `${resourceType} not found` }, null];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const raw = await readFile(fullPath, 'utf-8');
|
|
19
|
+
const meta = parseFrontmatter(raw);
|
|
20
|
+
|
|
21
|
+
return [null, meta.locked === 'true'];
|
|
22
|
+
};
|
|
@@ -3,6 +3,7 @@ import { join } from 'path';
|
|
|
3
3
|
import { existsSync } from 'fs';
|
|
4
4
|
import crypto from 'crypto';
|
|
5
5
|
import { GLOBAL_FILE, parseEntries } from './helpers.js';
|
|
6
|
+
import triggerHook from '../hooks/triggerHook.js';
|
|
6
7
|
|
|
7
8
|
const serializeEntries = entries => entries.map(e => {
|
|
8
9
|
const attrs = [
|
|
@@ -54,5 +55,7 @@ export default async ({ rootDir, name, location, priority, author }) => {
|
|
|
54
55
|
entries.push(entry);
|
|
55
56
|
await writeFile(filePath, serializeEntries(entries) + '\n', 'utf-8');
|
|
56
57
|
|
|
58
|
+
await triggerHook('globalContent:created', { id: entry.id, name, location });
|
|
59
|
+
|
|
57
60
|
return [null, entry];
|
|
58
61
|
};
|
|
@@ -2,6 +2,7 @@ import { readFile, writeFile } from 'fs/promises';
|
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { existsSync } from 'fs';
|
|
4
4
|
import { GLOBAL_FILE, parseEntries } from './helpers.js';
|
|
5
|
+
import triggerHook from '../hooks/triggerHook.js';
|
|
5
6
|
|
|
6
7
|
const serializeEntries = entries => entries.map(e => {
|
|
7
8
|
const attrs = [
|
|
@@ -37,9 +38,6 @@ export default async ({ rootDir, ids }) => {
|
|
|
37
38
|
|
|
38
39
|
for(const entry of entries){
|
|
39
40
|
if(!idSet.has(entry.id)) continue;
|
|
40
|
-
if(entry.locked){
|
|
41
|
-
return [{ code: 403, msg: `Cannot delete locked global content: ${entry.name}` }, null];
|
|
42
|
-
}
|
|
43
41
|
if(entry.owner && entry.owner !== 'custom'){
|
|
44
42
|
return [{ code: 403, msg: `Cannot delete global content: ${entry.name}` }, null];
|
|
45
43
|
}
|
|
@@ -48,5 +46,7 @@ export default async ({ rootDir, ids }) => {
|
|
|
48
46
|
const remaining = entries.filter(e => !idSet.has(e.id));
|
|
49
47
|
await writeFile(filePath, remaining.length ? serializeEntries(remaining) + '\n' : '', 'utf-8');
|
|
50
48
|
|
|
49
|
+
for(const id of ids) await triggerHook('globalContent:deleted', { id });
|
|
50
|
+
|
|
51
51
|
return [null, { deleted: ids.length }];
|
|
52
52
|
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { readFile } from 'fs/promises';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import { GLOBAL_FILE, parseEntries } from './helpers.js';
|
|
5
|
+
|
|
6
|
+
export default async ({ rootDir, id }) => {
|
|
7
|
+
if(!rootDir){
|
|
8
|
+
return [{ code: 400, msg: 'Root directory is required' }, null];
|
|
9
|
+
}
|
|
10
|
+
if(!id){
|
|
11
|
+
return [{ code: 400, msg: 'Global content ID is required' }, null];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const filePath = join(rootDir, GLOBAL_FILE);
|
|
15
|
+
if(!existsSync(filePath)){
|
|
16
|
+
return [{ code: 404, msg: 'Global content not found' }, null];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const raw = await readFile(filePath, 'utf-8');
|
|
20
|
+
const entries = parseEntries(raw);
|
|
21
|
+
const entry = entries.find(e => e.id === id);
|
|
22
|
+
|
|
23
|
+
if(!entry){
|
|
24
|
+
return [{ code: 404, msg: 'Global content not found' }, null];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return [null, {
|
|
28
|
+
name: entry.name,
|
|
29
|
+
owner: entry.owner,
|
|
30
|
+
locked: entry.locked,
|
|
31
|
+
author: entry.author,
|
|
32
|
+
createdAt: entry.createdAt,
|
|
33
|
+
updatedAt: entry.updatedAt,
|
|
34
|
+
location: entry.location,
|
|
35
|
+
priority: entry.priority
|
|
36
|
+
}];
|
|
37
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'fs/promises';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import { GLOBAL_FILE, parseEntries } from './helpers.js';
|
|
5
|
+
|
|
6
|
+
const serializeEntries = entries => entries.map(e => {
|
|
7
|
+
const attrs = [
|
|
8
|
+
`id="${e.id}"`,
|
|
9
|
+
`name="${e.name}"`,
|
|
10
|
+
`owner="${e.owner}"`,
|
|
11
|
+
`author="${e.author}"`,
|
|
12
|
+
`created="${e.createdAt}"`,
|
|
13
|
+
`updated="${e.updatedAt}"`,
|
|
14
|
+
`locked="${e.locked}"`,
|
|
15
|
+
`location="${e.location}"`,
|
|
16
|
+
`priority="${e.priority}"`
|
|
17
|
+
].join(' ');
|
|
18
|
+
return `<content ${attrs}>\n${e.markup}\n</content>`;
|
|
19
|
+
}).join('\n');
|
|
20
|
+
|
|
21
|
+
export default async ({ rootDir, id, locked }) => {
|
|
22
|
+
if(!rootDir) return [{ code: 400, msg: 'Root directory is required' }, null];
|
|
23
|
+
if(!id) return [{ code: 400, msg: 'Global content ID is required' }, null];
|
|
24
|
+
if(typeof locked !== 'boolean') return [{ code: 400, msg: 'locked must be a boolean' }, null];
|
|
25
|
+
|
|
26
|
+
const filePath = join(rootDir, GLOBAL_FILE);
|
|
27
|
+
if(!existsSync(filePath)){
|
|
28
|
+
return [{ code: 404, msg: 'Global content not found' }, null];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const raw = await readFile(filePath, 'utf-8');
|
|
32
|
+
const entries = parseEntries(raw);
|
|
33
|
+
const idx = entries.findIndex(e => e.id === id);
|
|
34
|
+
|
|
35
|
+
if(idx === -1){
|
|
36
|
+
return [{ code: 404, msg: 'Global content not found' }, null];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
entries[idx].locked = locked;
|
|
40
|
+
|
|
41
|
+
await writeFile(filePath, serializeEntries(entries) + '\n', 'utf-8');
|
|
42
|
+
|
|
43
|
+
return [null, { id, locked }];
|
|
44
|
+
};
|
|
@@ -2,6 +2,7 @@ import { readFile, writeFile } from 'fs/promises';
|
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { existsSync } from 'fs';
|
|
4
4
|
import { GLOBAL_FILE, parseEntries } from './helpers.js';
|
|
5
|
+
import triggerHook from '../hooks/triggerHook.js';
|
|
5
6
|
|
|
6
7
|
const serializeEntries = entries => entries.map(e => {
|
|
7
8
|
const attrs = [
|
|
@@ -39,10 +40,6 @@ export default async ({ rootDir, id, name, location, priority, markup }) => {
|
|
|
39
40
|
return [{ code: 404, msg: 'Global content not found' }, null];
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
if(entries[idx].locked){
|
|
43
|
-
return [{ code: 403, msg: 'This global content is locked and cannot be edited' }, null];
|
|
44
|
-
}
|
|
45
|
-
|
|
46
43
|
const now = new Date().toISOString();
|
|
47
44
|
if(name !== undefined) entries[idx].name = name;
|
|
48
45
|
if(location !== undefined) entries[idx].location = location;
|
|
@@ -52,5 +49,7 @@ export default async ({ rootDir, id, name, location, priority, markup }) => {
|
|
|
52
49
|
|
|
53
50
|
await writeFile(filePath, serializeEntries(entries) + '\n', 'utf-8');
|
|
54
51
|
|
|
52
|
+
await triggerHook('globalContent:updated', { id, updatedAt: now });
|
|
53
|
+
|
|
55
54
|
return [null, { id, updatedAt: now }];
|
|
56
55
|
};
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { writeFile, mkdir } from 'fs/promises';
|
|
2
2
|
import { join, dirname } from 'path';
|
|
3
3
|
import { existsSync } from 'fs';
|
|
4
|
+
import triggerHook from '../hooks/triggerHook.js';
|
|
5
|
+
|
|
6
|
+
const RESERVED_KEYS = new Set(['owner', 'name', 'author', 'created', 'updated', 'locked', 'title', 'description']);
|
|
4
7
|
|
|
5
8
|
const slugify = name => name
|
|
6
9
|
.toLowerCase()
|
|
@@ -10,7 +13,7 @@ const slugify = name => name
|
|
|
10
13
|
.replace(/-+/g, '-')
|
|
11
14
|
.replace(/^-|-$/g, '');
|
|
12
15
|
|
|
13
|
-
export default async ({ rootDir, directory, name, template, author }) => {
|
|
16
|
+
export default async ({ rootDir, directory, name, template, author, locked = false, extraMetadata = {} }) => {
|
|
14
17
|
if(!rootDir){
|
|
15
18
|
return [{ code: 400, msg: 'Root directory is required' }, null];
|
|
16
19
|
}
|
|
@@ -34,18 +37,24 @@ export default async ({ rootDir, directory, name, template, author }) => {
|
|
|
34
37
|
return [{ code: 409, msg: 'A page with this name already exists in this location' }, null];
|
|
35
38
|
}
|
|
36
39
|
|
|
40
|
+
const conflictingKeys = Object.keys(extraMetadata).filter(k => RESERVED_KEYS.has(k));
|
|
41
|
+
if(conflictingKeys.length){
|
|
42
|
+
return [{ code: 400, msg: `Cannot use reserved metadata keys: ${conflictingKeys.join(', ')}` }, null];
|
|
43
|
+
}
|
|
44
|
+
|
|
37
45
|
const templateName = template || 'default';
|
|
38
46
|
|
|
39
47
|
const now = new Date().toISOString();
|
|
40
|
-
const
|
|
41
|
-
'<!--',
|
|
48
|
+
const defaultLines = [
|
|
42
49
|
` owner: custom`,
|
|
43
50
|
` name: ${name}`,
|
|
44
51
|
` author: ${author || ''}`,
|
|
45
52
|
` created: ${now}`,
|
|
46
|
-
` updated: ${now}
|
|
47
|
-
|
|
48
|
-
|
|
53
|
+
` updated: ${now}`
|
|
54
|
+
];
|
|
55
|
+
if(locked) defaultLines.push(` locked: true`);
|
|
56
|
+
const extraLines = Object.entries(extraMetadata).map(([k, v]) => ` ${k}: ${v}`);
|
|
57
|
+
const frontmatter = ['<!--', ...defaultLines, ...extraLines, '-->'].join('\n');
|
|
49
58
|
|
|
50
59
|
const pageContent = `${frontmatter}\n<page template="${templateName}" title="${name}">\n</page>\n`;
|
|
51
60
|
|
|
@@ -55,6 +64,9 @@ export default async ({ rootDir, directory, name, template, author }) => {
|
|
|
55
64
|
|
|
56
65
|
const relPath = (dir ? `${dir}/${slug}` : slug);
|
|
57
66
|
const url = '/' + relPath;
|
|
67
|
+
const result = { file: `${relPath}.page.html`, url, title: name, slug };
|
|
68
|
+
|
|
69
|
+
await triggerHook('page:created', { file: result.file, url, name, author, extraMetadata });
|
|
58
70
|
|
|
59
|
-
return [null,
|
|
71
|
+
return [null, result];
|
|
60
72
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFile, unlink } from 'fs/promises';
|
|
2
2
|
import { join, normalize, sep } from 'path';
|
|
3
3
|
import parseFrontmatter from '../fs/parseFrontmatter.js';
|
|
4
|
+
import triggerHook from '../hooks/triggerHook.js';
|
|
4
5
|
|
|
5
6
|
export default async ({ rootDir, files }) => {
|
|
6
7
|
if(!rootDir) return [{ code: 400, msg: 'Root directory is required' }, null];
|
|
@@ -21,14 +22,12 @@ export default async ({ rootDir, files }) => {
|
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
const meta = parseFrontmatter(content);
|
|
24
|
-
if(meta.locked === 'true'){
|
|
25
|
-
return [{ code: 403, msg: `Cannot delete locked page: ${file}` }, null];
|
|
26
|
-
}
|
|
27
25
|
if(meta.owner && meta.owner !== 'custom'){
|
|
28
26
|
return [{ code: 403, msg: `Cannot delete page: ${file}` }, null];
|
|
29
27
|
}
|
|
30
28
|
|
|
31
29
|
await unlink(fullPath);
|
|
30
|
+
await triggerHook('page:deleted', { file });
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
return [null, { deleted: files.length }];
|
|
@@ -23,9 +23,6 @@ export default async ({ rootDir, file }) => {
|
|
|
23
23
|
|
|
24
24
|
const content = await readFile(fullPath, 'utf-8');
|
|
25
25
|
const meta = parseFrontmatter(content);
|
|
26
|
-
if(meta.locked === 'true'){
|
|
27
|
-
return [{ code: 403, msg: 'Cannot disable a locked page' }, null];
|
|
28
|
-
}
|
|
29
26
|
|
|
30
27
|
const disabledPath = fullPath.replace(/\.page\.html$/, '.page-disabled.html');
|
|
31
28
|
if(existsSync(disabledPath)){
|
|
@@ -3,6 +3,8 @@ import { join } from 'path';
|
|
|
3
3
|
import { existsSync } from 'fs';
|
|
4
4
|
import parseFrontmatter from '../fs/parseFrontmatter.js';
|
|
5
5
|
|
|
6
|
+
const RESERVED_KEYS = new Set(['owner', 'name', 'author', 'created', 'updated', 'locked', 'title', 'description']);
|
|
7
|
+
|
|
6
8
|
const parseContents = content => {
|
|
7
9
|
const blocks = [];
|
|
8
10
|
const regex = /<content(?:\s+location="([^"]*)")?\s*>([\s\S]*?)<\/content>/g;
|
|
@@ -37,6 +39,10 @@ export default async ({ rootDir, file }) => {
|
|
|
37
39
|
const templateMatch = raw.match(/<page\s[^>]*template="([^"]*)"/);
|
|
38
40
|
const contents = parseContents(raw);
|
|
39
41
|
|
|
42
|
+
const extraMetadata = Object.fromEntries(
|
|
43
|
+
Object.entries(meta).filter(([k]) => !RESERVED_KEYS.has(k))
|
|
44
|
+
);
|
|
45
|
+
|
|
40
46
|
return [null, {
|
|
41
47
|
file: safePath,
|
|
42
48
|
name: meta.name || '',
|
|
@@ -48,6 +54,7 @@ export default async ({ rootDir, file }) => {
|
|
|
48
54
|
description: meta.description || '',
|
|
49
55
|
createdAt: meta.created || '',
|
|
50
56
|
updatedAt: meta.updated || '',
|
|
57
|
+
extraMetadata,
|
|
51
58
|
contents
|
|
52
59
|
}];
|
|
53
60
|
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { readFile } from 'fs/promises';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import parseFrontmatter from '../fs/parseFrontmatter.js';
|
|
5
|
+
|
|
6
|
+
export default async ({ rootDir, file }) => {
|
|
7
|
+
if(!rootDir){
|
|
8
|
+
return [{ code: 400, msg: 'Root directory is required' }, null];
|
|
9
|
+
}
|
|
10
|
+
if(!file){
|
|
11
|
+
return [{ code: 400, msg: 'File path is required' }, null];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const safePath = file.replace(/\.\./g, '').replace(/^\//, '');
|
|
15
|
+
const fullPath = join(rootDir, safePath);
|
|
16
|
+
|
|
17
|
+
if(!existsSync(fullPath)){
|
|
18
|
+
return [{ code: 404, msg: 'Page not found' }, null];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const raw = await readFile(fullPath, 'utf-8');
|
|
22
|
+
const meta = parseFrontmatter(raw);
|
|
23
|
+
|
|
24
|
+
return [null, {
|
|
25
|
+
name: meta.name,
|
|
26
|
+
owner: meta.owner,
|
|
27
|
+
locked: meta.locked === 'true',
|
|
28
|
+
author: meta.author,
|
|
29
|
+
created: meta.created,
|
|
30
|
+
updated: meta.updated
|
|
31
|
+
}];
|
|
32
|
+
};
|
|
@@ -3,7 +3,9 @@ import { relative } from 'path';
|
|
|
3
3
|
import scanDir from '../fs/scanDir.js';
|
|
4
4
|
import parseFrontmatter from '../fs/parseFrontmatter.js';
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
const RESERVED_KEYS = new Set(['owner', 'name', 'author', 'created', 'updated', 'locked', 'title', 'description']);
|
|
7
|
+
|
|
8
|
+
export default async ({ rootDir, owner }) => {
|
|
7
9
|
if(!rootDir){
|
|
8
10
|
return [{ code: 400, msg: 'Root directory is required' }, null];
|
|
9
11
|
}
|
|
@@ -20,6 +22,9 @@ export default async ({ rootDir }) => {
|
|
|
20
22
|
.replace(/\.page-disabled\.html$/, '')
|
|
21
23
|
.replace(/\.page\.html$/, '')
|
|
22
24
|
.replace(/(^|\/)index$/, '$1');
|
|
25
|
+
const extraMetadata = Object.fromEntries(
|
|
26
|
+
Object.entries(meta).filter(([k]) => !RESERVED_KEYS.has(k))
|
|
27
|
+
);
|
|
23
28
|
return {
|
|
24
29
|
file: relPath,
|
|
25
30
|
url: urlPath || '/',
|
|
@@ -31,10 +36,13 @@ export default async ({ rootDir }) => {
|
|
|
31
36
|
author: meta.author || '',
|
|
32
37
|
description: meta.description || '',
|
|
33
38
|
createdAt: meta.created || '',
|
|
34
|
-
updatedAt: meta.updated || ''
|
|
39
|
+
updatedAt: meta.updated || '',
|
|
40
|
+
extraMetadata
|
|
35
41
|
};
|
|
36
42
|
});
|
|
37
43
|
|
|
38
|
-
|
|
44
|
+
const filtered = owner ? pages.filter(page => page.owner === owner) : pages;
|
|
45
|
+
|
|
46
|
+
return [null, { pages: filtered, total: filtered.length }];
|
|
39
47
|
};
|
|
40
48
|
|
|
@@ -28,9 +28,6 @@ export default async ({ rootDir, file, newFile }) => {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
const meta = parseFrontmatter(content);
|
|
31
|
-
if(meta.locked === 'true'){
|
|
32
|
-
return [{ code: 403, msg: 'Cannot move a locked page' }, null];
|
|
33
|
-
}
|
|
34
31
|
if(meta.owner && meta.owner !== 'custom'){
|
|
35
32
|
return [{ code: 403, msg: 'Cannot move system pages' }, null];
|
|
36
33
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { readFile } from 'fs/promises';
|
|
2
|
+
import { relative } from 'path';
|
|
3
|
+
import scanDir from '../fs/scanDir.js';
|
|
4
|
+
import parseFrontmatter from '../fs/parseFrontmatter.js';
|
|
5
|
+
|
|
6
|
+
const RESERVED_KEYS = new Set(['owner', 'name', 'author', 'created', 'updated', 'locked', 'title', 'description']);
|
|
7
|
+
|
|
8
|
+
export default async ({ rootDir, query = {} }) => {
|
|
9
|
+
if(!rootDir){
|
|
10
|
+
return [{ code: 400, msg: 'Root directory is required' }, null];
|
|
11
|
+
}
|
|
12
|
+
if(!Object.keys(query).length){
|
|
13
|
+
return [{ code: 400, msg: 'Search query must have at least one key' }, null];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const queryKeys = Object.keys(query);
|
|
17
|
+
const reservedConflicts = queryKeys.filter(k => RESERVED_KEYS.has(k));
|
|
18
|
+
if(reservedConflicts.length){
|
|
19
|
+
return [{ code: 400, msg: `Cannot search reserved metadata keys: ${reservedConflicts.join(', ')}` }, null];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const pages = await scanDir(rootDir, rootDir, async (fullPath, entry, root) => {
|
|
23
|
+
if(entry.isDirectory() || !entry.name.endsWith('.page.html')) return null;
|
|
24
|
+
const content = await readFile(fullPath, 'utf-8');
|
|
25
|
+
const meta = parseFrontmatter(content);
|
|
26
|
+
|
|
27
|
+
const matches = queryKeys.every(k => {
|
|
28
|
+
const val = meta[k];
|
|
29
|
+
if(val === undefined) return false;
|
|
30
|
+
const expected = String(query[k]);
|
|
31
|
+
return val === expected;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if(!matches) return null;
|
|
35
|
+
|
|
36
|
+
const relPath = relative(root, fullPath).replace(/\\/g, '/');
|
|
37
|
+
const urlPath = '/' + relPath
|
|
38
|
+
.replace(/\.page\.html$/, '')
|
|
39
|
+
.replace(/(^|\/)index$/, '$1');
|
|
40
|
+
|
|
41
|
+
const extraMetadata = Object.fromEntries(
|
|
42
|
+
Object.entries(meta).filter(([k]) => !RESERVED_KEYS.has(k))
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
file: relPath,
|
|
47
|
+
url: urlPath || '/',
|
|
48
|
+
name: meta.name || meta.title || '',
|
|
49
|
+
title: meta.title || '',
|
|
50
|
+
owner: meta.owner || 'custom',
|
|
51
|
+
locked: meta.locked === 'true',
|
|
52
|
+
author: meta.author || '',
|
|
53
|
+
description: meta.description || '',
|
|
54
|
+
createdAt: meta.created || '',
|
|
55
|
+
updatedAt: meta.updated || '',
|
|
56
|
+
extraMetadata
|
|
57
|
+
};
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return [null, { pages, total: pages.length }];
|
|
61
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'fs/promises';
|
|
2
|
+
import { join, normalize, sep } from 'path';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
|
|
5
|
+
export default async ({ rootDir, file, locked }) => {
|
|
6
|
+
if(!rootDir) return [{ code: 400, msg: 'Root directory is required' }, null];
|
|
7
|
+
if(!file) return [{ code: 400, msg: 'File path is required' }, null];
|
|
8
|
+
if(typeof locked !== 'boolean') return [{ code: 400, msg: 'locked must be a boolean' }, null];
|
|
9
|
+
|
|
10
|
+
const normalized = normalize(file);
|
|
11
|
+
if(normalized.startsWith('..') || normalized.includes('..' + sep)){
|
|
12
|
+
return [{ code: 400, msg: 'Invalid file path' }, null];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const fullPath = join(rootDir, normalized);
|
|
16
|
+
if(!existsSync(fullPath)){
|
|
17
|
+
return [{ code: 404, msg: 'Page not found' }, null];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const raw = await readFile(fullPath, 'utf-8');
|
|
21
|
+
const frontmatterMatch = raw.match(/^<!--\s*\n([\s\S]*?)\n\s*-->/);
|
|
22
|
+
const existingMeta = {};
|
|
23
|
+
|
|
24
|
+
if(frontmatterMatch){
|
|
25
|
+
for(const line of frontmatterMatch[1].split('\n')){
|
|
26
|
+
const idx = line.indexOf(':');
|
|
27
|
+
if(idx === -1) continue;
|
|
28
|
+
const key = line.slice(0, idx).trim();
|
|
29
|
+
const value = line.slice(idx + 1).trim();
|
|
30
|
+
if(key) existingMeta[key] = value;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if(locked){
|
|
35
|
+
existingMeta.locked = 'true';
|
|
36
|
+
} else {
|
|
37
|
+
delete existingMeta.locked;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const frontmatter = '<!--\n' +
|
|
41
|
+
Object.entries(existingMeta).map(([k, v]) => ` ${k}: ${v}`).join('\n') +
|
|
42
|
+
'\n-->';
|
|
43
|
+
|
|
44
|
+
const body = frontmatterMatch ? raw.slice(frontmatterMatch[0].length) : '\n' + raw;
|
|
45
|
+
await writeFile(fullPath, frontmatter + body, 'utf-8');
|
|
46
|
+
|
|
47
|
+
return [null, { file: normalized, locked }];
|
|
48
|
+
};
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { readFile, writeFile } from 'fs/promises';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { existsSync } from 'fs';
|
|
4
|
+
import triggerHook from '../hooks/triggerHook.js';
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
const RESERVED_KEYS = new Set(['owner', 'name', 'author', 'created', 'updated', 'locked', 'title', 'description']);
|
|
7
|
+
|
|
8
|
+
export default async ({ rootDir, file, name, title, description, author, template, contents, extraMetadata, force = false }) => {
|
|
6
9
|
if(!rootDir){
|
|
7
10
|
return [{ code: 400, msg: 'Root directory is required' }, null];
|
|
8
11
|
}
|
|
@@ -20,8 +23,7 @@ export default async ({ rootDir, file, name, title, description, author, templat
|
|
|
20
23
|
const raw = await readFile(fullPath, 'utf-8');
|
|
21
24
|
|
|
22
25
|
const frontmatterMatch = raw.match(/^<!--\s*\n([\s\S]*?)\n\s*-->/);
|
|
23
|
-
|
|
24
|
-
if(lockedMatch) return [{ code: 403, msg: 'This page is locked and cannot be edited' }, null];
|
|
26
|
+
|
|
25
27
|
const existingMeta = {};
|
|
26
28
|
if(frontmatterMatch){
|
|
27
29
|
for(const line of frontmatterMatch[1].split('\n')){
|
|
@@ -33,6 +35,13 @@ export default async ({ rootDir, file, name, title, description, author, templat
|
|
|
33
35
|
}
|
|
34
36
|
}
|
|
35
37
|
|
|
38
|
+
if(extraMetadata !== undefined){
|
|
39
|
+
const conflictingKeys = Object.keys(extraMetadata).filter(k => RESERVED_KEYS.has(k));
|
|
40
|
+
if(conflictingKeys.length){
|
|
41
|
+
return [{ code: 400, msg: `Cannot use reserved metadata keys: ${conflictingKeys.join(', ')}` }, null];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
36
45
|
const now = new Date().toISOString();
|
|
37
46
|
const newMeta = {
|
|
38
47
|
...existingMeta,
|
|
@@ -42,6 +51,11 @@ export default async ({ rootDir, file, name, title, description, author, templat
|
|
|
42
51
|
if(title !== undefined) newMeta.title = title;
|
|
43
52
|
if(description !== undefined) newMeta.description = description;
|
|
44
53
|
if(author !== undefined) newMeta.author = author;
|
|
54
|
+
if(extraMetadata !== undefined){
|
|
55
|
+
for(const [k, v] of Object.entries(extraMetadata)){
|
|
56
|
+
newMeta[k] = v;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
45
59
|
|
|
46
60
|
const templateMatch = raw.match(/<page\s[^>]*template="([^"]*)"/)
|
|
47
61
|
const resolvedTemplate = template !== undefined ? template : (templateMatch ? templateMatch[1] : 'default');
|
|
@@ -70,5 +84,7 @@ export default async ({ rootDir, file, name, title, description, author, templat
|
|
|
70
84
|
|
|
71
85
|
await writeFile(fullPath, newPageContent, 'utf-8');
|
|
72
86
|
|
|
87
|
+
await triggerHook('page:updated', { file: safePath, updatedAt: now });
|
|
88
|
+
|
|
73
89
|
return [null, { file: safePath, updatedAt: now }];
|
|
74
90
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
2
2
|
import { join, dirname } from 'path';
|
|
3
3
|
import { existsSync } from 'fs';
|
|
4
|
+
import triggerHook from '../hooks/triggerHook.js';
|
|
4
5
|
|
|
5
6
|
const slugify = name => name
|
|
6
7
|
.toLowerCase()
|
|
@@ -10,7 +11,7 @@ const slugify = name => name
|
|
|
10
11
|
.replace(/-+/g, '-')
|
|
11
12
|
.replace(/^-|-$/g, '');
|
|
12
13
|
|
|
13
|
-
export default async ({ rootDir, directory, name, author, copyFrom }) => {
|
|
14
|
+
export default async ({ rootDir, directory, name, author, copyFrom, locked = false }) => {
|
|
14
15
|
if(!rootDir) return [{ code: 400, msg: 'Root directory is required' }, null];
|
|
15
16
|
if(!name) return [{ code: 400, msg: 'Template name is required' }, null];
|
|
16
17
|
|
|
@@ -33,6 +34,7 @@ export default async ({ rootDir, directory, name, author, copyFrom }) => {
|
|
|
33
34
|
` author: ${author || ''}`,
|
|
34
35
|
` created: ${now}`,
|
|
35
36
|
` updated: ${now}`,
|
|
37
|
+
...(locked ? [' locked: true'] : []),
|
|
36
38
|
'-->'
|
|
37
39
|
].join('\n');
|
|
38
40
|
|
|
@@ -67,5 +69,7 @@ export default async ({ rootDir, directory, name, author, copyFrom }) => {
|
|
|
67
69
|
|
|
68
70
|
const relPath = dir ? `${dir}/${slug}.template.html` : `${slug}.template.html`;
|
|
69
71
|
|
|
72
|
+
await triggerHook('template:created', { file: relPath, name, slug });
|
|
73
|
+
|
|
70
74
|
return [null, { file: relPath, name, slug }];
|
|
71
75
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFile, unlink } from 'fs/promises';
|
|
2
2
|
import { join, normalize, sep } from 'path';
|
|
3
3
|
import parseFrontmatter from '../fs/parseFrontmatter.js';
|
|
4
|
+
import triggerHook from '../hooks/triggerHook.js';
|
|
4
5
|
|
|
5
6
|
export default async ({ rootDir, files }) => {
|
|
6
7
|
if(!rootDir) return [{ code: 400, msg: 'Root directory is required' }, null];
|
|
@@ -21,14 +22,12 @@ export default async ({ rootDir, files }) => {
|
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
const meta = parseFrontmatter(content);
|
|
24
|
-
if(meta.locked === 'true'){
|
|
25
|
-
return [{ code: 403, msg: `Cannot delete locked template: ${file}` }, null];
|
|
26
|
-
}
|
|
27
25
|
if(meta.owner && meta.owner !== 'custom'){
|
|
28
26
|
return [{ code: 403, msg: `Cannot delete template: ${file}` }, null];
|
|
29
27
|
}
|
|
30
28
|
|
|
31
29
|
await unlink(fullPath);
|
|
30
|
+
await triggerHook('template:deleted', { file });
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
return [null, { deleted: files.length }];
|
|
@@ -23,9 +23,6 @@ export default async ({ rootDir, file }) => {
|
|
|
23
23
|
|
|
24
24
|
const content = await readFile(fullPath, 'utf-8');
|
|
25
25
|
const meta = parseFrontmatter(content);
|
|
26
|
-
if(meta.locked === 'true'){
|
|
27
|
-
return [{ code: 403, msg: 'Cannot disable a locked template' }, null];
|
|
28
|
-
}
|
|
29
26
|
|
|
30
27
|
const disabledPath = fullPath.replace(/\.template\.html$/, '.template-disabled.html');
|
|
31
28
|
if(existsSync(disabledPath)){
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { readFile } from 'fs/promises';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import parseFrontmatter from '../fs/parseFrontmatter.js';
|
|
5
|
+
|
|
6
|
+
export default async ({ rootDir, file }) => {
|
|
7
|
+
if(!rootDir){
|
|
8
|
+
return [{ code: 400, msg: 'Root directory is required' }, null];
|
|
9
|
+
}
|
|
10
|
+
if(!file){
|
|
11
|
+
return [{ code: 400, msg: 'File path is required' }, null];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const safePath = file.replace(/\.\./g, '').replace(/^\//, '');
|
|
15
|
+
const fullPath = join(rootDir, safePath);
|
|
16
|
+
|
|
17
|
+
if(!existsSync(fullPath)){
|
|
18
|
+
return [{ code: 404, msg: 'Template not found' }, null];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const raw = await readFile(fullPath, 'utf-8');
|
|
22
|
+
const meta = parseFrontmatter(raw);
|
|
23
|
+
|
|
24
|
+
return [null, {
|
|
25
|
+
name: meta.name,
|
|
26
|
+
owner: meta.owner,
|
|
27
|
+
locked: meta.locked === 'true',
|
|
28
|
+
author: meta.author,
|
|
29
|
+
created: meta.created,
|
|
30
|
+
updated: meta.updated
|
|
31
|
+
}];
|
|
32
|
+
};
|
|
@@ -28,9 +28,6 @@ export default async ({ rootDir, file, newFile }) => {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
const meta = parseFrontmatter(content);
|
|
31
|
-
if(meta.locked === 'true'){
|
|
32
|
-
return [{ code: 403, msg: 'Cannot move a locked template' }, null];
|
|
33
|
-
}
|
|
34
31
|
if(meta.owner && meta.owner !== 'custom'){
|
|
35
32
|
return [{ code: 403, msg: 'Cannot move system templates' }, null];
|
|
36
33
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { readFile, writeFile } from 'fs/promises';
|
|
2
|
+
import { join, normalize, sep } from 'path';
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
|
|
5
|
+
export default async ({ rootDir, file, locked }) => {
|
|
6
|
+
if(!rootDir) return [{ code: 400, msg: 'Root directory is required' }, null];
|
|
7
|
+
if(!file) return [{ code: 400, msg: 'File path is required' }, null];
|
|
8
|
+
if(typeof locked !== 'boolean') return [{ code: 400, msg: 'locked must be a boolean' }, null];
|
|
9
|
+
|
|
10
|
+
const normalized = normalize(file);
|
|
11
|
+
if(normalized.startsWith('..') || normalized.includes('..' + sep)){
|
|
12
|
+
return [{ code: 400, msg: 'Invalid file path' }, null];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const fullPath = join(rootDir, normalized);
|
|
16
|
+
if(!existsSync(fullPath)){
|
|
17
|
+
return [{ code: 404, msg: 'Template not found' }, null];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const raw = await readFile(fullPath, 'utf-8');
|
|
21
|
+
const frontmatterMatch = raw.match(/^<!--\s*\n([\s\S]*?)\n\s*-->/);
|
|
22
|
+
const existingMeta = {};
|
|
23
|
+
|
|
24
|
+
if(frontmatterMatch){
|
|
25
|
+
for(const line of frontmatterMatch[1].split('\n')){
|
|
26
|
+
const idx = line.indexOf(':');
|
|
27
|
+
if(idx === -1) continue;
|
|
28
|
+
const key = line.slice(0, idx).trim();
|
|
29
|
+
const value = line.slice(idx + 1).trim();
|
|
30
|
+
if(key) existingMeta[key] = value;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if(locked){
|
|
35
|
+
existingMeta.locked = 'true';
|
|
36
|
+
} else {
|
|
37
|
+
delete existingMeta.locked;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const frontmatter = '<!--\n' +
|
|
41
|
+
Object.entries(existingMeta).map(([k, v]) => ` ${k}: ${v}`).join('\n') +
|
|
42
|
+
'\n-->';
|
|
43
|
+
|
|
44
|
+
const body = frontmatterMatch ? raw.slice(frontmatterMatch[0].length) : '\n' + raw;
|
|
45
|
+
await writeFile(fullPath, frontmatter + body, 'utf-8');
|
|
46
|
+
|
|
47
|
+
return [null, { file: normalized, locked }];
|
|
48
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFile, writeFile } from 'fs/promises';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { existsSync } from 'fs';
|
|
4
|
+
import triggerHook from '../hooks/triggerHook.js';
|
|
4
5
|
|
|
5
6
|
export default async ({ rootDir, file, author, markup }) => {
|
|
6
7
|
if(!rootDir) return [{ code: 400, msg: 'Root directory is required' }, null];
|
|
@@ -13,9 +14,6 @@ export default async ({ rootDir, file, author, markup }) => {
|
|
|
13
14
|
|
|
14
15
|
const raw = await readFile(fullPath, 'utf-8');
|
|
15
16
|
|
|
16
|
-
const lockedMatch = raw.match(/^<!--[\s\S]*?locked:\s*true[\s\S]*?-->/);
|
|
17
|
-
if(lockedMatch) return [{ code: 403, msg: 'This template is locked and cannot be edited' }, null];
|
|
18
|
-
|
|
19
17
|
const frontmatterMatch = raw.match(/^<!--\s*\n([\s\S]*?)\n\s*-->/);
|
|
20
18
|
const existingMeta = {};
|
|
21
19
|
if(frontmatterMatch){
|
|
@@ -42,5 +40,7 @@ export default async ({ rootDir, file, author, markup }) => {
|
|
|
42
40
|
|
|
43
41
|
await writeFile(fullPath, newContent, 'utf-8');
|
|
44
42
|
|
|
43
|
+
await triggerHook('template:updated', { file: safePath, updatedAt: now });
|
|
44
|
+
|
|
45
45
|
return [null, { file: safePath, updatedAt: now }];
|
|
46
46
|
};
|