|
@@ -1,7 +1,7 @@
|
|
|
import validator from 'validator';
|
|
|
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
|
|
-import { appTable, App } from '@/server/db/schema';
|
|
|
-import { and, asc, eq, ne, notInArray } from 'drizzle-orm';
|
|
|
+import { App } from '@/server/db/schema';
|
|
|
+import { AppQueries } from '@/server/queries/apps/apps.queries';
|
|
|
import { checkAppRequirements, checkEnvFile, generateEnvFile, getAvailableApps, ensureAppFolder, AppInfo, getAppInfo, getUpdateInfo } from './apps.helpers';
|
|
|
import { getConfig } from '../../core/TipiConfig';
|
|
|
import { EventDispatcher } from '../../core/EventDispatcher';
|
|
@@ -22,10 +22,10 @@ const filterApp = (app: AppInfo): boolean => {
|
|
|
const filterApps = (apps: AppInfo[]): AppInfo[] => apps.sort(sortApps).filter(filterApp);
|
|
|
|
|
|
export class AppServiceClass {
|
|
|
- private db;
|
|
|
+ private queries;
|
|
|
|
|
|
constructor(p: NodePgDatabase) {
|
|
|
- this.db = p;
|
|
|
+ this.queries = new AppQueries(p);
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -33,17 +33,12 @@ export class AppServiceClass {
|
|
|
* It finds all the running apps and starts them by regenerating the env file, checking the env file and dispatching the start event.
|
|
|
* If the start event is successful, the app's status is updated to 'running', otherwise, it is updated to 'stopped'
|
|
|
* If there is an error while starting the app, it logs the error and updates the app's status to 'stopped'.
|
|
|
- *
|
|
|
- * @returns {Promise<void>} - A promise that resolves when all apps are started.
|
|
|
*/
|
|
|
public async startAllApps() {
|
|
|
- const apps = await this.db.select().from(appTable).where(eq(appTable.status, 'running')).orderBy(asc(appTable.id));
|
|
|
+ const apps = await this.queries.getAppsByStatus('running');
|
|
|
|
|
|
// Update all apps with status different than running or stopped to stopped
|
|
|
- await this.db
|
|
|
- .update(appTable)
|
|
|
- .set({ status: 'stopped' })
|
|
|
- .where(notInArray(appTable.status, ['running', 'stopped']));
|
|
|
+ await this.queries.updateAppsByStatusNotIn(['running', 'stopped', 'missing'], { status: 'stopped' });
|
|
|
|
|
|
await Promise.all(
|
|
|
apps.map(async (app) => {
|
|
@@ -53,17 +48,17 @@ export class AppServiceClass {
|
|
|
generateEnvFile(app);
|
|
|
checkEnvFile(app.id);
|
|
|
|
|
|
- await this.db.update(appTable).set({ status: 'starting' }).where(eq(appTable.id, app.id));
|
|
|
+ await this.queries.updateApp(app.id, { status: 'starting' });
|
|
|
|
|
|
EventDispatcher.dispatchEventAsync('app', ['start', app.id]).then(({ success }) => {
|
|
|
if (success) {
|
|
|
- this.db.update(appTable).set({ status: 'running' }).where(eq(appTable.id, app.id)).execute();
|
|
|
+ this.queries.updateApp(app.id, { status: 'running' });
|
|
|
} else {
|
|
|
- this.db.update(appTable).set({ status: 'stopped' }).where(eq(appTable.id, app.id)).execute();
|
|
|
+ this.queries.updateApp(app.id, { status: 'stopped' });
|
|
|
}
|
|
|
});
|
|
|
} catch (e) {
|
|
|
- await this.db.update(appTable).set({ status: 'stopped' }).where(eq(appTable.id, app.id));
|
|
|
+ await this.queries.updateApp(app.id, { status: 'stopped' });
|
|
|
Logger.error(e);
|
|
|
}
|
|
|
}),
|
|
@@ -75,13 +70,10 @@ export class AppServiceClass {
|
|
|
* It updates the app's status in the database to 'starting' and 'running' if the start process is successful, otherwise it updates the status to 'stopped'.
|
|
|
*
|
|
|
* @param {string} appName - The name of the app to start
|
|
|
- * @returns {Promise<App | null>} - Returns a promise that resolves with the updated app information.
|
|
|
* @throws {Error} - If the app is not found or the start process fails.
|
|
|
*/
|
|
|
public startApp = async (appName: string) => {
|
|
|
- const apps = await this.db.select().from(appTable).where(eq(appTable.id, appName));
|
|
|
- const app = apps[0];
|
|
|
-
|
|
|
+ const app = await this.queries.getApp(appName);
|
|
|
if (!app) {
|
|
|
throw new Error(`App ${appName} not found`);
|
|
|
}
|
|
@@ -91,18 +83,18 @@ export class AppServiceClass {
|
|
|
generateEnvFile(app);
|
|
|
checkEnvFile(appName);
|
|
|
|
|
|
- await this.db.update(appTable).set({ status: 'starting' }).where(eq(appTable.id, appName));
|
|
|
+ await this.queries.updateApp(appName, { status: 'starting' });
|
|
|
const { success, stdout } = await EventDispatcher.dispatchEventAsync('app', ['start', app.id]);
|
|
|
|
|
|
if (success) {
|
|
|
- await this.db.update(appTable).set({ status: 'running' }).where(eq(appTable.id, appName));
|
|
|
+ await this.queries.updateApp(appName, { status: 'running' });
|
|
|
} else {
|
|
|
- await this.db.update(appTable).set({ status: 'stopped' }).where(eq(appTable.id, appName));
|
|
|
+ await this.queries.updateApp(appName, { status: 'stopped' });
|
|
|
throw new Error(`App ${appName} failed to start\nstdout: ${stdout}`);
|
|
|
}
|
|
|
|
|
|
- const updateApps = await this.db.select().from(appTable).where(eq(appTable.id, appName));
|
|
|
- return updateApps[0];
|
|
|
+ const updatedApp = await this.queries.getApp(appName);
|
|
|
+ return updatedApp;
|
|
|
};
|
|
|
|
|
|
/**
|
|
@@ -112,11 +104,9 @@ export class AppServiceClass {
|
|
|
* @param {Record<string, string>} form - The form data submitted by the user
|
|
|
* @param {boolean} [exposed] - A flag indicating if the app will be exposed to the internet
|
|
|
* @param {string} [domain] - The domain name to expose the app to the internet, required if exposed is true
|
|
|
- * @returns {Promise<App | null>} Returns a promise that resolves to the installed app object
|
|
|
*/
|
|
|
public installApp = async (id: string, form: Record<string, string>, exposed?: boolean, domain?: string) => {
|
|
|
- const apps = await this.db.select().from(appTable).where(eq(appTable.id, id));
|
|
|
- const app = apps[0];
|
|
|
+ const app = await this.queries.getApp(id);
|
|
|
|
|
|
if (app) {
|
|
|
await this.startApp(id);
|
|
@@ -150,21 +140,14 @@ export class AppServiceClass {
|
|
|
}
|
|
|
|
|
|
if (exposed && domain) {
|
|
|
- const appsWithSameDomain = await this.db
|
|
|
- .select()
|
|
|
- .from(appTable)
|
|
|
- .where(and(eq(appTable.domain, domain), eq(appTable.exposed, true)));
|
|
|
+ const appsWithSameDomain = await this.queries.getAppsByDomain(domain, id);
|
|
|
|
|
|
if (appsWithSameDomain.length > 0) {
|
|
|
throw new Error(`Domain ${domain} already in use by app ${appsWithSameDomain[0]?.id}`);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- const newApps = await this.db
|
|
|
- .insert(appTable)
|
|
|
- .values({ id, status: 'installing', config: form, version: appInfo.tipi_version, exposed: exposed || false, domain: domain || null })
|
|
|
- .returning();
|
|
|
- const newApp = newApps[0];
|
|
|
+ const newApp = await this.queries.createApp({ id, status: 'installing', config: form, version: appInfo.tipi_version, exposed: exposed || false, domain: domain || null });
|
|
|
|
|
|
if (newApp) {
|
|
|
// Create env file
|
|
@@ -175,19 +158,17 @@ export class AppServiceClass {
|
|
|
const { success, stdout } = await EventDispatcher.dispatchEventAsync('app', ['install', id]);
|
|
|
|
|
|
if (!success) {
|
|
|
- await this.db.delete(appTable).where(eq(appTable.id, id));
|
|
|
+ await this.queries.deleteApp(id);
|
|
|
throw new Error(`App ${id} failed to install\nstdout: ${stdout}`);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- const updatedApp = await this.db.update(appTable).set({ status: 'running' }).where(eq(appTable.id, id)).returning();
|
|
|
- return updatedApp[0];
|
|
|
+ const updatedApp = await this.queries.updateApp(id, { status: 'running' });
|
|
|
+ return updatedApp;
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* Lists available apps
|
|
|
- *
|
|
|
- * @returns {Promise<{apps: Array<AppInfo>, total: number }>} An object containing list of apps and total number of apps
|
|
|
*/
|
|
|
public static listApps = async () => {
|
|
|
const apps = await getAvailableApps();
|
|
@@ -203,7 +184,6 @@ export class AppServiceClass {
|
|
|
* @param {object} form - The new configuration of the app.
|
|
|
* @param {boolean} [exposed=false] - If the app should be exposed or not.
|
|
|
* @param {string} [domain] - The domain for the app if exposed is true.
|
|
|
- * @returns {Promise<App | null>} The updated app
|
|
|
*/
|
|
|
public updateAppConfig = async (id: string, form: Record<string, string>, exposed?: boolean, domain?: string) => {
|
|
|
if (exposed && !domain) {
|
|
@@ -214,8 +194,7 @@ export class AppServiceClass {
|
|
|
throw new Error(`Domain ${domain} is not valid`);
|
|
|
}
|
|
|
|
|
|
- const apps = await this.db.select().from(appTable).where(eq(appTable.id, id));
|
|
|
- const app = apps[0];
|
|
|
+ const app = await this.queries.getApp(id);
|
|
|
|
|
|
if (!app) {
|
|
|
throw new Error(`App ${id} not found`);
|
|
@@ -236,23 +215,14 @@ export class AppServiceClass {
|
|
|
}
|
|
|
|
|
|
if (exposed && domain) {
|
|
|
- const appsWithSameDomain = await this.db
|
|
|
- .select()
|
|
|
- .from(appTable)
|
|
|
- .where(and(eq(appTable.domain, domain), eq(appTable.exposed, true), ne(appTable.id, id)));
|
|
|
+ const appsWithSameDomain = await this.queries.getAppsByDomain(domain, id);
|
|
|
|
|
|
if (appsWithSameDomain.length > 0) {
|
|
|
throw new Error(`Domain ${domain} already in use by app ${appsWithSameDomain[0]?.id}`);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- const updateApps = await this.db
|
|
|
- .update(appTable)
|
|
|
- .set({ exposed: exposed || false, domain: domain || null, config: form })
|
|
|
- .where(eq(appTable.id, id))
|
|
|
- .returning();
|
|
|
-
|
|
|
- const updatedApp = updateApps[0];
|
|
|
+ const updatedApp = await this.queries.updateApp(id, { exposed: exposed || false, domain: domain || null, config: form });
|
|
|
|
|
|
if (updatedApp) {
|
|
|
generateEnvFile(updatedApp);
|
|
@@ -265,12 +235,10 @@ export class AppServiceClass {
|
|
|
* Stops a running application by its id
|
|
|
*
|
|
|
* @param {string} id - The id of the application to stop
|
|
|
- * @returns {Promise<App>} - The stopped application
|
|
|
* @throws {Error} - If the app cannot be found or if stopping the app failed
|
|
|
*/
|
|
|
public stopApp = async (id: string) => {
|
|
|
- const apps = await this.db.select().from(appTable).where(eq(appTable.id, id));
|
|
|
- const app = apps[0];
|
|
|
+ const app = await this.queries.getApp(id);
|
|
|
|
|
|
if (!app) {
|
|
|
throw new Error(`App ${id} not found`);
|
|
@@ -280,31 +248,29 @@ export class AppServiceClass {
|
|
|
generateEnvFile(app);
|
|
|
|
|
|
// Run script
|
|
|
- await this.db.update(appTable).set({ status: 'stopping' }).where(eq(appTable.id, id));
|
|
|
+ await this.queries.updateApp(id, { status: 'stopping' });
|
|
|
|
|
|
const { success, stdout } = await EventDispatcher.dispatchEventAsync('app', ['stop', id]);
|
|
|
|
|
|
if (success) {
|
|
|
- await this.db.update(appTable).set({ status: 'stopped' }).where(eq(appTable.id, id));
|
|
|
+ await this.queries.updateApp(id, { status: 'stopped' });
|
|
|
} else {
|
|
|
- await this.db.update(appTable).set({ status: 'running' }).where(eq(appTable.id, id));
|
|
|
+ await this.queries.updateApp(id, { status: 'running' });
|
|
|
throw new Error(`App ${id} failed to stop\nstdout: ${stdout}`);
|
|
|
}
|
|
|
|
|
|
- const updatedApps = await this.db.update(appTable).set({ status: 'stopped' }).where(eq(appTable.id, id)).returning();
|
|
|
- return updatedApps[0];
|
|
|
+ const updatedApp = await this.queries.getApp(id);
|
|
|
+ return updatedApp;
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* Uninstalls an app by stopping it, running the app's `uninstall` script, and removing its data
|
|
|
*
|
|
|
* @param {string} id - The id of the app to uninstall
|
|
|
- * @returns {Promise<{id: string, status: string, config: object}>} - An object containing the id of the uninstalled app, the status of the app ('missing'), and the config object
|
|
|
* @throws {Error} - If the app is not found or if the app's `uninstall` script fails
|
|
|
*/
|
|
|
public uninstallApp = async (id: string) => {
|
|
|
- const apps = await this.db.select().from(appTable).where(eq(appTable.id, id));
|
|
|
- const app = apps[0];
|
|
|
+ const app = await this.queries.getApp(id);
|
|
|
|
|
|
if (!app) {
|
|
|
throw new Error(`App ${id} not found`);
|
|
@@ -316,16 +282,16 @@ export class AppServiceClass {
|
|
|
ensureAppFolder(id);
|
|
|
generateEnvFile(app);
|
|
|
|
|
|
- await this.db.update(appTable).set({ status: 'uninstalling' }).where(eq(appTable.id, id));
|
|
|
+ await this.queries.updateApp(id, { status: 'uninstalling' });
|
|
|
|
|
|
const { success, stdout } = await EventDispatcher.dispatchEventAsync('app', ['uninstall', id]);
|
|
|
|
|
|
if (!success) {
|
|
|
- await this.db.update(appTable).set({ status: 'stopped' }).where(eq(appTable.id, id));
|
|
|
+ await this.queries.updateApp(id, { status: 'stopped' });
|
|
|
throw new Error(`App ${id} failed to uninstall\nstdout: ${stdout}`);
|
|
|
}
|
|
|
|
|
|
- await this.db.delete(appTable).where(eq(appTable.id, id));
|
|
|
+ await this.queries.deleteApp(id);
|
|
|
|
|
|
return { id, status: 'missing', config: {} };
|
|
|
};
|
|
@@ -334,11 +300,9 @@ export class AppServiceClass {
|
|
|
* Returns the app with the provided id. If the app is not found, it returns a default app object
|
|
|
*
|
|
|
* @param {string} id - The id of the app to retrieve
|
|
|
- * @returns {Promise<App>} - The app object
|
|
|
*/
|
|
|
public getApp = async (id: string) => {
|
|
|
- const apps = await this.db.select().from(appTable).where(eq(appTable.id, id));
|
|
|
- let app = apps[0];
|
|
|
+ let app = await this.queries.getApp(id);
|
|
|
const info = getAppInfo(id, app?.status);
|
|
|
const updateInfo = getUpdateInfo(id);
|
|
|
|
|
@@ -357,12 +321,10 @@ export class AppServiceClass {
|
|
|
* Updates an app with the specified ID
|
|
|
*
|
|
|
* @param {string} id - ID of the app to update
|
|
|
- * @returns {Promise<App>} - An object representing the updated app
|
|
|
* @throws {Error} - If the app is not found or if the update process fails.
|
|
|
*/
|
|
|
public updateApp = async (id: string) => {
|
|
|
- const apps = await this.db.select().from(appTable).where(eq(appTable.id, id));
|
|
|
- const app = apps[0];
|
|
|
+ const app = await this.queries.getApp(id);
|
|
|
|
|
|
if (!app) {
|
|
|
throw new Error(`App ${id} not found`);
|
|
@@ -371,30 +333,28 @@ export class AppServiceClass {
|
|
|
ensureAppFolder(id);
|
|
|
generateEnvFile(app);
|
|
|
|
|
|
- await this.db.update(appTable).set({ status: 'updating' }).where(eq(appTable.id, id));
|
|
|
+ await this.queries.updateApp(id, { status: 'updating' });
|
|
|
|
|
|
const { success, stdout } = await EventDispatcher.dispatchEventAsync('app', ['update', id]);
|
|
|
|
|
|
if (success) {
|
|
|
const appInfo = getAppInfo(app.id, app.status);
|
|
|
|
|
|
- await this.db.update(appTable).set({ status: 'running', version: appInfo?.tipi_version }).where(eq(appTable.id, id));
|
|
|
+ await this.queries.updateApp(id, { status: 'running', version: appInfo?.tipi_version });
|
|
|
} else {
|
|
|
- await this.db.update(appTable).set({ status: 'stopped' }).where(eq(appTable.id, id));
|
|
|
+ await this.queries.updateApp(id, { status: 'stopped' });
|
|
|
throw new Error(`App ${id} failed to update\nstdout: ${stdout}`);
|
|
|
}
|
|
|
|
|
|
- const updatedApps = await this.db.update(appTable).set({ status: 'stopped' }).where(eq(appTable.id, id)).returning();
|
|
|
- return updatedApps[0];
|
|
|
+ const updatedApp = await this.queries.updateApp(id, { status: 'stopped' });
|
|
|
+ return updatedApp;
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
* Returns a list of all installed apps
|
|
|
- *
|
|
|
- * @returns {Promise<App[]>} - An array of app objects
|
|
|
*/
|
|
|
public installedApps = async () => {
|
|
|
- const apps = await this.db.select().from(appTable).orderBy(asc(appTable.id));
|
|
|
+ const apps = await this.queries.getApps();
|
|
|
|
|
|
return apps
|
|
|
.map((app) => {
|