Update various configuration files, components, and assets; enhance notification system and API endpoints; improve documentation and styles across the application.
This commit is contained in:
24
server/api/devtool/api/file-code.js
Normal file
24
server/api/devtool/api/file-code.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const query = await getQuery(event);
|
||||
|
||||
try {
|
||||
// Get vue code from path in query
|
||||
const filePath = path.join(process.cwd() + "/server/", query.path + ".js");
|
||||
const code = fs.readFileSync(filePath, "utf8");
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Code successfully loaded",
|
||||
data: code,
|
||||
};
|
||||
} catch (error) {
|
||||
// console.log(error);
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "File not found",
|
||||
};
|
||||
}
|
||||
});
|
||||
191
server/api/devtool/api/linter.js
Normal file
191
server/api/devtool/api/linter.js
Normal file
@@ -0,0 +1,191 @@
|
||||
// import esline vue
|
||||
import { ESLint } from "eslint";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event);
|
||||
|
||||
try {
|
||||
if (body.code === undefined) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
message: "Bad Request",
|
||||
};
|
||||
}
|
||||
|
||||
// run linter
|
||||
const code = body.code;
|
||||
const validateNitroCode = (code) => {
|
||||
// Check if this is a server route file
|
||||
const isServerRoute = code.includes("defineEventHandler");
|
||||
|
||||
if (isServerRoute) {
|
||||
let lineNumber = 1;
|
||||
|
||||
// 1. Validate event handler structure
|
||||
if (!code.includes("export default defineEventHandler")) {
|
||||
throw {
|
||||
message:
|
||||
"Nitro route handlers must use 'export default defineEventHandler'",
|
||||
line: 1,
|
||||
column: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// 2. Check for proper request handling
|
||||
const hasRequestBody = code.includes("await readBody(event)");
|
||||
const hasRequestQuery = code.includes("getQuery(event)");
|
||||
const usesEventWithoutImport =
|
||||
code.includes("event.") && !hasRequestBody && !hasRequestQuery;
|
||||
|
||||
if (usesEventWithoutImport) {
|
||||
// Find the line where event is improperly used
|
||||
const lines = code.split("\n");
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (
|
||||
lines[i].includes("event.") &&
|
||||
!lines[i].includes("readBody") &&
|
||||
!lines[i].includes("getQuery")
|
||||
) {
|
||||
throw {
|
||||
message:
|
||||
"Use 'readBody(event)' for POST data or 'getQuery(event)' for query parameters",
|
||||
line: i + 1,
|
||||
column: lines[i].indexOf("event."),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Validate response structure
|
||||
const responseRegex = /return\s+{([^}]+)}/g;
|
||||
let match;
|
||||
let lastIndex = 0;
|
||||
|
||||
while ((match = responseRegex.exec(code)) !== null) {
|
||||
lineNumber += (code.slice(lastIndex, match.index).match(/\n/g) || [])
|
||||
.length;
|
||||
lastIndex = match.index;
|
||||
|
||||
const responseContent = match[1];
|
||||
|
||||
// Check for required response properties
|
||||
if (!responseContent.includes("statusCode")) {
|
||||
throw {
|
||||
message: "API responses must include a 'statusCode' property",
|
||||
line: lineNumber,
|
||||
column: match.index - code.lastIndexOf("\n", match.index),
|
||||
};
|
||||
}
|
||||
|
||||
// Validate status code usage
|
||||
const statusMatch = responseContent.match(/statusCode:\s*(\d+)/);
|
||||
if (statusMatch) {
|
||||
const statusCode = parseInt(statusMatch[1]);
|
||||
if (![200, 201, 400, 401, 403, 404, 500].includes(statusCode)) {
|
||||
throw {
|
||||
message: `Invalid status code: ${statusCode}. Use standard HTTP status codes.`,
|
||||
line: lineNumber,
|
||||
column: statusMatch.index,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Check error handling
|
||||
if (code.includes("try") && !code.includes("catch")) {
|
||||
throw {
|
||||
message:
|
||||
"Missing error handling. Add a catch block for try statements.",
|
||||
line:
|
||||
code.split("\n").findIndex((line) => line.includes("try")) + 1,
|
||||
column: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// 5. Validate async/await usage
|
||||
const asyncLines = code.match(/async.*=>/g) || [];
|
||||
const awaitLines = code.match(/await\s+/g) || [];
|
||||
|
||||
if (awaitLines.length > 0 && asyncLines.length === 0) {
|
||||
throw {
|
||||
message: "Using 'await' requires an async function",
|
||||
line:
|
||||
code.split("\n").findIndex((line) => line.includes("await")) + 1,
|
||||
column: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// // 6. Check for proper imports
|
||||
// const requiredImports = new Set();
|
||||
// if (hasRequestBody) requiredImports.add("readBody");
|
||||
// if (hasRequestQuery) requiredImports.add("getQuery");
|
||||
|
||||
// const importLines = code.match(/import.*from/g) || [];
|
||||
// requiredImports.forEach((imp) => {
|
||||
// if (!importLines.some((line) => line.includes(imp))) {
|
||||
// throw {
|
||||
// message: `Missing import for '${imp}' utility`,
|
||||
// line: 1,
|
||||
// column: 0,
|
||||
// };
|
||||
// }
|
||||
// });
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
validateNitroCode(code);
|
||||
|
||||
const eslint = new ESLint({
|
||||
overrideConfig: {
|
||||
parser: "@babel/eslint-parser",
|
||||
extends: ["@kiwicom"],
|
||||
parserOptions: {
|
||||
requireConfigFile: false,
|
||||
ecmaVersion: 2020,
|
||||
sourceType: "module",
|
||||
},
|
||||
},
|
||||
useEslintrc: false,
|
||||
});
|
||||
|
||||
const results = await eslint.lintText(code);
|
||||
|
||||
if (results[0].messages.length > 0) {
|
||||
const messages = results[0].messages[0];
|
||||
|
||||
if (messages.fatal === true) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
message: "Bad Linter Test",
|
||||
data: messages,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Good Linter test",
|
||||
data: messages,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return {
|
||||
statusCode: 400,
|
||||
message: "Bad Linter Test",
|
||||
data: {
|
||||
message: error.message,
|
||||
line: error.line || 1,
|
||||
column: error.column || 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Internal Server Error",
|
||||
errror: error,
|
||||
};
|
||||
}
|
||||
});
|
||||
77
server/api/devtool/api/list.js
Normal file
77
server/api/devtool/api/list.js
Normal file
@@ -0,0 +1,77 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
// get api folder path from server root folder and its files and folders inside it
|
||||
const apiFolderPath = path.join(process.cwd() + "/server/api");
|
||||
const apis = fs.readdirSync(apiFolderPath);
|
||||
|
||||
const apiList = getFilesAndFolders(apiFolderPath);
|
||||
|
||||
const apiUrls = getApiUrls(apiList);
|
||||
const jsonObject = JSON.parse(JSON.stringify(apiUrls));
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "API List successfully fetched",
|
||||
data: jsonObject,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: error.message,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
function getFilesAndFolders(folderPath) {
|
||||
const folderFiles = fs.readdirSync(folderPath);
|
||||
const files = [];
|
||||
const folders = [];
|
||||
const apiURL = "/api";
|
||||
|
||||
folderFiles.forEach((file) => {
|
||||
const filePath = path.join(folderPath + "/" + file);
|
||||
if (file == "devtool") return;
|
||||
if (fs.lstatSync(filePath).isDirectory()) {
|
||||
folders.push(getFilesAndFolders(filePath));
|
||||
} else {
|
||||
const processPath = path.join(process.cwd() + "/server/api");
|
||||
const apiUrl = filePath
|
||||
.replace(processPath, apiURL)
|
||||
.replace(/\\/g, "/")
|
||||
.replace(".js", "");
|
||||
const fileName = file.replace(".js", "");
|
||||
const parentFolder = folderPath
|
||||
.replace(processPath, "")
|
||||
.replace(/\\/g, "");
|
||||
|
||||
files.push({
|
||||
name: fileName,
|
||||
parentName: parentFolder,
|
||||
url: apiUrl,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return { files, folders };
|
||||
}
|
||||
|
||||
function getApiUrls(folder) {
|
||||
const apiUrls = [];
|
||||
|
||||
folder.files.forEach((file) => {
|
||||
apiUrls.push({
|
||||
name: file.name,
|
||||
parentName: file.parentName,
|
||||
url: file.url,
|
||||
});
|
||||
});
|
||||
|
||||
folder.folders.forEach((nestedFolder) => {
|
||||
apiUrls.push(...getApiUrls(nestedFolder));
|
||||
});
|
||||
|
||||
return apiUrls;
|
||||
}
|
||||
27
server/api/devtool/api/prettier-format.js
Normal file
27
server/api/devtool/api/prettier-format.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import prettier from "prettier";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event);
|
||||
|
||||
try {
|
||||
if (body.code === undefined) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
message: "Bad Request",
|
||||
};
|
||||
}
|
||||
|
||||
const code = prettier.format(body.code, { semi: false, parser: "babel" });
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Code successfully formatted",
|
||||
data: code,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Internal Server Error",
|
||||
};
|
||||
}
|
||||
});
|
||||
71
server/api/devtool/api/save.js
Normal file
71
server/api/devtool/api/save.js
Normal file
@@ -0,0 +1,71 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event);
|
||||
|
||||
try {
|
||||
const codeDefault = `
|
||||
export default defineEventHandler(async (event) => {
|
||||
|
||||
// const query = await getQuery(event); // Get Params from URL
|
||||
// const body = await readBody(event); // Get Body Data
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "API Route Created",
|
||||
};
|
||||
|
||||
});`;
|
||||
|
||||
// Overwrite vue code from path in body with new code
|
||||
const filePath = path.join(process.cwd() + "/server/", body.path + ".js");
|
||||
|
||||
if (body.type == "update") {
|
||||
fs.writeFileSync(filePath, body.code, "utf8");
|
||||
} else if (body.type == "add") {
|
||||
// if the folder doesn't exist, create it
|
||||
if (!fs.existsSync(path.dirname(filePath))) {
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(filePath, codeDefault, "utf8");
|
||||
} else if (body.type == "edit") {
|
||||
// if the folder doesn't exist, create it
|
||||
if (!fs.existsSync(path.dirname(filePath))) {
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
}
|
||||
|
||||
// Copy the file from the default path to the new path
|
||||
const oldPath = path.join(
|
||||
process.cwd() + "/server/",
|
||||
body.oldPath + ".js"
|
||||
);
|
||||
|
||||
// Copy file
|
||||
fs.copyFileSync(oldPath, filePath);
|
||||
|
||||
// Delete old file
|
||||
fs.unlinkSync(oldPath);
|
||||
} else if (body.type == "delete") {
|
||||
// Delete file from path
|
||||
fs.unlinkSync(filePath);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Code successfully deleted",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Code successfully saved",
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Internal Server Error",
|
||||
};
|
||||
}
|
||||
});
|
||||
90
server/api/devtool/config/add-custom-theme.js
Normal file
90
server/api/devtool/config/add-custom-theme.js
Normal file
@@ -0,0 +1,90 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const method = getMethod(event);
|
||||
|
||||
if (method !== "POST") {
|
||||
return {
|
||||
statusCode: 405,
|
||||
message: "Method not allowed",
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await readBody(event);
|
||||
const { themeName, themeCSS } = body;
|
||||
|
||||
if (!themeName || !themeCSS) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
message: "Theme name and CSS are required",
|
||||
};
|
||||
}
|
||||
|
||||
// Validate theme name (alphanumeric and hyphens only)
|
||||
if (!/^[a-zA-Z0-9-_]+$/.test(themeName)) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
message: "Theme name can only contain letters, numbers, hyphens, and underscores",
|
||||
};
|
||||
}
|
||||
|
||||
// Path to theme.css file
|
||||
const themeCSSPath = path.join(process.cwd(), 'assets', 'style', 'css', 'base', 'theme.css');
|
||||
|
||||
// Check if theme.css exists
|
||||
if (!fs.existsSync(themeCSSPath)) {
|
||||
return {
|
||||
statusCode: 404,
|
||||
message: "theme.css file not found",
|
||||
};
|
||||
}
|
||||
|
||||
// Read current theme.css content
|
||||
let currentContent = fs.readFileSync(themeCSSPath, 'utf8');
|
||||
|
||||
// Check if theme already exists
|
||||
const themePattern = new RegExp(`html\\[data-theme="${themeName}"\\]`, 'g');
|
||||
if (themePattern.test(currentContent)) {
|
||||
return {
|
||||
statusCode: 409,
|
||||
message: `Theme "${themeName}" already exists`,
|
||||
};
|
||||
}
|
||||
|
||||
// Format the new theme CSS
|
||||
const formattedThemeCSS = themeCSS.trim();
|
||||
|
||||
// Ensure the CSS starts with the correct selector if not provided
|
||||
let finalThemeCSS;
|
||||
if (!formattedThemeCSS.includes(`html[data-theme="${themeName}"]`)) {
|
||||
finalThemeCSS = `html[data-theme="${themeName}"] {\n${formattedThemeCSS}\n}`;
|
||||
} else {
|
||||
finalThemeCSS = formattedThemeCSS;
|
||||
}
|
||||
|
||||
// Add the new theme to the end of the file
|
||||
const newContent = currentContent + '\n\n' + finalThemeCSS + '\n';
|
||||
|
||||
// Write the updated content back to the file
|
||||
fs.writeFileSync(themeCSSPath, newContent, 'utf8');
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Custom theme added successfully",
|
||||
data: {
|
||||
themeName,
|
||||
success: true
|
||||
},
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error("Add custom theme error:", error);
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Internal server error",
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
});
|
||||
22
server/api/devtool/config/env.js
Normal file
22
server/api/devtool/config/env.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
// Get .env file, parse and return
|
||||
const envFile = path.join(process.cwd(), ".env");
|
||||
|
||||
if (!fs.existsSync(envFile)) {
|
||||
return {
|
||||
statusCode: 404,
|
||||
message: "File not found",
|
||||
};
|
||||
}
|
||||
|
||||
const env = fs.readFileSync(envFile, "utf-8");
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Success",
|
||||
data: env,
|
||||
};
|
||||
});
|
||||
44
server/api/devtool/config/loading-logo.js
Normal file
44
server/api/devtool/config/loading-logo.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const method = getMethod(event);
|
||||
|
||||
try {
|
||||
if (method === "GET") {
|
||||
// Get only the loading logo and site name for faster loading
|
||||
const settings = await prisma.site_settings.findFirst({
|
||||
select: {
|
||||
siteLoadingLogo: true,
|
||||
siteName: true,
|
||||
},
|
||||
orderBy: { settingID: "desc" },
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Success",
|
||||
data: {
|
||||
siteLoadingLogo: settings?.siteLoadingLogo || '',
|
||||
siteName: settings?.siteName || 'corradAF',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 405,
|
||||
message: "Method not allowed",
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Loading logo API error:", error);
|
||||
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Internal server error",
|
||||
error: error.message,
|
||||
};
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
});
|
||||
217
server/api/devtool/config/site-settings.js
Normal file
217
server/api/devtool/config/site-settings.js
Normal file
@@ -0,0 +1,217 @@
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const method = getMethod(event);
|
||||
|
||||
try {
|
||||
if (method === "GET") {
|
||||
// Get site settings
|
||||
let settings = await prisma.site_settings.findFirst({
|
||||
orderBy: { settingID: "desc" },
|
||||
});
|
||||
|
||||
// If no settings exist, create default ones
|
||||
if (!settings) {
|
||||
settings = await prisma.site_settings.create({
|
||||
data: {
|
||||
siteName: "corradAF",
|
||||
siteDescription: "corradAF Base Project",
|
||||
themeMode: "biasa",
|
||||
showSiteNameInHeader: true,
|
||||
seoRobots: "index, follow",
|
||||
seoTwitterCard: "summary_large_image",
|
||||
settingCreatedDate: new Date(),
|
||||
settingModifiedDate: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Transform data to match new structure
|
||||
const transformedSettings = {
|
||||
siteName: settings.siteName || "corradAF",
|
||||
siteNameFontSize: settings.siteNameFontSize || 18,
|
||||
siteDescription: settings.siteDescription || "corradAF Base Project",
|
||||
siteLogo: settings.siteLogo || "",
|
||||
siteLoadingLogo: settings.siteLoadingLogo || "",
|
||||
siteFavicon: settings.siteFavicon || "",
|
||||
siteLoginLogo: settings.siteLoginLogo || "",
|
||||
showSiteNameInHeader: settings.showSiteNameInHeader !== false,
|
||||
customCSS: settings.customCSS || "",
|
||||
selectedTheme: settings.themeMode || "biasa", // Use themeMode as selectedTheme
|
||||
customThemeFile: settings.customThemeFile || "",
|
||||
currentFont: settings.currentFont || "",
|
||||
fontSource: settings.fontSource || "",
|
||||
// SEO fields
|
||||
seoTitle: settings.seoTitle || "",
|
||||
seoDescription: settings.seoDescription || "",
|
||||
seoKeywords: settings.seoKeywords || "",
|
||||
seoAuthor: settings.seoAuthor || "",
|
||||
seoOgImage: settings.seoOgImage || "",
|
||||
seoTwitterCard: settings.seoTwitterCard || "summary_large_image",
|
||||
seoCanonicalUrl: settings.seoCanonicalUrl || "",
|
||||
seoRobots: settings.seoRobots || "index, follow",
|
||||
seoGoogleAnalytics: settings.seoGoogleAnalytics || "",
|
||||
seoGoogleTagManager: settings.seoGoogleTagManager || "",
|
||||
seoFacebookPixel: settings.seoFacebookPixel || ""
|
||||
};
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Success",
|
||||
data: transformedSettings,
|
||||
};
|
||||
}
|
||||
|
||||
if (method === "POST") {
|
||||
let body;
|
||||
try {
|
||||
body = await readBody(event);
|
||||
} catch (bodyError) {
|
||||
console.error("Error reading request body:", bodyError);
|
||||
return {
|
||||
statusCode: 400,
|
||||
message: "Invalid request body",
|
||||
error: bodyError.message,
|
||||
};
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if (!body || typeof body !== 'object') {
|
||||
return {
|
||||
statusCode: 400,
|
||||
message: "Request body must be a valid JSON object",
|
||||
};
|
||||
}
|
||||
|
||||
// Check if settings exist
|
||||
const existingSettings = await prisma.site_settings.findFirst();
|
||||
|
||||
// Prepare data for database (use themeMode instead of selectedTheme)
|
||||
// Filter out undefined values to avoid database errors
|
||||
const dbData = {};
|
||||
|
||||
// Only add fields that are not undefined
|
||||
if (body.siteName !== undefined) dbData.siteName = body.siteName;
|
||||
if (body.siteNameFontSize !== undefined) dbData.siteNameFontSize = body.siteNameFontSize;
|
||||
if (body.siteDescription !== undefined) dbData.siteDescription = body.siteDescription;
|
||||
if (body.siteLogo !== undefined) dbData.siteLogo = body.siteLogo;
|
||||
if (body.siteLoadingLogo !== undefined) dbData.siteLoadingLogo = body.siteLoadingLogo;
|
||||
if (body.siteFavicon !== undefined) dbData.siteFavicon = body.siteFavicon;
|
||||
if (body.siteLoginLogo !== undefined) dbData.siteLoginLogo = body.siteLoginLogo;
|
||||
if (body.showSiteNameInHeader !== undefined) dbData.showSiteNameInHeader = body.showSiteNameInHeader;
|
||||
if (body.customCSS !== undefined) dbData.customCSS = body.customCSS;
|
||||
if (body.selectedTheme !== undefined) dbData.themeMode = body.selectedTheme;
|
||||
if (body.customThemeFile !== undefined) dbData.customThemeFile = body.customThemeFile;
|
||||
if (body.currentFont !== undefined) dbData.currentFont = body.currentFont;
|
||||
if (body.fontSource !== undefined) dbData.fontSource = body.fontSource;
|
||||
if (body.seoTitle !== undefined) dbData.seoTitle = body.seoTitle;
|
||||
if (body.seoDescription !== undefined) dbData.seoDescription = body.seoDescription;
|
||||
if (body.seoKeywords !== undefined) dbData.seoKeywords = body.seoKeywords;
|
||||
if (body.seoAuthor !== undefined) dbData.seoAuthor = body.seoAuthor;
|
||||
if (body.seoOgImage !== undefined) dbData.seoOgImage = body.seoOgImage;
|
||||
if (body.seoTwitterCard !== undefined) dbData.seoTwitterCard = body.seoTwitterCard;
|
||||
if (body.seoCanonicalUrl !== undefined) dbData.seoCanonicalUrl = body.seoCanonicalUrl;
|
||||
if (body.seoRobots !== undefined) dbData.seoRobots = body.seoRobots;
|
||||
if (body.seoGoogleAnalytics !== undefined) dbData.seoGoogleAnalytics = body.seoGoogleAnalytics;
|
||||
if (body.seoGoogleTagManager !== undefined) dbData.seoGoogleTagManager = body.seoGoogleTagManager;
|
||||
if (body.seoFacebookPixel !== undefined) dbData.seoFacebookPixel = body.seoFacebookPixel;
|
||||
|
||||
dbData.settingModifiedDate = new Date();
|
||||
|
||||
let settings;
|
||||
if (existingSettings) {
|
||||
// Update existing settings
|
||||
settings = await prisma.site_settings.update({
|
||||
where: { settingID: existingSettings.settingID },
|
||||
data: dbData,
|
||||
});
|
||||
} else {
|
||||
// Create new settings
|
||||
settings = await prisma.site_settings.create({
|
||||
data: {
|
||||
...dbData,
|
||||
settingCreatedDate: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Transform response to match new structure
|
||||
const transformedSettings = {
|
||||
siteName: settings.siteName || "corradAF",
|
||||
siteNameFontSize: settings.siteNameFontSize || 18,
|
||||
siteDescription: settings.siteDescription || "corradAF Base Project",
|
||||
siteLogo: settings.siteLogo || "",
|
||||
siteLoadingLogo: settings.siteLoadingLogo || "",
|
||||
siteFavicon: settings.siteFavicon || "",
|
||||
siteLoginLogo: settings.siteLoginLogo || "",
|
||||
showSiteNameInHeader: settings.showSiteNameInHeader !== false,
|
||||
customCSS: settings.customCSS || "",
|
||||
selectedTheme: settings.themeMode || "biasa", // Use themeMode as selectedTheme
|
||||
customThemeFile: settings.customThemeFile || "",
|
||||
currentFont: settings.currentFont || "",
|
||||
fontSource: settings.fontSource || "",
|
||||
// SEO fields
|
||||
seoTitle: settings.seoTitle || "",
|
||||
seoDescription: settings.seoDescription || "",
|
||||
seoKeywords: settings.seoKeywords || "",
|
||||
seoAuthor: settings.seoAuthor || "",
|
||||
seoOgImage: settings.seoOgImage || "",
|
||||
seoTwitterCard: settings.seoTwitterCard || "summary_large_image",
|
||||
seoCanonicalUrl: settings.seoCanonicalUrl || "",
|
||||
seoRobots: settings.seoRobots || "index, follow",
|
||||
seoGoogleAnalytics: settings.seoGoogleAnalytics || "",
|
||||
seoGoogleTagManager: settings.seoGoogleTagManager || "",
|
||||
seoFacebookPixel: settings.seoFacebookPixel || ""
|
||||
};
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Settings updated successfully",
|
||||
data: transformedSettings,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 405,
|
||||
message: "Method not allowed",
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Site settings API error:", error);
|
||||
|
||||
// Provide more specific error messages
|
||||
if (error.code === 'P2002') {
|
||||
return {
|
||||
statusCode: 400,
|
||||
message: "Duplicate entry error",
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
|
||||
if (error.code === 'P2025') {
|
||||
return {
|
||||
statusCode: 404,
|
||||
message: "Record not found",
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
|
||||
if (error.code && error.code.startsWith('P')) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
message: "Database error",
|
||||
error: error.message,
|
||||
code: error.code,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Internal server error",
|
||||
error: error.message,
|
||||
};
|
||||
} finally {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
});
|
||||
134
server/api/devtool/config/upload-file.js
Normal file
134
server/api/devtool/config/upload-file.js
Normal file
@@ -0,0 +1,134 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const method = getMethod(event);
|
||||
|
||||
if (method !== "POST") {
|
||||
return {
|
||||
statusCode: 405,
|
||||
message: "Method not allowed",
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const form = await readMultipartFormData(event);
|
||||
|
||||
if (!form || form.length === 0) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
message: "No file uploaded",
|
||||
};
|
||||
}
|
||||
|
||||
const file = form[0];
|
||||
const fileType = form.find(field => field.name === 'type')?.data?.toString() || 'logo';
|
||||
|
||||
// Validate file type
|
||||
const allowedTypes = {
|
||||
logo: ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/svg+xml'],
|
||||
'loading-logo': ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/svg+xml'],
|
||||
'login-logo': ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/svg+xml'],
|
||||
favicon: ['image/x-icon', 'image/vnd.microsoft.icon', 'image/png'],
|
||||
'og-image': ['image/jpeg', 'image/jpg', 'image/png'],
|
||||
theme: ['text/css', 'application/octet-stream']
|
||||
};
|
||||
|
||||
if (!allowedTypes[fileType] || !allowedTypes[fileType].includes(file.type)) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
message: `Invalid file type for ${fileType}. Allowed types: ${allowedTypes[fileType].join(', ')}`,
|
||||
};
|
||||
}
|
||||
|
||||
let uploadDir, fileUrl;
|
||||
|
||||
// Determine upload directory based on file type
|
||||
if (fileType === 'theme') {
|
||||
// Theme files go to assets/style/css
|
||||
uploadDir = path.join(process.cwd(), 'assets', 'style', 'css');
|
||||
if (!fs.existsSync(uploadDir)) {
|
||||
fs.mkdirSync(uploadDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Generate unique filename for theme
|
||||
const fileExtension = path.extname(file.filename || '');
|
||||
const uniqueFilename = `custom-theme-${uuidv4()}${fileExtension}`;
|
||||
const filePath = path.join(uploadDir, uniqueFilename);
|
||||
|
||||
// Save file
|
||||
fs.writeFileSync(filePath, file.data);
|
||||
|
||||
// Return relative path for theme files
|
||||
fileUrl = `/assets/style/css/${uniqueFilename}`;
|
||||
} else {
|
||||
// Logo, loading-logo, favicon, and og-image files go to public/uploads
|
||||
uploadDir = path.join(process.cwd(), 'public', 'uploads', 'site-settings');
|
||||
if (!fs.existsSync(uploadDir)) {
|
||||
fs.mkdirSync(uploadDir, { recursive: true });
|
||||
}
|
||||
|
||||
const fileExtension = path.extname(file.filename || '');
|
||||
let baseFilename;
|
||||
|
||||
switch (fileType) {
|
||||
case 'logo':
|
||||
baseFilename = 'site-logo';
|
||||
break;
|
||||
case 'loading-logo':
|
||||
baseFilename = 'loading-logo';
|
||||
break;
|
||||
case 'login-logo':
|
||||
baseFilename = 'login-logo';
|
||||
break;
|
||||
case 'favicon':
|
||||
baseFilename = 'favicon';
|
||||
break;
|
||||
case 'og-image':
|
||||
baseFilename = 'og-image';
|
||||
break;
|
||||
default:
|
||||
// This case should ideally not be reached if fileType is validated earlier
|
||||
// and is one of the image types.
|
||||
// However, as a fallback, use the fileType itself or a generic name.
|
||||
// For safety, and to avoid using uuidv4 for these specific types as requested,
|
||||
// we should ensure this path isn't taken for the specified image types.
|
||||
// If an unexpected fileType gets here, it might be better to error or use a UUID.
|
||||
// For now, we'll stick to the primary requirement of fixed names for specified types.
|
||||
// If we need UUID for other non-logo image types, that logic can be added.
|
||||
// console.warn(`Unexpected fileType received: ${fileType} for non-theme upload.`);
|
||||
// For simplicity, if it's an image type not explicitly handled, it will get a name like 'unknown-type.ext'
|
||||
baseFilename = fileType;
|
||||
}
|
||||
|
||||
const filenameWithExt = `${baseFilename}${fileExtension}`;
|
||||
const filePath = path.join(uploadDir, filenameWithExt);
|
||||
|
||||
// Save file (overwrites if exists)
|
||||
fs.writeFileSync(filePath, file.data);
|
||||
|
||||
// Return file URL
|
||||
fileUrl = `/uploads/site-settings/${filenameWithExt}`;
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "File uploaded successfully",
|
||||
data: {
|
||||
filename: path.basename(fileUrl),
|
||||
url: fileUrl,
|
||||
type: fileType,
|
||||
size: file.data.length,
|
||||
},
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error("Upload error:", error);
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Internal server error",
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
});
|
||||
48
server/api/devtool/content/canvas/file-code.js
Normal file
48
server/api/devtool/content/canvas/file-code.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const query = await getQuery(event);
|
||||
|
||||
let code = "";
|
||||
// console.log(query.path);
|
||||
|
||||
try {
|
||||
// Get vue code from path in query
|
||||
const filePath = path.join(process.cwd() + "/pages/", query.path + ".vue");
|
||||
try {
|
||||
code = fs.readFileSync(filePath, "utf8");
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Code successfully loaded",
|
||||
data: code,
|
||||
};
|
||||
} catch (error) {}
|
||||
|
||||
// Check if there is path with index.vue
|
||||
const filePathIndex = path.join(
|
||||
process.cwd() + "/pages/",
|
||||
query.path + "/index.vue"
|
||||
);
|
||||
|
||||
code = fs.readFileSync(filePathIndex, "utf8");
|
||||
|
||||
// Only get the template part of the code and make sure its from the first template tag to the last template tag
|
||||
code = code.substring(
|
||||
code.indexOf("<template>") + 10,
|
||||
code.lastIndexOf("</template>")
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Code successfully loaded",
|
||||
data: code,
|
||||
mode: "index",
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "File not found",
|
||||
};
|
||||
}
|
||||
});
|
||||
42
server/api/devtool/content/code/file-code.js
Normal file
42
server/api/devtool/content/code/file-code.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const query = await getQuery(event);
|
||||
|
||||
let code = "";
|
||||
// console.log(query.path);
|
||||
|
||||
try {
|
||||
// Get vue code from path in query
|
||||
const filePath = path.join(process.cwd() + "/pages/", query.path + ".vue");
|
||||
try {
|
||||
code = fs.readFileSync(filePath, "utf8");
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Code successfully loaded",
|
||||
data: code,
|
||||
};
|
||||
} catch (error) {}
|
||||
|
||||
// Check if there is path with index.vue
|
||||
const filePathIndex = path.join(
|
||||
process.cwd() + "/pages/",
|
||||
query.path + "/index.vue"
|
||||
);
|
||||
|
||||
code = fs.readFileSync(filePathIndex, "utf8");
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Code successfully loaded",
|
||||
data: code,
|
||||
mode: "index",
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "File not found",
|
||||
};
|
||||
}
|
||||
});
|
||||
450
server/api/devtool/content/code/linter.js
Normal file
450
server/api/devtool/content/code/linter.js
Normal file
@@ -0,0 +1,450 @@
|
||||
import { ESLint } from "eslint";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event);
|
||||
|
||||
try {
|
||||
if (body.code === undefined) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
message: "Bad Request",
|
||||
};
|
||||
}
|
||||
|
||||
const code = body.code;
|
||||
|
||||
// Extract script and template content once
|
||||
const scriptContent =
|
||||
code.match(/<script\b[^>]*>([\s\S]*?)<\/script>/)?.[1] || "";
|
||||
const templateContent = code.match(/<template>([\s\S]*)<\/template>/)?.[1];
|
||||
|
||||
// Validate FormKit inputs
|
||||
const validateFormKit = (content) => {
|
||||
// List of valid FormKit input types
|
||||
const validFormKitTypes = [
|
||||
"text",
|
||||
"email",
|
||||
"url",
|
||||
"tel",
|
||||
"password",
|
||||
"number",
|
||||
"date",
|
||||
"datetime-local",
|
||||
"time",
|
||||
"month",
|
||||
"week",
|
||||
"search",
|
||||
"color",
|
||||
"file",
|
||||
"range",
|
||||
"checkbox",
|
||||
"radio",
|
||||
"select",
|
||||
"textarea",
|
||||
"submit",
|
||||
"button",
|
||||
"mask",
|
||||
"form",
|
||||
];
|
||||
|
||||
// Find all FormKit components
|
||||
const formKitRegex = /<FormKit[^>]*>/g;
|
||||
let formKitMatch;
|
||||
|
||||
// Start counting from template tag
|
||||
let lineNumber = content
|
||||
.slice(0, content.indexOf("<template"))
|
||||
.split("\n").length;
|
||||
let lastIndex = 0;
|
||||
|
||||
while ((formKitMatch = formKitRegex.exec(content)) !== null) {
|
||||
// Calculate correct line number including the lines before template
|
||||
lineNumber += (
|
||||
content.slice(lastIndex, formKitMatch.index).match(/\n/g) || []
|
||||
).length;
|
||||
lastIndex = formKitMatch.index;
|
||||
|
||||
const formKitTag = formKitMatch[0];
|
||||
|
||||
// Extract type attribute
|
||||
const typeMatch = formKitTag.match(/type=["']([^"']+)["']/);
|
||||
if (!typeMatch) {
|
||||
throw {
|
||||
message: "FormKit component missing required 'type' attribute",
|
||||
line: lineNumber,
|
||||
column:
|
||||
formKitMatch.index -
|
||||
content.lastIndexOf("\n", formKitMatch.index),
|
||||
};
|
||||
}
|
||||
|
||||
const inputType = typeMatch[1];
|
||||
if (!validFormKitTypes.includes(inputType)) {
|
||||
throw {
|
||||
message: `Invalid FormKit type: "${inputType}". Please use a valid input type.`,
|
||||
line: lineNumber,
|
||||
column:
|
||||
formKitMatch.index -
|
||||
content.lastIndexOf("\n", formKitMatch.index),
|
||||
};
|
||||
}
|
||||
|
||||
// Check for options in select, radio, and checkbox types
|
||||
if (["select", "radio", "checkbox"].includes(inputType)) {
|
||||
// Look for :options or v-model
|
||||
const hasOptions =
|
||||
formKitTag.includes(":options=") || formKitTag.includes("v-model=");
|
||||
const hasSlotContent =
|
||||
content
|
||||
.slice(
|
||||
formKitMatch.index,
|
||||
content.indexOf(">", formKitMatch.index)
|
||||
)
|
||||
.includes(">") &&
|
||||
content
|
||||
.slice(
|
||||
formKitMatch.index,
|
||||
content.indexOf("</FormKit>", formKitMatch.index)
|
||||
)
|
||||
.includes("<option");
|
||||
|
||||
if (!hasOptions && !hasSlotContent) {
|
||||
throw {
|
||||
message: `FormKit ${inputType} requires options. Add :options prop or option slots.`,
|
||||
line: lineNumber,
|
||||
column:
|
||||
formKitMatch.index -
|
||||
content.lastIndexOf("\n", formKitMatch.index),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Add new function to validate mustache syntax
|
||||
const validateMustacheSyntax = (content) => {
|
||||
const stack = [];
|
||||
let lineNumber = 1;
|
||||
let lastIndex = 0;
|
||||
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
if (content[i] === "\n") {
|
||||
lineNumber++;
|
||||
lastIndex = i + 1;
|
||||
}
|
||||
|
||||
if (content[i] === "{" && content[i + 1] === "{") {
|
||||
stack.push({
|
||||
position: i,
|
||||
line: lineNumber,
|
||||
column: i - lastIndex,
|
||||
});
|
||||
i++; // Skip next '{'
|
||||
} else if (content[i] === "}" && content[i + 1] === "}") {
|
||||
if (stack.length === 0) {
|
||||
throw {
|
||||
message:
|
||||
"Unexpected closing mustache brackets '}}' without matching opening brackets",
|
||||
line: lineNumber,
|
||||
column: i - lastIndex,
|
||||
};
|
||||
}
|
||||
stack.pop();
|
||||
i++; // Skip next '}'
|
||||
}
|
||||
}
|
||||
|
||||
if (stack.length > 0) {
|
||||
const unclosed = stack[0];
|
||||
throw {
|
||||
message:
|
||||
"Unclosed mustache brackets '{{'. Missing closing brackets '}}",
|
||||
line: unclosed.line,
|
||||
column: unclosed.column,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Check template content and FormKit validation
|
||||
if (templateContent) {
|
||||
try {
|
||||
validateMustacheSyntax(templateContent);
|
||||
validateFormKit(templateContent);
|
||||
} catch (error) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
message: "Template Syntax Error",
|
||||
data: {
|
||||
message: error.message,
|
||||
line: error.line,
|
||||
column: error.column,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Check for undefined variables
|
||||
const definedVariables = new Set();
|
||||
|
||||
// Add common Vue variables
|
||||
const commonVueVars = [
|
||||
"$route",
|
||||
"$router",
|
||||
"$refs",
|
||||
"$emit",
|
||||
"$slots",
|
||||
"$attrs",
|
||||
];
|
||||
commonVueVars.forEach((v) => definedVariables.add(v));
|
||||
|
||||
// Extract refs and other variables from script
|
||||
const refRegex = /(?:const|let|var)\s+(\w+)\s*=/g;
|
||||
let varMatch;
|
||||
while ((varMatch = refRegex.exec(scriptContent)) !== null) {
|
||||
definedVariables.add(varMatch[1]);
|
||||
}
|
||||
|
||||
// Extract defineProps if any
|
||||
const propsMatch = scriptContent.match(/defineProps\(\s*{([^}]+)}\s*\)/);
|
||||
if (propsMatch) {
|
||||
const propsContent = propsMatch[1];
|
||||
const propNames = propsContent.match(/(\w+)\s*:/g);
|
||||
propNames?.forEach((prop) => {
|
||||
definedVariables.add(prop.replace(":", "").trim());
|
||||
});
|
||||
}
|
||||
|
||||
// Check template for undefined variables
|
||||
const mustacheRegex = /{{([^}]+)}}/g;
|
||||
let lineNumber = 1;
|
||||
let lastIndex = 0;
|
||||
let mustacheMatch;
|
||||
|
||||
while ((mustacheMatch = mustacheRegex.exec(templateContent)) !== null) {
|
||||
// Calculate line number
|
||||
lineNumber += (
|
||||
templateContent.slice(lastIndex, mustacheMatch.index).match(/\n/g) ||
|
||||
[]
|
||||
).length;
|
||||
lastIndex = mustacheMatch.index;
|
||||
|
||||
const expression = mustacheMatch[1].trim();
|
||||
// Split expression and check each variable
|
||||
const variables = expression.split(/[\s.()[\]]+/);
|
||||
|
||||
for (const variable of variables) {
|
||||
// Skip numbers, operators, and empty strings
|
||||
if (
|
||||
!variable ||
|
||||
variable.match(/^[\d+\-*/&|!%<>=?:]+$/) ||
|
||||
variable === "true" ||
|
||||
variable === "false"
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!definedVariables.has(variable)) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
message: "Template Reference Error",
|
||||
data: {
|
||||
message: `Variable "${variable}" is not defined`,
|
||||
line: lineNumber,
|
||||
column:
|
||||
mustacheMatch.index -
|
||||
templateContent.lastIndexOf("\n", mustacheMatch.index),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate template structure
|
||||
const validateTemplateStructure = (code) => {
|
||||
// Add new validation for script tags inside template
|
||||
const templateContent1 = code.match(
|
||||
/<template>([\s\S]*)<\/template>/
|
||||
)?.[1];
|
||||
if (templateContent1) {
|
||||
const scriptInTemplate = templateContent1.match(/<script\b[^>]*>/i);
|
||||
if (scriptInTemplate) {
|
||||
const lineNumber = templateContent1
|
||||
.slice(0, scriptInTemplate.index)
|
||||
.split("\n").length;
|
||||
const column =
|
||||
scriptInTemplate.index -
|
||||
templateContent1.lastIndexOf("\n", scriptInTemplate.index);
|
||||
|
||||
throw {
|
||||
message: "Script tags are not allowed inside template section",
|
||||
line: lineNumber,
|
||||
column: column,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Check for root level template and script tags
|
||||
const rootTemplateCount = (
|
||||
code.match(/^[\s\S]*<template>[\s\S]*<\/template>/g) || []
|
||||
).length;
|
||||
const rootScriptCount = (
|
||||
code.match(/^[\s\S]*<script>[\s\S]*<\/script>/g) || []
|
||||
).length;
|
||||
|
||||
if (rootTemplateCount > 1 || rootScriptCount > 1) {
|
||||
throw new Error(
|
||||
"Vue components must have only one root <template> and one <script> tag"
|
||||
);
|
||||
}
|
||||
|
||||
// Extract template content for further validation
|
||||
const templateContent2 = code.match(
|
||||
/<template>([\s\S]*)<\/template>/
|
||||
)?.[1];
|
||||
if (templateContent2) {
|
||||
const tagStack = [];
|
||||
const tagRegex = /<\/?([a-zA-Z][a-zA-Z0-9:-]*)\s*([^>]*?)(\/?)>/g;
|
||||
let match;
|
||||
let lineNumber = 1;
|
||||
let lastIndex = 0;
|
||||
|
||||
while ((match = tagRegex.exec(templateContent2)) !== null) {
|
||||
const [fullTag, tagName, attributes, selfClosing] = match;
|
||||
|
||||
// Calculate line number
|
||||
lineNumber += (
|
||||
templateContent2.slice(lastIndex, match.index).match(/\n/g) || []
|
||||
).length;
|
||||
lastIndex = match.index;
|
||||
|
||||
// Skip comments
|
||||
if (templateContent2.slice(match.index).startsWith("<!--")) {
|
||||
const commentEnd = templateContent2.indexOf("-->", match.index);
|
||||
if (commentEnd !== -1) {
|
||||
tagRegex.lastIndex = commentEnd + 3;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!fullTag.endsWith(">")) {
|
||||
throw {
|
||||
message: `Malformed tag found: ${fullTag}`,
|
||||
line: lineNumber,
|
||||
column:
|
||||
match.index - templateContent2.lastIndexOf("\n", match.index),
|
||||
};
|
||||
}
|
||||
|
||||
if (selfClosing || fullTag.endsWith("/>")) continue;
|
||||
|
||||
if (!fullTag.startsWith("</")) {
|
||||
tagStack.push({
|
||||
name: tagName,
|
||||
line: lineNumber,
|
||||
column:
|
||||
match.index - templateContent2.lastIndexOf("\n", match.index),
|
||||
});
|
||||
} else {
|
||||
if (tagStack.length === 0) {
|
||||
throw {
|
||||
message: `Unexpected closing tag </${tagName}> found without matching opening tag`,
|
||||
line: lineNumber,
|
||||
column:
|
||||
match.index - templateContent2.lastIndexOf("\n", match.index),
|
||||
};
|
||||
}
|
||||
|
||||
const lastTag = tagStack[tagStack.length - 1];
|
||||
if (lastTag.name !== tagName) {
|
||||
throw {
|
||||
message: `Mismatched tags: expected closing tag for "${lastTag.name}" but found "${tagName}"`,
|
||||
line: lineNumber,
|
||||
column:
|
||||
match.index - templateContent2.lastIndexOf("\n", match.index),
|
||||
};
|
||||
}
|
||||
tagStack.pop();
|
||||
}
|
||||
}
|
||||
|
||||
if (tagStack.length > 0) {
|
||||
const unclosedTag = tagStack[tagStack.length - 1];
|
||||
throw {
|
||||
message: `Unclosed tag: ${unclosedTag.name}`,
|
||||
line: unclosedTag.line,
|
||||
column: unclosedTag.column,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
try {
|
||||
validateTemplateStructure(code);
|
||||
} catch (structureError) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
message: "Template Structure Error",
|
||||
data: {
|
||||
message: structureError.message,
|
||||
line: structureError.line || 1,
|
||||
column: structureError.column || 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ESLint configuration
|
||||
const eslint = new ESLint({
|
||||
overrideConfig: {
|
||||
extends: ["plugin:vue/vue3-recommended"],
|
||||
parserOptions: {
|
||||
parser: "espree",
|
||||
ecmaVersion: 2022,
|
||||
sourceType: "module",
|
||||
},
|
||||
},
|
||||
useEslintrc: false,
|
||||
});
|
||||
|
||||
const results = await eslint.lintText(code);
|
||||
|
||||
if (results[0].messages.length > 0) {
|
||||
const message = results[0].messages[0];
|
||||
|
||||
if (message.fatal === true) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
message: "Bad Linter Test",
|
||||
data: {
|
||||
message: message.message,
|
||||
line: message.line,
|
||||
column: message.column,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Good Linter test",
|
||||
data: {
|
||||
message: message.message,
|
||||
line: message.line,
|
||||
column: message.column,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Code validation passed",
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Internal Server Error",
|
||||
error: error.message,
|
||||
};
|
||||
}
|
||||
});
|
||||
27
server/api/devtool/content/code/prettier-format.js
Normal file
27
server/api/devtool/content/code/prettier-format.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import prettier from "prettier";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event);
|
||||
|
||||
try {
|
||||
if (body.code === undefined) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
message: "Bad Request",
|
||||
};
|
||||
}
|
||||
|
||||
const code = prettier.format(body.code, { semi: false, parser: "vue" });
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Code successfully formatted",
|
||||
data: code,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Internal Server Error",
|
||||
};
|
||||
}
|
||||
});
|
||||
22
server/api/devtool/content/code/save.js
Normal file
22
server/api/devtool/content/code/save.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event);
|
||||
|
||||
try {
|
||||
// Overwrite vue code from path in body with new code
|
||||
const filePath = path.join(process.cwd() + "/pages/", body.path + ".vue");
|
||||
fs.writeFileSync(filePath, body.code, "utf8");
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Code successfully saved",
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Internal Server Error",
|
||||
};
|
||||
}
|
||||
});
|
||||
29
server/api/devtool/content/template/get-list.js
Normal file
29
server/api/devtool/content/template/get-list.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import templates from "@@/templates/index.js";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const query = await getQuery(event);
|
||||
const id = query.id;
|
||||
|
||||
if (!templates || templates?.data.length == 0)
|
||||
return {
|
||||
statusCode: 404,
|
||||
message: "Template data not found",
|
||||
};
|
||||
|
||||
// Search template by id
|
||||
const template = templates.data.find((item) => item.id == id);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Template data successfully fetched",
|
||||
data: template,
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Internal server error",
|
||||
};
|
||||
}
|
||||
});
|
||||
57
server/api/devtool/content/template/import.js
Normal file
57
server/api/devtool/content/template/import.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
import templates from "@@/templates/index.js";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const query = await getQuery(event);
|
||||
const pagePath = query.path;
|
||||
const templateId = query.templateId;
|
||||
|
||||
// Get pageName path and check if it exists
|
||||
const filePath = path.join(process.cwd() + "/pages/", pagePath + ".vue");
|
||||
console.log(filePath);
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "File path not found",
|
||||
};
|
||||
}
|
||||
|
||||
// Get template id from templates
|
||||
const template = templates.data.find(
|
||||
(template) => template.id === templateId
|
||||
);
|
||||
|
||||
// Get template path and check if it exists
|
||||
const templatePath = path.join(
|
||||
process.cwd() + "/templates/",
|
||||
template.filename + ".vue"
|
||||
);
|
||||
|
||||
if (!fs.existsSync(templatePath)) {
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Template not found",
|
||||
};
|
||||
}
|
||||
|
||||
// Get template code
|
||||
const templateCode = fs.readFileSync(templatePath, "utf8");
|
||||
|
||||
// Write template code to pageName path
|
||||
fs.writeFileSync(filePath, templateCode, "utf8");
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Template successfully imported",
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Internal server error",
|
||||
};
|
||||
}
|
||||
});
|
||||
23
server/api/devtool/content/template/list.js
Normal file
23
server/api/devtool/content/template/list.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import templates from "@@/templates/index.js";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
if (!templates || templates?.data.length == 0)
|
||||
return {
|
||||
statusCode: 404,
|
||||
message: "Template data not found",
|
||||
};
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "List template data successfully fetched",
|
||||
data: templates.data,
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Internal server error",
|
||||
};
|
||||
}
|
||||
});
|
||||
23
server/api/devtool/content/template/tag.js
Normal file
23
server/api/devtool/content/template/tag.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import templates from "@@/templates/index.js";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
if (!templates || templates?.tags.length == 0)
|
||||
return {
|
||||
statusCode: 404,
|
||||
message: "Template tags not found",
|
||||
};
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "List template tags successfully fetched",
|
||||
data: templates.tags,
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Internal server error",
|
||||
};
|
||||
}
|
||||
});
|
||||
0
server/api/devtool/lookup/list.js
Normal file
0
server/api/devtool/lookup/list.js
Normal file
51
server/api/devtool/menu/add.js
Normal file
51
server/api/devtool/menu/add.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event);
|
||||
|
||||
try {
|
||||
// Check if last character is not slash
|
||||
if (body.formData.path.slice(-1) != "/") {
|
||||
body.formData.path = body.formData.path + "/";
|
||||
}
|
||||
|
||||
// Check if the path already exists
|
||||
if (fs.existsSync(path.join(process.cwd(), "pages", body.formData.path))) {
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Path already exists. Please choose another path.",
|
||||
};
|
||||
}
|
||||
|
||||
// Create new file path with index.vue
|
||||
const newFilePath = path.join(
|
||||
process.cwd(),
|
||||
"pages",
|
||||
body.formData.path,
|
||||
"index.vue"
|
||||
);
|
||||
|
||||
// Create the folder if doesn't exist
|
||||
fs.mkdirSync(path.dirname(newFilePath), { recursive: true });
|
||||
|
||||
// Create template content
|
||||
const templateContent = buildNuxtTemplate({
|
||||
title: body.formData.title || body.formData.name,
|
||||
name: body.formData.name,
|
||||
});
|
||||
|
||||
// Write file with template
|
||||
fs.writeFileSync(newFilePath, templateContent);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Menu successfully added!",
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: error.message,
|
||||
};
|
||||
}
|
||||
});
|
||||
61
server/api/devtool/menu/delete.js
Normal file
61
server/api/devtool/menu/delete.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import navigationData from "~/navigation";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event);
|
||||
|
||||
try {
|
||||
// Get file path
|
||||
const filePath = path.join(process.cwd() + "/pages/", body.filePath);
|
||||
|
||||
// Delete path
|
||||
fs.rmSync(filePath, { recursive: true, force: true });
|
||||
|
||||
// Remove menu from navigation
|
||||
removeMenuFromNavigation(body.filePath);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Menu successfully deleted and removed from navigation!",
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: error.message,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
function removeMenuFromNavigation(menuPath) {
|
||||
const removeMenuItem = (items) => {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i].path === menuPath) {
|
||||
items.splice(i, 1);
|
||||
return true;
|
||||
}
|
||||
if (items[i].child && items[i].child.length > 0) {
|
||||
if (removeMenuItem(items[i].child)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
navigationData.forEach((section) => {
|
||||
if (section.child) {
|
||||
removeMenuItem(section.child);
|
||||
}
|
||||
});
|
||||
|
||||
// Save updated navigation data
|
||||
const navigationFilePath = path.join(process.cwd(), "navigation", "index.js");
|
||||
const navigationContent = `export default ${JSON.stringify(
|
||||
navigationData,
|
||||
null,
|
||||
2
|
||||
)};`;
|
||||
fs.writeFileSync(navigationFilePath, navigationContent, "utf8");
|
||||
}
|
||||
65
server/api/devtool/menu/edit.js
Normal file
65
server/api/devtool/menu/edit.js
Normal file
@@ -0,0 +1,65 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event);
|
||||
|
||||
// Normalize paths
|
||||
const oldPath = body.filePath.endsWith("/")
|
||||
? body.filePath
|
||||
: body.filePath + "/";
|
||||
const newPath = body.formData.path.endsWith("/")
|
||||
? body.formData.path
|
||||
: body.formData.path + "/";
|
||||
|
||||
// Get file paths
|
||||
const oldFilePath = path.join(process.cwd(), "pages", oldPath, "index.vue");
|
||||
const newFilePath = path.join(process.cwd(), "pages", newPath, "index.vue");
|
||||
|
||||
try {
|
||||
// Create template content
|
||||
const templateContent = buildNuxtTemplate({
|
||||
title: body.formData.title || body.formData.name,
|
||||
name: body.formData.name,
|
||||
});
|
||||
|
||||
if (oldPath !== newPath) {
|
||||
// Create the new folder if it doesn't exist
|
||||
fs.mkdirSync(path.dirname(newFilePath), { recursive: true });
|
||||
|
||||
// Write the new file
|
||||
fs.writeFileSync(newFilePath, templateContent);
|
||||
|
||||
// Delete the old file
|
||||
fs.unlinkSync(oldFilePath);
|
||||
|
||||
// Remove empty directories
|
||||
let dirToCheck = path.dirname(oldFilePath);
|
||||
while (dirToCheck !== path.join(process.cwd(), "pages")) {
|
||||
if (fs.readdirSync(dirToCheck).length === 0) {
|
||||
fs.rmdirSync(dirToCheck);
|
||||
dirToCheck = path.dirname(dirToCheck);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Update existing file
|
||||
fs.writeFileSync(oldFilePath, templateContent);
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message:
|
||||
oldPath !== newPath
|
||||
? "Menu successfully moved and updated"
|
||||
: "Menu successfully updated",
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: error.message,
|
||||
};
|
||||
}
|
||||
});
|
||||
5
server/api/devtool/menu/new-add.js
Normal file
5
server/api/devtool/menu/new-add.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event);
|
||||
|
||||
// try {
|
||||
});
|
||||
25
server/api/devtool/menu/overwrite-navigation.js
Normal file
25
server/api/devtool/menu/overwrite-navigation.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event);
|
||||
|
||||
try {
|
||||
// get menu path
|
||||
const menuPath = path.join(process.cwd() + "/navigation/", "index.js");
|
||||
fs.writeFileSync(
|
||||
menuPath,
|
||||
`export default ${JSON.stringify(body.menuData, null, 2)}`
|
||||
);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Menu successfully saved",
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: error.message,
|
||||
};
|
||||
}
|
||||
});
|
||||
33
server/api/devtool/menu/role-list.js
Normal file
33
server/api/devtool/menu/role-list.js
Normal file
@@ -0,0 +1,33 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const roles = await prisma.role.findMany({
|
||||
select: {
|
||||
roleID: true,
|
||||
roleName: true,
|
||||
},
|
||||
where: {
|
||||
roleStatus: {
|
||||
not: "DELETED",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (roles) {
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Roles successfully fetched",
|
||||
data: roles,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
statusCode: 404,
|
||||
message: "No Roles found",
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: error.message,
|
||||
};
|
||||
}
|
||||
});
|
||||
33
server/api/devtool/menu/user-list.js
Normal file
33
server/api/devtool/menu/user-list.js
Normal file
@@ -0,0 +1,33 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const users = await prisma.user.findMany({
|
||||
select: {
|
||||
userID: true,
|
||||
userUsername: true,
|
||||
},
|
||||
where: {
|
||||
userStatus: {
|
||||
not: "DELETED",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (users) {
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Users successfully fetched",
|
||||
data: users,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
statusCode: 404,
|
||||
message: "No Users found",
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: error.message,
|
||||
};
|
||||
}
|
||||
});
|
||||
36
server/api/devtool/orm/data/get.get.js
Normal file
36
server/api/devtool/orm/data/get.get.js
Normal file
@@ -0,0 +1,36 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const { tableName } = getQuery(event);
|
||||
|
||||
if (!tableName) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
message: "Table name is required",
|
||||
};
|
||||
}
|
||||
|
||||
// const JSONSchemaTable = getPrismaSchemaTable(tableName);
|
||||
// console.log(JSONSchemaTable);
|
||||
|
||||
const getData = await prisma.$queryRawUnsafe(`SELECT * FROM ${tableName}`);
|
||||
|
||||
if (getData.length === 0) {
|
||||
return {
|
||||
statusCode: 404,
|
||||
message: "Data not found",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Data successfully fetched",
|
||||
data: getData,
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error.message);
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Internal Server Error",
|
||||
};
|
||||
}
|
||||
});
|
||||
37
server/api/devtool/orm/schema.get.js
Normal file
37
server/api/devtool/orm/schema.get.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const { type } = getQuery(event);
|
||||
|
||||
if (!type) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
message: "Type is required",
|
||||
};
|
||||
}
|
||||
|
||||
if (type !== "table" && type !== "field") {
|
||||
return {
|
||||
statusCode: 400,
|
||||
message: "Invalid type",
|
||||
};
|
||||
}
|
||||
|
||||
let schema = null;
|
||||
if (type == "table") schema = getPrismaSchemaTable();
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Schema successfully fetched",
|
||||
data: schema,
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error.message);
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Internal Server Error",
|
||||
};
|
||||
}
|
||||
});
|
||||
34
server/api/devtool/orm/studio.get.js
Normal file
34
server/api/devtool/orm/studio.get.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import { exec } from "node:child_process";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
let error = false;
|
||||
|
||||
// Run command yarn prisma studio
|
||||
exec("npx prisma studio", (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error(`exec error: ${error}`);
|
||||
error = true;
|
||||
}
|
||||
console.log(`stdout: ${stdout}`);
|
||||
console.error(`stderr: ${stderr}`);
|
||||
});
|
||||
|
||||
if (error)
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Internal Server Error",
|
||||
};
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Prisma Studio successfully launched",
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error.message);
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Internal Server Error",
|
||||
};
|
||||
}
|
||||
});
|
||||
106
server/api/devtool/orm/table/config/configuration.json
Normal file
106
server/api/devtool/orm/table/config/configuration.json
Normal file
@@ -0,0 +1,106 @@
|
||||
{
|
||||
"columnTypes": [
|
||||
{
|
||||
"group": "Numbers",
|
||||
"options": [
|
||||
"TINYINT",
|
||||
"SMALLINT",
|
||||
"MEDIUMINT",
|
||||
"INT",
|
||||
"BIGINT",
|
||||
"DECIMAL",
|
||||
"FLOAT",
|
||||
"DOUBLE"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Date and Time",
|
||||
"options": ["DATE", "TIME", "DATETIME", "TIMESTAMP", "YEAR"]
|
||||
},
|
||||
{
|
||||
"group": "Strings",
|
||||
"options": [
|
||||
"CHAR",
|
||||
"VARCHAR",
|
||||
"TINYTEXT",
|
||||
"TEXT",
|
||||
"MEDIUMTEXT",
|
||||
"LONGTEXT",
|
||||
"JSON"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Lists",
|
||||
"options": ["ENUM", "SET"]
|
||||
},
|
||||
{
|
||||
"group": "Binary",
|
||||
"options": [
|
||||
"BIT",
|
||||
"BINARY",
|
||||
"VARBINARY",
|
||||
"TINYBLOB",
|
||||
"BLOB",
|
||||
"MEDIUMBLOB",
|
||||
"LONGBLOB"
|
||||
]
|
||||
},
|
||||
{
|
||||
"group": "Geometry",
|
||||
"options": [
|
||||
"GEOMETRY",
|
||||
"POINT",
|
||||
"LINESTRING",
|
||||
"POLYGON",
|
||||
"MULTIPOINT",
|
||||
"MULTILINESTRING",
|
||||
"MULTIPOLYGON",
|
||||
"GEOMETRYCOLLECTION"
|
||||
]
|
||||
}
|
||||
],
|
||||
"dataTypes": [
|
||||
"",
|
||||
"INT",
|
||||
"TINYINT",
|
||||
"SMALLINT",
|
||||
"MEDIUMINT",
|
||||
"BIGINT",
|
||||
"DECIMAL",
|
||||
"NUMERIC",
|
||||
"FLOAT",
|
||||
"DOUBLE",
|
||||
"CHAR",
|
||||
"VARCHAR",
|
||||
"TEXT",
|
||||
"ENUM",
|
||||
"SET",
|
||||
"BINARY",
|
||||
"VARBINARY",
|
||||
"BLOB",
|
||||
"DATE",
|
||||
"TIME",
|
||||
"DATETIME",
|
||||
"TIMESTAMP",
|
||||
"YEAR",
|
||||
"BOOL",
|
||||
"BOOLEAN",
|
||||
"JSON",
|
||||
"JSONB",
|
||||
"XML",
|
||||
"UUID",
|
||||
"GEOMETRY",
|
||||
"POINT",
|
||||
"LINESTRING",
|
||||
"POLYGON"
|
||||
],
|
||||
"tableField": [
|
||||
"name",
|
||||
"type",
|
||||
"length",
|
||||
"defaultValue",
|
||||
"nullable",
|
||||
"primaryKey",
|
||||
"actions"
|
||||
]
|
||||
}
|
||||
81
server/api/devtool/orm/table/config/index.get.js
Normal file
81
server/api/devtool/orm/table/config/index.get.js
Normal file
@@ -0,0 +1,81 @@
|
||||
import fileConfig from "./configuration.json";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
// read configuration file if it exists and return error if it doesn't
|
||||
if (!fileConfig) {
|
||||
return {
|
||||
statusCode: 404,
|
||||
message: "Configuration file not found",
|
||||
};
|
||||
}
|
||||
|
||||
// Get all tables with primary key
|
||||
const tables = await getAllTableWithPK();
|
||||
if (!tables) {
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Please check your database connection",
|
||||
};
|
||||
}
|
||||
|
||||
// Remove columnTypes [{"group": "Foreign Keys", "options": [{"label": "TABLE_NAME (COLUMN_NAME)", "value": "TABLE_NAME"}]}] from fileconfig before appending
|
||||
fileConfig.columnTypes = fileConfig.columnTypes.filter(
|
||||
(columnType) => columnType.group !== "Foreign Keys"
|
||||
);
|
||||
|
||||
// Append columnTypes from fileconfig with tables
|
||||
fileConfig.columnTypes.push({
|
||||
...tables,
|
||||
});
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Configuration file successfully loaded",
|
||||
data: fileConfig,
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error.message);
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Internal Server Error",
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
async function getAllTableWithPK() {
|
||||
try {
|
||||
const tables = await prisma.$queryRaw` SELECT
|
||||
table_name,
|
||||
column_name
|
||||
FROM
|
||||
information_schema.columns
|
||||
WHERE table_schema = DATABASE()
|
||||
AND column_key = 'PRI'`;
|
||||
|
||||
if (!tables) return false;
|
||||
|
||||
// Reformat to {group: "table_name", options: [{label: "TABLE_NAME (COLUMN_NAME)", value: "TABLE_NAME"}]}
|
||||
const remapTables = tables.reduce((acc, table) => {
|
||||
const group = "Foreign Keys";
|
||||
const option = {
|
||||
label: `${table.TABLE_NAME} (${table.COLUMN_NAME})`,
|
||||
value: `[[${table.TABLE_NAME}]]`,
|
||||
};
|
||||
const existingGroup = acc.find((item) => item.group === group);
|
||||
|
||||
if (existingGroup) {
|
||||
existingGroup.options.push(option);
|
||||
} else {
|
||||
acc.push({ group, options: [option] });
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return remapTables[0];
|
||||
} catch (error) {
|
||||
console.log(error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
171
server/api/devtool/orm/table/create/index.post.js
Normal file
171
server/api/devtool/orm/table/create/index.post.js
Normal file
@@ -0,0 +1,171 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { fileURLToPath } from "url";
|
||||
import { dirname, resolve } from "path";
|
||||
import os from "os";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const { tableName, tableSchema, autoIncrementColumn } =
|
||||
await readBody(event);
|
||||
|
||||
if (!tableName || !tableSchema) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
message: "Bad Request",
|
||||
};
|
||||
}
|
||||
|
||||
// Create Table
|
||||
const isTableCreated = await createTable(
|
||||
tableName,
|
||||
tableSchema,
|
||||
autoIncrementColumn
|
||||
);
|
||||
|
||||
if (isTableCreated.statusCode !== 200)
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: isTableCreated.message,
|
||||
};
|
||||
|
||||
// Run Prisma Command
|
||||
const isPrismaCommandRun = await runPrismaCommand();
|
||||
if (!isPrismaCommandRun)
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Prisma Command Failed",
|
||||
};
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Table Created",
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Internal Server Error",
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
async function createTable(tableName, tableSchema) {
|
||||
try {
|
||||
let rawSchema = ``;
|
||||
for (let i = 0; i < tableSchema.length; i++) {
|
||||
const column = tableSchema[i];
|
||||
|
||||
// Sanitize rawSchema
|
||||
if (column.type.includes("[[") && column.type.includes("]]")) {
|
||||
const FKTableName = column.type.replace("[[", "").replace("]]", "");
|
||||
const primaryKey = await prisma.$queryRawUnsafe(
|
||||
"SHOW COLUMNS from " + FKTableName + " where `Key` = 'PRI'"
|
||||
);
|
||||
|
||||
rawSchema += `${column.name} INT NOT NULL, FOREIGN KEY (${column.name}) REFERENCES ${FKTableName}(${primaryKey[0].Field})`;
|
||||
} else {
|
||||
rawSchema += `${column.name}
|
||||
${column.type}${column.length ? "(" + column.length + ")" : ""}
|
||||
${column.defaultValue ? " DEFAULT " + column.defaultValue : ""}
|
||||
${column.nullable ? " NULL" : " NOT NULL "}
|
||||
${column.primaryKey ? " PRIMARY KEY AUTO_INCREMENT" : ""}`;
|
||||
}
|
||||
|
||||
if (i < tableSchema.length - 1) rawSchema += ", ";
|
||||
}
|
||||
|
||||
const sqlStatement = `CREATE TABLE ${tableName} (${rawSchema})`;
|
||||
console.log(sqlStatement);
|
||||
|
||||
const createTable = await prisma.$queryRawUnsafe(sqlStatement);
|
||||
if (!createTable)
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Table Creation Failed",
|
||||
};
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Table Created",
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error.message);
|
||||
|
||||
// Get Message
|
||||
if (error.message.includes("already exists")) {
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: `Table '${tableName}' already exists!`,
|
||||
};
|
||||
}
|
||||
|
||||
if (error.message.includes("1064")) {
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Please ensure the SQL syntax is correct!",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Table Creation Failed",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function runPrismaCommand() {
|
||||
try {
|
||||
console.log("---------- Run Prisma Command ----------");
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const directory = resolve(__dirname, "../..");
|
||||
|
||||
// Command to execute
|
||||
const command = "npx prisma db pull && npx prisma generate";
|
||||
|
||||
// Determine the appropriate shell command based on the platform
|
||||
let shellCommand;
|
||||
let spawnOptions;
|
||||
switch (os.platform()) {
|
||||
case "win32":
|
||||
shellCommand = `Start-Process cmd -ArgumentList '/c cd "${directory}" && ${command}' -Verb RunAs`;
|
||||
spawnOptions = {
|
||||
shell: "powershell.exe",
|
||||
args: ["-Command", shellCommand],
|
||||
};
|
||||
break;
|
||||
case "darwin":
|
||||
case "linux":
|
||||
shellCommand = `cd "${directory}" && ${command}`;
|
||||
spawnOptions = {
|
||||
shell: "sh",
|
||||
args: ["-c", shellCommand],
|
||||
};
|
||||
break;
|
||||
default:
|
||||
console.error("Unsupported platform:", os.platform());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Spawn child process using the appropriate shell command
|
||||
const childProcess = spawn(spawnOptions.shell, spawnOptions.args, {
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
// Listen for child process events
|
||||
return new Promise((resolve, reject) => {
|
||||
childProcess.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
console.log("Prisma commands executed successfully");
|
||||
resolve(true);
|
||||
} else {
|
||||
console.error(`Child process exited with code ${code}`);
|
||||
reject(new Error(`Child process exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error running Prisma commands:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
90
server/api/devtool/orm/table/delete/[table]/index.delete.js
Normal file
90
server/api/devtool/orm/table/delete/[table]/index.delete.js
Normal file
@@ -0,0 +1,90 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { fileURLToPath } from "url";
|
||||
import { dirname, resolve } from "path";
|
||||
import os from "os";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const tableName = event.context.params.table;
|
||||
|
||||
try {
|
||||
// Drop the table
|
||||
await prisma.$executeRawUnsafe(`DROP TABLE IF EXISTS ${tableName}`);
|
||||
|
||||
// Run Prisma Command to update the schema
|
||||
const isPrismaCommandRun = await runPrismaCommand();
|
||||
if (!isPrismaCommandRun) {
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Prisma Command Failed after table deletion",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: `Table '${tableName}' has been successfully deleted.`,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error deleting table:", error);
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: `Failed to delete table '${tableName}'. Error: ${error.message}`,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
async function runPrismaCommand() {
|
||||
try {
|
||||
console.log("---------- Run Prisma Command ----------");
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const directory = resolve(__dirname, "../..");
|
||||
|
||||
// Command to execute
|
||||
const command = "npx prisma db pull && npx prisma generate";
|
||||
|
||||
// Determine the appropriate shell command based on the platform
|
||||
let shellCommand;
|
||||
let spawnOptions;
|
||||
switch (os.platform()) {
|
||||
case "win32":
|
||||
shellCommand = `Start-Process cmd -ArgumentList '/c cd "${directory}" && ${command}' -Verb RunAs`;
|
||||
spawnOptions = {
|
||||
shell: "powershell.exe",
|
||||
args: ["-Command", shellCommand],
|
||||
};
|
||||
break;
|
||||
case "darwin":
|
||||
case "linux":
|
||||
shellCommand = `cd "${directory}" && ${command}`;
|
||||
spawnOptions = {
|
||||
shell: "sh",
|
||||
args: ["-c", shellCommand],
|
||||
};
|
||||
break;
|
||||
default:
|
||||
console.error("Unsupported platform:", os.platform());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Spawn child process using the appropriate shell command
|
||||
const childProcess = spawn(spawnOptions.shell, spawnOptions.args, {
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
// Listen for child process events
|
||||
return new Promise((resolve, reject) => {
|
||||
childProcess.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
console.log("Prisma commands executed successfully");
|
||||
resolve(true);
|
||||
} else {
|
||||
console.error(`Child process exited with code ${code}`);
|
||||
reject(new Error(`Child process exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error running Prisma commands:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
113
server/api/devtool/orm/table/modify/get.get.js
Normal file
113
server/api/devtool/orm/table/modify/get.get.js
Normal file
@@ -0,0 +1,113 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const { tableName } = getQuery(event);
|
||||
|
||||
if (!tableName) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
message: "Table name is required",
|
||||
};
|
||||
}
|
||||
|
||||
const result = await prisma.$queryRaw`SELECT DATABASE() AS db_name`;
|
||||
// console.log(result[0].db_name);
|
||||
|
||||
if (result.length === 0) {
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Please check your database connection",
|
||||
};
|
||||
}
|
||||
|
||||
let sqlRaw = ` SELECT
|
||||
c.COLUMN_NAME,
|
||||
c.DATA_TYPE,
|
||||
c.CHARACTER_MAXIMUM_LENGTH,
|
||||
c.COLUMN_DEFAULT,
|
||||
c.IS_NULLABLE,
|
||||
c.COLUMN_KEY,
|
||||
kcu.REFERENCED_TABLE_NAME,
|
||||
kcu.REFERENCED_COLUMN_NAME
|
||||
FROM
|
||||
INFORMATION_SCHEMA.COLUMNS c
|
||||
LEFT JOIN
|
||||
INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu ON
|
||||
c.TABLE_SCHEMA = kcu.TABLE_SCHEMA AND
|
||||
c.TABLE_NAME = kcu.TABLE_NAME AND
|
||||
c.COLUMN_NAME = kcu.COLUMN_NAME
|
||||
WHERE
|
||||
c.TABLE_SCHEMA = '${result[0].db_name}' AND
|
||||
c.TABLE_NAME = '${tableName}';`;
|
||||
|
||||
// console.log(sqlRaw);
|
||||
|
||||
const getTableDetails = await prisma.$queryRawUnsafe(sqlRaw);
|
||||
// console.log(getTableDetails);
|
||||
|
||||
/*
|
||||
[{
|
||||
"actions": "",
|
||||
"defaultValue": "",
|
||||
"length": "",
|
||||
"name": "PID",
|
||||
"nullable": "",
|
||||
"primaryKey": true,
|
||||
"type": "INT"
|
||||
},
|
||||
{
|
||||
"actions": "",
|
||||
"defaultValue": "",
|
||||
"length": "",
|
||||
"name": "Pproduct",
|
||||
"nullable": true,
|
||||
"primaryKey": "",
|
||||
"type": "VARCHAR"
|
||||
},
|
||||
{
|
||||
"actions": "",
|
||||
"defaultValue": "",
|
||||
"length": "",
|
||||
"name": "userID",
|
||||
"nullable": "",
|
||||
"primaryKey": "",
|
||||
"type": "[[user]]"
|
||||
}]
|
||||
*/
|
||||
|
||||
let tableDetailsData = [];
|
||||
|
||||
// Loop through the result and convert bigInt to number
|
||||
for (let i = 0; i < getTableDetails.length; i++) {
|
||||
const table = getTableDetails[i];
|
||||
|
||||
tableDetailsData.push({
|
||||
name: table.COLUMN_NAME,
|
||||
type: table.REFERENCED_TABLE_NAME
|
||||
? `[[${table.REFERENCED_TABLE_NAME}]]`
|
||||
: table.DATA_TYPE.toUpperCase(),
|
||||
length: bigIntToNumber(table.CHARACTER_MAXIMUM_LENGTH),
|
||||
defaultValue: table.COLUMN_DEFAULT,
|
||||
nullable: table.IS_NULLABLE === "YES",
|
||||
primaryKey: table.COLUMN_KEY === "PRI",
|
||||
actions: {},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Success",
|
||||
data: tableDetailsData,
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error.message);
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Internal Server Error",
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
function bigIntToNumber(bigInt) {
|
||||
if (bigInt === null) return null;
|
||||
return Number(bigInt.toString());
|
||||
}
|
||||
191
server/api/devtool/orm/table/modify/index.post.js
Normal file
191
server/api/devtool/orm/table/modify/index.post.js
Normal file
@@ -0,0 +1,191 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { fileURLToPath } from "url";
|
||||
import { dirname, resolve } from "path";
|
||||
import os from "os";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
try {
|
||||
const { tableName, tableSchema, autoIncrementColumn } =
|
||||
await readBody(event);
|
||||
|
||||
if (!tableName || !tableSchema) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
message: "Bad Request",
|
||||
};
|
||||
}
|
||||
|
||||
// Get existing table structure
|
||||
const existingColumns = await prisma.$queryRaw`
|
||||
SELECT COLUMN_NAME, DATA_TYPE, IS_NULLABLE, COLUMN_KEY
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ${tableName}
|
||||
`;
|
||||
|
||||
// Compare and modify table structure
|
||||
for (const column of tableSchema) {
|
||||
const existingColumn = existingColumns.find(
|
||||
(c) => c.COLUMN_NAME === column.name
|
||||
);
|
||||
|
||||
if (existingColumn) {
|
||||
// Modify existing column
|
||||
await modifyColumn(tableName, column, existingColumn);
|
||||
} else {
|
||||
// Add new column
|
||||
await addColumn(tableName, column);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove columns that are not in the new schema
|
||||
for (const existingColumn of existingColumns) {
|
||||
if (!tableSchema.find((c) => c.name === existingColumn.COLUMN_NAME)) {
|
||||
await removeColumn(tableName, existingColumn.COLUMN_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
// Update auto-increment column if necessary
|
||||
if (autoIncrementColumn) {
|
||||
await updateAutoIncrement(tableName, autoIncrementColumn);
|
||||
}
|
||||
|
||||
// Run Prisma Command to update the schema
|
||||
const isPrismaCommandRun = await runPrismaCommand();
|
||||
if (!isPrismaCommandRun) {
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Prisma Command Failed",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Table modified successfully",
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Internal Server Error",
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
async function modifyColumn(tableName, newColumn, existingColumn) {
|
||||
let alterStatement = `ALTER TABLE ${tableName} MODIFY COLUMN ${newColumn.name} ${newColumn.type}`;
|
||||
|
||||
if (newColumn.length) {
|
||||
alterStatement += `(${newColumn.length})`;
|
||||
}
|
||||
|
||||
alterStatement += newColumn.nullable ? " NULL" : " NOT NULL";
|
||||
|
||||
if (newColumn.defaultValue) {
|
||||
alterStatement += ` DEFAULT ${newColumn.defaultValue}`;
|
||||
}
|
||||
|
||||
await prisma.$executeRawUnsafe(alterStatement);
|
||||
}
|
||||
|
||||
async function addColumn(tableName, column) {
|
||||
let alterStatement = `ALTER TABLE ${tableName} ADD COLUMN ${column.name} ${column.type}`;
|
||||
|
||||
if (column.length) {
|
||||
alterStatement += `(${column.length})`;
|
||||
}
|
||||
|
||||
alterStatement += column.nullable ? " NULL" : " NOT NULL";
|
||||
|
||||
if (column.defaultValue) {
|
||||
alterStatement += ` DEFAULT ${column.defaultValue}`;
|
||||
}
|
||||
|
||||
await prisma.$executeRawUnsafe(alterStatement);
|
||||
}
|
||||
|
||||
async function removeColumn(tableName, columnName) {
|
||||
await prisma.$executeRawUnsafe(
|
||||
`ALTER TABLE ${tableName} DROP COLUMN ${columnName}`
|
||||
);
|
||||
}
|
||||
|
||||
async function updateAutoIncrement(tableName, autoIncrementColumn) {
|
||||
await prisma.$executeRawUnsafe(
|
||||
`ALTER TABLE ${tableName} MODIFY ${autoIncrementColumn} INT AUTO_INCREMENT`
|
||||
);
|
||||
}
|
||||
|
||||
async function runPrismaCommand(retries = 3) {
|
||||
try {
|
||||
console.log("---------- Run Prisma Command ----------");
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const directory = resolve(__dirname, "../..");
|
||||
|
||||
// Command to execute
|
||||
const command = "npx prisma db pull && npx prisma generate";
|
||||
|
||||
// Determine the appropriate shell command based on the platform
|
||||
let shellCommand;
|
||||
let spawnOptions;
|
||||
switch (os.platform()) {
|
||||
case "win32":
|
||||
shellCommand = `Start-Process cmd -ArgumentList '/c cd "${directory}" && ${command}' -Verb RunAs`;
|
||||
spawnOptions = {
|
||||
shell: "powershell.exe",
|
||||
args: ["-Command", shellCommand],
|
||||
};
|
||||
break;
|
||||
case "darwin":
|
||||
case "linux":
|
||||
shellCommand = `cd "${directory}" && ${command}`;
|
||||
spawnOptions = {
|
||||
shell: "sh",
|
||||
args: ["-c", shellCommand],
|
||||
};
|
||||
break;
|
||||
default:
|
||||
console.error("Unsupported platform:", os.platform());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Spawn child process using the appropriate shell command
|
||||
const childProcess = spawn(spawnOptions.shell, spawnOptions.args, {
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
// Listen for child process events
|
||||
return new Promise((resolve, reject) => {
|
||||
childProcess.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
console.log("Prisma commands executed successfully");
|
||||
resolve(true);
|
||||
} else {
|
||||
console.error(`Child process exited with code ${code}`);
|
||||
reject(new Error(`Child process exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error running Prisma commands:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function spawnCommand(command, args, cwd) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const process = spawn(command, args, {
|
||||
cwd,
|
||||
stdio: "inherit",
|
||||
shell: true,
|
||||
});
|
||||
|
||||
process.on("close", (code) => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Command failed with exit code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
87
server/api/devtool/role/add.js
Normal file
87
server/api/devtool/role/add.js
Normal file
@@ -0,0 +1,87 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event);
|
||||
|
||||
try {
|
||||
// Check if the role already exists
|
||||
const allRole = await prisma.role.findMany({
|
||||
where: {
|
||||
roleStatus: "ACTIVE",
|
||||
},
|
||||
});
|
||||
|
||||
const roleExist = allRole.find((role) => {
|
||||
return role?.roleName.toLowerCase() === body?.name.toLowerCase();
|
||||
});
|
||||
|
||||
if (roleExist) {
|
||||
return {
|
||||
statusCode: 400,
|
||||
message: "Role already exists",
|
||||
};
|
||||
}
|
||||
|
||||
// add new role
|
||||
const role = await prisma.role.create({
|
||||
data: {
|
||||
roleName: body.name,
|
||||
roleDescription: body.description || "",
|
||||
roleStatus: "ACTIVE",
|
||||
roleCreatedDate: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
if (role) {
|
||||
// Add User to the role if users are provided
|
||||
if (body.users && Array.isArray(body.users)) {
|
||||
const userRoles = await Promise.all(
|
||||
body.users.map(async (el) => {
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
userUsername: el.value,
|
||||
},
|
||||
});
|
||||
|
||||
if (user) {
|
||||
return prisma.userrole.create({
|
||||
data: {
|
||||
userRoleUserID: user.userID,
|
||||
userRoleRoleID: role.roleID,
|
||||
userRoleCreatedDate: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
return null;
|
||||
})
|
||||
);
|
||||
|
||||
const validUserRoles = userRoles.filter(Boolean);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Role successfully added!",
|
||||
data: {
|
||||
role,
|
||||
assignedUsers: validUserRoles.length,
|
||||
totalUsers: body.users.length,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Role successfully added!",
|
||||
data: { role },
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Something went wrong! Please contact your administrator.",
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: error.message,
|
||||
};
|
||||
}
|
||||
});
|
||||
28
server/api/devtool/role/delete.js
Normal file
28
server/api/devtool/role/delete.js
Normal file
@@ -0,0 +1,28 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event);
|
||||
|
||||
try {
|
||||
// Delete user
|
||||
const user = await prisma.role.updateMany({
|
||||
where: {
|
||||
roleID: body.id,
|
||||
},
|
||||
data: {
|
||||
roleStatus: "DELETED",
|
||||
roleModifiedDate: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
if (user) {
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "User deleted successfully",
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: error.message,
|
||||
};
|
||||
}
|
||||
});
|
||||
77
server/api/devtool/role/edit.js
Normal file
77
server/api/devtool/role/edit.js
Normal file
@@ -0,0 +1,77 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event);
|
||||
try {
|
||||
// Edit role
|
||||
const role = await prisma.role.update({
|
||||
where: {
|
||||
roleID: body.id,
|
||||
},
|
||||
data: {
|
||||
roleName: body.name,
|
||||
roleDescription: body.description,
|
||||
roleModifiedDate: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
if (role) {
|
||||
// Delete all user roles for this role
|
||||
await prisma.userrole.deleteMany({
|
||||
where: {
|
||||
userRoleRoleID: body.id,
|
||||
},
|
||||
});
|
||||
|
||||
// Add User to the role if users are provided
|
||||
if (body.users && Array.isArray(body.users)) {
|
||||
const userRoles = await Promise.all(
|
||||
body.users.map(async (el) => {
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
userUsername: el.value,
|
||||
},
|
||||
});
|
||||
|
||||
if (user) {
|
||||
return prisma.userrole.create({
|
||||
data: {
|
||||
userRoleUserID: user.userID,
|
||||
userRoleRoleID: body.id,
|
||||
userRoleCreatedDate: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
return null;
|
||||
})
|
||||
);
|
||||
|
||||
const validUserRoles = userRoles.filter(Boolean);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Role successfully edited!",
|
||||
data: {
|
||||
role,
|
||||
assignedUsers: validUserRoles.length,
|
||||
totalUsers: body.users.length,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Role successfully edited!",
|
||||
data: { role },
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Something went wrong! Please contact your administrator.",
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: error.message,
|
||||
};
|
||||
}
|
||||
});
|
||||
59
server/api/devtool/role/list.js
Normal file
59
server/api/devtool/role/list.js
Normal file
@@ -0,0 +1,59 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
// Get all users from database
|
||||
|
||||
try {
|
||||
const roles = await prisma.role.findMany({
|
||||
select: {
|
||||
roleID: true,
|
||||
roleName: true,
|
||||
roleDescription: true,
|
||||
roleStatus: true,
|
||||
roleCreatedDate: true,
|
||||
roleModifiedDate: true,
|
||||
},
|
||||
where: {
|
||||
roleStatus: {
|
||||
not: "DELETED",
|
||||
},
|
||||
roleID: {
|
||||
not: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (roles) {
|
||||
for (let i = 0; i < roles.length; i++) {
|
||||
let userOfRole = await prisma.userrole.findMany({
|
||||
select: {
|
||||
user: {
|
||||
select: {
|
||||
userUsername: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
userRoleRoleID: roles[i].roleID,
|
||||
},
|
||||
});
|
||||
|
||||
roles[i].users = userOfRole;
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Roles successfully fetched",
|
||||
data: roles,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
statusCode: 404,
|
||||
message: "No Roles found",
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: error.message,
|
||||
};
|
||||
}
|
||||
});
|
||||
138
server/api/devtool/user/add.js
Normal file
138
server/api/devtool/user/add.js
Normal file
@@ -0,0 +1,138 @@
|
||||
import sha256 from "crypto-js/sha256.js";
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event);
|
||||
|
||||
const password = sha256("abc123").toString();
|
||||
let secretKey = generateSecretKey();
|
||||
|
||||
try {
|
||||
// Get user from database
|
||||
const allUser = await prisma.user.findMany({
|
||||
where: {
|
||||
userStatus: "ACTIVE",
|
||||
},
|
||||
});
|
||||
|
||||
// Check if the user already exists
|
||||
const userExist = allUser.find((user) => {
|
||||
return user?.userUsername.toLowerCase() === body?.username.toLowerCase();
|
||||
});
|
||||
|
||||
if (userExist)
|
||||
return {
|
||||
statusCode: 400,
|
||||
message: "Username already exists",
|
||||
};
|
||||
|
||||
// Validate secret key
|
||||
do {
|
||||
secretKey = generateSecretKey();
|
||||
} while (
|
||||
allUser.find((user) => {
|
||||
return user?.userSecretKey === secretKey;
|
||||
})
|
||||
);
|
||||
|
||||
// Add New User
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
userSecretKey: secretKey,
|
||||
userUsername: body.username,
|
||||
userPassword: password,
|
||||
userFullName: body?.fullname || "",
|
||||
userEmail: body?.email || "",
|
||||
userPhone: body?.phone || "",
|
||||
userStatus: "ACTIVE",
|
||||
userCreatedDate: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
if (user) {
|
||||
// Add user roles if provided
|
||||
if (body.role && Array.isArray(body.role)) {
|
||||
const userRoles = await Promise.all(
|
||||
body.role.map(async (role) => {
|
||||
const existingRole = await prisma.role.findFirst({
|
||||
where: {
|
||||
roleID: role.value,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingRole) {
|
||||
return prisma.userrole.create({
|
||||
data: {
|
||||
userRoleUserID: user.userID,
|
||||
userRoleRoleID: role.value,
|
||||
userRoleCreatedDate: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
return null;
|
||||
})
|
||||
);
|
||||
|
||||
const validUserRoles = userRoles.filter(Boolean);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "User successfully added!",
|
||||
data: {
|
||||
user,
|
||||
assignedRoles: validUserRoles.length,
|
||||
totalRoles: body.role.length,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "User successfully added!",
|
||||
data: { user },
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: "Something went wrong! Please contact your administrator.",
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: error.message,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
function generateSecretKey() {
|
||||
// Generate Secret Key number and alphabet. Format : xxxx-xxxx-xxxx-xxxx
|
||||
let secretKey = "";
|
||||
let possible =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
for (let j = 0; j < 4; j++) {
|
||||
secretKey += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||
}
|
||||
|
||||
if (i < 3) {
|
||||
secretKey += "-";
|
||||
}
|
||||
}
|
||||
|
||||
return secretKey;
|
||||
}
|
||||
|
||||
async function checkRoleID(roleID) {
|
||||
const role = await prisma.role.findFirst({
|
||||
where: {
|
||||
roleID: roleID,
|
||||
},
|
||||
});
|
||||
|
||||
if (!role) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
28
server/api/devtool/user/delete.js
Normal file
28
server/api/devtool/user/delete.js
Normal file
@@ -0,0 +1,28 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event);
|
||||
|
||||
try {
|
||||
// Delete user
|
||||
const user = await prisma.user.updateMany({
|
||||
where: {
|
||||
userUsername: body.username,
|
||||
},
|
||||
data: {
|
||||
userStatus: "DELETED",
|
||||
userModifiedDate: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
if (user) {
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "User deleted successfully",
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: error.message,
|
||||
};
|
||||
}
|
||||
});
|
||||
86
server/api/devtool/user/edit.js
Normal file
86
server/api/devtool/user/edit.js
Normal file
@@ -0,0 +1,86 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event);
|
||||
|
||||
try {
|
||||
// Update user
|
||||
const user = await prisma.user.updateMany({
|
||||
where: {
|
||||
userUsername: body.username,
|
||||
},
|
||||
data: {
|
||||
userFullName: body?.fullname || "",
|
||||
userEmail: body?.email || "",
|
||||
userPhone: body?.phone || "",
|
||||
userStatus: body.status,
|
||||
userModifiedDate: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
if (user.count > 0) {
|
||||
const getUserID = await prisma.user.findFirst({
|
||||
where: {
|
||||
userUsername: body.username,
|
||||
},
|
||||
});
|
||||
|
||||
if (getUserID) {
|
||||
// Delete all user roles
|
||||
await prisma.userrole.deleteMany({
|
||||
where: {
|
||||
userRoleUserID: getUserID.userID,
|
||||
},
|
||||
});
|
||||
|
||||
// Add new user roles
|
||||
if (body.role && Array.isArray(body.role)) {
|
||||
const userRoles = await Promise.all(
|
||||
body.role.map(async (role) => {
|
||||
const existingRole = await prisma.role.findFirst({
|
||||
where: {
|
||||
roleID: role.value,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingRole) {
|
||||
return prisma.userrole.create({
|
||||
data: {
|
||||
userRoleUserID: getUserID.userID,
|
||||
userRoleRoleID: role.value,
|
||||
userRoleCreatedDate: new Date(),
|
||||
},
|
||||
});
|
||||
}
|
||||
return null;
|
||||
})
|
||||
);
|
||||
|
||||
const validUserRoles = userRoles.filter(Boolean);
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "User updated successfully",
|
||||
data: {
|
||||
assignedRoles: validUserRoles.length,
|
||||
totalRoles: body.role.length,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "User updated successfully",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 404,
|
||||
message: "User not found",
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: error.message,
|
||||
};
|
||||
}
|
||||
});
|
||||
60
server/api/devtool/user/list.js
Normal file
60
server/api/devtool/user/list.js
Normal file
@@ -0,0 +1,60 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
// Get all users from database except userStatus = DELETED
|
||||
|
||||
try {
|
||||
const users = await prisma.user.findMany({
|
||||
select: {
|
||||
userID: true,
|
||||
userUsername: true,
|
||||
userFullName: true,
|
||||
userEmail: true,
|
||||
userPhone: true,
|
||||
userStatus: true,
|
||||
userCreatedDate: true,
|
||||
userModifiedDate: true,
|
||||
},
|
||||
where: {
|
||||
userStatus: {
|
||||
not: "DELETED",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (users) {
|
||||
// Get all roles for each user
|
||||
for (let i = 0; i < users.length; i++) {
|
||||
let roleOfUser = await prisma.userrole.findMany({
|
||||
select: {
|
||||
role: {
|
||||
select: {
|
||||
roleID: true,
|
||||
roleName: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: {
|
||||
userRoleUserID: users[i].userID,
|
||||
},
|
||||
});
|
||||
|
||||
users[i].roles = roleOfUser;
|
||||
}
|
||||
|
||||
return {
|
||||
statusCode: 200,
|
||||
message: "Users successfully fetched",
|
||||
data: users,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
statusCode: 404,
|
||||
message: "No users found",
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
statusCode: 500,
|
||||
message: error.message,
|
||||
};
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user