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);