Files
code-center/src/routes/app-manager/list.ts
abearxiong 0d73941127 chore: 更新依赖版本,提升 @kevisual/query 和 @kevisual/context 的版本
feat: 在应用管理路由中添加元数据,增强版本检测和发布功能
2026-02-19 03:59:29 +08:00

525 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { App as AppType, AppList, AppData } from './module/app-drizzle.ts';
import { app, db, schema } from '@/app.ts';
import { uniqBy } from 'es-toolkit';
import { getUidByUsername, prefixFix } from './util.ts';
import { deleteFiles, getMinioListAndSetToAppList } from '../file/index.ts';
import { setExpire } from './revoke.ts';
import { User } from '@/models/user.ts';
import { callDetectAppVersion } from './export.ts';
import { eq, and, desc } from 'drizzle-orm';
import { z } from 'zod';
import { logger } from '@/modules/logger.ts';
app
.route({
path: 'app',
key: 'list',
middleware: ['auth'],
description: '获取应用列表根据key进行过滤',
metadata: {
args: {
data: z.object({
key: z.string().describe('应用的唯一标识')
})
}
}
})
.define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
const data = ctx.query.data || {};
if (!data.key) {
ctx.throw('key is required');
}
const list = await db.select()
.from(schema.kvAppList)
.where(and(
eq(schema.kvAppList.uid, tokenUser.id),
eq(schema.kvAppList.key, data.key)
))
.orderBy(desc(schema.kvAppList.updatedAt));
ctx.body = list.map((item) => prefixFix(item, tokenUser.username));
return ctx;
})
.addTo(app);
app
.route({
path: 'app',
key: 'get',
middleware: ['auth'],
description: '获取应用详情可以通过id或者key+version来获取',
})
.define(async (ctx) => {
console.log('get app manager called');
const tokenUser = ctx.state.tokenUser;
const id = ctx.query.id;
const { key, version, create = false } = ctx.query?.data || {};
if (!id && (!key || !version)) {
ctx.throw('id is required');
}
let appListModel: AppList | undefined;
if (id) {
const apps = await db.select().from(schema.kvAppList).where(eq(schema.kvAppList.id, id)).limit(1);
appListModel = apps[0];
} else if (key && version) {
const apps = await db.select().from(schema.kvAppList).where(and(
eq(schema.kvAppList.key, key),
eq(schema.kvAppList.version, version),
eq(schema.kvAppList.uid, tokenUser.id)
)).limit(1);
appListModel = apps[0];
}
if (!appListModel && create) {
const newApps = await db.insert(schema.kvAppList).values({
key,
version,
uid: tokenUser.id,
data: {},
}).returning();
appListModel = newApps[0];
const appModels = await db.select().from(schema.kvApp).where(and(
eq(schema.kvApp.key, key),
eq(schema.kvApp.uid, tokenUser.id)
)).limit(1);
const appModel = appModels[0];
if (!appModel) {
await db.insert(schema.kvApp).values({
key,
uid: tokenUser.id,
user: tokenUser.username,
version,
title: key,
description: '',
data: {},
});
}
const res = await callDetectAppVersion({ appKey: key, version, username: tokenUser.username }, ctx.query.token);
if (res.code !== 200) {
ctx.throw(res.message || 'detect version list error');
}
const apps2 = await db.select().from(schema.kvAppList).where(and(
eq(schema.kvAppList.key, key),
eq(schema.kvAppList.version, version),
eq(schema.kvAppList.uid, tokenUser.id)
)).limit(1);
appListModel = apps2[0];
}
if (!appListModel) {
ctx.throw('app not found');
}
logger.debug('get app', appListModel.id, appListModel.key, appListModel.version);
ctx.body = prefixFix(appListModel, tokenUser.username);
})
.addTo(app);
app
.route({
path: 'app',
key: 'update',
middleware: ['auth'],
description: '创建或更新应用信息如果传入id则为更新否则为创建',
})
.define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
const { data, id, ...rest } = ctx.query.data;
if (id) {
const apps = await db.select().from(schema.kvAppList).where(eq(schema.kvAppList.id, id)).limit(1);
const app = apps[0];
if (app) {
const appData = app.data as AppData;
const newData = { ...appData, ...data };
const updateResult = await db.update(schema.kvAppList)
.set({ data: newData, ...rest, updatedAt: new Date().toISOString() })
.where(eq(schema.kvAppList.id, id))
.returning();
const newApp = updateResult[0];
ctx.body = newApp;
setExpire(newApp.id, 'test');
} else {
ctx.throw('app not found');
}
return;
}
if (!rest.key) {
ctx.throw('key is required');
}
const newApps = await db.insert(schema.kvAppList).values({ data, ...rest, uid: tokenUser.id }).returning();
ctx.body = newApps[0];
return ctx;
})
.addTo(app);
app
.route({
path: 'app',
key: 'delete',
middleware: ['auth'],
description: '删除应用信息,如果应用已发布,则不允许删除',
})
.define(async (ctx) => {
const id = ctx.query.id;
const deleteFile = !!ctx.query.deleteFile; // 是否删除文件, 默认不删除
if (!id) {
ctx.throw('id is required');
}
const apps = await db.select().from(schema.kvAppList).where(eq(schema.kvAppList.id, id)).limit(1);
const app = apps[0];
if (!app) {
ctx.throw('app not found');
}
const ams = await db.select().from(schema.kvApp).where(and(
eq(schema.kvApp.key, app.key),
eq(schema.kvApp.uid, app.uid)
)).limit(1);
const am = ams[0];
if (!am) {
ctx.throw('app not found');
}
if (am.version === app.version) {
ctx.throw('app is published');
}
const appData = app.data as AppData;
const files = appData.files || [];
if (deleteFile && files.length > 0) {
await deleteFiles(files.map((item) => item.path));
}
await db.delete(schema.kvAppList).where(eq(schema.kvAppList.id, id));
ctx.body = 'success';
return ctx;
})
.addTo(app);
app
.route({
path: 'app',
key: 'uploadFiles',
middleware: ['auth'],
isDebug: true,
description: '上传应用文件,如果应用版本不存在,则创建应用版本记录',
})
.define(async (ctx) => {
try {
const tokenUser = ctx.state.tokenUser;
const { appKey, files, version, username, description } = ctx.query.data;
if (!appKey) {
ctx.throw('appKey is required');
}
if (!files || !files.length) {
ctx.throw('files is required');
}
let uid = tokenUser.id;
let userPrefix = tokenUser.username;
if (username) {
try {
const _user = await User.getUserByToken(ctx.query.token);
if (_user.hasUser(username)) {
const upUser = await User.findOne({ username });
uid = upUser.id;
userPrefix = username;
}
} catch (e) {
console.log('getUserByToken error', e);
ctx.throw('user not found');
}
}
const ams = await db.select().from(schema.kvApp).where(and(
eq(schema.kvApp.key, appKey),
eq(schema.kvApp.uid, uid)
)).limit(1);
let am = ams[0];
let appIsNew = false;
if (!am) {
appIsNew = true;
const newAms = await db.insert(schema.kvApp).values({
user: userPrefix,
key: appKey,
uid,
version: version || '0.0.0',
title: appKey,
proxy: appKey.includes('center') ? false : true,
description: description || '',
data: {
files: files || [],
},
}).returning();
am = newAms[0];
}
const apps = await db.select().from(schema.kvAppList).where(and(
eq(schema.kvAppList.version, version),
eq(schema.kvAppList.key, appKey),
eq(schema.kvAppList.uid, uid)
)).limit(1);
let app = apps[0];
if (!app) {
const newApps = await db.insert(schema.kvAppList).values({
key: appKey,
version,
uid: uid,
data: {
files: [],
},
}).returning();
app = newApps[0];
}
const appData = app.data as AppData;
const dataFiles = appData.files || [];
const newFiles = uniqBy([...dataFiles, ...files], (item) => item.name);
const updateResult = await db.update(schema.kvAppList)
.set({ data: { ...appData, files: newFiles }, updatedAt: new Date().toISOString() })
.where(eq(schema.kvAppList.id, app.id))
.returning();
const res = updateResult[0];
if (version === am.version && !appIsNew) {
const amData = am.data as AppData;
await db.update(schema.kvApp)
.set({ data: { ...amData, files: newFiles }, updatedAt: new Date().toISOString() })
.where(eq(schema.kvApp.id, am.id));
}
setExpire(app.id, 'test');
ctx.body = prefixFix(res, userPrefix);
} catch (e) {
console.log('update error', e);
ctx.throw(e.message);
}
})
.addTo(app);
app
.route({
path: 'app',
key: 'publish',
middleware: ['auth'],
description: '发布应用,将某个版本的应用设置为当前应用的版本',
metadata: {
args: {
data: z.object({
id: z.string().optional().describe('应用版本记录id'),
username: z.string().optional().describe('用户名,默认为当前用户'),
appKey: z.string().optional().describe('应用的唯一标识'),
version: z.string().describe('应用版本'),
detect: z.boolean().optional().describe('是否自动检测版本列表默认false'),
})
}
}
})
.define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
const { id, username, appKey, version, detect } = ctx.query.data;
if (!id && !appKey) {
ctx.throw('id or appKey is required');
}
const uid = await getUidByUsername(app, ctx, username);
let appList: AppList | undefined = undefined;
if (id) {
const appLists = await db.select().from(schema.kvAppList).where(eq(schema.kvAppList.id, id)).limit(1);
appList = appLists[0];
if (appList?.uid !== uid) {
ctx.throw('no permission');
}
}
if (!appList && appKey) {
if (!version) {
ctx.throw('version is required');
}
const appLists = await db.select().from(schema.kvAppList).where(and(
eq(schema.kvAppList.key, appKey),
eq(schema.kvAppList.version, version),
eq(schema.kvAppList.uid, uid)
)).limit(1);
appList = appLists[0];
}
if (!appList) {
ctx.throw('app 未发现');
}
let isDetect = false;
if (detect) {
const appKey = appList.key;
const version = appList.version;
// 自动检测最新版本
const res = await callDetectAppVersion({ appKey, version, username: username || tokenUser.username }, ctx.query.token);
if (res.code !== 200) {
ctx.throw(res.message || '检测版本列表失败');
}
const appLists2 = await db.select().from(schema.kvAppList).where(eq(schema.kvAppList.id, appList.id)).limit(1);
appList = appLists2[0];
isDetect = true;
}
if (!appList) {
ctx.throw('app 未发现');
}
const appListData = appList.data as AppData;
const files = appListData.files || [];
const ams = await db.select().from(schema.kvApp).where(and(
eq(schema.kvApp.key, appList.key),
eq(schema.kvApp.uid, uid)
)).limit(1);
const am = ams[0];
if (!am) {
ctx.throw('app 未发现');
}
const amData = am.data as AppData;
if (version !== am.version) {
// 发布版本和当前版本不一致
await db.update(schema.kvApp)
.set({ data: { ...amData, files }, version: appList.version, updatedAt: new Date().toISOString() })
.where(eq(schema.kvApp.id, am.id));
}
setExpire(appList.key, am.user);
ctx.body = {
key: appList.key,
version: appList.version,
appManager: am,
user: am.user,
};
})
.addTo(app);
app
.route({
path: 'app',
key: 'getApp',
description: '获取应用信息可以通过id或者key+version来获取, 参数在data中传入',
})
.define(async (ctx) => {
const { user, key, id } = ctx.query.data;
let app: AppType | undefined;
if (id) {
const apps = await db.select().from(schema.kvApp).where(eq(schema.kvApp.id, id)).limit(1);
app = apps[0];
} else if (user && key) {
const apps = await db.select().from(schema.kvApp).where(and(
eq(schema.kvApp.user, user),
eq(schema.kvApp.key, key)
)).limit(1);
app = apps[0];
} else {
ctx.throw('user or key is required');
}
if (!app) {
ctx.throw('app not found');
}
ctx.body = app;
})
.addTo(app);
app
.route({
path: 'app',
key: 'get-minio-list',
description: '获取minio列表',
middleware: ['auth'],
})
.define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
const { key, version } = ctx.query?.data || {};
if (!key || !version) {
ctx.throw('key and version are required');
}
const files = await getMinioListAndSetToAppList({ username: tokenUser.username, appKey: key, version });
ctx.body = files;
})
.addTo(app);
app
.route({
path: 'app',
key: 'detectVersionList',
description: '检测版本列表, 对存储内容的网关暴露对应的的模块',
middleware: ['auth'],
metadata: {
args: {
data: z.object({
appKey: z.string().describe('应用的唯一标识'),
version: z.string().describe('应用版本'),
username: z.string().optional().describe('用户名,默认为当前用户'),
})
}
}
})
.define(async (ctx) => {
const tokenUser = ctx.state.tokenUser;
let { appKey, version, username } = ctx.query?.data || {};
if (!appKey || !version) {
ctx.throw('appKey and version are required');
}
const uid = await getUidByUsername(app, ctx, username);
const appLists = await db.select().from(schema.kvAppList).where(and(
eq(schema.kvAppList.key, appKey),
eq(schema.kvAppList.version, version),
eq(schema.kvAppList.uid, uid)
)).limit(1);
let appList = appLists[0];
if (!appList) {
const newAppLists = await db.insert(schema.kvAppList).values({
key: appKey,
version,
uid,
data: {
files: [],
},
}).returning();
appList = newAppLists[0];
}
const checkUsername = username || tokenUser.username;
const files = await getMinioListAndSetToAppList({ username: checkUsername, appKey, version });
const newFiles = files.map((item) => {
return {
name: item.name.replace(`${checkUsername}/${appKey}/${version}/`, ''),
path: item.name,
};
});
const appListData = appList.data as AppData;
let appListFiles = appListData?.files || [];
const needAddFiles = newFiles.map((item) => {
const findFile = appListFiles.find((appListFile) => appListFile.name === item.name);
if (findFile && findFile.name === item.name) {
return { ...findFile, ...item };
}
return item;
});
const updateResult = await db.update(schema.kvAppList)
.set({ data: { files: needAddFiles }, updatedAt: new Date().toISOString() })
.where(eq(schema.kvAppList.id, appList.id))
.returning();
appList = updateResult[0];
setExpire(appList.id, 'test');
const ams = await db.select().from(schema.kvApp).where(and(
eq(schema.kvApp.key, appKey),
eq(schema.kvApp.uid, uid)
)).limit(1);
let am = ams[0];
if (!am) {
// 如果应用不存在则创建应用记录版本为0.0.1
const newAms = await db.insert(schema.kvApp).values({
title: appKey,
key: appKey,
version: version || '0.0.1',
user: checkUsername,
uid,
data: { files: needAddFiles },
proxy: true,
}).returning();
am = newAms[0];
} else {
// 如果应用存在,并且版本相同,则更新应用记录的文件列表
const appModels = await db.select().from(schema.kvApp).where(and(
eq(schema.kvApp.key, appKey),
eq(schema.kvApp.version, version),
eq(schema.kvApp.uid, uid)
)).limit(1);
const appModel = appModels[0];
if (appModel) {
const data = appModel.data as AppData;
await db.update(schema.kvApp)
.set({ data: { ...data, files: needAddFiles }, updatedAt: new Date().toISOString() })
.where(eq(schema.kvApp.id, appModel.id));
setExpire(appModel.key, appModel.user);
}
}
ctx.body = appList;
})
.addTo(app);