diff --git a/packages/caching/src/CacheManager.ts b/packages/caching/src/CacheManager.ts index 179c4869..b8e97d42 100644 --- a/packages/caching/src/CacheManager.ts +++ b/packages/caching/src/CacheManager.ts @@ -1,5 +1,5 @@ -import type { Configuration } from '@jitar/configuration'; +import type { RuntimeConfiguration } from '@jitar/configuration'; import { Files, FileManagerBuilder, type FileManager } from '@jitar/sourcing'; import { ApplicationReader } from './source'; @@ -14,7 +14,7 @@ export default class CacheManager #applicationReader: ApplicationReader; #applicationBuilder: ApplicationBuilder; - constructor(configuration: Configuration) + constructor(configuration: RuntimeConfiguration) { const fileManagerBuilder = new FileManagerBuilder('./'); diff --git a/packages/caching/src/target/Builder.ts b/packages/caching/src/target/Builder.ts index 465d6fad..55abfe1a 100644 --- a/packages/caching/src/target/Builder.ts +++ b/packages/caching/src/target/Builder.ts @@ -5,23 +5,27 @@ import type { Application } from '../source'; import ModuleBuilder from './ModuleBuilder'; import SegmentBuilder from './SegmentBuilder'; +import JitarBuilder from './JitarBuilder'; export default class Builder { #moduleBuilder: ModuleBuilder; #segmentBuilder: SegmentBuilder; + #jitBuilder: JitarBuilder; constructor(fileManager: FileManager) { this.#moduleBuilder = new ModuleBuilder(fileManager); this.#segmentBuilder = new SegmentBuilder(fileManager); + this.#jitBuilder = new JitarBuilder(fileManager); } async build(application: Application): Promise { await Promise.all([ this.#moduleBuilder.build(application), - this.#segmentBuilder.build(application) + this.#segmentBuilder.build(application), + this.#jitBuilder.build() ]); } } diff --git a/packages/caching/src/target/JitarBuilder.ts b/packages/caching/src/target/JitarBuilder.ts new file mode 100644 index 00000000..0a35fda1 --- /dev/null +++ b/packages/caching/src/target/JitarBuilder.ts @@ -0,0 +1,20 @@ + +import type { FileManager } from '@jitar/sourcing'; + +const filename = 'jitar.js'; +const code = `export default async (specifier) => import(specifier);`; + +export default class JitarBuilder +{ + #fileManager: FileManager; + + constructor(fileManager: FileManager) + { + this.#fileManager = fileManager; + } + + build(): Promise + { + return this.#fileManager.write(filename, code); + } +} diff --git a/packages/caching/src/target/SegmentBuilder.ts b/packages/caching/src/target/SegmentBuilder.ts index 03fa45a1..9bc84eb2 100644 --- a/packages/caching/src/target/SegmentBuilder.ts +++ b/packages/caching/src/target/SegmentBuilder.ts @@ -1,5 +1,5 @@ -import { VersionParser } from '@jitar/runtime'; +import { VersionParser } from '@jitar/execution'; import type { FileManager } from '@jitar/sourcing'; import { ReflectionDestructuredArray, ReflectionDestructuredObject, ReflectionField, ReflectionFunction, ReflectionParameter } from '@jitar/reflection'; diff --git a/packages/cli/package.json b/packages/cli/package.json index 758ffa01..55326fd2 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -27,7 +27,8 @@ }, "dependencies": { "@jitar/caching": "*", - "@jitar/configuration": "*" + "@jitar/configuration": "*", + "@jitar/sourcing": "*" }, "repository": { "type": "git", diff --git a/packages/cli/src/commands/implementations/BuildCache.ts b/packages/cli/src/commands/implementations/BuildCache.ts index ea4a08d0..a7adbdc4 100644 --- a/packages/cli/src/commands/implementations/BuildCache.ts +++ b/packages/cli/src/commands/implementations/BuildCache.ts @@ -10,7 +10,7 @@ export default class BuildCache implements Command { const configurationManager = new ConfigurationManager(); - const configuration = await configurationManager.configure(); + const configuration = await configurationManager.configureRuntime(); const cacheManager = new CacheManager(configuration); diff --git a/packages/configuration/src/ConfigurationManager.ts b/packages/configuration/src/ConfigurationManager.ts index 84f6d9b0..9029d2d2 100644 --- a/packages/configuration/src/ConfigurationManager.ts +++ b/packages/configuration/src/ConfigurationManager.ts @@ -1,35 +1,31 @@ -import { type FileManager, FileManagerBuilder } from '@jitar/sourcing'; +import { RuntimeConfiguration, RuntimeConfigurationBuilder } from './runtime'; +import { ServerConfiguration, ServerConfigurationBuilder } from './server'; +import { ConfigurationReader, ConfigurationValidator } from './utils'; -import Configuration from './models/Configuration'; - -const DEFAULT_CONFIGURATION_FILENAME = 'jitar.json'; +const DEFAULT_ROOT_PATH = './'; export default class ConfigurationManager { - #fileManager: FileManager; + #runtimeConfigurationBuilder: RuntimeConfigurationBuilder; + #serverConfigurationBuilder : ServerConfigurationBuilder; - constructor() + constructor(rootPath = DEFAULT_ROOT_PATH) { - this.#fileManager = new FileManagerBuilder('./').buildLocal(); - } + const reader = new ConfigurationReader(rootPath); + const validator = new ConfigurationValidator(); - async configure(filename = DEFAULT_CONFIGURATION_FILENAME): Promise - { - return await this.#fileManager.exists(filename) - ? this.#configureFromFile(filename) - : this.#configureDefault(); + this.#runtimeConfigurationBuilder = new RuntimeConfigurationBuilder(reader, validator); + this.#serverConfigurationBuilder = new ServerConfigurationBuilder(reader, validator); } - async #configureFromFile(filename: string): Promise + configureRuntime(filename?: string): Promise { - const file = this.#fileManager.read(filename); - - return new Configuration("source", "target"); + return this.#runtimeConfigurationBuilder.build(filename); } - #configureDefault(): Configuration + configureServer(filename: string): Promise { - return new Configuration('./dist', './dist'); + return this.#serverConfigurationBuilder.build(filename); } } diff --git a/packages/configuration/src/index.ts b/packages/configuration/src/index.ts index c7e239aa..401ed95f 100644 --- a/packages/configuration/src/index.ts +++ b/packages/configuration/src/index.ts @@ -1,3 +1,5 @@ -export { default as Configuration } from './models/Configuration'; +export { RuntimeConfiguration } from './runtime'; +export { ServerConfiguration, StandaloneConfiguration, ProxyConfiguration, WorkerConfiguration, GatewayConfiguration, RepositoryConfiguration } from './server'; + export { default as ConfigurationManager } from './ConfigurationManager'; diff --git a/packages/configuration/src/models/Configuration.ts b/packages/configuration/src/models/Configuration.ts deleted file mode 100644 index 09a7b31c..00000000 --- a/packages/configuration/src/models/Configuration.ts +++ /dev/null @@ -1,17 +0,0 @@ - -export default class Configuration -{ - #source: string; - - #target: string; - - constructor(source: string, target: string) - { - this.#source = source; - this.#target = target; - } - - get source() { return this.#source; } - - get target() { return this.#target; } -} diff --git a/packages/configuration/src/runtime/ConfigurationBuilder.ts b/packages/configuration/src/runtime/ConfigurationBuilder.ts new file mode 100644 index 00000000..f5391600 --- /dev/null +++ b/packages/configuration/src/runtime/ConfigurationBuilder.ts @@ -0,0 +1,33 @@ + +import type { ConfigurationReader, ConfigurationValidator } from '../utils'; + +import RuntimeConfiguration, { DefaultValues, validationScheme } from './definitions/RuntimeConfiguration'; +import RuntimeConfigurationInvalid from './errors/RuntimeConfigurationInvalid'; + +export default class ConfigurationBuilder +{ + #reader: ConfigurationReader; + #validator: ConfigurationValidator; + + constructor(reader: ConfigurationReader, validator: ConfigurationValidator) + { + this.#reader = reader; + this.#validator = validator; + } + + async build(filename: string = DefaultValues.FILENAME): Promise + { + const configuration = await this.#reader.read(filename) as RuntimeConfiguration; + const validation = this.#validator.validate(configuration, validationScheme); + + if (validation.valid === false) + { + throw new RuntimeConfigurationInvalid(validation); + } + + configuration.source ??= DefaultValues.SOURCE; + configuration.target ??= DefaultValues.TARGET; + + return configuration; + } +} diff --git a/packages/configuration/src/runtime/definitions/RuntimeConfiguration.ts b/packages/configuration/src/runtime/definitions/RuntimeConfiguration.ts new file mode 100644 index 00000000..4a54aaca --- /dev/null +++ b/packages/configuration/src/runtime/definitions/RuntimeConfiguration.ts @@ -0,0 +1,25 @@ + +import { ValidationScheme } from '../../utils'; + +type RuntimeConfiguration = +{ + source: string; + target: string; +}; + +export default RuntimeConfiguration; + +const DefaultValues = +{ + FILENAME: './jitar.json', + SOURCE: './src', + TARGET: './dist' +} as const; + +const validationScheme: ValidationScheme = +{ + source: { type: 'string', required: false }, + target: { type: 'string', required: false } +} as const; + +export { DefaultValues, validationScheme }; diff --git a/packages/configuration/src/runtime/errors/RuntimeConfigurationInvalid.ts b/packages/configuration/src/runtime/errors/RuntimeConfigurationInvalid.ts new file mode 100644 index 00000000..adb66c6d --- /dev/null +++ b/packages/configuration/src/runtime/errors/RuntimeConfigurationInvalid.ts @@ -0,0 +1,10 @@ + +import { ValidationResult } from '../../utils'; + +export default class RuntimeConfigurationInvalid extends Error +{ + public constructor(validation: ValidationResult) + { + super(validation.errors.join('\n')); + } +} diff --git a/packages/configuration/src/runtime/index.ts b/packages/configuration/src/runtime/index.ts new file mode 100644 index 00000000..19271b5c --- /dev/null +++ b/packages/configuration/src/runtime/index.ts @@ -0,0 +1,3 @@ + +export { default as RuntimeConfiguration } from './definitions/RuntimeConfiguration'; +export { default as RuntimeConfigurationBuilder } from './ConfigurationBuilder'; diff --git a/packages/configuration/src/server/ConfigurationBuilder.ts b/packages/configuration/src/server/ConfigurationBuilder.ts new file mode 100644 index 00000000..95516679 --- /dev/null +++ b/packages/configuration/src/server/ConfigurationBuilder.ts @@ -0,0 +1,30 @@ + +import type { ConfigurationReader, ConfigurationValidator } from '../utils'; + +import ServerConfiguration, { validationScheme } from './definitions/ServerConfiguration'; +import ServerConfigurationInvalid from './errors/ServerConfigurationInvalid'; + +export default class ConfigurationBuilder +{ + #reader: ConfigurationReader; + #validator: ConfigurationValidator; + + constructor(reader: ConfigurationReader, validator: ConfigurationValidator) + { + this.#reader = reader; + this.#validator = validator; + } + + async build(filename: string): Promise + { + const configuration = await this.#reader.read(filename) as ServerConfiguration; + const validation = this.#validator.validate(configuration, validationScheme); + + if (validation.valid === false) + { + throw new ServerConfigurationInvalid(validation); + } + + return configuration; + } +} diff --git a/packages/configuration/src/server/definitions/GatewayConfiguration.ts b/packages/configuration/src/server/definitions/GatewayConfiguration.ts new file mode 100644 index 00000000..6e7ee8b5 --- /dev/null +++ b/packages/configuration/src/server/definitions/GatewayConfiguration.ts @@ -0,0 +1,22 @@ + +import { ValidationScheme } from '../../utils'; + +type GatewayConfiguration = +{ + monitor: number; + middleware: string[]; + healthChecks: string[]; + trustKey?: string; +}; + +export default GatewayConfiguration; + +const validationScheme: ValidationScheme = +{ + monitor: { type: 'integer', required: false }, + middleware: { type: 'list', required: false, items: { type: 'string' } }, + healthChecks: { type: 'list', required: false, items: { type: 'string' } }, + trustKey: { type: 'string', required: false } +} as const; + +export { validationScheme }; diff --git a/packages/configuration/src/server/definitions/ProxyConfiguration.ts b/packages/configuration/src/server/definitions/ProxyConfiguration.ts new file mode 100644 index 00000000..f27f55a4 --- /dev/null +++ b/packages/configuration/src/server/definitions/ProxyConfiguration.ts @@ -0,0 +1,20 @@ + +import { ValidationScheme } from '../../utils'; + +type ProxyConfiguration = +{ + gateway: string; + repository: string; + middleware: string[]; +}; + +export default ProxyConfiguration; + +const validationScheme: ValidationScheme = +{ + gateway: { type: 'url', required: true }, + repository: { type: 'url', required: true }, + middleware: { type: 'list', required: false, items: { type: 'string' } } +} as const; + +export { validationScheme }; diff --git a/packages/configuration/src/server/definitions/RepositoryConfiguration.ts b/packages/configuration/src/server/definitions/RepositoryConfiguration.ts new file mode 100644 index 00000000..24507497 --- /dev/null +++ b/packages/configuration/src/server/definitions/RepositoryConfiguration.ts @@ -0,0 +1,20 @@ + +import { ValidationScheme } from '../../utils'; + +type RepositoryConfiguration = +{ + index: string; + serveIndexOnNotFound: boolean; + assets: string[]; +}; + +export default RepositoryConfiguration; + +const validationScheme: ValidationScheme = +{ + index: { type: 'string', required: false }, + serveIndexOnNotFound: { type: 'boolean', required: false }, + assets: { type: 'list', required: false, items: { type: 'string' } } +} as const; + +export { validationScheme }; diff --git a/packages/configuration/src/server/definitions/ServerConfiguration.ts b/packages/configuration/src/server/definitions/ServerConfiguration.ts new file mode 100644 index 00000000..d9c9d866 --- /dev/null +++ b/packages/configuration/src/server/definitions/ServerConfiguration.ts @@ -0,0 +1,38 @@ + +import { ValidationScheme } from '../../utils'; + +import GatewayConfiguration, { validationScheme as gatewayValidationScheme } from './GatewayConfiguration'; +import ProxyConfiguration, { validationScheme as proxyValidationScheme } from './ProxyConfiguration'; +import RepositoryConfiguration, { validationScheme as repositoryValidationScheme } from './RepositoryConfiguration'; +import StandaloneConfiguration, { validationScheme as standaloneValidationScheme } from './StandaloneConfiguration'; +import WorkerConfiguration, { validationScheme as workerValidationScheme } from './WorkerConfiguration'; + +type ServerConfiguration = +{ + url: string; + setup?: string[]; + teardown?: string[]; + + gateway?: GatewayConfiguration; + proxy?: ProxyConfiguration; + repository?: RepositoryConfiguration; + standalone?: StandaloneConfiguration; + worker?: WorkerConfiguration; +}; + +export default ServerConfiguration; + +const validationScheme: ValidationScheme = +{ + url: { type: 'url', required: true }, + setup: { type: 'list', required: false, items: { type: 'string' } }, + teardown: { type: 'list', required: false, items: { type: 'string' } }, + + gateway: { type: 'group', required: false, fields: gatewayValidationScheme }, + proxy: { type: 'group', required: false, fields: proxyValidationScheme }, + repository: { type: 'group', required: false, fields: repositoryValidationScheme }, + standalone: { type: 'group', required: false, fields: standaloneValidationScheme }, + worker: { type: 'group', required: false, fields: workerValidationScheme } +} as const; + +export { validationScheme }; diff --git a/packages/configuration/src/server/definitions/StandaloneConfiguration.ts b/packages/configuration/src/server/definitions/StandaloneConfiguration.ts new file mode 100644 index 00000000..92c072d4 --- /dev/null +++ b/packages/configuration/src/server/definitions/StandaloneConfiguration.ts @@ -0,0 +1,28 @@ + +import { ValidationScheme } from '../../utils'; + +type StandaloneConfiguration = +{ + segments: string[]; + middleware: string[]; + healthChecks: string[]; + + index: string; + serveIndexOnNotFound: boolean; + assets: string[]; +}; + +export default StandaloneConfiguration; + +const validationScheme: ValidationScheme = +{ + segments: { type: 'list', required: true, items: { type: 'string' } }, + middleware: { type: 'list', required: false, items: { type: 'string' } }, + healthChecks: { type: 'list', required: false, items: { type: 'string' } }, + + index: { type: 'string', required: false }, + serveIndexOnNotFound: { type: 'boolean', required: false }, + assets: { type: 'list', required: false, items: { type: 'string' } } +} as const; + +export { validationScheme }; diff --git a/packages/configuration/src/server/definitions/WorkerConfiguration.ts b/packages/configuration/src/server/definitions/WorkerConfiguration.ts new file mode 100644 index 00000000..829e1870 --- /dev/null +++ b/packages/configuration/src/server/definitions/WorkerConfiguration.ts @@ -0,0 +1,24 @@ + +import { ValidationScheme } from '../../utils'; + +type WorkerConfiguration = +{ + gateway?: string; + segments: string[]; + middleware: string[]; + healthChecks: string[]; + trustKey?: string; +}; + +export default WorkerConfiguration; + +const validationScheme: ValidationScheme = +{ + gateway: { type: 'url', required: false }, + segments: { type: 'list', required: true, items: { type: 'string' } }, + middleware: { type: 'list', required: false, items: { type: 'string' } }, + healthChecks: { type: 'list', required: false, items: { type: 'string' } }, + trustKey: { type: 'string', required: false } +} as const; + +export { validationScheme }; diff --git a/packages/configuration/src/server/errors/ServerConfigurationInvalid.ts b/packages/configuration/src/server/errors/ServerConfigurationInvalid.ts new file mode 100644 index 00000000..d795f3a8 --- /dev/null +++ b/packages/configuration/src/server/errors/ServerConfigurationInvalid.ts @@ -0,0 +1,10 @@ + +import { ValidationResult } from '../../utils'; + +export default class ServerConfigurationInvalid extends Error +{ + public constructor(validation: ValidationResult) + { + super(validation.errors.join('\n')); + } +} diff --git a/packages/configuration/src/server/index.ts b/packages/configuration/src/server/index.ts new file mode 100644 index 00000000..e93287f3 --- /dev/null +++ b/packages/configuration/src/server/index.ts @@ -0,0 +1,9 @@ + +export { default as ServerConfiguration } from './definitions/ServerConfiguration'; +export { default as StandaloneConfiguration } from './definitions/StandaloneConfiguration'; +export { default as ProxyConfiguration } from './definitions/ProxyConfiguration'; +export { default as WorkerConfiguration } from './definitions/WorkerConfiguration'; +export { default as GatewayConfiguration } from './definitions/GatewayConfiguration'; +export { default as RepositoryConfiguration } from './definitions/RepositoryConfiguration'; + +export { default as ServerConfigurationBuilder } from './ConfigurationBuilder'; diff --git a/packages/configuration/src/utils/ConfigurationReader.ts b/packages/configuration/src/utils/ConfigurationReader.ts new file mode 100644 index 00000000..77311095 --- /dev/null +++ b/packages/configuration/src/utils/ConfigurationReader.ts @@ -0,0 +1,35 @@ + +import { type FileManager, FileManagerBuilder } from '@jitar/sourcing'; + +export default class ConfigurationReader +{ + #fileManager: FileManager; + + constructor(rootPath: string) + { + this.#fileManager = new FileManagerBuilder(rootPath).buildLocal(); + } + + async read(filename: string): Promise> + { + const fileExists = await this.#fileManager.exists(filename); + + if (fileExists === false) + { + return {}; + } + + const file = await this.#fileManager.read(filename); + + const isJsonFile = file.type.includes('json'); + + if (isJsonFile === false) + { + return {}; + } + + const content = file.content.toString(); + + return JSON.parse(content); + } +} diff --git a/packages/configuration/src/utils/ConfigurationValidator.ts b/packages/configuration/src/utils/ConfigurationValidator.ts new file mode 100644 index 00000000..e0ffcaa8 --- /dev/null +++ b/packages/configuration/src/utils/ConfigurationValidator.ts @@ -0,0 +1,165 @@ + +type FieldValidation = PrimitiveValidation | GroupValidation | ListValidation; + +type PrimitiveValidation = +{ + type: 'string' | 'integer' | 'real' | 'boolean' | 'url'; + required?: boolean; +}; + +type GroupValidation = +{ + type: 'group'; + required?: boolean; + fields: Record; +} + +type ListValidation = +{ + type: 'list'; + required?: boolean; + items: PrimitiveValidation; +}; + +type ValidationScheme = Record; + +type ValidationResult = +{ + valid: boolean; + errors: string[]; +}; + +export { ValidationScheme, ValidationResult }; + +export default class ConfigurationValidator +{ + validate(data: Record, scheme: ValidationScheme): ValidationResult + { + const errors: string[] = []; + + this.#validateData('', data, scheme, errors); + + const valid = errors.length === 0; + + return { valid, errors }; + } + + #validateData(key: string, data: Record, scheme: ValidationScheme, errors: string[]): void + { + const fieldKeys = Object.keys(scheme); + + for (const fieldKey of fieldKeys) + { + const fieldKeyFull = `${key}${fieldKey}`; + const fieldScheme = scheme[fieldKey]; + const value = data[fieldKey]; + + this.#validateField(fieldKeyFull, value, fieldScheme, errors); + } + } + + #validateField(key: string, value: unknown, scheme: FieldValidation, errors: string[]): void + { + if (value === undefined) + { + if (scheme.required === true) + { + errors.push(`Field '${key}' is required`); + } + + return; + } + + switch (scheme.type) + { + case 'string': + return this.#validateString(key, value, scheme, errors); + case 'integer': + return this.#validateInteger(key, value, scheme, errors); + case 'real': + return this.#validateReal(key, value, scheme, errors); + case 'boolean': + return this.#validateBoolean(key, value, scheme, errors); + case 'url': + return this.#validateUrl(key, value, scheme, errors); + case 'group': + return this.#validateGroup(key, value, scheme, errors); + case 'list': + return this.#validateList(key, value, scheme, errors); + } + } + + #validateString(key: string, value: unknown, scheme: PrimitiveValidation, errors: string[]): void + { + if (typeof value !== 'string') + { + errors.push(`Field '${key}' is not a string`); + } + } + + #validateInteger(key: string, value: unknown, scheme: PrimitiveValidation, errors: string[]): void + { + if (typeof value !== 'number' || Number.isInteger(value) === false) + { + errors.push(`Field '${key}' is not an integer`); + } + } + + #validateReal(key: string, value: unknown, scheme: PrimitiveValidation, errors: string[]): void + { + if (typeof value !== 'number') + { + errors.push(`Field '${key}' is not a real number`); + } + } + + #validateBoolean(key: string, value: unknown, scheme: PrimitiveValidation, errors: string[]): void + { + if (typeof value !== 'boolean') + { + errors.push(`Field '${key}' is not a boolean`); + } + } + + #validateUrl(key: string, value: unknown, scheme: PrimitiveValidation, errors: string[]): void + { + if (typeof value !== 'string' || value.startsWith('http') === false) + { + errors.push(`Field '${key}' is not a valid URL`); + } + } + + #validateGroup(key: string, value: unknown, scheme: GroupValidation, errors: string[]): void + { + if (typeof value !== 'object') + { + errors.push(`Field '${key}' is not an object`); + + return; + } + + const dataKey = `${key}.`; + + this.#validateData(dataKey, value as Record, scheme.fields, errors); + } + + #validateList(key: string, value: unknown, scheme: ListValidation, errors: string[]): void + { + if (!Array.isArray(value)) + { + errors.push(`Field '${key}' is not a list`); + + return; + } + + const data = value as unknown[]; + + for (const itemIndex in data) + { + const itemKey = `${key}.${itemIndex}.`; + const itemValue = data[itemIndex]; + + this.#validateField(itemKey, itemValue, scheme.items, errors) + } + } +} diff --git a/packages/configuration/src/utils/index.ts b/packages/configuration/src/utils/index.ts new file mode 100644 index 00000000..f60ddd65 --- /dev/null +++ b/packages/configuration/src/utils/index.ts @@ -0,0 +1,3 @@ + +export { default as ConfigurationReader } from './ConfigurationReader'; +export { default as ConfigurationValidator, ValidationScheme, ValidationResult } from './ConfigurationValidator';