feat: create shared package between main app and cli
This commit is contained in:
parent
c89b9fe752
commit
8b7952c6db
11 changed files with 312 additions and 0 deletions
14
packages/shared/package.json
Normal file
14
packages/shared/package.json
Normal 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"
|
||||
}
|
||||
}
|
29
packages/shared/src/helpers/env-helpers.ts
Normal file
29
packages/shared/src/helpers/env-helpers.ts
Normal 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');
|
||||
};
|
1
packages/shared/src/helpers/index.ts
Normal file
1
packages/shared/src/helpers/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './env-helpers';
|
3
packages/shared/src/index.ts
Normal file
3
packages/shared/src/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export * from './schemas';
|
||||
export * from './helpers';
|
||||
export { createLogger } from './utils/logger';
|
84
packages/shared/src/schemas/app-schemas.ts
Normal file
84
packages/shared/src/schemas/app-schemas.ts
Normal 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>;
|
50
packages/shared/src/schemas/env-schemas.ts
Normal file
50
packages/shared/src/schemas/env-schemas.ts
Normal 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());
|
3
packages/shared/src/schemas/index.ts
Normal file
3
packages/shared/src/schemas/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export * from './app-schemas';
|
||||
export * from './env-schemas';
|
||||
export * from './queue-schemas';
|
36
packages/shared/src/schemas/queue-schemas.ts
Normal file
36
packages/shared/src/schemas/queue-schemas.ts
Normal 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>;
|
50
packages/shared/src/utils/logger/Logger.ts
Normal file
50
packages/shared/src/utils/logger/Logger.ts
Normal 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,
|
||||
});
|
||||
};
|
1
packages/shared/src/utils/logger/index.ts
Normal file
1
packages/shared/src/utils/logger/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { newLogger as createLogger } from './Logger';
|
41
packages/shared/tsconfig.json
Normal file
41
packages/shared/tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
Loading…
Reference in a new issue