feat: create shared package between main app and cli

This commit is contained in:
Nicolas Meienberger 2023-08-15 22:50:52 +02:00 committed by Nicolas Meienberger
parent c89b9fe752
commit 8b7952c6db
11 changed files with 312 additions and 0 deletions

View file

@ -0,0 +1,14 @@
{
"name": "@runtipi/shared",
"version": "1.0.0",
"description": "",
"main": "src/index.ts",
"scripts": {},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"winston": "^3.9.0",
"zod": "^3.21.4"
}
}

View file

@ -0,0 +1,29 @@
/**
* Convert a string of environment variables to a Map
*
* @param {string} envString - String of environment variables
*/
export const envStringToMap = (envString: string) => {
const envMap = new Map<string, string>();
const envArray = envString.split('\n');
envArray.forEach((env) => {
const [key, value] = env.split('=');
if (key && value) {
envMap.set(key, value);
}
});
return envMap;
};
/**
* Convert a Map of environment variables to a valid string of environment variables
* that can be used in a .env file
*
* @param {Map<string, string>} envMap - Map of environment variables
*/
export const envMapToString = (envMap: Map<string, string>) => {
const envArray = Array.from(envMap).map(([key, value]) => `${key}=${value}`);
return envArray.join('\n');
};

View file

@ -0,0 +1 @@
export * from './env-helpers';

View file

@ -0,0 +1,3 @@
export * from './schemas';
export * from './helpers';
export { createLogger } from './utils/logger';

View file

@ -0,0 +1,84 @@
import { z } from 'zod';
import { ARCHITECTURES } from './env-schemas';
export const APP_CATEGORIES = {
NETWORK: 'network',
MEDIA: 'media',
DEVELOPMENT: 'development',
AUTOMATION: 'automation',
SOCIAL: 'social',
UTILITIES: 'utilities',
PHOTOGRAPHY: 'photography',
SECURITY: 'security',
FEATURED: 'featured',
BOOKS: 'books',
DATA: 'data',
MUSIC: 'music',
FINANCE: 'finance',
GAMING: 'gaming',
AI: 'ai',
} as const;
export type AppCategory = (typeof APP_CATEGORIES)[keyof typeof APP_CATEGORIES];
export const FIELD_TYPES = {
TEXT: 'text',
PASSWORD: 'password',
EMAIL: 'email',
NUMBER: 'number',
FQDN: 'fqdn',
IP: 'ip',
FQDNIP: 'fqdnip',
URL: 'url',
RANDOM: 'random',
BOOLEAN: 'boolean',
} as const;
export const formFieldSchema = z.object({
type: z.nativeEnum(FIELD_TYPES).catch(() => FIELD_TYPES.TEXT),
label: z.string(),
placeholder: z.string().optional(),
max: z.number().optional(),
min: z.number().optional(),
hint: z.string().optional(),
options: z.object({ label: z.string(), value: z.string() }).array().optional(),
required: z.boolean().optional().default(false),
default: z.union([z.boolean(), z.string()]).optional(),
regex: z.string().optional(),
pattern_error: z.string().optional(),
env_variable: z.string(),
});
export const appInfoSchema = z.object({
id: z.string(),
available: z.boolean(),
port: z.number().min(1).max(65535),
name: z.string(),
description: z.string().optional().default(''),
version: z.string().optional().default('latest'),
tipi_version: z.number(),
short_desc: z.string(),
author: z.string(),
source: z.string(),
website: z.string().optional(),
force_expose: z.boolean().optional().default(false),
generate_vapid_keys: z.boolean().optional().default(false),
categories: z
.nativeEnum(APP_CATEGORIES)
.array()
.catch(() => {
return [APP_CATEGORIES.UTILITIES];
}),
url_suffix: z.string().optional(),
form_fields: z.array(formFieldSchema).optional().default([]),
https: z.boolean().optional().default(false),
exposable: z.boolean().optional().default(false),
no_gui: z.boolean().optional().default(false),
supported_architectures: z.nativeEnum(ARCHITECTURES).array().optional(),
uid: z.number().optional(),
gid: z.number().optional(),
});
// Derived types
export type AppInfo = z.infer<typeof appInfoSchema>;
export type FormField = z.infer<typeof formFieldSchema>;

View file

