fix: return correct update info
This commit is contained in:
parent
a47606b472
commit
f1c295e84d
10 changed files with 143 additions and 135 deletions
|
@ -101,6 +101,7 @@
|
||||||
"ts-jest": "^29.0.3",
|
"ts-jest": "^29.0.3",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "4.9.4",
|
"typescript": "4.9.4",
|
||||||
|
"wait-for-expect": "^3.0.2",
|
||||||
"whatwg-fetch": "^3.6.2"
|
"whatwg-fetch": "^3.6.2"
|
||||||
},
|
},
|
||||||
"msw": {
|
"msw": {
|
||||||
|
|
|
@ -54,6 +54,8 @@ export const createAppEntity = (params: CreateAppEntityParams): AppWithInfo => {
|
||||||
numOpened: 0,
|
numOpened: 0,
|
||||||
createdAt: faker.date.past(),
|
createdAt: faker.date.past(),
|
||||||
updatedAt: faker.date.past(),
|
updatedAt: faker.date.past(),
|
||||||
|
latestVersion: 1,
|
||||||
|
latestDockerVersion: '1.0.0',
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,7 +19,7 @@ describe('Test: AppDetailsContainer', () => {
|
||||||
|
|
||||||
it('should display update button when update is available', async () => {
|
it('should display update button when update is available', async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const app = createAppEntity({ overrides: { version: 2 }, overridesInfo: { tipi_version: 3 } });
|
const app = createAppEntity({ overrides: { version: 2, latestVersion: 3 } });
|
||||||
render(<AppDetailsContainer app={app} />);
|
render(<AppDetailsContainer app={app} />);
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
@ -173,7 +173,7 @@ describe('Test: AppDetailsContainer', () => {
|
||||||
describe('Test: Update app', () => {
|
describe('Test: Update app', () => {
|
||||||
it('should display toast success when update success', async () => {
|
it('should display toast success when update success', async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const app = createAppEntity({ overrides: { version: 2 }, overridesInfo: { tipi_version: 3 } });
|
const app = createAppEntity({ overrides: { version: 2, latestVersion: 3 } });
|
||||||
server.use(getTRPCMock({ path: ['app', 'updateApp'], type: 'mutation', response: app }));
|
server.use(getTRPCMock({ path: ['app', 'updateApp'], type: 'mutation', response: app }));
|
||||||
const { result } = renderHook(() => useToastStore());
|
const { result } = renderHook(() => useToastStore());
|
||||||
render(<AppDetailsContainer app={app} />);
|
render(<AppDetailsContainer app={app} />);
|
||||||
|
@ -195,7 +195,7 @@ describe('Test: AppDetailsContainer', () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const { result } = renderHook(() => useToastStore());
|
const { result } = renderHook(() => useToastStore());
|
||||||
server.use(getTRPCMockError({ path: ['app', 'updateApp'], type: 'mutation', message: 'my big error' }));
|
server.use(getTRPCMockError({ path: ['app', 'updateApp'], type: 'mutation', message: 'my big error' }));
|
||||||
const app = createAppEntity({ overrides: { version: 2 }, overridesInfo: { tipi_version: 3 } });
|
const app = createAppEntity({ overrides: { version: 2, latestVersion: 3 }, overridesInfo: { tipi_version: 3 } });
|
||||||
render(<AppDetailsContainer app={app} />);
|
render(<AppDetailsContainer app={app} />);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
|
|
|
@ -105,7 +105,7 @@ export const AppDetailsContainer: React.FC<IProps> = ({ app }) => {
|
||||||
onError: (e) => addToast({ title: 'Update error', description: e.message, status: 'error' }),
|
onError: (e) => addToast({ title: 'Update error', description: e.message, status: 'error' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateAvailable = Number(app?.version || 0) < Number(app?.info.tipi_version);
|
const updateAvailable = Number(app.version || 0) < Number(app?.latestVersion || 0);
|
||||||
|
|
||||||
const handleInstallSubmit = async (values: FormValues) => {
|
const handleInstallSubmit = async (values: FormValues) => {
|
||||||
const { exposed, domain, ...form } = values;
|
const { exposed, domain, ...form } = values;
|
||||||
|
@ -144,7 +144,7 @@ export const AppDetailsContainer: React.FC<IProps> = ({ app }) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const newVersion = [app?.info.version ? `${app?.info.version}` : '', `(${String(app?.info.tipi_version)})`].join(' ');
|
const newVersion = [app?.latestDockerVersion ? `${app?.latestDockerVersion}` : '', `(${String(app?.latestVersion)})`].join(' ');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card" data-testid="app-details">
|
<div className="card" data-testid="app-details">
|
||||||
|
|
|
@ -12,7 +12,7 @@ export const AppsPage: NextPage = () => {
|
||||||
const { data, isLoading, error } = trpc.app.installedApps.useQuery();
|
const { data, isLoading, error } = trpc.app.installedApps.useQuery();
|
||||||
|
|
||||||
const renderApp = (app: AppRouterOutput['installedApps'][number]) => {
|
const renderApp = (app: AppRouterOutput['installedApps'][number]) => {
|
||||||
const updateAvailable = Number(app.version) < Number(app.info.tipi_version);
|
const updateAvailable = Number(app.version) < Number(app.latestVersion);
|
||||||
|
|
||||||
if (app.info?.available) return <AppTile key={app.id} app={app.info} status={app.status} updateAvailable={updateAvailable} />;
|
if (app.info?.available) return <AppTile key={app.id} app={app.info} status={app.status} updateAvailable={updateAvailable} />;
|
||||||
|
|
||||||
|
|
|
@ -403,33 +403,26 @@ describe('getUpdateInfo', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should return update info', async () => {
|
it('Should return update info', async () => {
|
||||||
const updateInfo = await getUpdateInfo(app1.id, 1);
|
const updateInfo = getUpdateInfo(app1.id);
|
||||||
|
|
||||||
expect(updateInfo?.latest).toBe(app1.tipi_version);
|
expect(updateInfo?.latestVersion).toBe(app1.tipi_version);
|
||||||
expect(updateInfo?.current).toBe(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should return null if app is not installed', async () => {
|
it('Should return default values if app is not installed', async () => {
|
||||||
const updateInfo = await getUpdateInfo(faker.random.word(), 1);
|
const updateInfo = getUpdateInfo(faker.random.word());
|
||||||
|
|
||||||
expect(updateInfo).toBeNull();
|
expect(updateInfo).toEqual({ latestVersion: 0, latestDockerVersion: '0.0.0' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should return null if config.json is invalid', async () => {
|
it('Should return default values if config.json is invalid', async () => {
|
||||||
const { appInfo, MockFiles } = await createApp({ installed: true }, db);
|
const { appInfo, MockFiles } = await createApp({ installed: true }, db);
|
||||||
MockFiles[`/runtipi/repos/repo-id/apps/${appInfo.id}/config.json`] = 'invalid json';
|
MockFiles[`/runtipi/repos/repo-id/apps/${appInfo.id}/config.json`] = 'invalid json';
|
||||||
// @ts-expect-error - Mocking fs
|
// @ts-expect-error - Mocking fs
|
||||||
fs.__createMockFiles(MockFiles);
|
fs.__createMockFiles(MockFiles);
|
||||||
|
|
||||||
const updateInfo = await getUpdateInfo(appInfo.id, 1);
|
const updateInfo = getUpdateInfo(appInfo.id);
|
||||||
|
|
||||||
expect(updateInfo).toBeNull();
|
expect(updateInfo).toEqual({ latestVersion: 0, latestDockerVersion: '0.0.0' });
|
||||||
});
|
|
||||||
|
|
||||||
it('should return null if version is not provided', async () => {
|
|
||||||
const updateInfo = await getUpdateInfo(app1.id);
|
|
||||||
|
|
||||||
expect(updateInfo).toBe(null);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -241,6 +241,30 @@ export const getAvailableApps = async () => {
|
||||||
return apps;
|
return apps;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function returns an object containing information about the updates available for the app with the provided id.
|
||||||
|
* It checks if the app is installed or not and looks for the config.json file in the appropriate directory.
|
||||||
|
* If the config.json file is invalid, it returns null.
|
||||||
|
* If the app is not found, it returns null.
|
||||||
|
*
|
||||||
|
* @param {string} id - The app id.
|
||||||
|
* @param {number} [version] - The current version of the app.
|
||||||
|
* @returns {Promise<{current: number, latest: number, dockerVersion: string} | null>} - Returns an object containing information about the updates available for the app or null if the app is not found or has an invalid config.json file.
|
||||||
|
*/
|
||||||
|
export const getUpdateInfo = (id: string) => {
|
||||||
|
const repoConfig = readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${id}/config.json`);
|
||||||
|
const parsedConfig = appInfoSchema.safeParse(repoConfig);
|
||||||
|
|
||||||
|
if (parsedConfig.success) {
|
||||||
|
return {
|
||||||
|
latestVersion: parsedConfig.data.tipi_version,
|
||||||
|
latestDockerVersion: parsedConfig.data.version,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { latestVersion: 0, latestDockerVersion: '0.0.0' };
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function reads the config.json and metadata/description.md files for the app with the provided id,
|
* This function reads the config.json and metadata/description.md files for the app with the provided id,
|
||||||
* parses the config file and returns an object with app information.
|
* parses the config file and returns an object with app information.
|
||||||
|
@ -284,37 +308,6 @@ export const getAppInfo = (id: string, status?: App['status']) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* This function returns an object containing information about the updates available for the app with the provided id.
|
|
||||||
* It checks if the app is installed or not and looks for the config.json file in the appropriate directory.
|
|
||||||
* If the config.json file is invalid, it returns null.
|
|
||||||
* If the app is not found, it returns null.
|
|
||||||
*
|
|
||||||
* @param {string} id - The app id.
|
|
||||||
* @param {number} [version] - The current version of the app.
|
|
||||||
* @returns {Promise<{current: number, latest: number, dockerVersion: string} | null>} - Returns an object containing information about the updates available for the app or null if the app is not found or has an invalid config.json file.
|
|
||||||
*/
|
|
||||||
export const getUpdateInfo = async (id: string, version?: number) => {
|
|
||||||
const doesFileExist = fileExists(`/runtipi/repos/${getConfig().appsRepoId}/apps/${id}`);
|
|
||||||
|
|
||||||
if (!doesFileExist || !version) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const repoConfig = readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${id}/config.json`);
|
|
||||||
const parsedConfig = appInfoSchema.safeParse(repoConfig);
|
|
||||||
|
|
||||||
if (parsedConfig.success) {
|
|
||||||
return {
|
|
||||||
current: version || 0,
|
|
||||||
latest: parsedConfig.data.tipi_version,
|
|
||||||
dockerVersion: parsedConfig.data.version,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function ensures that the app folder for the app with the provided name exists.
|
* This function ensures that the app folder for the app with the provided name exists.
|
||||||
* If the cleanup parameter is set to true, it deletes the app folder if it exists.
|
* If the cleanup parameter is set to true, it deletes the app folder if it exists.
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
|
import waitForExpect from 'wait-for-expect';
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
import { AppServiceClass } from './apps.service';
|
import { AppServiceClass } from './apps.service';
|
||||||
import { EventDispatcher, EVENT_TYPES } from '../../core/EventDispatcher';
|
import { EventDispatcher, EVENT_TYPES } from '../../core/EventDispatcher';
|
||||||
|
@ -557,61 +558,6 @@ describe('List apps', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe.skip('Start all apps', () => {
|
|
||||||
it('Should correctly start all apps', async () => {
|
|
||||||
// arrange
|
|
||||||
const app1create = await createApp({ installed: true }, db);
|
|
||||||
const app2create = await createApp({ installed: true }, db);
|
|
||||||
const app1 = app1create.appInfo;
|
|
||||||
const app2 = app2create.appInfo;
|
|
||||||
const apps = [app1, app2].sort((a, b) => a.id.localeCompare(b.id));
|
|
||||||
const spy = jest.spyOn(EventDispatcher, 'dispatchEventAsync');
|
|
||||||
// @ts-expect-error - Mocking fs
|
|
||||||
fs.__createMockFiles(Object.assign(app1create.MockFiles, app2create.MockFiles));
|
|
||||||
|
|
||||||
await AppsService.startAllApps();
|
|
||||||
|
|
||||||
expect(spy.mock.calls.length).toBe(2);
|
|
||||||
|
|
||||||
const expectedCalls = apps.map((app) => [EVENT_TYPES.APP, ['start', app.id]]);
|
|
||||||
|
|
||||||
expect(spy.mock.calls).toEqual(expect.arrayContaining(expectedCalls));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Should not start apps which have not status RUNNING', async () => {
|
|
||||||
// arrange
|
|
||||||
const app1 = await createApp({ installed: true, status: 'running' }, db);
|
|
||||||
const app2 = await createApp({ installed: true, status: 'running' }, db);
|
|
||||||
const app3 = await createApp({ installed: true, status: 'stopped' }, db);
|
|
||||||
const spy = jest.spyOn(EventDispatcher, 'dispatchEventAsync');
|
|
||||||
// @ts-expect-error - Mocking fs
|
|
||||||
fs.__createMockFiles(Object.assign(app1.MockFiles, app2.MockFiles, app3.MockFiles));
|
|
||||||
|
|
||||||
await AppsService.startAllApps();
|
|
||||||
const apps = await db.app.findMany();
|
|
||||||
|
|
||||||
expect(spy.mock.calls.length).toBe(2);
|
|
||||||
expect(apps.length).toBe(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Should put app status to STOPPED if start script fails', async () => {
|
|
||||||
// Arrange
|
|
||||||
await createApp({ installed: true }, db);
|
|
||||||
await createApp({ installed: true }, db);
|
|
||||||
EventDispatcher.dispatchEventAsync = jest.fn().mockResolvedValueOnce({ success: false, stdout: 'error' });
|
|
||||||
|
|
||||||
// Act
|
|
||||||
await AppsService.startAllApps();
|
|
||||||
|
|
||||||
const apps = await db.app.findMany();
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
expect(apps.length).toBe(2);
|
|
||||||
expect(apps[0]?.status).toBe(APP_STATUS.STOPPED);
|
|
||||||
expect(apps[1]?.status).toBe(APP_STATUS.STOPPED);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Update app', () => {
|
describe('Update app', () => {
|
||||||
it('Should correctly update app', async () => {
|
it('Should correctly update app', async () => {
|
||||||
const app1create = await createApp({ installed: true }, db);
|
const app1create = await createApp({ installed: true }, db);
|
||||||
|
@ -646,3 +592,73 @@ describe('Update app', () => {
|
||||||
expect(app?.status).toBe(APP_STATUS.STOPPED);
|
expect(app?.status).toBe(APP_STATUS.STOPPED);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('installedApps', () => {
|
||||||
|
it('Should list installed apps', async () => {
|
||||||
|
// Arrange
|
||||||
|
const app1 = await createApp({ installed: true }, db);
|
||||||
|
const app2 = await createApp({ installed: true }, db);
|
||||||
|
const app3 = await createApp({ installed: true }, db);
|
||||||
|
const app4 = await createApp({ installed: false }, db);
|
||||||
|
// @ts-expect-error - Mocking fs
|
||||||
|
fs.__createMockFiles(Object.assign(app1.MockFiles, app2.MockFiles, app3.MockFiles, app4.MockFiles));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const apps = await AppsService.installedApps();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(apps.length).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should not list app with invalid config', async () => {
|
||||||
|
// Arrange
|
||||||
|
const app1 = await createApp({ installed: true }, db);
|
||||||
|
const app2 = await createApp({ installed: true }, db);
|
||||||
|
const app3 = await createApp({ installed: true }, db);
|
||||||
|
const app4 = await createApp({ installed: false }, db);
|
||||||
|
// @ts-expect-error - Mocking fs
|
||||||
|
fs.__createMockFiles(Object.assign(app2.MockFiles, app3.MockFiles, app4.MockFiles, { [`/runtipi/repos/repo-id/apps/${app1.appInfo.id}/config.json`]: 'invalid json' }));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
const apps = await AppsService.installedApps();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(apps.length).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('startAllApps', () => {
|
||||||
|
it('should start all apps with status RUNNING', async () => {
|
||||||
|
// Arrange
|
||||||
|
const app1 = await createApp({ installed: true, status: 'running' }, db);
|
||||||
|
const app2 = await createApp({ installed: true, status: 'running' }, db);
|
||||||
|
const app3 = await createApp({ installed: true, status: 'stopped' }, db);
|
||||||
|
const spy = jest.spyOn(EventDispatcher, 'dispatchEventAsync');
|
||||||
|
// @ts-expect-error - Mocking fs
|
||||||
|
fs.__createMockFiles(Object.assign(app1.MockFiles, app2.MockFiles, app3.MockFiles));
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await AppsService.startAllApps();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
expect(spy.mock.calls.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should put status to STOPPED if start script fails', async () => {
|
||||||
|
// Arrange
|
||||||
|
const app1 = await createApp({ installed: true, status: 'running' }, db);
|
||||||
|
const spy = jest.spyOn(EventDispatcher, 'dispatchEventAsync');
|
||||||
|
// @ts-expect-error - Mocking fs
|
||||||
|
fs.__createMockFiles(Object.assign(app1.MockFiles));
|
||||||
|
spy.mockResolvedValueOnce({ success: false, stdout: 'error' });
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await AppsService.startAllApps();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await waitForExpect(async () => {
|
||||||
|
const apps = await db.app.findMany();
|
||||||
|
expect(apps[0]?.status).toBe(APP_STATUS.STOPPED);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import validator from 'validator';
|
import validator from 'validator';
|
||||||
import { App, PrismaClient } from '@prisma/client';
|
import { App, PrismaClient } from '@prisma/client';
|
||||||
import { checkAppRequirements, checkEnvFile, generateEnvFile, getAvailableApps, ensureAppFolder, appInfoSchema, AppInfo, getAppInfo } from './apps.helpers';
|
import { checkAppRequirements, checkEnvFile, generateEnvFile, getAvailableApps, ensureAppFolder, AppInfo, getAppInfo, getUpdateInfo } from './apps.helpers';
|
||||||
import { getConfig } from '../../core/TipiConfig';
|
import { getConfig } from '../../core/TipiConfig';
|
||||||
import { EventDispatcher } from '../../core/EventDispatcher';
|
import { EventDispatcher } from '../../core/EventDispatcher';
|
||||||
import { Logger } from '../../core/Logger';
|
import { Logger } from '../../core/Logger';
|
||||||
import { createFolder, readJsonFile } from '../../common/fs.helpers';
|
import { createFolder } from '../../common/fs.helpers';
|
||||||
|
import { notEmpty } from '../../common/typescript.helpers';
|
||||||
|
|
||||||
const sortApps = (a: AppInfo, b: AppInfo) => a.name.localeCompare(b.name);
|
const sortApps = (a: AppInfo, b: AppInfo) => a.name.localeCompare(b.name);
|
||||||
const filterApp = (app: AppInfo): boolean => {
|
const filterApp = (app: AppInfo): boolean => {
|
||||||
|
@ -125,14 +126,13 @@ export class AppServiceClass {
|
||||||
// Create app folder
|
// Create app folder
|
||||||
createFolder(`/app/storage/app-data/${id}`);
|
createFolder(`/app/storage/app-data/${id}`);
|
||||||
|
|
||||||
const appInfo = readJsonFile(`/runtipi/apps/${id}/config.json`);
|
const appInfo = getAppInfo(id);
|
||||||
const parsedAppInfo = appInfoSchema.safeParse(appInfo);
|
|
||||||
|
|
||||||
if (!parsedAppInfo.success) {
|
if (!appInfo) {
|
||||||
throw new Error(`App ${id} has invalid config.json file`);
|
throw new Error(`App ${id} has invalid config.json file`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!parsedAppInfo.data.exposable && exposed) {
|
if (!appInfo.exposable && exposed) {
|
||||||
throw new Error(`App ${id} is not exposable`);
|
throw new Error(`App ${id} is not exposable`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,7 +143,7 @@ export class AppServiceClass {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
app = await this.prisma.app.create({ data: { id, status: 'installing', config: form, version: parsedAppInfo.data.tipi_version, exposed: exposed || false, domain } });
|
app = await this.prisma.app.create({ data: { id, status: 'installing', config: form, version: appInfo.tipi_version, exposed: exposed || false, domain } });
|
||||||
|
|
||||||
if (app) {
|
if (app) {
|
||||||
// Create env file
|
// Create env file
|
||||||
|
@ -200,14 +200,13 @@ export class AppServiceClass {
|
||||||
throw new Error(`App ${id} not found`);
|
throw new Error(`App ${id} not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const appInfo = readJsonFile(`/runtipi/apps/${id}/config.json`);
|
const appInfo = getAppInfo(app.id, app.status);
|
||||||
const parsedAppInfo = appInfoSchema.safeParse(appInfo);
|
|
||||||
|
|
||||||
if (!parsedAppInfo.success) {
|
if (!appInfo) {
|
||||||
throw new Error(`App ${id} has invalid config.json`);
|
throw new Error(`App ${id} has invalid config.json`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!parsedAppInfo.data.exposable && exposed) {
|
if (!appInfo.exposable && exposed) {
|
||||||
throw new Error(`App ${id} is not exposable`);
|
throw new Error(`App ${id} is not exposable`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -302,14 +301,14 @@ export class AppServiceClass {
|
||||||
public getApp = async (id: string) => {
|
public getApp = async (id: string) => {
|
||||||
let app = await this.prisma.app.findUnique({ where: { id } });
|
let app = await this.prisma.app.findUnique({ where: { id } });
|
||||||
const info = getAppInfo(id, app?.status);
|
const info = getAppInfo(id, app?.status);
|
||||||
const parsedInfo = appInfoSchema.safeParse(info);
|
const updateInfo = getUpdateInfo(id);
|
||||||
|
|
||||||
if (parsedInfo.success) {
|
if (info) {
|
||||||
if (!app) {
|
if (!app) {
|
||||||
app = { id, status: 'missing', config: {}, exposed: false, domain: '' } as App;
|
app = { id, status: 'missing', config: {}, exposed: false, domain: '' } as App;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ...app, info: { ...parsedInfo.data } };
|
return { ...app, ...updateInfo, info };
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`App ${id} has invalid config.json`);
|
throw new Error(`App ${id} has invalid config.json`);
|
||||||
|
@ -337,10 +336,9 @@ export class AppServiceClass {
|
||||||
const { success, stdout } = await EventDispatcher.dispatchEventAsync('app', ['update', id]);
|
const { success, stdout } = await EventDispatcher.dispatchEventAsync('app', ['update', id]);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
const appInfo = readJsonFile(`/runtipi/apps/${id}/config.json`);
|
const appInfo = getAppInfo(app.id, app.status);
|
||||||
const parsedAppInfo = appInfoSchema.parse(appInfo);
|
|
||||||
|
|
||||||
await this.prisma.app.update({ where: { id }, data: { status: 'running', version: parsedAppInfo.tipi_version } });
|
await this.prisma.app.update({ where: { id }, data: { status: 'running', version: appInfo?.tipi_version } });
|
||||||
} else {
|
} else {
|
||||||
await this.prisma.app.update({ where: { id }, data: { status: 'stopped' } });
|
await this.prisma.app.update({ where: { id }, data: { status: 'stopped' } });
|
||||||
throw new Error(`App ${id} failed to update\nstdout: ${stdout}`);
|
throw new Error(`App ${id} failed to update\nstdout: ${stdout}`);
|
||||||
|
@ -354,20 +352,19 @@ export class AppServiceClass {
|
||||||
* Returns a list of all installed apps
|
* Returns a list of all installed apps
|
||||||
*
|
*
|
||||||
* @returns {Promise<App[]>} - An array of app objects
|
* @returns {Promise<App[]>} - An array of app objects
|
||||||
* @throws {Error} - If the app is not found or if the update process fails.
|
|
||||||
*/
|
*/
|
||||||
public installedApps = async () => {
|
public installedApps = async () => {
|
||||||
const apps = await this.prisma.app.findMany();
|
const apps = await this.prisma.app.findMany();
|
||||||
|
|
||||||
return apps.map((app) => {
|
return apps
|
||||||
const info = readJsonFile(`/runtipi/apps/${app.id}/config.json`);
|
.map((app) => {
|
||||||
const parsedInfo = appInfoSchema.safeParse(info);
|
const info = getAppInfo(app.id, app.status);
|
||||||
|
const updateInfo = getUpdateInfo(app.id);
|
||||||
if (parsedInfo.success) {
|
if (info) {
|
||||||
return { ...app, info: { ...parsedInfo.data } };
|
return { ...app, ...updateInfo, info };
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
throw new Error(`App ${app.id} has invalid config.json`);
|
})
|
||||||
});
|
.filter(notEmpty);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -99,6 +99,7 @@ importers:
|
||||||
typescript: 4.9.4
|
typescript: 4.9.4
|
||||||
uuid: ^9.0.0
|
uuid: ^9.0.0
|
||||||
validator: ^13.7.0
|
validator: ^13.7.0
|
||||||
|
wait-for-expect: ^3.0.2
|
||||||
whatwg-fetch: ^3.6.2
|
whatwg-fetch: ^3.6.2
|
||||||
winston: ^3.7.2
|
winston: ^3.7.2
|
||||||
zod: ^3.19.1
|
zod: ^3.19.1
|
||||||
|
@ -189,6 +190,7 @@ importers:
|
||||||
ts-jest: 29.0.3_iyz3vhhlowkpp2xbqliblzwv3y
|
ts-jest: 29.0.3_iyz3vhhlowkpp2xbqliblzwv3y
|
||||||
ts-node: 10.9.1_awa2wsr5thmg3i7jqycphctjfq
|
ts-node: 10.9.1_awa2wsr5thmg3i7jqycphctjfq
|
||||||
typescript: 4.9.4
|
typescript: 4.9.4
|
||||||
|
wait-for-expect: 3.0.2
|
||||||
whatwg-fetch: 3.6.2
|
whatwg-fetch: 3.6.2
|
||||||
|
|
||||||
packages/system-api:
|
packages/system-api:
|
||||||
|
@ -11306,6 +11308,10 @@ packages:
|
||||||
xml-name-validator: 4.0.0
|
xml-name-validator: 4.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/wait-for-expect/3.0.2:
|
||||||
|
resolution: {integrity: sha512-cfS1+DZxuav1aBYbaO/kE06EOS8yRw7qOFoD3XtjTkYvCvh3zUvNST8DXK/nPaeqIzIv3P3kL3lRJn8iwOiSag==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/walker/1.0.8:
|
/walker/1.0.8:
|
||||||
resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==}
|
resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
Loading…
Reference in a new issue