525 lines
16 KiB
TypeScript
525 lines
16 KiB
TypeScript
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);
|