diff --git a/lib/cache/service.ts b/lib/cache/service.ts index 95e3b6a..63d40be 100644 --- a/lib/cache/service.ts +++ b/lib/cache/service.ts @@ -1,10 +1,10 @@ import { Injectable, OnModuleInit } from '@nestjs/common'; -import { IntentConfig } from '../config/service'; import { logTime } from '../utils/helpers'; import { InternalLogger } from '../utils/logger'; import { InMemoryDriver } from './drivers/inMemory'; import { RedisDriver } from './drivers/redis'; import { CacheDriver, CacheOptions } from './interfaces'; +import { ConfigService } from '../config'; @Injectable() export class CacheService implements OnModuleInit { @@ -12,8 +12,8 @@ export class CacheService implements OnModuleInit { public static data: CacheOptions; static stores: Record; - constructor(config: IntentConfig) { - CacheService.data = config.get('cache'); + constructor(config: ConfigService) { + CacheService.data = config.get('cache') as CacheOptions; } onModuleInit() { diff --git a/lib/config/builder.ts b/lib/config/builder.ts new file mode 100644 index 0000000..49e6367 --- /dev/null +++ b/lib/config/builder.ts @@ -0,0 +1,26 @@ +import { + NamespacedConfigMapKeys, + NamespacedConfigMapValues, + RegisterNamespaceReturnType, +} from './options'; + +export class ConfigBuilder { + static async build( + namespaceObjects: RegisterNamespaceReturnType[], + ): Promise> { + const configMap = new Map(); + + for (const namespacedConfig of namespaceObjects) { + const namespacedMap = new Map< + NamespacedConfigMapKeys, + NamespacedConfigMapValues + >(); + namespacedMap.set('factory', namespacedConfig._.factory); + namespacedMap.set('static', await namespacedConfig._.factory()); + namespacedMap.set('encrypt', namespacedConfig._.options.encrypt); + configMap.set(namespacedConfig._.namespace, namespacedMap); + } + + return configMap; + } +} diff --git a/lib/config/command.ts b/lib/config/command.ts index 087b6e3..aa21c19 100644 --- a/lib/config/command.ts +++ b/lib/config/command.ts @@ -1,36 +1,35 @@ -import { Injectable } from '@nestjs/common'; -import pc from 'picocolors'; import { Command, ConsoleIO } from '../console'; -import { Obj } from '../utils'; -import { Arr } from '../utils/array'; -import { columnify } from '../utils/columnify'; -import { IntentConfig } from './service'; +import { ConfigMap } from './options'; +import { CONFIG_FACTORY } from './constant'; +import pc from 'picocolors'; +import archy from 'archy'; +import { Inject } from '../foundation'; +import { jsonToArchy } from '../utils/console-helpers'; -@Injectable() +@Command('config:view {--ns : Namespace of a particular config}', { + desc: 'Command to view config for a given namespace', +}) export class ViewConfigCommand { - constructor(private config: IntentConfig) {} + constructor(@Inject(CONFIG_FACTORY) private config: ConfigMap) {} + + async handle(_cli: ConsoleIO): Promise { + const nsFlag = _cli.option('ns'); + const printNsToConsole = (namespace, obj) => { + const values = obj.get('static') as Record; + console.log( + archy(jsonToArchy(values, pc.bgGreen(pc.black(` ${namespace} `)))), + ); + }; - @Command('config:view {namespace}', { - desc: 'Command to view config for a given namespace', - }) - async handle(_cli: ConsoleIO): Promise { - const namespace = _cli.argument('namespace'); - const config = this.config.get(namespace); - if (!config) { - _cli.error(`config with ${namespace} namespace not found`); + if (nsFlag) { + const ns = this.config.get(nsFlag); + printNsToConsole(nsFlag, ns); return; } - const columnifiedConfig = columnify( - Arr.toObj(Obj.entries(config), ['key', 'value']), - ); - const printRows = []; - for (const row of columnifiedConfig) { - printRows.push([pc.green(row[0]), pc.yellow(row[1])].join(' ')); - } - - // eslint-disable-next-line no-console - console.log(printRows.join('\n')); - return true; + // Example usage + for (const [namespace, obj] of this.config.entries()) { + printNsToConsole(namespace, obj); + } } } diff --git a/lib/config/constant.ts b/lib/config/constant.ts new file mode 100644 index 0000000..f2825ad --- /dev/null +++ b/lib/config/constant.ts @@ -0,0 +1 @@ +export const CONFIG_FACTORY = '@intentjs/config_factory'; diff --git a/lib/config/index.ts b/lib/config/index.ts new file mode 100644 index 0000000..e20cfe1 --- /dev/null +++ b/lib/config/index.ts @@ -0,0 +1,6 @@ +export * from './builder'; +export * from './register-namespace'; +export * from './options'; +export * from './service'; +export * from './command'; +export * from './constant'; diff --git a/lib/config/options.ts b/lib/config/options.ts new file mode 100644 index 0000000..9cec9f0 --- /dev/null +++ b/lib/config/options.ts @@ -0,0 +1,36 @@ +export type RegisterNamespaceOptions = { + encrypt?: boolean; +}; + +export type BuildConfigFromNS< + T extends RegisterNamespaceReturnType[], +> = { + [N in T[number]['_']['namespace']]: Extract< + T[number], + { _: { namespace: N } } + >['$inferConfig']; +}; + +export type RegisterNamespaceReturnType< + N extends string, + T extends Record, +> = { + _: { + namespace: N; + options: RegisterNamespaceOptions; + factory: () => T | Promise; + }; + $inferConfig: T; +}; + +export type NamespacedConfigMapKeys = 'factory' | 'static' | 'encrypt'; + +export type NamespacedConfigMapValues = + // eslint-disable-next-line @typescript-eslint/ban-types + Function | object | boolean | string | number; + +export type NamespacedConfigMap = Map< + NamespacedConfigMapKeys, + NamespacedConfigMapValues +>; +export type ConfigMap = Map; diff --git a/lib/config/register-namespace.ts b/lib/config/register-namespace.ts new file mode 100644 index 0000000..8af11c7 --- /dev/null +++ b/lib/config/register-namespace.ts @@ -0,0 +1,25 @@ +import { LiteralString } from '../type-helpers'; +import { + RegisterNamespaceOptions, + RegisterNamespaceReturnType, +} from './options'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +require('dotenv').config(); + +export const registerNamespace = ( + namespace: LiteralString, + factory: () => T | Promise, + options?: RegisterNamespaceOptions, +): RegisterNamespaceReturnType, T> => { + return { + _: { + namespace, + options: { + encrypt: options?.encrypt ?? false, + }, + factory, + }, + $inferConfig: {} as T, + }; +}; diff --git a/lib/config/service.ts b/lib/config/service.ts index a20c4cd..a2ffb79 100644 --- a/lib/config/service.ts +++ b/lib/config/service.ts @@ -1,28 +1,51 @@ -import { Injectable } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; +import { Inject, Injectable } from '../foundation'; +import { DotNotation, GetNestedPropertyType } from '../type-helpers'; +import { Obj } from '../utils'; +import { CONFIG_FACTORY } from './constant'; +import { ConfigMap, NamespacedConfigMapValues } from './options'; + +type ConfigPaths = DotNotation; @Injectable() -export class IntentConfig { - private static cachedConfig: Map; - private static config: ConfigService; +export class ConfigService { + private static cachedConfig = new Map(); + private static config: ConfigMap; - constructor(private config: ConfigService) { - IntentConfig.cachedConfig = new Map(); - IntentConfig.config = config; + constructor(@Inject(CONFIG_FACTORY) private config: ConfigMap) { + ConfigService.cachedConfig = new Map, any>(); + ConfigService.config = this.config; } - get(key: string): T { - if (IntentConfig.cachedConfig.has(key)) - return IntentConfig.cachedConfig.get(key); - const value = this.config.get(key); - IntentConfig.cachedConfig.set(key, value); - return value; + get

, F = any>( + key: P, + ): GetNestedPropertyType | Promise> { + return ConfigService.get(key); } - static get(key: string): T { - if (this.cachedConfig.has(key)) return this.cachedConfig.get(key); - const value = this.config.get(key); - this.cachedConfig.set(key, value); - return value; + static get, F = any>( + key: P, + ): GetNestedPropertyType | Promise> { + const cachedValue = ConfigService.cachedConfig.get(key); + if (cachedValue) return cachedValue; + + const [namespace, ...paths] = (key as string).split('.'); + const nsConfig = this.config.get(namespace); + /** + * Returns a null value if the namespace doesn't exist. + */ + if (!nsConfig) return null; + + if (!paths.length) return nsConfig.get('static') as any; + + const staticValues = nsConfig.get('static') as Omit< + NamespacedConfigMapValues, + 'function' + >; + + const valueOnPath = Obj.get(staticValues, paths.join('.')); + if (valueOnPath) { + this.cachedConfig.set(key as string, valueOnPath); + } + return valueOnPath; } } diff --git a/lib/database/service.ts b/lib/database/service.ts index 8309c38..5f2d310 100644 --- a/lib/database/service.ts +++ b/lib/database/service.ts @@ -1,18 +1,18 @@ import { Injectable, OnModuleInit } from '@nestjs/common'; import Knex, { Knex as KnexType } from 'knex'; -import { IntentConfig } from '../config/service'; import { logTime } from '../utils/helpers'; import { InternalLogger } from '../utils/logger'; import { BaseModel } from './baseModel'; import { ConnectionNotFound } from './exceptions'; import { DatabaseOptions, DbConnectionOptions } from './options'; +import { ConfigService } from '../config'; @Injectable() export class ObjectionService implements OnModuleInit { static config: DatabaseOptions; static dbConnections: Record; - constructor(config: IntentConfig) { + constructor(config: ConfigService) { const dbConfig = config.get('db') as DatabaseOptions; if (!dbConfig) return; const defaultConnection = dbConfig.connections[dbConfig.default]; diff --git a/lib/exceptions/intentExceptionFilter.ts b/lib/exceptions/intentExceptionFilter.ts index 9cf70d8..4f8c328 100644 --- a/lib/exceptions/intentExceptionFilter.ts +++ b/lib/exceptions/intentExceptionFilter.ts @@ -1,6 +1,6 @@ import { ArgumentsHost, HttpException, Type } from '@nestjs/common'; import { BaseExceptionFilter } from '@nestjs/core'; -import { IntentConfig } from '../config/service'; +import { ConfigService } from '../config/service'; import { Log } from '../logger'; import { Request, Response } from '../rest/foundation'; import { Package } from '../utils'; @@ -29,7 +29,7 @@ export abstract class IntentExceptionFilter extends BaseExceptionFilter { } reportToSentry(exception: any): void { - const sentryConfig = IntentConfig.get('app.sentry'); + const sentryConfig = ConfigService.get('app.sentry'); if (!sentryConfig?.dsn) return; const exceptionConstructor = exception?.constructor; diff --git a/lib/index.ts b/lib/index.ts index 208af8f..f557c44 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -16,7 +16,6 @@ export * from './serializers/validationErrorSerializer'; export * from './mailer'; export * from './events'; export * from './validator'; -export * from './config/service'; export * from './foundation'; -export { registerAs } from '@nestjs/config'; export * from './reflections'; +export * from './config'; diff --git a/lib/localization/service.ts b/lib/localization/service.ts index 7cbc54b..5654da8 100644 --- a/lib/localization/service.ts +++ b/lib/localization/service.ts @@ -2,7 +2,7 @@ import { join } from 'path'; import { Injectable } from '@nestjs/common'; import { path } from 'app-root-path'; import { readdirSync, readFileSync } from 'fs-extra'; -import { IntentConfig } from '../config/service'; +import { ConfigService } from '../config/service'; import { Obj } from '../utils'; import { Num } from '../utils/number'; import { Str } from '../utils/string'; @@ -19,8 +19,8 @@ export class LocalizationService { UNKNOWN: 0, }; - constructor(private config: IntentConfig) { - const options = config.get('localization'); + constructor(private config: ConfigService) { + const options = config.get('localization') as LocalizationOptions; const { path: dir, fallbackLang } = options; const data: Record = {}; diff --git a/lib/logger/service.ts b/lib/logger/service.ts index 8c8f5a8..1275be7 100644 --- a/lib/logger/service.ts +++ b/lib/logger/service.ts @@ -2,7 +2,7 @@ import { join } from 'path'; import { Injectable } from '@nestjs/common'; import { path } from 'app-root-path'; import * as winston from 'winston'; -import { IntentConfig } from '../config/service'; +import { ConfigService } from '../config/service'; import { Obj } from '../utils'; import { Num } from '../utils/number'; import { @@ -21,8 +21,8 @@ export class LoggerService { private static config: IntentLoggerOptions; private static options: any = {}; - constructor(private readonly config: IntentConfig) { - const options = this.config.get('logger'); + constructor(private readonly config: ConfigService) { + const options = this.config.get('logger') as IntentLoggerOptions; LoggerService.config = options; for (const conn in options.loggers) { LoggerService.options[conn] = LoggerService.createLogger( diff --git a/lib/mailer/message.ts b/lib/mailer/message.ts index 388b445..07e2eea 100644 --- a/lib/mailer/message.ts +++ b/lib/mailer/message.ts @@ -1,7 +1,7 @@ import { render } from '@react-email/render'; // eslint-disable-next-line import/no-named-as-default import IntentMailComponent from '../../resources/mail/emails'; -import { IntentConfig } from '../config/service'; +import { ConfigService } from '../config/service'; import { GENERIC_MAIL, RAW_MAIL, VIEW_BASED_MAIL } from './constants'; import { MailData, @@ -203,7 +203,7 @@ export class MailMessage { if (this.compiledHtml) return this.compiledHtml; if (this.mailType === GENERIC_MAIL) { - const templateConfig = IntentConfig.get('mailers.template'); + const templateConfig = ConfigService.get('mailers.template'); const html = await render( IntentMailComponent({ header: { value: { title: templateConfig.appName } }, diff --git a/lib/mailer/service.ts b/lib/mailer/service.ts index 7ac3006..510bac5 100644 --- a/lib/mailer/service.ts +++ b/lib/mailer/service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { IntentConfig } from '../config/service'; +import { ConfigService } from '../config/service'; import { logTime } from '../utils'; import { InternalLogger } from '../utils/logger'; import { MailData, MailerOptions } from './interfaces'; @@ -11,7 +11,7 @@ export class MailerService { private static options: MailerOptions; private static channels: Record; - constructor(private config: IntentConfig) { + constructor(private config: ConfigService) { const options = this.config.get('mailers') as MailerOptions; MailerService.options = options; diff --git a/lib/queue/metadata.ts b/lib/queue/metadata.ts index 9962471..4eda1d6 100644 --- a/lib/queue/metadata.ts +++ b/lib/queue/metadata.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { IntentConfig } from '../config/service'; +import { ConfigService } from '../config/service'; import { GenericFunction } from '../interfaces'; import { QueueOptions } from './interfaces'; import { JobOptions } from './strategy'; @@ -18,8 +18,8 @@ export class QueueMetadata { >; private static store: Record = { jobs: {} }; - constructor(private config: IntentConfig) { - const data = this.config.get('queue'); + constructor(private config: ConfigService) { + const data = this.config.get('queue') as QueueOptions; QueueMetadata.data = data; QueueMetadata.defaultOptions = { connection: data.default, diff --git a/lib/queue/service.ts b/lib/queue/service.ts index 42b570f..2cb9b60 100644 --- a/lib/queue/service.ts +++ b/lib/queue/service.ts @@ -1,5 +1,5 @@ import { Injectable, Type } from '@nestjs/common'; -import { IntentConfig } from '../config/service'; +import { ConfigService } from '../config/service'; import { logTime } from '../utils/helpers'; import { InternalLogger } from '../utils/logger'; import { Str } from '../utils/string'; @@ -24,8 +24,8 @@ export class QueueService { private static connections: Record = {}; - constructor(private config: IntentConfig) { - const options = this.config.get('queue'); + constructor(private config: ConfigService) { + const options = this.config.get('queue') as QueueOptions; if (!options) return; for (const connName in options.connections) { const time = Date.now(); diff --git a/lib/rest/foundation/server.ts b/lib/rest/foundation/server.ts index 4be6f98..d52f3be 100644 --- a/lib/rest/foundation/server.ts +++ b/lib/rest/foundation/server.ts @@ -2,7 +2,7 @@ import { HttpAdapterHost, NestFactory } from '@nestjs/core'; import { NestExpressApplication } from '@nestjs/platform-express'; import { useContainer } from 'class-validator'; import 'console.mute'; -import { IntentConfig } from '../../config/service'; +import { ConfigService } from '../../config/service'; import { IntentExceptionFilter } from '../../exceptions'; import { IntentAppContainer, ModuleBuilder } from '../../foundation'; import { Type } from '../../interfaces'; @@ -60,13 +60,13 @@ export class IntentHttpServer { await this.container.boot(app); - const config = app.get(IntentConfig, { strict: false }); + const config = app.get(ConfigService, { strict: false }); this.configureErrorReporter(config.get('app.sentry')); // options?.globalPrefix && app.setGlobalPrefix(options.globalPrefix); - await app.listen(config.get('app.port') || 5001); + await app.listen((config.get('app.port') as number) || 5001); } configureErrorReporter(config: Record) { diff --git a/lib/rest/middlewares/cors.ts b/lib/rest/middlewares/cors.ts index 8fa935f..419bbd9 100644 --- a/lib/rest/middlewares/cors.ts +++ b/lib/rest/middlewares/cors.ts @@ -1,17 +1,17 @@ -import cors from 'cors'; +import cors, { CorsOptions } from 'cors'; import { NextFunction } from 'express'; -import { IntentConfig } from '../../config/service'; +import { ConfigService } from '../../config/service'; import { Injectable } from '../../foundation'; import { IntentMiddleware, Request, Response } from '../foundation'; @Injectable() export class CorsMiddleware extends IntentMiddleware { - constructor(private readonly config: IntentConfig) { + constructor(private readonly config: ConfigService) { super(); } boot(req: Request, res: Response, next: NextFunction): void | Promise { - cors(this.config.get('app.cors')); + cors(this.config.get('app.cors') as CorsOptions); next(); } } diff --git a/lib/rest/middlewares/helmet.ts b/lib/rest/middlewares/helmet.ts index ace60d1..80546bb 100644 --- a/lib/rest/middlewares/helmet.ts +++ b/lib/rest/middlewares/helmet.ts @@ -1,17 +1,17 @@ import { NextFunction } from 'express'; import helmet from 'helmet'; -import { IntentConfig } from '../../config/service'; import { Injectable } from '../../foundation'; import { IntentMiddleware, Request, Response } from '../foundation'; +import { ConfigService } from '../../config'; @Injectable() export class HelmetMiddleware extends IntentMiddleware { - constructor(private readonly config: IntentConfig) { + constructor(private readonly config: ConfigService) { super(); } boot(req: Request, res: Response, next: NextFunction): void | Promise { - helmet(this.config.get('app.cors')); + helmet(this.config.get('app.helmet') as any); next(); } } diff --git a/lib/rest/restServer.ts b/lib/rest/restServer.ts index 6916f4f..b02eb31 100644 --- a/lib/rest/restServer.ts +++ b/lib/rest/restServer.ts @@ -1,10 +1,11 @@ import { HttpAdapterHost, NestFactory } from '@nestjs/core'; import { NestExpressApplication } from '@nestjs/platform-express'; import { useContainer } from 'class-validator'; -import { IntentConfig } from '../config/service'; +import { ConfigService } from '../config/service'; import { Obj, Package } from '../utils'; import { ServerOptions } from './interfaces'; import { requestMiddleware } from './middlewares/functional/requestSerializer'; +import { CorsOptions } from '@nestjs/common/interfaces/external/cors-options.interface'; export class RestServer { /** @@ -20,10 +21,10 @@ export class RestServer { if (options?.addValidationContainer) { useContainer(app.select(module), { fallbackOnErrors: true }); } - const config = app.get(IntentConfig, { strict: false }); + const config = app.get(ConfigService, { strict: false }); if (config.get('app.cors') || options?.cors) { - const corsRule = options?.cors ?? config.get('app.cors'); + const corsRule = options?.cors ?? (config.get('app.cors') as CorsOptions); app.enableCors(corsRule); } @@ -47,7 +48,7 @@ export class RestServer { app.setGlobalPrefix(options.globalPrefix); } - await app.listen(options?.port || config.get('app.port')); + await app.listen(options?.port || (config.get('app.port') as number)); } static configureErrorReporter(config: Record) { diff --git a/lib/serviceProvider.ts b/lib/serviceProvider.ts index 7ea00ec..66ef059 100644 --- a/lib/serviceProvider.ts +++ b/lib/serviceProvider.ts @@ -1,10 +1,8 @@ -import { ConfigModule } from '@nestjs/config'; import { DiscoveryModule } from '@nestjs/core'; import { CacheService } from './cache'; import { CodegenCommand } from './codegen/command'; import { CodegenService } from './codegen/service'; import { ViewConfigCommand } from './config/command'; -import { IntentConfig } from './config/service'; import { ListCommands } from './console'; import { ObjectionService } from './database'; import { DbOperationsCommand } from './database/commands/migrations'; @@ -21,20 +19,14 @@ import { QueueMetadata } from './queue/metadata'; import { StorageService } from './storage/service'; import { BuildProjectCommand } from './dev-server/build'; import { DevServerCommand } from './dev-server/serve'; +import { CONFIG_FACTORY, ConfigBuilder, ConfigService } from './config'; export const IntentProvidersFactory = ( config: any[], ): Type => { return class extends ServiceProvider { register() { - this.import( - DiscoveryModule, - ConfigModule.forRoot({ - isGlobal: true, - expandVariables: true, - load: config, - }), - ); + this.import(DiscoveryModule); this.bind( IntentExplorer, ListCommands, @@ -48,14 +40,17 @@ export const IntentProvidersFactory = ( CodegenCommand, CodegenService, ViewConfigCommand, - IntentConfig, MailerService, LocalizationService, EventQueueWorker, LoggerService, BuildProjectCommand, DevServerCommand, + ConfigService, ); + this.bindWithFactory(CONFIG_FACTORY, async () => { + return ConfigBuilder.build(config); + }, []); } /** diff --git a/lib/storage/service.ts b/lib/storage/service.ts index 05a710b..5d86f24 100644 --- a/lib/storage/service.ts +++ b/lib/storage/service.ts @@ -1,5 +1,5 @@ import { Injectable, Type } from '@nestjs/common'; -import { IntentConfig } from '../config/service'; +import { ConfigService } from '../config/service'; import { logTime } from '../utils/helpers'; import { InternalLogger } from '../utils/logger'; import { Local, S3Storage } from './drivers'; @@ -21,8 +21,8 @@ export class StorageService { private static disks: { [key: string]: any }; private static options: StorageOptions; - constructor(private config: IntentConfig) { - StorageService.options = this.config.get('filesystem'); + constructor(private config: ConfigService) { + StorageService.options = this.config.get('filesystem') as StorageOptions; const disksConfig = StorageService.options.disks; StorageService.disks = {}; for (const diskName in StorageService.options.disks) { diff --git a/lib/type-helpers/index.ts b/lib/type-helpers/index.ts new file mode 100644 index 0000000..0189e5d --- /dev/null +++ b/lib/type-helpers/index.ts @@ -0,0 +1,43 @@ +export type LiteralString = string extends T ? never : T; + +export type StaticObjOrFunc = T | (() => T | Promise); + +export type Primitive = string | number | boolean | null | undefined; + +type Depth = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + +export type DotNotation< + T, + D extends number = 5, + Prefix extends string = '', +> = D extends 0 + ? never + : T extends Primitive + ? Prefix + : T extends Array + ? DotNotation< + U, + Depth[D], + `${Prefix}${Prefix extends '' ? '' : '.'}${number}` + > + : { + [K in keyof T]: T[K] extends Primitive + ? `${Prefix}${Prefix extends '' ? '' : '.'}${K & string}` + : DotNotation< + T[K], + Depth[D], + `${Prefix}${Prefix extends '' ? '' : '.'}${K & string}` + >; + }[keyof T]; + +export type GetNestedPropertyType< + T, + K extends string, + F = any, +> = K extends keyof T + ? T[K] + : K extends `${infer P}.${infer R}` + ? P extends keyof T + ? GetNestedPropertyType + : F + : F; diff --git a/lib/utils/console-helpers.ts b/lib/utils/console-helpers.ts new file mode 100644 index 0000000..c38c7d0 --- /dev/null +++ b/lib/utils/console-helpers.ts @@ -0,0 +1,46 @@ +import pc from 'picocolors'; + +export const colorizeJSON = (obj: Record) => { + const jsonString = JSON.stringify(obj, null, 2); + return jsonString.replace( + /("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, + function (match) { + let coloredMatch = match; + if (/^"/.test(match)) { + if (/:$/.test(match)) { + // Key + coloredMatch = pc.green(match); + } else { + // String + coloredMatch = pc.yellow(match); + } + } else if (/true|false/.test(match)) { + // Boolean + coloredMatch = pc.blue(match); + } else if (/null/.test(match)) { + // Null + coloredMatch = pc.magenta(match); + } else { + // Number + coloredMatch = pc.cyan(match); + } + return coloredMatch; + }, + ); +}; + +export const jsonToArchy = (obj: Record, key = '') => { + if (typeof obj === 'function') { + return `${pc.cyan(`${key}${key ? ': ' : ''}`)} ${pc.yellow('Function()')} ${pc.gray('(function)')}`; + } + if (typeof obj !== 'object' || obj === null) { + const type = obj === null ? 'null' : typeof obj; + const value = obj === null ? 'null' : JSON.stringify(obj); + return `${pc.cyan(`${key}${key ? ': ' : ''}`)}${pc.yellow(value)} ${pc.gray(`(${type})`)}`; + } + + const label = pc.cyan(key) || (Array.isArray(obj) ? 'Array' : 'Object'); + const nodes = Object.entries(obj).map(([k, v]) => jsonToArchy(v, k)); + + return { label, nodes }; +}; diff --git a/lib/utils/number.ts b/lib/utils/number.ts index 9b2fc7b..4df63c9 100644 --- a/lib/utils/number.ts +++ b/lib/utils/number.ts @@ -1,4 +1,4 @@ -import { IntentConfig } from '../config/service'; +import { ConfigService } from '../config/service'; interface NumOptions { precision?: number; @@ -12,7 +12,8 @@ export class Num { } static abbreviate(num: number, options?: NumOptions): string { - const locale = options?.locale ?? IntentConfig.get('app.locale'); + const locale = + options?.locale ?? (ConfigService.get('app.locale') as string); return Intl.NumberFormat(locale, { notation: 'compact', maximumFractionDigits: options?.precision ?? 1, @@ -24,8 +25,10 @@ export class Num { } static currency(num: number, options?: NumOptions): string { - const locale = options?.locale ?? IntentConfig.get('app.locale'); - const currency = options?.currency ?? IntentConfig.get('app.currency'); + const locale = + options?.locale ?? (ConfigService.get('app.locale') as string); + const currency = + options?.currency ?? (ConfigService.get('app.currency') as string); return Intl.NumberFormat(locale, { style: 'currency', currency: currency, @@ -44,7 +47,8 @@ export class Num { } static forHumans(num: number, options?: NumOptions): string { - const locale = options?.locale ?? IntentConfig.get('app.locale'); + const locale = + options?.locale ?? (ConfigService.get('app.locale') as string); return Intl.NumberFormat(locale, { notation: 'compact', compactDisplay: 'long', @@ -53,14 +57,16 @@ export class Num { } static format(num: number, options?: NumOptions): string { - const locale = options?.locale ?? IntentConfig.get('app.locale'); + const locale = + options?.locale ?? (ConfigService.get('app.locale') as string); return Intl.NumberFormat(locale, { maximumFractionDigits: options?.precision ?? 1, }).format(num); } static percentage(num: number, options?: NumOptions): string { - const locale = options?.locale ?? IntentConfig.get('app.locale'); + const locale = + options?.locale ?? (ConfigService.get('app.locale') as string); return Intl.NumberFormat(locale, { style: 'percent', minimumFractionDigits: options?.precision ?? 1, diff --git a/lib/validator/decorators/isValueFromConfig.ts b/lib/validator/decorators/isValueFromConfig.ts index dfdbbac..b8ad30b 100644 --- a/lib/validator/decorators/isValueFromConfig.ts +++ b/lib/validator/decorators/isValueFromConfig.ts @@ -6,7 +6,7 @@ import { ValidatorConstraint, ValidatorConstraintInterface, } from 'class-validator'; -import { IntentConfig } from '../../config/service'; +import { ConfigService } from '../../config/service'; import { Obj } from '../../utils'; import { Arr } from '../../utils/array'; import { isEmpty } from '../../utils/helpers'; @@ -16,7 +16,7 @@ import { isEmpty } from '../../utils/helpers'; export class IsValueFromConfigConstraint implements ValidatorConstraintInterface { - constructor(private config: IntentConfig) {} + constructor(private config: ConfigService) {} validate(value: string, args: ValidationArguments): boolean { const [options] = args.constraints; @@ -42,7 +42,7 @@ export class IsValueFromConfigConstraint } private getValues(key: string): any { - let validValues: Array = this.config.get(key); + let validValues: Array = this.config.get(key) as string[]; if (Obj.isObj(validValues)) { validValues = Object.values(validValues); } diff --git a/lib/validator/validator.ts b/lib/validator/validator.ts index ed6d400..c0c56db 100644 --- a/lib/validator/validator.ts +++ b/lib/validator/validator.ts @@ -1,7 +1,7 @@ import { Type } from '@nestjs/common'; import { plainToInstance } from 'class-transformer'; import { ValidationError, validate } from 'class-validator'; -import { IntentConfig } from '../config/service'; +import { ConfigService } from '../config/service'; import { ValidationFailed } from '../exceptions/validationfailed'; import { Obj } from '../utils'; @@ -50,7 +50,7 @@ export class Validator { * Throws new ValidationFailed Exception with validation errors */ async processErrorsFromValidation(errors: ValidationError[]): Promise { - const serializerClass = IntentConfig.get( + const serializerClass = ConfigService.get( 'app.error.validationErrorSerializer', ); if (!serializerClass) throw new ValidationFailed(errors); diff --git a/package-lock.json b/package-lock.json index 71dc9f9..ed95e25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,15 +9,18 @@ "version": "0.1.35", "license": "MIT", "dependencies": { - "@nestjs/config": "^3.2.0", + "@nestjs/common": "^10.4.4", + "@nestjs/core": "^10.4.4", "@nestjs/platform-express": "^10.4.1", "@react-email/components": "^0.0.25", "app-root-path": "^3.1.0", + "archy": "^1.0.0", "axios": "^1.7.7", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "cli-table3": "^0.6.3", "console.mute": "^0.3.0", + "dotenv": "^16.4.5", "enquirer": "^2.4.1", "eta": "^3.5.0", "express": "^4.21.0", @@ -40,6 +43,7 @@ "@jest/globals": "^29.7.0", "@nestjs/testing": "^10.3.9", "@stylistic/eslint-plugin-ts": "^2.6.1", + "@types/archy": "^0.0.36", "@types/express": "^4.17.21", "@types/fs-extra": "^11.0.1", "@types/inquirer": "^9.0.7", @@ -1468,7 +1472,6 @@ "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -1478,7 +1481,6 @@ "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.4.tgz", "integrity": "sha512-0j2/zqRw9nvHV1GKTktER8B/hIC/Z8CYFjN/ZqUuvwayCH+jZZBhCR2oRyuvLTXdnlSmtCAg2xvQ0ULqQvzqhA==", "license": "MIT", - "peer": true, "dependencies": { "iterare": "1.2.1", "tslib": "2.7.0", @@ -1503,28 +1505,12 @@ } } }, - "node_modules/@nestjs/config": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.2.3.tgz", - "integrity": "sha512-p6yv/CvoBewJ72mBq4NXgOAi2rSQNWx3a+IMJLVKS2uiwFCOQQuiIatGwq6MRjXV3Jr+B41iUO8FIf4xBrZ4/w==", - "license": "MIT", - "dependencies": { - "dotenv": "16.4.5", - "dotenv-expand": "10.0.0", - "lodash": "4.17.21" - }, - "peerDependencies": { - "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", - "rxjs": "^7.1.0" - } - }, "node_modules/@nestjs/core": { "version": "10.4.4", "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.4.tgz", "integrity": "sha512-y9tjmAzU6LTh1cC/lWrRsCcOd80khSR0qAHAqwY2svbW+AhsR/XCzgpZrAAKJrm/dDfjLCZKyxJSayeirGcW5Q==", "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "@nuxtjs/opencollective": "0.3.2", "fast-safe-stringify": "2.1.1", @@ -1678,7 +1664,6 @@ "resolved": "https://registry.npmjs.org/@nuxtjs/opencollective/-/opencollective-0.3.2.tgz", "integrity": "sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA==", "license": "MIT", - "peer": true, "dependencies": { "chalk": "^4.1.0", "consola": "^2.15.0", @@ -2115,6 +2100,13 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@types/archy": { + "version": "0.0.36", + "resolved": "https://registry.npmjs.org/@types/archy/-/archy-0.0.36.tgz", + "integrity": "sha512-toTRTGD8trLtJsOaYbReoU/fyvRF1a4C5WtqjDZ3NnT/zvO807opSBRBJBr5VhD66JnY3BUz2UvnY5/KMAt6mw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2842,6 +2834,12 @@ "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", "license": "MIT" }, + "node_modules/archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", + "license": "MIT" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -3766,8 +3764,7 @@ "version": "2.15.3", "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/console.mute": { "version": "0.3.0", @@ -4176,15 +4173,6 @@ "url": "https://dotenvx.com" } }, - "node_modules/dotenv-expand": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", - "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - } - }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -5268,8 +5256,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/fast-uri": { "version": "3.0.2", @@ -6574,7 +6561,6 @@ "resolved": "https://registry.npmjs.org/iterare/-/iterare-1.2.1.tgz", "integrity": "sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==", "license": "ISC", - "peer": true, "engines": { "node": ">=6" } @@ -7942,7 +7928,6 @@ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "license": "MIT", - "peer": true, "dependencies": { "whatwg-url": "^5.0.0" }, @@ -8405,8 +8390,7 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/peberminta": { "version": "0.9.0", @@ -10001,8 +9985,7 @@ "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/triple-beam": { "version": "1.4.1", @@ -10278,7 +10261,6 @@ "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", "license": "MIT", - "peer": true, "dependencies": { "@lukeed/csprng": "^1.0.0" }, @@ -10446,15 +10428,13 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "license": "BSD-2-Clause", - "peer": true + "license": "BSD-2-Clause" }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", "license": "MIT", - "peer": true, "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" diff --git a/package.json b/package.json index f1e9318..64eaf6d 100644 --- a/package.json +++ b/package.json @@ -33,16 +33,17 @@ "@jest/globals": "^29.7.0", "@nestjs/testing": "^10.3.9", "@stylistic/eslint-plugin-ts": "^2.6.1", + "@types/archy": "^0.0.36", "@types/express": "^4.17.21", "@types/fs-extra": "^11.0.1", "@types/inquirer": "^9.0.7", "@types/jest": "^29.5.13", "@types/lodash": "^4.14.202", "@types/ms": "^0.7.34", - "eslint": "^8.57.0", "@types/node": "^22.5.5", "@typescript-eslint/eslint-plugin": "^8.5.0", "@typescript-eslint/parser": "^8.5.0", + "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-import-resolver-typescript": "^3.6.1", "eslint-plugin-import": "^2.29.1", @@ -56,15 +57,18 @@ "typescript": "^5.5.2" }, "dependencies": { - "@nestjs/config": "^3.2.0", + "@nestjs/common": "^10.4.4", + "@nestjs/core": "^10.4.4", "@nestjs/platform-express": "^10.4.1", "@react-email/components": "^0.0.25", "app-root-path": "^3.1.0", + "archy": "^1.0.0", "axios": "^1.7.7", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "cli-table3": "^0.6.3", "console.mute": "^0.3.0", + "dotenv": "^16.4.5", "enquirer": "^2.4.1", "eta": "^3.5.0", "express": "^4.21.0", diff --git a/resources/stubs/config.eta b/resources/stubs/config.eta index aa7d3c1..440eb19 100644 --- a/resources/stubs/config.eta +++ b/resources/stubs/config.eta @@ -1,3 +1,3 @@ -import { registerAs } from '@nestjs/config'; +import { registerNamespace } from '@intentjs/core'; -export default registerAs('<%= it.namespace %>', () => ({})); \ No newline at end of file +export default registerNamespace('<%= it.namespace %>', () => ({})); \ No newline at end of file