From 6164d277184f36ce41bca9c200d6c9d80f634bc6 Mon Sep 17 00:00:00 2001 From: Vinayak Sarawagi Date: Sun, 6 Oct 2024 18:20:55 +0530 Subject: [PATCH 1/3] eject @nestjs/config add self config service --- lib/cache/service.ts | 6 +- lib/config/builder.ts | 38 +++++++++ lib/config/command.ts | 12 ++- lib/config/constant.ts | 1 + lib/config/index.ts | 6 ++ lib/config/options.ts | 37 +++++++++ lib/config/register-namespace.ts | 23 ++++++ lib/config/service.ts | 82 ++++++++++++++----- lib/database/service.ts | 4 +- lib/exceptions/intentExceptionFilter.ts | 4 +- lib/index.ts | 3 +- lib/localization/service.ts | 6 +- lib/logger/service.ts | 6 +- lib/mailer/message.ts | 4 +- lib/mailer/service.ts | 4 +- lib/queue/metadata.ts | 6 +- lib/queue/service.ts | 6 +- lib/rest/foundation/server.ts | 6 +- lib/rest/middlewares/cors.ts | 8 +- lib/rest/middlewares/helmet.ts | 6 +- lib/rest/restServer.ts | 9 +- lib/serviceProvider.ts | 17 ++-- lib/storage/service.ts | 6 +- lib/type-helpers/index.ts | 31 +++++++ lib/utils/number.ts | 20 +++-- lib/validator/decorators/isValueFromConfig.ts | 6 +- lib/validator/validator.ts | 4 +- package-lock.json | 37 --------- package.json | 3 +- resources/stubs/config.eta | 4 +- 30 files changed, 273 insertions(+), 132 deletions(-) create mode 100644 lib/config/builder.ts create mode 100644 lib/config/constant.ts create mode 100644 lib/config/index.ts create mode 100644 lib/config/options.ts create mode 100644 lib/config/register-namespace.ts create mode 100644 lib/type-helpers/index.ts 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..6788bcb --- /dev/null +++ b/lib/config/builder.ts @@ -0,0 +1,38 @@ +import { Injectable } from '@nestjs/common'; +import { + NamespacedConfigMapKeys, + NamespacedConfigMapValues, + RegisterNamespaceReturnType, +} from './options'; + +@Injectable() +export class ConfigBuilder { + constructor() {} + + 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); + if (!namespacedConfig._.options.dynamic) { + namespacedMap.set('static', namespacedConfig._.factory()); + } + + /** + * Set options + */ + namespacedMap.set('dynamic', namespacedConfig._.options.dynamic); + 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 3cc7ebc..4c4bf94 100644 --- a/lib/config/command.ts +++ b/lib/config/command.ts @@ -1,14 +1,15 @@ -import { Injectable } from '@nestjs/common'; -import pc from 'picocolors'; +import { Inject, Injectable } from '@nestjs/common'; import { Command, ConsoleIO } from '../console'; import { Obj } from '../utils'; import { Arr } from '../utils/array'; import { columnify } from '../utils/columnify'; -import { IntentConfig } from './service'; +import * as pc from 'picocolors'; +import { ConfigMap } from './options'; +import { CONFIG_FACTORY } from './constant'; @Injectable() export class ViewConfigCommand { - constructor(private config: IntentConfig) {} + constructor(@Inject(CONFIG_FACTORY) private config: ConfigMap) {} @Command('config:view {namespace}', { desc: 'Command to view config for a given namespace', @@ -28,9 +29,6 @@ export class ViewConfigCommand { printRows.push([pc.green(row[0]), pc.yellow(row[1])].join(' ')); } - // eslint-disable-next-line no-console console.log(printRows.join('\n')); - - process.exit(); } } 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..adde277 --- /dev/null +++ b/lib/config/options.ts @@ -0,0 +1,37 @@ +export type RegisterNamespaceOptions = { + dynamic?: boolean; + encrypt?: boolean; +}; + +export type BuildConfigFromNS> = + { + [N in T as T['_']['namespace']]: T['$inferConfig']; + }; + +export type RegisterNamespaceReturnType< + N extends string, + T extends Record, +> = { + _: { + namespace: N; + options: RegisterNamespaceOptions; + factory: () => T | Promise; + }; + $inferConfig: T; +}; + +export type NamespacedConfigMapKeys = + | 'factory' + | 'static' + | 'dynamic' + | '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..adbdda4 --- /dev/null +++ b/lib/config/register-namespace.ts @@ -0,0 +1,23 @@ +import { LiteralString } from '../type-helpers'; +import { + RegisterNamespaceOptions, + RegisterNamespaceReturnType, +} from './options'; + +export const registerNamespace = ( + namespace: LiteralString, + factory: () => T | Promise, + options?: RegisterNamespaceOptions, +): RegisterNamespaceReturnType, T> => { + return { + _: { + namespace, + options: { + dynamic: options?.dynamic ?? false, + encrypt: options?.encrypt ?? false, + }, + factory, + }, + $inferConfig: {} as T, + }; +}; diff --git a/lib/config/service.ts b/lib/config/service.ts index a20c4cd..3498580 100644 --- a/lib/config/service.ts +++ b/lib/config/service.ts @@ -1,28 +1,72 @@ -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; + + /** + * Build value as per the dynamicity. + */ + if (nsConfig.get('dynamic')) { + return new Promise(async res => { + // eslint-disable-next-line @typescript-eslint/ban-types + const factory = nsConfig.get('factory') as Function; + const values = await factory(); + + if (!paths.length) { + res(values); + return; + } + + const valueOnPath = Obj.get(values, paths.join('.')); + res(valueOnPath); + }); + } else { + 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..ffb9dd6 --- /dev/null +++ b/lib/type-helpers/index.ts @@ -0,0 +1,31 @@ +export type LiteralString = string extends T ? never : T; + +export type StaticObjOrFunc = T | (() => T | Promise); + +export type Primitive = string | number | boolean | null | undefined; + +// Recursive type to build dot notations +export type DotNotation = T extends Primitive + ? string + : T extends Array + ? DotNotation + : { + [K in keyof T]: T[K] extends Primitive + ? `${Prefix}${Prefix extends '' ? '' : '.'}${K & string}` + : DotNotation< + T[K], + `${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/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 e9305fa..6f30f02 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "0.1.34", "license": "MIT", "dependencies": { - "@nestjs/config": "^3.2.0", "@nestjs/platform-express": "^10.4.1", "@react-email/components": "^0.0.25", "app-root-path": "^3.1.0", @@ -1855,21 +1854,6 @@ } } }, - "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", @@ -4644,27 +4628,6 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, - "node_modules/dotenv": { - "version": "16.4.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", - "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=12" - }, - "funding": { - "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", diff --git a/package.json b/package.json index 9f2d988..ebea12b 100644 --- a/package.json +++ b/package.json @@ -39,10 +39,10 @@ "@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,7 +56,6 @@ "typescript": "^5.5.2" }, "dependencies": { - "@nestjs/config": "^3.2.0", "@nestjs/platform-express": "^10.4.1", "@react-email/components": "^0.0.25", "app-root-path": "^3.1.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 From 699ea96e907d5969b05b837ddfcbd6063e27d061 Mon Sep 17 00:00:00 2001 From: Vinayak Sarawagi Date: Sun, 6 Oct 2024 18:25:23 +0530 Subject: [PATCH 2/3] add @nestjs/common @nestjs/core dependencies --- package-lock.json | 25 +++++++------------------ package.json | 2 ++ 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6f30f02..e578ba5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "0.1.34", "license": "MIT", "dependencies": { + "@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", @@ -1819,7 +1821,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" } @@ -1829,7 +1830,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", @@ -1860,7 +1860,6 @@ "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", @@ -2142,7 +2141,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", @@ -4230,8 +4228,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", @@ -5711,8 +5708,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", @@ -7017,7 +7013,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" } @@ -8385,7 +8380,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" }, @@ -8848,8 +8842,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", @@ -10444,8 +10437,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", @@ -10721,7 +10713,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" }, @@ -10889,15 +10880,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 ebea12b..d26d0c4 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,8 @@ "typescript": "^5.5.2" }, "dependencies": { + "@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", From 23b96b90b4812ca6799c697ed63c5db4ea7e9e6c Mon Sep 17 00:00:00 2001 From: Vinayak Sarawagi Date: Sun, 6 Oct 2024 23:24:28 +0530 Subject: [PATCH 3/3] refactor the console command to support the new namespace configurations and tree like structure on console --- lib/config/builder.ts | 14 +--------- lib/config/command.ts | 43 ++++++++++++++--------------- lib/config/options.ts | 19 +++++++------ lib/config/register-namespace.ts | 4 ++- lib/config/service.ts | 39 +++++++-------------------- lib/type-helpers/index.ts | 38 +++++++++++++++++--------- lib/utils/console-helpers.ts | 46 ++++++++++++++++++++++++++++++++ package-lock.json | 28 +++++++++++++++++++ package.json | 3 +++ 9 files changed, 146 insertions(+), 88 deletions(-) create mode 100644 lib/utils/console-helpers.ts diff --git a/lib/config/builder.ts b/lib/config/builder.ts index 6788bcb..49e6367 100644 --- a/lib/config/builder.ts +++ b/lib/config/builder.ts @@ -1,14 +1,10 @@ -import { Injectable } from '@nestjs/common'; import { NamespacedConfigMapKeys, NamespacedConfigMapValues, RegisterNamespaceReturnType, } from './options'; -@Injectable() export class ConfigBuilder { - constructor() {} - static async build( namespaceObjects: RegisterNamespaceReturnType[], ): Promise> { @@ -20,16 +16,8 @@ export class ConfigBuilder { NamespacedConfigMapValues >(); namespacedMap.set('factory', namespacedConfig._.factory); - if (!namespacedConfig._.options.dynamic) { - namespacedMap.set('static', namespacedConfig._.factory()); - } - - /** - * Set options - */ - namespacedMap.set('dynamic', namespacedConfig._.options.dynamic); + namespacedMap.set('static', await namespacedConfig._.factory()); namespacedMap.set('encrypt', namespacedConfig._.options.encrypt); - configMap.set(namespacedConfig._.namespace, namespacedMap); } diff --git a/lib/config/command.ts b/lib/config/command.ts index 4c4bf94..aa21c19 100644 --- a/lib/config/command.ts +++ b/lib/config/command.ts @@ -1,34 +1,35 @@ -import { Inject, Injectable } from '@nestjs/common'; import { Command, ConsoleIO } from '../console'; -import { Obj } from '../utils'; -import { Arr } from '../utils/array'; -import { columnify } from '../utils/columnify'; -import * as pc from 'picocolors'; 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(@Inject(CONFIG_FACTORY) private config: ConfigMap) {} - @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`); + 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} `)))), + ); + }; + + 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(' ')); - } - console.log(printRows.join('\n')); + // Example usage + for (const [namespace, obj] of this.config.entries()) { + printNsToConsole(namespace, obj); + } } } diff --git a/lib/config/options.ts b/lib/config/options.ts index adde277..9cec9f0 100644 --- a/lib/config/options.ts +++ b/lib/config/options.ts @@ -1,12 +1,15 @@ export type RegisterNamespaceOptions = { - dynamic?: boolean; encrypt?: boolean; }; -export type BuildConfigFromNS> = - { - [N in T as T['_']['namespace']]: T['$inferConfig']; - }; +export type BuildConfigFromNS< + T extends RegisterNamespaceReturnType[], +> = { + [N in T[number]['_']['namespace']]: Extract< + T[number], + { _: { namespace: N } } + >['$inferConfig']; +}; export type RegisterNamespaceReturnType< N extends string, @@ -20,11 +23,7 @@ export type RegisterNamespaceReturnType< $inferConfig: T; }; -export type NamespacedConfigMapKeys = - | 'factory' - | 'static' - | 'dynamic' - | 'encrypt'; +export type NamespacedConfigMapKeys = 'factory' | 'static' | 'encrypt'; export type NamespacedConfigMapValues = // eslint-disable-next-line @typescript-eslint/ban-types diff --git a/lib/config/register-namespace.ts b/lib/config/register-namespace.ts index adbdda4..8af11c7 100644 --- a/lib/config/register-namespace.ts +++ b/lib/config/register-namespace.ts @@ -4,6 +4,9 @@ import { RegisterNamespaceReturnType, } from './options'; +// eslint-disable-next-line @typescript-eslint/no-var-requires +require('dotenv').config(); + export const registerNamespace = ( namespace: LiteralString, factory: () => T | Promise, @@ -13,7 +16,6 @@ export const registerNamespace = ( _: { namespace, options: { - dynamic: options?.dynamic ?? false, encrypt: options?.encrypt ?? false, }, factory, diff --git a/lib/config/service.ts b/lib/config/service.ts index 3498580..a2ffb79 100644 --- a/lib/config/service.ts +++ b/lib/config/service.ts @@ -35,38 +35,17 @@ export class ConfigService { */ if (!nsConfig) return null; - /** - * Build value as per the dynamicity. - */ - if (nsConfig.get('dynamic')) { - return new Promise(async res => { - // eslint-disable-next-line @typescript-eslint/ban-types - const factory = nsConfig.get('factory') as Function; - const values = await factory(); - - if (!paths.length) { - res(values); - return; - } - - const valueOnPath = Obj.get(values, paths.join('.')); - res(valueOnPath); - }); - } else { - if (!paths.length) { - return nsConfig.get('static') as any; - } + if (!paths.length) return nsConfig.get('static') as any; - const staticValues = nsConfig.get('static') as Omit< - NamespacedConfigMapValues, - 'function' - >; + 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; + const valueOnPath = Obj.get(staticValues, paths.join('.')); + if (valueOnPath) { + this.cachedConfig.set(key as string, valueOnPath); } + return valueOnPath; } } diff --git a/lib/type-helpers/index.ts b/lib/type-helpers/index.ts index ffb9dd6..0189e5d 100644 --- a/lib/type-helpers/index.ts +++ b/lib/type-helpers/index.ts @@ -4,19 +4,31 @@ export type StaticObjOrFunc = T | (() => T | Promise); export type Primitive = string | number | boolean | null | undefined; -// Recursive type to build dot notations -export type DotNotation = T extends Primitive - ? string - : T extends Array - ? DotNotation - : { - [K in keyof T]: T[K] extends Primitive - ? `${Prefix}${Prefix extends '' ? '' : '.'}${K & string}` - : DotNotation< - T[K], - `${Prefix}${Prefix extends '' ? '' : '.'}${K & string}` - >; - }[keyof T]; +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, 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/package-lock.json b/package-lock.json index e578ba5..5d38a08 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,11 +14,13 @@ "@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", @@ -41,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", @@ -2577,6 +2580,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", @@ -3304,6 +3314,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", @@ -4625,6 +4641,18 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", diff --git a/package.json b/package.json index d26d0c4..ff4ede2 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,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", @@ -61,11 +62,13 @@ "@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",