diff --git a/package-lock.json b/package-lock.json index 2780e8f9..30b2649b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12799,6 +12799,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zod": { + "version": "3.21.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", + "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "packages/caching": { "name": "@jitar/caching", "version": "0.4.0", @@ -12881,7 +12889,8 @@ "glob-promise": "6.0.2", "mime-types": "^2.1.35", "tslog": "^4.8.2", - "yargs": "^17.7.1" + "yargs": "^17.7.1", + "zod": "^3.21.4" }, "engines": { "node": ">=18.7" diff --git a/packages/server-nodejs/package.json b/packages/server-nodejs/package.json index c04ddd49..194db89c 100644 --- a/packages/server-nodejs/package.json +++ b/packages/server-nodejs/package.json @@ -28,7 +28,8 @@ "glob-promise": "6.0.2", "mime-types": "^2.1.35", "tslog": "^4.8.2", - "yargs": "^17.7.1" + "yargs": "^17.7.1", + "zod": "^3.21.4" }, "engines": { "node": ">=18.7" @@ -45,4 +46,4 @@ "jitar", "nodejs" ] -} \ No newline at end of file +} diff --git a/packages/server-nodejs/src/JitarServer.ts b/packages/server-nodejs/src/JitarServer.ts index d8f56d33..29203e4a 100644 --- a/packages/server-nodejs/src/JitarServer.ts +++ b/packages/server-nodejs/src/JitarServer.ts @@ -57,8 +57,8 @@ export default class JitarServer { console.log(STARTUP_MESSAGE); - const options = await ServerOptionsReader.read(); - const configuration = await RuntimeConfigurationLoader.load(options.config); + const options = ServerOptionsReader.read(); + const configuration = RuntimeConfigurationLoader.load(options.config); const runtime = await RuntimeConfigurator.configure(configuration); const logger = LogBuilder.build(options.loglevel); diff --git a/packages/server-nodejs/src/configuration/GatewayConfiguration.ts b/packages/server-nodejs/src/configuration/GatewayConfiguration.ts index bced7f8f..95fd227a 100644 --- a/packages/server-nodejs/src/configuration/GatewayConfiguration.ts +++ b/packages/server-nodejs/src/configuration/GatewayConfiguration.ts @@ -1,12 +1,22 @@ -import { IsOptional, IsNumber, IsUrl } from 'class-validator'; +import { z } from 'zod'; + +export const gatewaySchema = z + .object({ + monitor: z.number().optional(), + repository: z.string().url().optional() + }) + .strict() + .transform((value) => new GatewayConfiguration(value.monitor, value.repository)); export default class GatewayConfiguration { - @IsNumber() - @IsOptional() monitor?: number; + repository?: string; - @IsUrl() - repository = ''; + constructor(monitor?: number, repository?: string) + { + this.monitor = monitor; + this.repository = repository; + } } diff --git a/packages/server-nodejs/src/configuration/NodeConfiguration.ts b/packages/server-nodejs/src/configuration/NodeConfiguration.ts index d88ce23c..35d8cfc0 100644 --- a/packages/server-nodejs/src/configuration/NodeConfiguration.ts +++ b/packages/server-nodejs/src/configuration/NodeConfiguration.ts @@ -1,15 +1,25 @@ -import { IsArray, IsUrl, IsOptional } from 'class-validator'; +import { z } from 'zod'; + +export const nodeSchema = z + .object({ + gateway: z.string().url().optional(), + repository: z.string().url().optional(), + segments: z.array(z.string()).nonempty() + }) + .strict() + .transform((value) => new NodeConfiguration(value.gateway, value.repository, value.segments)); export default class NodeConfiguration { - @IsUrl() - @IsOptional() gateway?: string; + repository?: string; + segments: string[]; - @IsUrl() - repository = ''; - - @IsArray() - segments?: string[]; + constructor(gateway: string | undefined, repository: string | undefined, segments: string[]) + { + this.gateway = gateway; + this.repository = repository; + this.segments = segments; + } } diff --git a/packages/server-nodejs/src/configuration/ProxyConfiguration.ts b/packages/server-nodejs/src/configuration/ProxyConfiguration.ts index 6b801802..228c1634 100644 --- a/packages/server-nodejs/src/configuration/ProxyConfiguration.ts +++ b/packages/server-nodejs/src/configuration/ProxyConfiguration.ts @@ -1,16 +1,46 @@ -import { IsUrl, IsOptional } from 'class-validator'; +import { z } from 'zod'; + +export const proxySchema = z + .object({ + node: z.string().url().optional(), + gateway: z.string().url().optional(), + repository: z.string().url() + }) + .strict() + .superRefine((value, ctx) => + { + if (value.node === undefined && value.gateway === undefined) + { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Either node or gateway must be defined', + path: ['node', 'gateway'] + }); + } + + if (value.node !== undefined && value.gateway !== undefined) + { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Only node or gateway must be defined', + path: ['node', 'gateway'], + + }); + } + }) + .transform((value) => new ProxyConfiguration(value.node, value.gateway, value.repository)); export default class ProxyConfiguration { - @IsUrl() - @IsOptional() node?: string; - - @IsUrl() - @IsOptional() gateway?: string; + repository: string; - @IsUrl() - repository?: string; + constructor(node: string | undefined, gateway: string | undefined, repository: string) + { + this.node = node; + this.gateway = gateway; + this.repository = repository; + } } diff --git a/packages/server-nodejs/src/configuration/RepositoryConfiguration.ts b/packages/server-nodejs/src/configuration/RepositoryConfiguration.ts index b1344851..a710431a 100644 --- a/packages/server-nodejs/src/configuration/RepositoryConfiguration.ts +++ b/packages/server-nodejs/src/configuration/RepositoryConfiguration.ts @@ -1,21 +1,28 @@ -import { IsArray, IsOptional, IsString } from 'class-validator'; +import { z } from 'zod'; + +export const repositorySchema = z + .object({ + source: z.string().optional(), + cache: z.string().optional(), + index: z.string().optional(), + assets: z.array(z.string()).optional() + }) + .strict() + .transform((value) => new RepositoryConfiguration(value.source, value.cache, value.index, value.assets)); export default class RepositoryConfiguration { - @IsString() - @IsOptional() source?: string; - - @IsString() - @IsOptional() cache?: string; - - @IsString() - @IsOptional() index?: string; - - @IsArray() - @IsOptional() assets?: string[]; + + constructor(source?: string, cache?: string, index?: string, assets?: string[]) + { + this.source = source; + this.cache = cache; + this.index = index; + this.assets = assets; + } } diff --git a/packages/server-nodejs/src/configuration/RuntimeConfiguration.ts b/packages/server-nodejs/src/configuration/RuntimeConfiguration.ts index 48dde4da..c6b86255 100644 --- a/packages/server-nodejs/src/configuration/RuntimeConfiguration.ts +++ b/packages/server-nodejs/src/configuration/RuntimeConfiguration.ts @@ -1,30 +1,40 @@ -import { IsOptional, IsUrl } from 'class-validator'; - -import GatewayConfiguration from './GatewayConfiguration.js'; -import NodeConfiguration from './NodeConfiguration.js'; -import ProxyConfiguration from './ProxyConfiguration.js'; -import RepositoryConfiguration from './RepositoryConfiguration.js'; -import StandaloneConfiguration from './StandaloneConfiguration.js'; +import { z } from 'zod'; + +import GatewayConfiguration, { gatewaySchema } from './GatewayConfiguration.js'; +import NodeConfiguration, { nodeSchema } from './NodeConfiguration.js'; +import ProxyConfiguration, { proxySchema } from './ProxyConfiguration.js'; +import RepositoryConfiguration, { repositorySchema } from './RepositoryConfiguration.js'; +import StandaloneConfiguration, { standaloneSchema } from './StandaloneConfiguration.js'; + +export const runtimeSchema = z + .object({ + url: z.string().optional(), + standalone: standaloneSchema.optional(), + repository: repositorySchema.optional(), + gateway: gatewaySchema.optional(), + node: nodeSchema.optional(), + proxy: proxySchema.optional(), + }) + .strict() + .transform((value) => new RuntimeConfiguration(value.url, value.standalone, value.repository, value.gateway, value.node, value.proxy)); export default class RuntimeConfiguration { - @IsUrl() - @IsOptional() url?: string; - - @IsOptional() standalone?: StandaloneConfiguration; - - @IsOptional() repository?: RepositoryConfiguration; - - @IsOptional() gateway?: GatewayConfiguration; - - @IsOptional() node?: NodeConfiguration; - - @IsOptional() proxy?: ProxyConfiguration; + + constructor(url?: string, standalone?: StandaloneConfiguration, repository?: RepositoryConfiguration, gateway?: GatewayConfiguration, node?: NodeConfiguration, proxy?: ProxyConfiguration) + { + this.url = url; + this.standalone = standalone; + this.repository = repository; + this.gateway = gateway; + this.node = node; + this.proxy = proxy; + } } diff --git a/packages/server-nodejs/src/configuration/ServerOptions.ts b/packages/server-nodejs/src/configuration/ServerOptions.ts index dae98d72..076f0ebe 100644 --- a/packages/server-nodejs/src/configuration/ServerOptions.ts +++ b/packages/server-nodejs/src/configuration/ServerOptions.ts @@ -1,16 +1,22 @@ -import { Contains, IsEnum, IsOptional, IsString } from 'class-validator'; +import { z } from 'zod'; import { LogLevel } from '../utils/LogBuilder.js'; +export const serverOptionsSchema = z + .object({ + loglevel: z.nativeEnum(LogLevel).optional(), + config: z.string().endsWith('.json').optional() + }) + .transform((value) => new ServerOptions(value.loglevel, value.config)); + export default class ServerOptions { - @IsString() - @IsOptional() - @IsEnum(LogLevel) loglevel = 'info'; - - @IsString() - @Contains('.json') - @IsOptional() config = 'config.json'; + + constructor(loglevel = 'info', config = 'config.json') + { + this.loglevel = loglevel; + this.config = config; + } } diff --git a/packages/server-nodejs/src/configuration/StandaloneConfiguration.ts b/packages/server-nodejs/src/configuration/StandaloneConfiguration.ts index 454337e6..2fa667e7 100644 --- a/packages/server-nodejs/src/configuration/StandaloneConfiguration.ts +++ b/packages/server-nodejs/src/configuration/StandaloneConfiguration.ts @@ -1,25 +1,31 @@ -import { IsArray, IsOptional, IsString } from 'class-validator'; +import { z } from 'zod'; + +export const standaloneSchema = z + .object({ + source: z.string().optional(), + cache: z.string().optional(), + index: z.string().optional(), + segments: z.array(z.string()).optional(), + assets: z.array(z.string()).optional() + }) + .strict() + .transform((value) => new StandaloneConfiguration(value.source, value.cache, value.index, value.segments, value.assets)); export default class StandaloneConfiguration { - @IsString() - @IsOptional() source?: string; - - @IsString() - @IsOptional() cache?: string; - - @IsString() - @IsOptional() index?: string; - - @IsArray() - @IsOptional() segments?: string[]; - - @IsArray() - @IsOptional() assets?: string[]; + + constructor(source?: string, cache?: string, index?: string, segments?: string[], assets?: string[]) + { + this.source = source; + this.cache = cache; + this.index = index; + this.segments = segments; + this.assets = assets; + } } diff --git a/packages/server-nodejs/src/controllers/NodesController.ts b/packages/server-nodejs/src/controllers/NodesController.ts index 6698bc47..cfa7a9ef 100644 --- a/packages/server-nodejs/src/controllers/NodesController.ts +++ b/packages/server-nodejs/src/controllers/NodesController.ts @@ -4,7 +4,7 @@ import { Logger } from 'tslog'; import { LocalGateway, RemoteNode } from '@jitar/runtime'; -import NodeDto from '../models/NodeDto.js'; +import NodeDto, { nodeDtoSchema } from '../models/NodeDto.js'; import DataConverter from '../utils/DataConverter.js'; export default class NodesController @@ -34,7 +34,7 @@ export default class NodesController { try { - const nodeDto = await DataConverter.convert(NodeDto, request.body); + const nodeDto = DataConverter.convert(nodeDtoSchema, request.body); const node = new RemoteNode(nodeDto.url, nodeDto.procedureNames); this.#gateway.addNode(node); diff --git a/packages/server-nodejs/src/errors/MissingConfigurationValue.ts b/packages/server-nodejs/src/errors/MissingConfigurationValue.ts deleted file mode 100644 index 6ef90c9c..00000000 --- a/packages/server-nodejs/src/errors/MissingConfigurationValue.ts +++ /dev/null @@ -1,8 +0,0 @@ - -export default class MissingConfigurationValue extends Error -{ - constructor(propertyName: string) - { - super(`Missing configuration value for '${propertyName}'`); - } -} diff --git a/packages/server-nodejs/src/models/HealthDto.ts b/packages/server-nodejs/src/models/HealthDto.ts deleted file mode 100644 index 91413e71..00000000 --- a/packages/server-nodejs/src/models/HealthDto.ts +++ /dev/null @@ -1,11 +0,0 @@ - -import { IsBoolean } from 'class-validator'; - -export default class HealthDto -{ - @IsBoolean() - healthy = false; - - @IsBoolean({ each: true }) - checks: Map = new Map(); -} diff --git a/packages/server-nodejs/src/models/NodeDto.ts b/packages/server-nodejs/src/models/NodeDto.ts index 59a6679a..2d3f7118 100644 --- a/packages/server-nodejs/src/models/NodeDto.ts +++ b/packages/server-nodejs/src/models/NodeDto.ts @@ -1,11 +1,22 @@ -import { ArrayNotEmpty, IsUrl } from 'class-validator'; +import { z } from 'zod'; + +export const nodeDtoSchema = z + .object({ + url: z.string().url(), + procedureNames: z.array(z.string()).optional() + }) + .strict() + .transform((value) => new NodeDto(value.url, value.procedureNames)); export default class NodeDto { - @IsUrl() - url = ''; + url: string; + procedureNames: string[]; - @ArrayNotEmpty() - procedureNames: string[] = []; + constructor(url: string, procedureNames: string[] = []) + { + this.url = url; + this.procedureNames = procedureNames; + } } diff --git a/packages/server-nodejs/src/utils/DataConverter.ts b/packages/server-nodejs/src/utils/DataConverter.ts index dc1aaaf5..7024f5c7 100644 --- a/packages/server-nodejs/src/utils/DataConverter.ts +++ b/packages/server-nodejs/src/utils/DataConverter.ts @@ -1,15 +1,10 @@ -import { ClassConstructor, plainToClass } from 'class-transformer'; -import { validateOrReject } from 'class-validator'; +import { z } from 'zod'; export default class DataConverter { - static async convert(targetClass: ClassConstructor, dataObject: object): Promise + static convert(schema: z.ZodSchema, dataObject: object): Type { - const createdObject: Type = plainToClass(targetClass, dataObject); - - await validateOrReject(createdObject); - - return createdObject; + return schema.parse(dataObject); } } diff --git a/packages/server-nodejs/src/utils/RuntimeConfigurationLoader.ts b/packages/server-nodejs/src/utils/RuntimeConfigurationLoader.ts index 54ffedc2..9d36fe41 100644 --- a/packages/server-nodejs/src/utils/RuntimeConfigurationLoader.ts +++ b/packages/server-nodejs/src/utils/RuntimeConfigurationLoader.ts @@ -1,19 +1,17 @@ import { readFileSync } from 'fs'; -import RuntimeConfiguration from '../configuration/RuntimeConfiguration.js'; +import RuntimeConfiguration, { runtimeSchema } from '../configuration/RuntimeConfiguration.js'; import DataConverter from './DataConverter.js'; export default class RuntimeConfigurationLoader { - static async load(filename: string): Promise + static load(filename: string): RuntimeConfiguration { const plainContents = readFileSync(filename, 'utf-8'); const parsedContents = JSON.parse(plainContents); - const configuration = await DataConverter.convert(RuntimeConfiguration, parsedContents); - - return configuration; + return DataConverter.convert(runtimeSchema, parsedContents); } } diff --git a/packages/server-nodejs/src/utils/RuntimeConfigurator.ts b/packages/server-nodejs/src/utils/RuntimeConfigurator.ts index f5fff6e6..bbcc246d 100644 --- a/packages/server-nodejs/src/utils/RuntimeConfigurator.ts +++ b/packages/server-nodejs/src/utils/RuntimeConfigurator.ts @@ -13,7 +13,6 @@ import ProxyConfiguration from '../configuration/ProxyConfiguration.js'; import RuntimeDefaults from '../definitions/RuntimeDefaults.js'; -import MissingConfigurationValue from '../errors/MissingConfigurationValue.js'; import UnknownRuntimeMode from '../errors/UnknownRuntimeMode.js'; export default class RuntimeConfigurator @@ -78,14 +77,8 @@ export default class RuntimeConfigurator static async #configureProxy(url: string, configuration: ProxyConfiguration): Promise { - const repository = this.#getRemoteRepository(configuration.repository); - - if (repository === undefined) - { - throw new MissingConfigurationValue('proxy.repository'); - } - - const gateway = this.#getRemoteGateway(configuration.gateway); + const repository = this.#getRemoteRepository(configuration.repository) as RemoteRepository; + const gateway = this.#getRemoteGateway(configuration.gateway) as RemoteGateway; const node = configuration.node !== undefined ? new RemoteNode(configuration.node, []) @@ -93,11 +86,6 @@ export default class RuntimeConfigurator const runner = gateway ?? node; - if (runner === undefined) - { - throw new MissingConfigurationValue('proxy.gateway or proxy.node'); - } - return this.#buildProxy(url, repository, runner); } diff --git a/packages/server-nodejs/src/utils/ServerOptionsReader.ts b/packages/server-nodejs/src/utils/ServerOptionsReader.ts index 8edce9a6..5509ed1a 100644 --- a/packages/server-nodejs/src/utils/ServerOptionsReader.ts +++ b/packages/server-nodejs/src/utils/ServerOptionsReader.ts @@ -1,16 +1,16 @@ import yargs from 'yargs'; -import ServerOptions from '../configuration/ServerOptions.js'; +import ServerOptions, { serverOptionsSchema } from '../configuration/ServerOptions.js'; import DataConverter from './DataConverter.js'; export default class ServerOptionsReader { - static async read(): Promise + static read(): ServerOptions { const args: object = yargs(process.argv).argv; - const options = await DataConverter.convert(ServerOptions, args); + const options = DataConverter.convert(serverOptionsSchema, args); return options; } diff --git a/packages/server-nodejs/tsconfig.json b/packages/server-nodejs/tsconfig.json index 10aafdd4..e803accf 100644 --- a/packages/server-nodejs/tsconfig.json +++ b/packages/server-nodejs/tsconfig.json @@ -1,9 +1,7 @@ { "compilerOptions": { - "target": "es2022", - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "module": "es2022", + "target": "ESNext", + "module": "ESNext", "rootDir": "./src/", "moduleResolution": "node", "declaration": true,