feat(container): add CRUD operations for container management

- Implemented routes for listing, retrieving, updating, and deleting containers.
- Added ContainerModel with necessary fields and methods for data handling.
- Created utility functions for fetching container data by ID.

feat(page): enhance page management with CRUD and publish functionality

- Developed routes for managing pages, including listing, updating, and deleting.
- Integrated caching and zip file generation for page exports.
- Added publish functionality to manage app versions and file uploads.

feat(prompts): implement prompt management with CRUD operations

- Created routes for listing, updating, and deleting prompts.
- Added pagination and search capabilities for prompt listing.

test: add common query utilities and prompt tests

- Implemented common query utilities for API interactions.
- Added tests for prompt listing functionality.
This commit is contained in:
2025-12-30 13:28:50 +08:00
parent 27e5fb5e82
commit 8731801b52
28 changed files with 411 additions and 103 deletions

View File

@@ -0,0 +1,193 @@
import { useFileStore } from '@kevisual/use-config/file-store';
import { PageModel } from '../models/index.ts';
import { ContainerModel } from '@/old-apps/container/models/index.ts';
import { Op } from 'sequelize';
import { getContainerData } from './get-container.ts';
import path from 'node:path';
import fs from 'node:fs';
import { getHTML, getDataJs, getOneHTML } from './file-template.ts';
import { minioClient } from '@/app.ts';
import { bucketName } from '@/modules/minio.ts';
import { getContentType } from '@/utils/get-content-type.ts';
import archiver from 'archiver';
import { CustomError } from '@kevisual/router';
import { nanoid } from 'nanoid';
export const cacheFile = useFileStore('cache-file', {
needExists: true,
});
export const getDeck = async (page: PageModel) => {
const { data } = page;
const { nodes = [], edges } = data;
const containerList = nodes
.map((item) => {
const { data } = item;
return data?.cid;
})
.filter((item) => item);
const quchong = Array.from(new Set(containerList));
const containers = await ContainerModel.findAll({
where: {
id: {
[Op.in]: quchong,
},
},
});
const pageData = {
page,
containerList: containers,
};
return pageData;
};
export const cachePage = async (page: PageModel, opts: { tokenUser: any; key; version }) => {
const _result = await getDeck(page);
const result = getContainerData(_result);
const key = 'data-' + nanoid(6);
const html = getHTML({ rootId: page.id, title: page?.publish?.key, dataKey: key });
const dataJs = getDataJs(result);
const htmlPath = path.resolve(cacheFile, `${page.id}.html`);
const dataJsPath = path.resolve(cacheFile, `${page.id}.js`);
fs.writeFileSync(htmlPath, html);
fs.writeFileSync(dataJsPath, dataJs);
const minioHTML = await uploadMinio({ ...opts, path: `index.html`, filePath: htmlPath });
const minioData = await uploadMinio({ ...opts, path: `${key || 'data'}.js`, filePath: dataJsPath });
return [
{
name: 'index.html',
path: minioHTML,
},
{
name: `${key || 'data'}.js`,
path: minioData,
},
];
};
export const uploadMinioContainer = async ({ tokenUser, key, version, code, filePath, saveHTML }) => {
if ((filePath as string).includes('..')) {
throw new CustomError('file path is invalid');
}
const uploadFiles = [];
const minioKeyVersion = `${tokenUser.username}/${key}/${version}`;
const minioPath = path.join(minioKeyVersion, filePath);
const minioFileName = path.basename(minioPath);
if (!minioFileName.endsWith('.js')) {
saveHTML = false;
}
console.log('minioPath', minioPath);
// const isHTML = filePath.endsWith('.html');
const name = minioPath.replace(minioKeyVersion + '/', '');
await minioClient.putObject(bucketName, minioPath, code, code.length, {
'Content-Type': getContentType(filePath),
'app-source': 'user-app',
'Cache-Control': 'no-cache', // no-cache
});
uploadFiles.push({
name,
path: minioPath,
});
if (saveHTML) {
const htmlPath = minioPath.replace('.js', '.html');
const code = getOneHTML({ title: 'Kevisual', file: minioFileName.replace('.js', '') });
await minioClient.putObject(bucketName, htmlPath, code, code.length, {
'Content-Type': 'text/html',
'app-source': 'user-app',
'Cache-Control': 'max-age=31536000, immutable',
});
uploadFiles.push({
name: 'index.html',
path: htmlPath,
});
}
return uploadFiles;
};
export const uploadMinio = async ({ tokenUser, key, version, path, filePath }) => {
const minioPath = `${tokenUser.username}/${key}/${version}/${path}`;
const isHTML = filePath.endsWith('.html');
await minioClient.fPutObject(bucketName, minioPath, filePath, {
'Content-Type': getContentType(filePath),
'app-source': 'user-app',
'Cache-Control': isHTML ? 'no-cache' : 'max-age=31536000, immutable', // 缓存一年
});
fs.unlinkSync(filePath); // 删除临时文件
return minioPath;
};
export const uploadMinioTemp = async ({ tokenUser, filePath, path }) => {
const minioPath = `${tokenUser.username}/temp/${path}`;
const isHTML = filePath.endsWith('.html');
await minioClient.fPutObject(bucketName, minioPath, filePath, {
'Content-Type': getContentType(filePath),
'app-source': 'user-app',
'Cache-Control': isHTML ? 'no-cache' : 'max-age=31536000, immutable', // 缓存一年
});
fs.unlinkSync(filePath); // 删除临时文件
return minioPath;
};
export const getZip = async (page: PageModel, opts: { tokenUser: any }) => {
const _result = await getDeck(page);
const result = getContainerData(_result);
const html = getHTML({ rootId: page.id, title: page?.publish?.key });
const dataJs = getDataJs(result);
const zip = archiver('zip', {
zlib: { level: 9 },
});
// 创建 zip 文件的输出流
const zipCache = path.join(cacheFile, `${page.id}.zip`);
if (checkFileExistsSync(zipCache)) {
throw new CustomError('page is on uploading');
}
return await new Promise((resolve, reject) => {
const output = fs.createWriteStream(zipCache);
// 监听事件
output.on('close', async () => {
console.log(`Zip file has been created successfully. Total size: ${zip.pointer()} bytes.`);
let time = (new Date().getTime() / 1000).toFixed(0);
const name = page.title || page.id;
const minioPath = await uploadMinioTemp({ ...opts, filePath: zipCache, path: `${name + '-' + time}.zip` });
resolve(minioPath);
});
output.on('end', () => {
console.log('Data has been drained.'); // 数据已被耗尽
throw new CustomError('Data has been drained.');
});
zip.on('warning', (err) => {
if (err.code === 'ENOENT') {
console.warn('File not found:', err);
} else {
throw err;
}
});
zip.on('error', (err) => {
throw err;
});
// 通过管道将 zip 数据流输出到指定文件
zip.pipe(output);
// 添加 HTML 字符串作为文件到 zip 中
zip.append(html, { name: 'index.html' });
// 添加 JavaScript 字符串作为文件到 zip 中
zip.append(dataJs, { name: 'data.js' });
zip.append(JSON.stringify(page), { name: 'app.config.json' });
// 可以继续添加更多内容,文件或目录等
// zip.append('Another content', { name: 'other.txt' });
// 结束归档(必须调用,否则 zip 文件无法完成)
zip.finalize();
});
};
export const checkFileExistsSync = (filePath: string) => {
try {
// 使用 F_OK 检查文件或目录是否存在
fs.accessSync(filePath, fs.constants.F_OK);
return true;
} catch (err) {
return false;
}
};