chore: fix eslint rules

This commit is contained in:
Nicolas Meienberger 2022-11-05 20:43:14 +01:00
parent ef93cdd669
commit 35ebb1069a
30 changed files with 241 additions and 251 deletions

View file

@ -1,6 +1,6 @@
module.exports = {
env: { node: true, jest: true },
extends: ['airbnb-typescript', 'eslint:recommended', 'plugin:import/typescript'],
plugins: ['@typescript-eslint', 'import', 'react'],
extends: ['airbnb-base', 'airbnb-typescript/base', 'eslint:recommended', 'plugin:import/typescript', 'plugin:@typescript-eslint/recommended', 'prettier'],
parser: '@typescript-eslint/parser',
parserOptions: {
project: './tsconfig.json',
@ -8,18 +8,19 @@ module.exports = {
ecmaVersion: 'latest',
sourceType: 'module',
},
plugins: ['@typescript-eslint', 'import', 'react'],
rules: {
'arrow-body-style': 0,
'no-restricted-exports': 0,
'max-len': [1, { code: 200 }],
'import/extensions': ['error', 'ignorePackages', { js: 'never', jsx: 'never', ts: 'never', tsx: 'never' }],
indent: 'off',
'@typescript-eslint/indent': 0,
'no-unused-vars': [1, { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-unused-vars': [1, { argsIgnorePattern: '^_' }],
'max-classes-per-file': 0,
'class-methods-use-this': 0,
'import/prefer-default-export': 0,
'no-underscore-dangle': 0,
'@typescript-eslint/ban-ts-comment': 0,
},
globals: {
NodeJS: true,
},
env: { node: true, jest: true },
};

View file

@ -1,6 +1,6 @@
const childProcess: { execFile: typeof execFile } = jest.genMockFromModule('child_process');
const execFile = (_path: string, _args: string[], _thing: any, callback: Function) => {
const execFile = (_path: string, _args: string[], _thing: any, callback: any) => {
callback();
};

View file

@ -1,4 +1,5 @@
import path from 'path';
const fs: {
__createMockFiles: typeof createMockFiles;
__resetAllMocks: typeof resetAllMocks;
@ -20,7 +21,7 @@ const createMockFiles = (newMockFiles: Record<string, string>) => {
mockFiles = Object.create(null);
// Create folder tree
for (const file in newMockFiles) {
Object.keys(newMockFiles).forEach((file) => {
const dir = path.dirname(file);
if (!mockFiles[dir]) {
@ -29,16 +30,12 @@ const createMockFiles = (newMockFiles: Record<string, string>) => {
mockFiles[dir].push(path.basename(file));
mockFiles[file] = newMockFiles[file];
}
});
};
const readFileSync = (p: string) => {
return mockFiles[p];
};
const readFileSync = (p: string) => mockFiles[p];
const existsSync = (p: string) => {
return mockFiles[p] !== undefined;
};
const existsSync = (p: string) => mockFiles[p] !== undefined;
const writeFileSync = (p: string, data: any) => {
mockFiles[p] = data;
@ -85,7 +82,7 @@ const copySync = (source: string, destination: string) => {
if (mockFiles[source] instanceof Array) {
mockFiles[source].forEach((file: string) => {
mockFiles[destination + '/' + file] = mockFiles[source + '/' + file];
mockFiles[`${destination}/${file}`] = mockFiles[`${source}/${file}`];
});
}
};
@ -120,4 +117,5 @@ fs.createFileSync = createFileSync;
fs.__createMockFiles = createMockFiles;
fs.__resetAllMocks = resetAllMocks;
module.exports = fs;
export default fs;
// module.exports = fs;

View file

@ -9,16 +9,10 @@ module.exports = {
values.set(key, value);
expirations.set(key, exp);
},
get: (key: string) => {
return values.get(key);
},
get: (key: string) => values.get(key),
quit: jest.fn(),
del: (key: string) => {
return values.delete(key);
},
ttl: (key: string) => {
return expirations.get(key);
},
del: (key: string) => values.delete(key),
ttl: (key: string) => expirations.get(key),
};
}),
};

View file

@ -22,5 +22,5 @@ export default new DataSource({
logging: !__prod__,
synchronize: false,
entities: [App, User, Update],
migrations: [process.cwd() + '/dist/config/migrations/*.js'],
migrations: [`${process.cwd()}/dist/config/migrations/*.js`],
});

View file

@ -4,15 +4,13 @@ import { __prod__ } from '../constants/constants';
import logger from './logger';
const ApolloLogs: PluginDefinition = {
requestDidStart: async () => {
return {
requestDidStart: async () => ({
async didEncounterErrors(errors) {
if (!__prod__) {
logger.error(JSON.stringify(errors.errors));
}
},
};
},
}),
};
export { ApolloLogs };

View file

@ -105,7 +105,7 @@ class Config {
this.config = parsed;
}
public setConfig<T extends keyof typeof configSchema.shape>(key: T, value: z.infer<typeof configSchema>[T], writeFile: boolean = false) {
public setConfig<T extends keyof typeof configSchema.shape>(key: T, value: z.infer<typeof configSchema>[T], writeFile = false) {
const newConf: z.infer<typeof configSchema> = { ...this.getConfig() };
newConf[key] = value;
@ -122,7 +122,7 @@ class Config {
}
}
export const setConfig = <T extends keyof typeof configSchema.shape>(key: T, value: z.infer<typeof configSchema>[T], writeFile: boolean = false) => {
export const setConfig = <T extends keyof typeof configSchema.shape>(key: T, value: z.infer<typeof configSchema>[T], writeFile = false) => {
Config.getInstance().setConfig(key, value, writeFile);
};

View file

@ -5,6 +5,7 @@ const WATCH_FILE = '/runtipi/state/events';
jest.mock('fs-extra');
// eslint-disable-next-line no-promise-executor-return
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
beforeEach(() => {
@ -29,7 +30,7 @@ describe('EventDispatcher - dispatchEvent', () => {
eventDispatcher.dispatchEvent(EventTypes.APP, ['--help']);
// @ts-ignore
const queue = eventDispatcher.queue;
const { queue } = eventDispatcher;
expect(queue.length).toBe(2);
});
@ -39,12 +40,12 @@ describe('EventDispatcher - dispatchEvent', () => {
eventDispatcher.dispatchEvent(EventTypes.UPDATE, ['--help']);
// @ts-ignore
const queue = eventDispatcher.queue;
const { queue } = eventDispatcher;
await wait(1050);
// @ts-ignore
const lock = eventDispatcher.lock;
const { lock } = eventDispatcher;
expect(queue.length).toBe(2);
expect(lock).toBeDefined();
@ -59,7 +60,7 @@ describe('EventDispatcher - dispatchEvent', () => {
await wait(1050);
// @ts-ignore
const queue = eventDispatcher.queue;
const { queue } = eventDispatcher;
expect(queue.length).toBe(0);
});
@ -72,7 +73,7 @@ describe('EventDispatcher - dispatchEvent', () => {
await wait(1050);
// @ts-ignore
const queue = eventDispatcher.queue;
const { queue } = eventDispatcher;
expect(queue.length).toBe(0);
});
@ -161,7 +162,7 @@ describe('EventDispatcher - clearEvent', () => {
eventDispatcher.clearEvent(event);
// @ts-ignore
const queue = eventDispatcher.queue;
const { queue } = eventDispatcher;
expect(queue.length).toBe(0);
});
@ -174,7 +175,7 @@ describe('EventDispatcher - pollQueue', () => {
// @ts-ignore
const id = eventDispatcher.pollQueue();
// @ts-ignore
const interval = eventDispatcher.interval;
const { interval } = eventDispatcher;
expect(interval).toBe(123);
expect(id).toBe(123);
@ -192,7 +193,7 @@ describe('EventDispatcher - collectLockStatusAndClean', () => {
eventDispatcher.collectLockStatusAndClean();
// @ts-ignore
const lock = eventDispatcher.lock;
const { lock } = eventDispatcher;
expect(lock).toBeNull();
});

View file

@ -1,7 +1,7 @@
import cron from 'node-cron';
import { getConfig } from '../../config/TipiConfig';
import startJobs from '../jobs';
import { eventDispatcher, EventTypes } from '../../../core/config/EventDispatcher';
import { eventDispatcher, EventTypes } from '../../config/EventDispatcher';
jest.mock('node-cron');
jest.mock('child_process');

View file

@ -1,6 +1,6 @@
import cron from 'node-cron';
import logger from '../../config/logger/logger';
import { getConfig } from '../../core/config/TipiConfig';
import { getConfig } from '../config/TipiConfig';
import { eventDispatcher, EventTypes } from '../config/EventDispatcher';
const startJobs = () => {

View file

@ -30,9 +30,7 @@ afterAll(async () => {
await teardownConnection(TEST_SUITE);
});
const createState = (apps: string[]) => {
return JSON.stringify({ installed: apps.join(' ') });
};
const createState = (apps: string[]) => JSON.stringify({ installed: apps.join(' ') });
describe('No state/apps.json', () => {
it('Should do nothing and create the update with status SUCCES', async () => {

View file

@ -1,9 +1,21 @@
import { DataSource } from 'typeorm';
import { BaseEntity, DataSource, DeepPartial } from 'typeorm';
import logger from '../../config/logger/logger';
import App from '../../modules/apps/app.entity';
import User from '../../modules/auth/user.entity';
import Update from '../../modules/system/update.entity';
const createUser = async (user: DeepPartial<BaseEntity>): Promise<void> => {
await User.create(user).save();
};
const createApp = async (app: DeepPartial<BaseEntity>): Promise<void> => {
await App.create(app).save();
};
const createUpdate = async (update: DeepPartial<BaseEntity>): Promise<void> => {
await Update.create(update).save();
};
const recover = async (datasource: DataSource) => {
logger.info('Recovering broken database');
@ -18,20 +30,14 @@ const recover = async (datasource: DataSource) => {
logger.info('running migrations');
await datasource.runMigrations();
// create users
for (const user of users) {
await User.create(user).save();
}
// recreate users
await Promise.all(users.map(createUser));
// create apps
for (const app of apps) {
await App.create(app).save();
}
await Promise.all(apps.map(createApp));
// create updates
for (const update of updates) {
await Update.create(update).save();
}
await Promise.all(updates.map(createUpdate));
logger.info(`Users recovered ${users.length}`);
logger.info(`Apps recovered ${apps.length}`);

View file

@ -10,6 +10,41 @@ type AppsState = { installed: string };
const UPDATE_NAME = 'v040';
const migrateApp = async (appId: string): Promise<void> => {
const app = await App.findOne({ where: { id: appId } });
if (!app) {
const envFile = readFile(`/app/storage/app-data/${appId}/app.env`).toString();
const envVars = envFile.split('\n');
const envVarsMap = new Map<string, string>();
envVars.forEach((envVar) => {
const [key, value] = envVar.split('=');
envVarsMap.set(key, value);
});
const form: Record<string, string> = {};
const configFile: AppInfo | null = readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${appId}/config.json`);
configFile?.form_fields?.forEach((field) => {
const envVar = field.env_variable;
const envVarValue = envVarsMap.get(envVar);
if (envVarValue) {
form[field.env_variable] = envVarValue;
}
});
await App.create({ id: appId, status: AppStatusEnum.STOPPED, config: form }).save();
} else {
logger.info('App already migrated');
}
};
const migrateUser = async (user: { email: string; password: string }): Promise<void> => {
await User.create({ username: user.email.trim().toLowerCase(), password: user.password }).save();
};
export const updateV040 = async (): Promise<void> => {
try {
const update = await Update.findOne({ where: { name: UPDATE_NAME } });
@ -24,36 +59,7 @@ export const updateV040 = async (): Promise<void> => {
const state: AppsState = await readJsonFile('/runtipi/state/apps.json');
const installed: string[] = state.installed.split(' ').filter(Boolean);
for (const appId of installed) {
const app = await App.findOne({ where: { id: appId } });
if (!app) {
const envFile = readFile(`/app/storage/app-data/${appId}/app.env`).toString();
const envVars = envFile.split('\n');
const envVarsMap = new Map<string, string>();
envVars.forEach((envVar) => {
const [key, value] = envVar.split('=');
envVarsMap.set(key, value);
});
const form: Record<string, string> = {};
const configFile: AppInfo | null = readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${appId}/config.json`);
configFile?.form_fields?.forEach((field) => {
const envVar = field.env_variable;
const envVarValue = envVarsMap.get(envVar);
if (envVarValue) {
form[field.env_variable] = envVarValue;
}
});
await App.create({ id: appId, status: AppStatusEnum.STOPPED, config: form }).save();
} else {
logger.info('App already migrated');
}
}
await Promise.all(installed.map((appId) => migrateApp(appId)));
deleteFolder('/runtipi/state/apps.json');
}
@ -61,9 +67,7 @@ export const updateV040 = async (): Promise<void> => {
if (fileExists('/state/users.json')) {
const state: { email: string; password: string }[] = await readJsonFile('/runtipi/state/users.json');
for (const user of state) {
await User.create({ username: user.email.trim().toLowerCase(), password: user.password }).save();
}
await Promise.all(state.map((user) => migrateUser(user)));
deleteFolder('/runtipi/state/users.json');
}

View file

@ -56,7 +56,7 @@ const createApp = async (props: IProps) => {
};
}
let MockFiles: any = {};
const MockFiles: any = {};
MockFiles['/runtipi/.env'] = 'TEST=test';
MockFiles['/runtipi/repos/repo-id'] = '';
MockFiles[`/runtipi/repos/repo-id/apps/${appInfo.id}/config.json`] = JSON.stringify(appInfo);
@ -71,6 +71,7 @@ const createApp = async (props: IProps) => {
status,
exposed,
domain,
version: 1,
}).save();
MockFiles[`/app/storage/app-data/${appInfo.id}`] = '';

View file

@ -4,7 +4,7 @@ import { DataSource } from 'typeorm';
import logger from '../../../config/logger/logger';
import { setupConnection, teardownConnection } from '../../../test/connection';
import App from '../app.entity';
import { checkAppRequirements, checkEnvFile, generateEnvFile, getAppInfo, getAvailableApps, getEnvMap, getUpdateInfo } from '../apps.helpers';
import { checkAppRequirements, checkEnvFile, ensureAppFolder, generateEnvFile, getAppInfo, getAvailableApps, getEnvMap, getUpdateInfo } from '../apps.helpers';
import { AppInfo } from '../apps.types';
import { createApp } from './apps.factory';
@ -336,15 +336,89 @@ describe('getUpdateInfo', () => {
});
it('Should return update info', async () => {
const updateInfo = await getUpdateInfo(app1.id);
const updateInfo = await getUpdateInfo(app1.id, 1);
expect(updateInfo?.latest).toBe(app1.tipi_version);
expect(updateInfo?.current).toBe(1);
});
it('Should return null if app is not installed', async () => {
const updateInfo = await getUpdateInfo(faker.random.word());
const updateInfo = await getUpdateInfo(faker.random.word(), 1);
expect(updateInfo).toBeNull();
});
});
describe('Test: ensureAppFolder', () => {
beforeEach(() => {
const mockFiles = {
[`/runtipi/repos/repo-id/apps/test`]: ['test.yml'],
};
// @ts-ignore
fs.__createMockFiles(mockFiles);
});
it('should copy the folder from repo', () => {
// Act
ensureAppFolder('test');
// Assert
const files = fs.readdirSync('/runtipi/apps/test');
expect(files).toEqual(['test.yml']);
});
it('should not copy the folder if it already exists', () => {
const mockFiles = {
[`/runtipi/repos/repo-id/apps/test`]: ['test.yml'],
'/runtipi/apps/test': ['docker-compose.yml'],
'/runtipi/apps/test/docker-compose.yml': 'test',
};
// @ts-ignore
fs.__createMockFiles(mockFiles);
// Act
ensureAppFolder('test');
// Assert
const files = fs.readdirSync('/runtipi/apps/test');
expect(files).toEqual(['docker-compose.yml']);
});
it('Should overwrite the folder if clean up is true', () => {
const mockFiles = {
[`/runtipi/repos/repo-id/apps/test`]: ['test.yml'],
'/runtipi/apps/test': ['docker-compose.yml'],
'/runtipi/apps/test/docker-compose.yml': 'test',
};
// @ts-ignore
fs.__createMockFiles(mockFiles);
// Act
ensureAppFolder('test', true);
// Assert
const files = fs.readdirSync('/runtipi/apps/test');
expect(files).toEqual(['test.yml']);
});
it('Should delete folder if it exists but has no docker-compose.yml file', () => {
// Arrange
const randomFileName = `${faker.random.word()}.yml`;
const mockFiles = {
[`/runtipi/repos/repo-id/apps/test`]: [randomFileName],
'/runtipi/apps/test': ['test.yml'],
};
// @ts-ignore
fs.__createMockFiles(mockFiles);
// Act
ensureAppFolder('test');
// Assert
const files = fs.readdirSync('/runtipi/apps/test');
expect(files).toEqual([randomFileName]);
});
});

View file

@ -1,6 +1,7 @@
import { DataSource } from 'typeorm';
import { setupConnection, teardownConnection } from '../../../test/connection';
import fs from 'fs-extra';
import { faker } from '@faker-js/faker';
import { setupConnection, teardownConnection } from '../../../test/connection';
import { gcall } from '../../../test/gcall';
import App from '../app.entity';
import { getAppQuery, InstalledAppsQuery, listAppInfosQuery } from '../../../test/queries';
@ -9,7 +10,6 @@ import { AppInfo, AppStatusEnum, ListAppsResonse } from '../apps.types';
import { createUser } from '../../auth/__tests__/user.factory';
import User from '../../auth/user.entity';
import { installAppMutation, startAppMutation, stopAppMutation, uninstallAppMutation, updateAppConfigMutation, updateAppMutation } from '../../../test/mutations';
import { faker } from '@faker-js/faker';
import EventDispatcher from '../../../core/config/EventDispatcher';
jest.mock('fs');

View file

@ -1,10 +1,10 @@
import AppsService from '../apps.service';
import fs from 'fs-extra';
import { DataSource } from 'typeorm';
import AppsService from '../apps.service';
import { AppInfo, AppStatusEnum, AppSupportedArchitecturesEnum } from '../apps.types';
import App from '../app.entity';
import { createApp } from './apps.factory';
import { setupConnection, teardownConnection } from '../../../test/connection';
import { DataSource } from 'typeorm';
import { getEnvMap } from '../apps.helpers';
import EventDispatcher, { eventDispatcher, EventTypes } from '../../../core/config/EventDispatcher';
import { setConfig } from '../../../core/config/TipiConfig';
@ -56,9 +56,9 @@ describe('Install app', () => {
const app = await App.findOne({ where: { id: app1.id } });
expect(app).toBeDefined();
expect(app!.id).toBe(app1.id);
expect(app!.config).toStrictEqual({ TEST_FIELD: 'test' });
expect(app!.status).toBe(AppStatusEnum.RUNNING);
expect(app?.id).toBe(app1.id);
expect(app?.config).toStrictEqual({ TEST_FIELD: 'test' });
expect(app?.status).toBe(AppStatusEnum.RUNNING);
});
it('Should start app if already installed', async () => {
@ -147,7 +147,7 @@ describe('Install app', () => {
const app2 = await createApp({ exposable: true });
const app3 = await createApp({ exposable: true });
// @ts-ignore
fs.__createMockFiles(Object.assign({}, app2.MockFiles, app3.MockFiles));
fs.__createMockFiles({ ...app2.MockFiles, ...app3.MockFiles });
await AppsService.installApp(app2.appInfo.id, { TEST_FIELD: 'test' }, true, 'test.com');
@ -203,8 +203,8 @@ describe('Uninstall app', () => {
// Assert
expect(app).toBeDefined();
expect(app!.id).toBe(app1.id);
expect(app!.status).toBe(AppStatusEnum.RUNNING);
expect(app?.id).toBe(app1.id);
expect(app?.status).toBe(AppStatusEnum.RUNNING);
});
it('Should correctly remove app from database', async () => {
@ -244,7 +244,7 @@ describe('Uninstall app', () => {
// Act & Assert
await expect(AppsService.uninstallApp(app1.id)).rejects.toThrow(`App ${app1.id} failed to uninstall\nstdout: test`);
const app = await App.findOne({ where: { id: app1.id } });
expect(app!.status).toBe(AppStatusEnum.STOPPED);
expect(app?.status).toBe(AppStatusEnum.STOPPED);
});
});
@ -300,7 +300,7 @@ describe('Start app', () => {
// Act & Assert
await expect(AppsService.startApp(app1.id)).rejects.toThrow(`App ${app1.id} failed to start\nstdout: test`);
const app = await App.findOne({ where: { id: app1.id } });
expect(app!.status).toBe(AppStatusEnum.STOPPED);
expect(app?.status).toBe(AppStatusEnum.STOPPED);
});
});
@ -333,7 +333,7 @@ describe('Stop app', () => {
// Act & Assert
await expect(AppsService.stopApp(app1.id)).rejects.toThrow(`App ${app1.id} failed to stop\nstdout: test`);
const app = await App.findOne({ where: { id: app1.id } });
expect(app!.status).toBe(AppStatusEnum.RUNNING);
expect(app?.status).toBe(AppStatusEnum.RUNNING);
});
});
@ -378,17 +378,13 @@ describe('Update app config', () => {
expect(envMap.get('RANDOM_FIELD')).toBe('test');
});
it('Should throw if app is exposed and domain is not provided', () => {
return expect(AppsService.updateAppConfig(app1.id, { TEST_FIELD: 'test' }, true)).rejects.toThrowError('Domain is required');
});
it('Should throw if app is exposed and domain is not provided', () => expect(AppsService.updateAppConfig(app1.id, { TEST_FIELD: 'test' }, true)).rejects.toThrowError('Domain is required'));
it('Should throw if app is exposed and domain is not valid', () => {
return expect(AppsService.updateAppConfig(app1.id, { TEST_FIELD: 'test' }, true, 'test')).rejects.toThrowError('Domain test is not valid');
});
it('Should throw if app is exposed and domain is not valid', () =>
expect(AppsService.updateAppConfig(app1.id, { TEST_FIELD: 'test' }, true, 'test')).rejects.toThrowError('Domain test is not valid'));
it('Should throw if app is exposed and config does not allow it', () => {
return expect(AppsService.updateAppConfig(app1.id, { TEST_FIELD: 'test' }, true, 'test.com')).rejects.toThrowError(`App ${app1.id} is not exposable`);
});
it('Should throw if app is exposed and config does not allow it', () =>
expect(AppsService.updateAppConfig(app1.id, { TEST_FIELD: 'test' }, true, 'test.com')).rejects.toThrowError(`App ${app1.id} is not exposable`));
it('Should throw if app is exposed and domain is already used', async () => {
const app2 = await createApp({ exposable: true, installed: true });

View file

@ -70,7 +70,7 @@ class App extends BaseEntity {
@Field(() => UpdateInfo, { nullable: true })
updateInfo(): Promise<UpdateInfo | null> {
return getUpdateInfo(this.id);
return getUpdateInfo(this.id, this.version);
}
}

View file

@ -0,0 +1,6 @@
export interface AppEntityType {
id: string;
config: Record<string, string>;
exposed: boolean;
domain?: string;
}

View file

@ -1,14 +1,12 @@
import { fileExists, getSeed, readdirSync, readFile, readJsonFile, writeFile } from '../fs/fs.helpers';
import crypto from 'crypto';
import fs from 'fs-extra';
import { deleteFolder, fileExists, getSeed, readdirSync, readFile, readJsonFile, writeFile } from '../fs/fs.helpers';
import { AppInfo, AppStatusEnum } from './apps.types';
import logger from '../../config/logger/logger';
import App from './app.entity';
import { getConfig } from '../../core/config/TipiConfig';
import fs from 'fs-extra';
import { AppEntityType } from './app.types';
export const checkAppRequirements = async (appName: string) => {
let valid = true;
const configFile: AppInfo | null = readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${appName}/config.json`);
if (!configFile) {
@ -19,7 +17,7 @@ export const checkAppRequirements = async (appName: string) => {
throw new Error(`App ${appName} is not supported on this architecture`);
}
return valid;
return true;
};
export const getEnvMap = (appName: string): Map<string, string> => {
@ -55,7 +53,7 @@ const getEntropy = (name: string, length: number) => {
return hash.digest('hex').substring(0, length);
};
export const generateEnvFile = (app: App) => {
export const generateEnvFile = (app: AppEntityType) => {
const configFile: AppInfo | null = readJsonFile(`/runtipi/apps/${app.id}/config.json`);
if (!configFile) {
@ -129,7 +127,8 @@ export const getAppInfo = (id: string, status?: AppStatusEnum): AppInfo | null =
const configFile: AppInfo = readJsonFile(`/runtipi/apps/${id}/config.json`);
configFile.description = readFile(`/runtipi/apps/${id}/metadata/description.md`).toString();
return configFile;
} else if (fileExists(`/runtipi/repos/${getConfig().appsRepoId}/apps/${id}/config.json`)) {
}
if (fileExists(`/runtipi/repos/${getConfig().appsRepoId}/apps/${id}/config.json`)) {
const configFile: AppInfo = readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${id}/config.json`);
configFile.description = readFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${id}/metadata/description.md`);
@ -145,20 +144,32 @@ export const getAppInfo = (id: string, status?: AppStatusEnum): AppInfo | null =
}
};
export const getUpdateInfo = async (id: string) => {
const app = await App.findOne({ where: { id } });
export const getUpdateInfo = async (id: string, version: number) => {
const doesFileExist = fileExists(`/runtipi/repos/${getConfig().appsRepoId}/apps/${id}`);
if (!app || !doesFileExist) {
if (!doesFileExist) {
return null;
}
const repoConfig: AppInfo = readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${id}/config.json`);
return {
current: app.version,
current: version,
latest: repoConfig.tipi_version,
dockerVersion: repoConfig.version,
};
};
export const ensureAppFolder = (appName: string, cleanup = false) => {
if (cleanup && fileExists(`/runtipi/apps/${appName}`)) {
deleteFolder(`/runtipi/apps/${appName}`);
}
if (!fileExists(`/runtipi/apps/${appName}/docker-compose.yml`)) {
if (fileExists(`/runtipi/apps/${appName}`)) {
deleteFolder(`/runtipi/apps/${appName}`);
}
// Copy from apps repo
fs.copySync(`/runtipi/repos/${getConfig().appsRepoId}/apps/${appName}`, `/runtipi/apps/${appName}`);
}
};

View file

@ -1,10 +1,10 @@
import validator from 'validator';
import { createFolder, ensureAppFolder, readFile, readJsonFile } from '../fs/fs.helpers';
import { checkAppRequirements, checkEnvFile, generateEnvFile, getAvailableApps } from './apps.helpers';
import { Not } from 'typeorm';
import { createFolder, readFile, readJsonFile } from '../fs/fs.helpers';
import { checkAppRequirements, checkEnvFile, generateEnvFile, getAvailableApps, ensureAppFolder } from './apps.helpers';
import { AppInfo, AppStatusEnum, ListAppsResonse } from './apps.types';
import App from './app.entity';
import logger from '../../config/logger/logger';
import { Not } from 'typeorm';
import { getConfig } from '../../core/config/TipiConfig';
import { eventDispatcher, EventTypes } from '../../core/config/EventDispatcher';
@ -18,9 +18,7 @@ const filterApp = (app: AppInfo): boolean => {
return app.supported_architectures.includes(arch);
};
const filterApps = (apps: AppInfo[]): AppInfo[] => {
return apps.sort(sortApps).filter(filterApp);
};
const filterApps = (apps: AppInfo[]): AppInfo[] => apps.sort(sortApps).filter(filterApp);
/**
* Start all apps which had the status RUNNING in the database
@ -157,11 +155,7 @@ const installApp = async (id: string, form: Record<string, string>, exposed?: bo
const listApps = async (): Promise<ListAppsResonse> => {
const folders: string[] = await getAvailableApps();
const apps: AppInfo[] = folders
.map((app) => {
return readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${app}/config.json`);
})
.filter(Boolean);
const apps: AppInfo[] = folders.map((app) => readJsonFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${app}/config.json`)).filter(Boolean);
const filteredApps = filterApps(apps).map((app) => {
const description = readFile(`/runtipi/repos/${getConfig().appsRepoId}/apps/${app.id}/metadata/description.md`);
@ -254,7 +248,7 @@ const stopApp = async (id: string): Promise<App> => {
* @returns - the app entity
*/
const uninstallApp = async (id: string): Promise<App> => {
let app = await App.findOne({ where: { id } });
const app = await App.findOne({ where: { id } });
if (!app) {
throw new Error(`App ${id} not found`);

View file

@ -7,7 +7,7 @@ import { setupConnection, teardownConnection } from '../../../test/connection';
import { gcall } from '../../../test/gcall';
import { loginMutation, registerMutation } from '../../../test/mutations';
import { isConfiguredQuery, MeQuery, refreshTokenQuery } from '../../../test/queries';
import User from '../../auth/user.entity';
import User from '../user.entity';
import { TokenResponse } from '../auth.types';
import { createUser } from './user.factory';
@ -214,7 +214,7 @@ describe('Test: refreshToken', () => {
const { data } = await gcall<{ refreshToken: TokenResponse }>({
source: refreshTokenQuery,
userId: user1.id,
session: session,
session,
});
const decoded = jwt.verify(data?.refreshToken?.token || '', getConfig().jwtSecret) as jwt.JwtPayload;

View file

@ -1,11 +1,11 @@
import * as argon2 from 'argon2';
import jwt from 'jsonwebtoken';
import { faker } from '@faker-js/faker';
import { DataSource } from 'typeorm';
import AuthService from '../auth.service';
import { createUser } from './user.factory';
import User from '../user.entity';
import { faker } from '@faker-js/faker';
import { setupConnection, teardownConnection } from '../../../test/connection';
import { DataSource } from 'typeorm';
import { setConfig } from '../../../core/config/TipiConfig';
import TipiCache from '../../../config/TipiCache';

View file

@ -1,6 +1,6 @@
import User from '../user.entity';
import * as argon2 from 'argon2';
import { faker } from '@faker-js/faker';
import User from '../user.entity';
const createUser = async (email?: string) => {
const hash = await argon2.hash('password');

View file

@ -1,5 +1,4 @@
import { Field, InputType, ObjectType } from 'type-graphql';
import User from './user.entity';
@InputType()
class UsernamePasswordInput {

View file

@ -1,7 +1,5 @@
import { readJsonFile, readFile, readdirSync, fileExists, writeFile, createFolder, deleteFolder, getSeed, ensureAppFolder } from '../fs.helpers';
import fs from 'fs-extra';
import { getConfig } from '../../../core/config/TipiConfig';
import { faker } from '@faker-js/faker';
import { readJsonFile, readFile, readdirSync, fileExists, writeFile, createFolder, deleteFolder, getSeed } from '../fs.helpers';
jest.mock('fs-extra');
@ -15,7 +13,7 @@ describe('Test: readJsonFile', () => {
// Arrange
const rawFile = '{"test": "test"}';
const mockFiles = {
['/runtipi/test-file.json']: rawFile,
'/runtipi/test-file.json': rawFile,
};
// @ts-ignore
fs.__createMockFiles(mockFiles);
@ -52,7 +50,7 @@ describe('Test: readFile', () => {
it('should return the file', () => {
const rawFile = 'test';
const mockFiles = {
['/runtipi/test-file.txt']: rawFile,
'/runtipi/test-file.txt': rawFile,
};
// @ts-ignore
@ -69,7 +67,7 @@ describe('Test: readFile', () => {
describe('Test: readdirSync', () => {
it('should return the files', () => {
const mockFiles = {
['/runtipi/test/test-file.txt']: 'test',
'/runtipi/test/test-file.txt': 'test',
};
// @ts-ignore
@ -86,7 +84,7 @@ describe('Test: readdirSync', () => {
describe('Test: fileExists', () => {
it('should return true if the file exists', () => {
const mockFiles = {
['/runtipi/test-file.txt']: 'test',
'/runtipi/test-file.txt': 'test',
};
// @ts-ignore
@ -133,7 +131,7 @@ describe('Test: deleteFolder', () => {
describe('Test: getSeed', () => {
it('should return the seed', () => {
const mockFiles = {
['/runtipi/state/seed']: 'test',
'/runtipi/state/seed': 'test',
};
// @ts-ignore
@ -142,77 +140,3 @@ describe('Test: getSeed', () => {
expect(getSeed()).toEqual('test');
});
});
describe('Test: ensureAppFolder', () => {
beforeEach(() => {
const mockFiles = {
[`/runtipi/repos/${getConfig().appsRepoId}/apps/test`]: ['test.yml'],
};
// @ts-ignore
fs.__createMockFiles(mockFiles);
});
it('should copy the folder from repo', () => {
// Act
ensureAppFolder('test');
// Assert
const files = fs.readdirSync('/runtipi/apps/test');
expect(files).toEqual(['test.yml']);
});
it('should not copy the folder if it already exists', () => {
const mockFiles = {
[`/runtipi/repos/${getConfig().appsRepoId}/apps/test`]: ['test.yml'],
['/runtipi/apps/test']: ['docker-compose.yml'],
['/runtipi/apps/test/docker-compose.yml']: 'test',
};
// @ts-ignore
fs.__createMockFiles(mockFiles);
// Act
ensureAppFolder('test');
// Assert
const files = fs.readdirSync('/runtipi/apps/test');
expect(files).toEqual(['docker-compose.yml']);
});
it('Should overwrite the folder if clean up is true', () => {
const mockFiles = {
[`/runtipi/repos/${getConfig().appsRepoId}/apps/test`]: ['test.yml'],
['/runtipi/apps/test']: ['docker-compose.yml'],
['/runtipi/apps/test/docker-compose.yml']: 'test',
};
// @ts-ignore
fs.__createMockFiles(mockFiles);
// Act
ensureAppFolder('test', true);
// Assert
const files = fs.readdirSync('/runtipi/apps/test');
expect(files).toEqual(['test.yml']);
});
it('Should delete folder if it exists but has no docker-compose.yml file', () => {
// Arrange
const randomFileName = `${faker.random.word()}.yml`;
const mockFiles = {
[`/runtipi/repos/${getConfig().appsRepoId}/apps/test`]: [randomFileName],
['/runtipi/apps/test']: ['test.yml'],
};
// @ts-ignore
fs.__createMockFiles(mockFiles);
// Act
ensureAppFolder('test');
// Assert
const files = fs.readdirSync('/runtipi/apps/test');
expect(files).toEqual([randomFileName]);
});
});

View file

@ -1,5 +1,4 @@
import fs from 'fs-extra';
import { getConfig } from '../../core/config/TipiConfig';
export const readJsonFile = (path: string): any => {
try {
@ -36,17 +35,3 @@ export const getSeed = () => {
const seed = readFile('/runtipi/state/seed');
return seed.toString();
};
export const ensureAppFolder = (appName: string, cleanup = false) => {
if (cleanup && fileExists(`/runtipi/apps/${appName}`)) {
deleteFolder(`/runtipi/apps/${appName}`);
}
if (!fileExists(`/runtipi/apps/${appName}/docker-compose.yml`)) {
if (fileExists(`/runtipi/apps/${appName}`)) {
deleteFolder(`/runtipi/apps/${appName}`);
}
// Copy from apps repo
fs.copySync(`/runtipi/repos/${getConfig().appsRepoId}/apps/${appName}`, `/runtipi/apps/${appName}`);
}
};

View file

@ -1,8 +1,8 @@
import fs from 'fs-extra';
import semver from 'semver';
import axios from 'axios';
import SystemService from '../system.service';
import { faker } from '@faker-js/faker';
import SystemService from '../system.service';
import TipiCache from '../../../config/TipiCache';
import { setConfig } from '../../../core/config/TipiConfig';
import logger from '../../../config/logger/logger';

View file

@ -2,9 +2,11 @@ import 'reflect-metadata';
import express from 'express';
import { ApolloServerPluginLandingPageGraphQLPlayground as Playground } from 'apollo-server-core';
import { ApolloServer } from 'apollo-server-express';
import { createServer } from 'http';
import { ZodError } from 'zod';
import cors from 'cors';
import { createSchema } from './schema';
import { ApolloLogs } from './config/logger/apollo.logger';
import { createServer } from 'http';
import logger from './config/logger/logger';
import getSessionMiddleware from './core/middlewares/sessionMiddleware';
import { MyContext } from './types';
@ -15,10 +17,8 @@ import { runUpdates } from './core/updates/run';
import recover from './core/updates/recover-migrations';
import startJobs from './core/jobs/jobs';
import { applyJsonConfig, getConfig, setConfig } from './core/config/TipiConfig';
import { ZodError } from 'zod';
import systemController from './modules/system/system.controller';
import { eventDispatcher, EventTypes } from './core/config/EventDispatcher';
import cors from 'cors';
const applyCustomConfig = () => {
try {

View file

@ -1,7 +1,7 @@
import { DataSource } from 'typeorm';
import pg from 'pg';
import App from '../modules/apps/app.entity';
import User from '../modules/auth/user.entity';
import pg from 'pg';
import Update from '../modules/system/update.entity';
const HOST = 'localhost';