@ -0,0 +1,50 @@
import { z } from 'zod';
export const ARCHITECTURES = {
ARM: 'arm',
ARM64: 'arm64',
AMD64: 'amd64',
} as const;
export type Architecture = (typeof ARCHITECTURES)[keyof typeof ARCHITECTURES];
export const envSchema = z.object({
NODE_ENV: z.union([z.literal('development'), z.literal('production'), z.literal('test')]),
REDIS_HOST: z.string(),
status: z.union([z.literal('RUNNING'), z.literal('UPDATING'), z.literal('RESTARTING')]),
architecture: z.nativeEnum(ARCHITECTURES),
dnsIp: z.string().ip().trim(),
rootFolder: z.string(),
internalIp: z.string(),
version: z.string(),
jwtSecret: z.string(),
appsRepoId: z.string(),
appsRepoUrl: z.string().url().trim(),
domain: z.string().trim(),
localDomain: z.string().trim(),
storagePath: z
.string()
.trim()
.optional()
.transform((value) => {
if (!value) return undefined;
return value?.replace(/\s/g, '');
}),
postgresHost: z.string(),
postgresDatabase: z.string(),
postgresUsername: z.string(),
postgresPassword: z.string(),
postgresPort: z.number(),
demoMode: z
.string()
.or(z.boolean())
.optional()
.transform((value) => {
if (typeof value === 'boolean') return value;
return value === 'true';
}),
});
export const settingsSchema = envSchema
.partial()
.pick({ dnsIp: true, internalIp: true, appsRepoUrl: true, domain: true, storagePath: true, localDomain: true, demoMode: true })
.and(z.object({ port: z.number(), sslPort: z.number(), listenIp: z.string().ip().trim() }).partial());

View file

@ -0,0 +1,3 @@
export * from './app-schemas';
export * from './env-schemas';
export * from './queue-schemas';

View file

@ -0,0 +1,36 @@
import { z } from 'zod';
export const EVENT_TYPES = {
SYSTEM: 'system',
REPO: 'repo',
APP: 'app',
} as const;
export type EventType = (typeof EVENT_TYPES)[keyof typeof EVENT_TYPES];
const appCommandSchema = z.object({
type: z.literal(EVENT_TYPES.APP),
command: z.union([z.literal('start'), z.literal('stop'), z.literal('install'), z.literal('uninstall'), z.literal('update'), z.literal('generate_env')]),
appid: z.string(),
form: z.object({}).catchall(z.any()),
});
const repoCommandSchema = z.object({
type: z.literal(EVENT_TYPES.REPO),
command: z.union([z.literal('clone'), z.literal('update')]),
url: z.string().url(),
});
const systemCommandSchema = z.object({
type: z.literal(EVENT_TYPES.SYSTEM),
command: z.union([z.literal('restart'), z.literal('update'), z.literal('system_info')]),
});
export const eventSchema = appCommandSchema.or(repoCommandSchema).or(systemCommandSchema);
export const eventResultSchema = z.object({
success: z.boolean(),
stdout: z.string(),
});
export type SystemEvent = z.infer<typeof eventSchema>;

View file

@ -0,0 +1,50 @@
import fs from 'fs';
import path from 'path';
import { createLogger, format, transports } from 'winston';
const { printf, timestamp, combine, colorize, align, label } = format;
type Transports = transports.ConsoleTransportInstance | transports.FileTransportInstance;
/**
* Given an id and a logs folder, creates a new winston logger
*
* @param {string} id - The id of the logger, used to identify the logger in the logs
* @param {string} logsFolder - The folder where the logs will be stored
*/
export const newLogger = (id: string, logsFolder: string) => {
if (!fs.existsSync(logsFolder)) {
fs.mkdirSync(logsFolder, { recursive: true });
}
const tr: Transports[] = [];
let exceptionHandlers: Transports[] = [new transports.Console()];
tr.push(
new transports.File({
filename: path.join(logsFolder, 'error.log'),
level: 'error',
}),
);
tr.push(
new transports.File({
filename: path.join(logsFolder, 'app.log'),
level: 'info',
}),
);
exceptionHandlers = [new transports.File({ filename: path.join(logsFolder, 'error.log') })];
return createLogger({
level: 'debug',
format: combine(
label({ label: id }),
colorize(),
timestamp(),
align(),
printf((info) => `${info.timestamp} - ${info.level} > ${info.message}`),
),
transports: tr,
exceptionHandlers,
exitOnError: false,
});
};

View file

@ -0,0 +1 @@
export { newLogger as createLogger } from './Logger';

View file

@ -0,0 +1,41 @@
{
"compilerOptions": {
"target": "es2017",
"baseUrl": ".",
"paths": {},
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "CommonJS",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"strictNullChecks": true,
"allowSyntheticDefaultImports": true,
"noUncheckedIndexedAccess": true,
"types": [
"node"
],
"experimentalDecorators": true
},
"include": [
"**/*.ts",
"**/*.tsx",
"**/*.mjs",
"**/*.js",
"**/*.jsx"
],
"exclude": [
"node_modules"
]
}