From 42d7729889338fb90d5f49ce2fc2b1c076248c01 Mon Sep 17 00:00:00 2001 From: yasser Date: Fri, 10 Apr 2026 12:16:55 +0100 Subject: [PATCH 1/3] removed template files and added new tests --- CHANGELOG.md | 6 + package.json | 2 +- src/config.module.spec.ts | 109 +++++++++++++++++- src/config.module.ts | 148 ++++++++++++++++++++----- src/config.service.ts | 25 +++++ src/controllers/example.controller.ts | 46 -------- src/decorators/example.decorator.ts | 47 -------- src/define-config.spec.ts | 37 ++++++- src/define-config.ts | 31 ++++++ src/dto/create-example.dto.ts | 36 ------ src/dto/update-example.dto.ts | 18 --- src/entities/example.entity.ts | 56 ---------- src/example-kit.module.ts | 106 ------------------ src/guards/example.guard.ts | 42 ------- src/index.ts | 9 +- src/repositories/example.repository.ts | 124 --------------------- src/services/example.service.ts | 54 --------- 17 files changed, 334 insertions(+), 562 deletions(-) delete mode 100644 src/controllers/example.controller.ts delete mode 100644 src/decorators/example.decorator.ts delete mode 100644 src/dto/create-example.dto.ts delete mode 100644 src/dto/update-example.dto.ts delete mode 100644 src/entities/example.entity.ts delete mode 100644 src/example-kit.module.ts delete mode 100644 src/guards/example.guard.ts delete mode 100644 src/repositories/example.repository.ts delete mode 100644 src/services/example.service.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b567a7..99167bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # @ciscode/config-kit +## 0.3.0 + +### Minor Changes + +- Added ConfigService.getAll(), InferConfig type helper, useClass/useExisting support in registerAsync, and removed template boilerplate from the published package + ## 0.2.0 ### Minor Changes diff --git a/package.json b/package.json index 44971d6..0f308b6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ciscode/config-kit", - "version": "0.2.0", + "version": "0.3.0", "description": "Typed env config for NestJS with Zod validation at startup. Fails fast on misconfiguration. Per-module namespace injection. Most foundational backend package.", "author": "CisCode", "publishConfig": { diff --git a/src/config.module.spec.ts b/src/config.module.spec.ts index 43df6c2..a248060 100644 --- a/src/config.module.spec.ts +++ b/src/config.module.spec.ts @@ -18,9 +18,9 @@ import { Module, Injectable } from "@nestjs/common"; import { Test } from "@nestjs/testing"; import { z } from "zod"; -import { ConfigModule } from "@/config.module"; +import { ConfigModule, ConfigModuleOptionsFactory } from "@/config.module"; import { ConfigService } from "@/config.service"; -import { defineConfig } from "@/define-config"; +import { defineConfig, ConfigDefinition } from "@/define-config"; import { ConfigValidationError } from "@/errors/config-validation.error"; // ───────────────────────────────────────────────────────────────────────────── @@ -212,3 +212,108 @@ describe("ConfigModule.forRoot()", () => { expect(featureService.config).toBeInstanceOf(ConfigService); }); }); + +// ───────────────────────────────────────────────────────────────────────────── +// ConfigService.getAll() +// ───────────────────────────────────────────────────────────────────────────── + +describe("ConfigService.getAll()", () => { + it("should return the complete validated config object", async () => { + const parsed = appDef.parse({ APP_HOST: "all-host", APP_PORT: "7777" }); + + const module = await Test.createTestingModule({ + imports: [ConfigModule.register(appDef)], + }) + .overrideProvider("CONFIG_KIT_VALUES") + .useValue(parsed) + .compile(); + + const service = module.get>(ConfigService); + + // getAll() should return the full config with all keys present + const all = service.getAll(); + expect(all.APP_HOST).toBe("all-host"); + expect(all.APP_PORT).toBe(7777); + }); + + it("should return the same reference as individual get() calls", async () => { + const parsed = appDef.parse({ APP_HOST: "ref-host", APP_PORT: "1234" }); + + const module = await Test.createTestingModule({ + imports: [ConfigModule.register(appDef)], + }) + .overrideProvider("CONFIG_KIT_VALUES") + .useValue(parsed) + .compile(); + + const service = module.get>(ConfigService); + + // Values returned by getAll() must match those returned by get() + const all = service.getAll(); + expect(all.APP_HOST).toBe(service.get("APP_HOST")); + expect(all.APP_PORT).toBe(service.get("APP_PORT")); + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// ConfigModule.registerAsync() — useClass / useExisting +// ───────────────────────────────────────────────────────────────────────────── + +describe("ConfigModule.registerAsync() — useClass", () => { + it("should instantiate the class and call createConfigDefinition()", async () => { + // Track whether createConfigDefinition was called on the class instance + let factoryCalled = false; + + @Injectable() + class TestConfigFactory implements ConfigModuleOptionsFactory { + createConfigDefinition(): ConfigDefinition< + z.ZodObject<{ USE_CLASS_VAR: z.ZodDefault }> + > { + factoryCalled = true; + return defineConfig(z.object({ USE_CLASS_VAR: z.string().default("class-value") })); + } + } + + const module = await Test.createTestingModule({ + imports: [ConfigModule.registerAsync({ useClass: TestConfigFactory })], + }).compile(); + + expect(factoryCalled).toBe(true); + expect(module.get(ConfigService)).toBeInstanceOf(ConfigService); + }); +}); + +describe("ConfigModule.registerAsync() — useExisting", () => { + it("should reuse the existing provider and call createConfigDefinition()", async () => { + let factoryCalled = false; + + @Injectable() + class ExistingConfigFactory implements ConfigModuleOptionsFactory { + createConfigDefinition(): ConfigDefinition< + z.ZodObject<{ USE_EXISTING_VAR: z.ZodDefault }> + > { + factoryCalled = true; + return defineConfig(z.object({ USE_EXISTING_VAR: z.string().default("existing-value") })); + } + } + + // Helper module that provides the factory so it can be reused via useExisting + @Module({ + providers: [ExistingConfigFactory], + exports: [ExistingConfigFactory], + }) + class FactoryModule {} + + const module = await Test.createTestingModule({ + imports: [ + ConfigModule.registerAsync({ + imports: [FactoryModule], + useExisting: ExistingConfigFactory, + }), + ], + }).compile(); + + expect(factoryCalled).toBe(true); + expect(module.get(ConfigService)).toBeInstanceOf(ConfigService); + }); +}); diff --git a/src/config.module.ts b/src/config.module.ts index 2d83869..e21c9ba 100644 --- a/src/config.module.ts +++ b/src/config.module.ts @@ -31,7 +31,7 @@ * - ConfigModule — the NestJS dynamic module */ -import { DynamicModule, Global, Module } from "@nestjs/common"; +import { DynamicModule, Global, Module, Type } from "@nestjs/common"; import type { Provider } from "@nestjs/common"; import type { z } from "zod"; @@ -51,21 +51,66 @@ type AnyZodObject = z.ZodObject; // ───────────────────────────────────────────────────────────────────────────── /** - * Options for `ConfigModule.registerAsync()`. + * Interface that classes must implement to work with `ConfigModule.registerAsync({ useClass })`. * - * The factory receives whatever NestJS injects via `inject[]` and must return - * a `ConfigDefinition` (the value returned by `defineConfig()`). ConfigModule - * will call `.parse(process.env)` on it before any dependent provider resolves. + * Implement this in a class and pass that class to `registerAsync({ useClass: MyFactory })`. + * NestJS will instantiate the class (injecting its constructor deps) and call + * `createConfigDefinition()` to retrieve the `ConfigDefinition`. * * @example * ```typescript + * @Injectable() + * export class AppConfigFactory implements ConfigModuleOptionsFactory { + * constructor(private readonly vault: VaultService) {} + * + * async createConfigDefinition() { + * const secret = await this.vault.getSecret('JWT_SECRET'); + * return defineConfig(z.object({ JWT_SECRET: z.string().default(secret) })); + * } + * } + * + * // app.module.ts + * ConfigModule.registerAsync({ useClass: AppConfigFactory }); + * ``` + */ +export interface ConfigModuleOptionsFactory { + /** + * Returns the `ConfigDefinition` to validate and provide. + * May be synchronous or async. + */ + createConfigDefinition(): Promise> | ConfigDefinition; +} + +/** + * Options for `ConfigModule.registerAsync()`. + * + * Supports three patterns — pick exactly one of `useFactory`, `useClass`, or `useExisting`: + * + * - **`useFactory`**: provide an inline factory function (most common for simple cases) + * - **`useClass`**: provide a class implementing `ConfigModuleOptionsFactory`; NestJS + * instantiates it (and injects its constructor deps) before calling `createConfigDefinition()` + * - **`useExisting`**: same as `useClass` but reuses an already-registered provider instead + * of creating a new instance (avoids double-instantiation) + * + * @example useFactory + * ```typescript * ConfigModule.registerAsync({ - * imports: [SomeModule], - * inject: [SomeService], - * useFactory: (svc: SomeService) => - * defineConfig(z.object({ PORT: z.coerce.number().default(svc.defaultPort()) })), + * imports: [VaultModule], + * inject: [VaultService], + * useFactory: (vault: VaultService) => + * defineConfig(z.object({ PORT: z.coerce.number().default(3000) })), * }); * ``` + * + * @example useClass + * ```typescript + * ConfigModule.registerAsync({ useClass: AppConfigFactory }); + * ``` + * + * @example useExisting + * ```typescript + * ConfigModule.registerAsync({ useExisting: AppConfigFactory }); + * ``` */ export interface ConfigModuleAsyncOptions { /** @@ -76,15 +121,27 @@ export interface ConfigModuleAsyncOptions /** * Tokens to inject into `useFactory` as positional arguments. - * Same semantics as `inject` in a standard NestJS `useFactory` provider. + * Only used with `useFactory`. Ignored when using `useClass` or `useExisting`. */ inject?: unknown[]; /** - * Factory function that returns the ConfigDefinition to validate. + * Inline factory function. Receives the `inject`-ed tokens as positional args. * May be synchronous or async. */ - useFactory: (...args: unknown[]) => Promise> | ConfigDefinition; + useFactory?: (...args: unknown[]) => Promise> | ConfigDefinition; + + /** + * A class implementing `ConfigModuleOptionsFactory`. NestJS creates a fresh + * instance (with its own injected deps) and calls `createConfigDefinition()`. + */ + useClass?: Type>; + + /** + * Same as `useClass` but reuses a provider already present in the DI container. + * The class must be provided elsewhere in `imports` or the root module. + */ + useExisting?: Type>; } // ───────────────────────────────────────────────────────────────────────────── @@ -196,20 +253,14 @@ export class ConfigModule { static registerAsync( options: ConfigModuleAsyncOptions, ): DynamicModule { - // Async provider — NestJS awaits this before resolving anything that injects - // CONFIG_VALUES_TOKEN (i.e. ConfigService) - const configValuesProvider: Provider = { - provide: CONFIG_VALUES_TOKEN, - useFactory: async (...args: unknown[]) => { - // Call the consumer's factory to get the ConfigDefinition - const definition = await options.useFactory(...args); + // Build the CONFIG_VALUES_TOKEN provider differently based on which + // async pattern the consumer chose (useFactory / useClass / useExisting) + const configValuesProvider = ConfigModule.createAsyncConfigProvider(options); - // Parse and validate synchronously inside the async factory - // Throwing here prevents the app from booting with bad config - return definition.parse(process.env); - }, - inject: (options.inject ?? []) as never[], - }; + // Extra providers needed when consumer uses useClass (NestJS must instantiate the class) + const extraProviders: Provider[] = options.useClass + ? [{ provide: options.useClass, useClass: options.useClass }] + : []; // Namespace registry shared with feature modules' asProvider() factories const namespaceRegistryProvider: Provider = { @@ -221,12 +272,57 @@ export class ConfigModule { module: ConfigModule, // Make the imported modules available so the factory can resolve them imports: options.imports ?? [], - providers: [configValuesProvider, namespaceRegistryProvider, ConfigService], + providers: [ + configValuesProvider, + ...extraProviders, + namespaceRegistryProvider, + ConfigService, + ], exports: [ConfigService, NAMESPACE_REGISTRY_TOKEN], global: false, }; } + /** + * Builds the `CONFIG_VALUES_TOKEN` provider for `registerAsync`. + * + * Handles all three async patterns: + * - `useFactory`: wraps the consumer's factory directly + * - `useClass` / `useExisting`: delegates to the factory class's `createConfigDefinition()` + */ + private static createAsyncConfigProvider( + options: ConfigModuleAsyncOptions, + ): Provider { + if (options.useFactory) { + // ── useFactory ───────────────────────────────────────────────────────── + return { + provide: CONFIG_VALUES_TOKEN, + useFactory: async (...args: unknown[]) => { + // Call the consumer's factory to get the ConfigDefinition + const definition = await options.useFactory!(...args); + // Parse and validate synchronously — throws on bad env before app boots + return definition.parse(process.env); + }, + inject: (options.inject ?? []) as never[], + }; + } + + // ── useClass / useExisting ──────────────────────────────────────────────── + // Both patterns delegate to ConfigModuleOptionsFactory.createConfigDefinition() + // useClass → NestJS creates a fresh instance of the class + // useExisting → NestJS reuses the already-registered instance + const factoryToken = (options.useClass ?? options.useExisting)!; + + return { + provide: CONFIG_VALUES_TOKEN, + useFactory: async (factory: ConfigModuleOptionsFactory) => { + const definition = await factory.createConfigDefinition(); + return definition.parse(process.env); + }, + inject: [factoryToken], + }; + } + // ── forRoot (global) ─────────────────────────────────────────────────────── /** diff --git a/src/config.service.ts b/src/config.service.ts index f125e73..e3c9e6f 100644 --- a/src/config.service.ts +++ b/src/config.service.ts @@ -136,4 +136,29 @@ export class ConfigService< // the shape matches ConfigOutput exactly return this._config[key as string] as ConfigOutput[K]; } + + /** + * Returns the entire validated config object as a readonly snapshot. + * + * Useful for: + * - Passing the full config to a third-party initialiser that expects a plain object + * - Debug logging (e.g. `console.log(config.getAll())`) + * - Spreading into another object when all keys are needed + * + * The returned object is the same frozen reference produced by `definition.parse()` — + * mutating it will throw in strict mode. + * + * @returns The complete, fully-typed validated config object. + * + * @example + * ```typescript + * const all = this.config.getAll(); + * // all.PORT → number + * // all.DATABASE_URL → string + * ``` + */ + getAll(): Readonly> { + // Cast is safe — same guarantee as get() + return this._config as unknown as Readonly>; + } } diff --git a/src/controllers/example.controller.ts b/src/controllers/example.controller.ts deleted file mode 100644 index 90cea1b..0000000 --- a/src/controllers/example.controller.ts +++ /dev/null @@ -1,46 +0,0 @@ -// import { ExampleService } from '@services/example.service'; -import { CreateExampleDto } from "@dtos/create-example.dto"; -import { Controller, Get, Post, Body, Param } from "@nestjs/common"; - -/** - * Example Controller - * - * HTTP endpoints for the Example Kit module. - * Controllers are optional - only include if your module provides HTTP endpoints. - * - * @example - * ```typescript - * // In your app's module - * @Module({ - * imports: [ExampleKitModule.forRoot()], - * controllers: [], // Controllers are auto-registered by the module - * }) - * export class AppModule {} - * ``` - */ -@Controller("example") -export class ExampleController { - // constructor(private readonly exampleService: ExampleService) {} - - /** - * Create a new example resource - * @param dto - Data transfer object - * @returns Created resource - */ - @Post() - async create(@Body() dto: CreateExampleDto) { - // return this.exampleService.doSomething(dto.name); - return { message: "Example created", data: dto }; - } - - /** - * Get resource by ID - * @param id - Resource identifier - * @returns Resource data - */ - @Get(":id") - async findOne(@Param("id") id: string) { - // return this.exampleService.findById(id); - return { id, data: "example" }; - } -} diff --git a/src/decorators/example.decorator.ts b/src/decorators/example.decorator.ts deleted file mode 100644 index bfde98f..0000000 --- a/src/decorators/example.decorator.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { createParamDecorator } from "@nestjs/common"; -import type { ExecutionContext } from "@nestjs/common"; - -/** - * Example Decorator - * - * Custom parameter decorator to extract data from the request. - * Useful for extracting user info, custom headers, or other request data. - * - * @example - * ```typescript - * @Controller('example') - * export class ExampleController { - * @Get() - * async findAll(@ExampleData() data: any) { - * // data is extracted from request - * return data; - * } - * } - * ``` - */ -export const ExampleData = createParamDecorator((data: unknown, ctx: ExecutionContext) => { - const request = ctx.switchToHttp().getRequest(); - - // Extract and return data from request - // Example: return request.user; - // Example: return request.headers['x-custom-header']; - - return request.user || null; -}); - -/** - * Example Decorator with parameter - * - * @example - * ```typescript - * @Get() - * async findOne(@ExampleParam('id') id: string) { - * // id is extracted from request params - * return id; - * } - * ``` - */ -export const ExampleParam = createParamDecorator((param: string, ctx: ExecutionContext) => { - const request = ctx.switchToHttp().getRequest(); - return request.params?.[param]; -}); diff --git a/src/define-config.spec.ts b/src/define-config.spec.ts index 977f6d4..3b7b67f 100644 --- a/src/define-config.spec.ts +++ b/src/define-config.spec.ts @@ -16,7 +16,7 @@ import { z } from "zod"; -import { ConfigDefinition, defineConfig } from "@/define-config"; +import { ConfigDefinition, defineConfig, InferConfig } from "@/define-config"; import { ConfigValidationError } from "@/errors/config-validation.error"; // ───────────────────────────────────────────────────────────────────────────── @@ -218,3 +218,38 @@ describe("ConfigDefinition.parse()", () => { expect(() => definition.parse({ NODE_ENV: "invalid" })).toThrow(ConfigValidationError); }); }); + +// ───────────────────────────────────────────────────────────────────────────── +// InferConfig type helper +// ───────────────────────────────────────────────────────────────────────────── + +describe("InferConfig", () => { + it("should produce the correct shape at runtime (smoke test via parse output)", () => { + const def = defineConfig( + z.object({ HOST: z.string().default("localhost"), PORT: z.coerce.number().default(8080) }), + ); + + // InferConfig is a compile-time type — we can verify it at runtime by + // checking that the parsed result matches the expected shape + const result: InferConfig = def.parse({}); + + expect(result.HOST).toBe("localhost"); + expect(result.PORT).toBe(8080); + }); + + it("should carry through all Zod output types (number, string, enum)", () => { + const schema = z.object({ + PORT: z.coerce.number().default(3000), + NODE_ENV: z.enum(["development", "production"]).default("development"), + NAME: z.string().default("app"), + }); + const def = defineConfig(schema); + + const result: InferConfig = def.parse({}); + + // TypeScript types are correct at compile time; verify values at runtime + expect(typeof result.PORT).toBe("number"); + expect(typeof result.NODE_ENV).toBe("string"); + expect(typeof result.NAME).toBe("string"); + }); +}); diff --git a/src/define-config.ts b/src/define-config.ts index ed2307f..dd68fd7 100644 --- a/src/define-config.ts +++ b/src/define-config.ts @@ -147,3 +147,34 @@ export function defineConfig(schema: T): ConfigDefinitio // Wrap the schema in a ConfigDefinition; no side effects at definition time return new ConfigDefinition(schema); } + +// ───────────────────────────────────────────────────────────────────────────── +// InferConfig +// ───────────────────────────────────────────────────────────────────────────── + +/** + * Extracts the fully-resolved config type from a `ConfigDefinition`. + * + * Saves consumers from writing the verbose `z.output` + * pattern. Use it to type variables, function parameters, or return types + * that need to reference the shape of a config without importing zod directly. + * + * @typeParam TDef - The `ConfigDefinition` returned by `defineConfig()`. + * + * @example + * ```typescript + * // app.config.ts + * export const appConfig = defineConfig(z.object({ + * PORT: z.coerce.number().default(3000), + * DATABASE_URL: z.string().url(), + * })); + * + * // Instead of: + * type AppConfig = z.output; + * + * // Write: + * type AppConfig = InferConfig; + * // → { PORT: number; DATABASE_URL: string } + * ``` + */ +export type InferConfig> = z.output; diff --git a/src/dto/create-example.dto.ts b/src/dto/create-example.dto.ts deleted file mode 100644 index 094c25c..0000000 --- a/src/dto/create-example.dto.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { IsString, IsNotEmpty, MinLength, MaxLength, IsOptional } from "class-validator"; - -/** - * DTO for creating a new example resource - * - * DTOs define the shape of data for API requests and provide validation. - * Always use class-validator decorators for input validation. - * - * @example - * ```typescript - * const dto: CreateExampleDto = { - * name: 'Test Example', - * description: 'Optional description', - * }; - * ``` - */ -export class CreateExampleDto { - /** - * Name of the example resource - * @example "My Example" - */ - @IsString() - @IsNotEmpty() - @MinLength(3) - @MaxLength(100) - name!: string; - - /** - * Optional description - * @example "This is an example description" - */ - @IsString() - @IsOptional() - @MaxLength(500) - description?: string; -} diff --git a/src/dto/update-example.dto.ts b/src/dto/update-example.dto.ts deleted file mode 100644 index 2f865bf..0000000 --- a/src/dto/update-example.dto.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { PartialType } from "@nestjs/mapped-types"; - -import { CreateExampleDto } from "./create-example.dto"; - -/** - * DTO for updating an existing example resource - * - * Uses PartialType to make all fields from CreateExampleDto optional. - * This follows NestJS best practices for update DTOs. - * - * @example - * ```typescript - * const dto: UpdateExampleDto = { - * name: 'Updated Name', // Only update name - * }; - * ``` - */ -export class UpdateExampleDto extends PartialType(CreateExampleDto) {} diff --git a/src/entities/example.entity.ts b/src/entities/example.entity.ts deleted file mode 100644 index 69811b2..0000000 --- a/src/entities/example.entity.ts +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Example Entity - * - * Represents the domain model for the Example resource. - * - * For Mongoose: - * ```typescript - * import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; - * import { Document } from 'mongoose'; - * - * @Schema({ timestamps: true }) - * export class Example extends Document { - * @Prop({ required: true }) - * name: string; - * - * @Prop() - * description?: string; - * } - * - * export const ExampleSchema = SchemaFactory.createForClass(Example); - * ``` - * - * For TypeORM: - * ```typescript - * import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm'; - * - * @Entity('examples') - * export class Example { - * @PrimaryGeneratedColumn('uuid') - * id: string; - * - * @Column() - * name: string; - * - * @Column({ nullable: true }) - * description?: string; - * - * @CreateDateColumn() - * createdAt: Date; - * - * @UpdateDateColumn() - * updatedAt: Date; - * } - * ``` - * - * NOTE: Entities are NEVER exported from the module's public API. - * They are internal implementation details. - */ - -export class Example { - id!: string; - name!: string; - description?: string; - createdAt!: Date; - updatedAt!: Date; -} diff --git a/src/example-kit.module.ts b/src/example-kit.module.ts deleted file mode 100644 index d0cb216..0000000 --- a/src/example-kit.module.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { Module, DynamicModule, Provider } from "@nestjs/common"; -import { ExampleService } from "@services/example.service"; - -/** - * Options for configuring the Example Kit module - */ -export interface ExampleKitOptions { - /** - * Enable debug mode - * @default false - */ - debug?: boolean; - - /** - * Custom configuration options - */ - // Add your configuration options here -} - -/** - * Async options for dynamic module configuration - */ -export interface ExampleKitAsyncOptions { - useFactory: () => Promise | ExampleKitOptions; - inject?: any[]; -} - -/** - * Example Kit Module - * - * A reusable NestJS module template demonstrating best practices - * for building npm packages. - * - * @example - * ```typescript - * // Synchronous configuration - * @Module({ - * imports: [ - * ExampleKitModule.forRoot({ - * debug: true, - * }), - * ], - * }) - * export class AppModule {} - * - * // Asynchronous configuration - * @Module({ - * imports: [ - * ExampleKitModule.forRootAsync({ - * useFactory: (config: ConfigService) => ({ - * debug: config.get('DEBUG') === 'true', - * }), - * inject: [ConfigService], - * }), - * ], - * }) - * export class AppModule {} - * ``` - */ -@Module({}) -export class ExampleKitModule { - /** - * Register the module with synchronous configuration - * @param options - Configuration options - * @returns Dynamic module - */ - static forRoot(options: ExampleKitOptions = {}): DynamicModule { - const providers: Provider[] = [ - { - provide: "EXAMPLE_KIT_OPTIONS", - useValue: options, - }, - ExampleService, - ]; - - return { - module: ExampleKitModule, - providers, - exports: [ExampleService], - global: false, - }; - } - - /** - * Register the module with asynchronous configuration - * @param options - Async configuration options - * @returns Dynamic module - */ - static forRootAsync(options: ExampleKitAsyncOptions): DynamicModule { - const providers: Provider[] = [ - { - provide: "EXAMPLE_KIT_OPTIONS", - useFactory: options.useFactory, - inject: options.inject || [], - }, - ExampleService, - ]; - - return { - module: ExampleKitModule, - providers, - exports: [ExampleService], - global: false, - }; - } -} diff --git a/src/guards/example.guard.ts b/src/guards/example.guard.ts deleted file mode 100644 index 7f5898c..0000000 --- a/src/guards/example.guard.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Injectable, CanActivate, ExecutionContext } from "@nestjs/common"; -import { Observable } from "rxjs"; - -/** - * Example Guard - * - * Guards determine whether a request should be handled by the route handler. - * Use guards for authentication, authorization, and request validation. - * - * @example - * ```typescript - * // Apply to controller - * @Controller('example') - * @UseGuards(ExampleGuard) - * export class ExampleController {} - * - * // Apply to specific route - * @Get() - * @UseGuards(ExampleGuard) - * async findAll() {} - * - * // Apply globally - * app.useGlobalGuards(new ExampleGuard()); - * ``` - */ -@Injectable() -export class ExampleGuard implements CanActivate { - /** - * Determines if the request should be allowed - * @param context - Execution context - * @returns True if request is allowed, false otherwise - */ - canActivate(_context: ExecutionContext): boolean | Promise | Observable { - // const request = _context.switchToHttp().getRequest(); - - // Implement your guard logic here - // Example: Check if user is authenticated - // return !!request.user; - - return true; - } -} diff --git a/src/index.ts b/src/index.ts index c9755e2..6b5ac56 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,18 +12,21 @@ import "reflect-metadata"; // ============================================================================ // defineConfig() — declare your env shape with a Zod schema // ConfigDefinition — the wrapper returned by defineConfig(); passed to ConfigModule +// InferConfig — utility type: extracts the resolved config type from a ConfigDefinition // ============================================================================ export { defineConfig, ConfigDefinition } from "./define-config"; +export type { InferConfig } from "./define-config"; // ============================================================================ // NESTJS MODULE & SERVICE (COMPT-51) // ============================================================================ // ConfigModule — dynamic module with register / registerAsync / forRoot -// ConfigService — injectable service with typed .get(key) method -// ConfigModuleAsyncOptions — options shape for registerAsync +// ConfigService — injectable service with typed .get(key) and .getAll() methods +// ConfigModuleAsyncOptions — options shape for registerAsync (useFactory / useClass / useExisting) +// ConfigModuleOptionsFactory — interface to implement for useClass / useExisting patterns // ============================================================================ export { ConfigModule } from "./config.module"; -export type { ConfigModuleAsyncOptions } from "./config.module"; +export type { ConfigModuleAsyncOptions, ConfigModuleOptionsFactory } from "./config.module"; export { ConfigService } from "./config.service"; // ============================================================================ diff --git a/src/repositories/example.repository.ts b/src/repositories/example.repository.ts deleted file mode 100644 index a31f940..0000000 --- a/src/repositories/example.repository.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { Example } from "@entities/example.entity"; -import { Injectable } from "@nestjs/common"; - -/** - * Example Repository - * - * Handles data access for Example entities. - * Repositories encapsulate database operations and provide a clean interface - * for services to interact with the database. - * - * For Mongoose: - * ```typescript - * import { Injectable } from '@nestjs/common'; - * import { InjectModel } from '@nestjs/mongoose'; - * import { Model } from 'mongoose'; - * import { Example } from '@entities/example.entity'; - * - * @Injectable() - * export class ExampleRepository { - * constructor( - * @InjectModel(Example.name) - * private readonly model: Model, - * ) {} - * - * async create(data: Partial): Promise { - * const created = new this.model(data); - * return created.save(); - * } - * - * async findById(id: string): Promise { - * return this.model.findById(id).lean().exec(); - * } - * } - * ``` - * - * For TypeORM: - * ```typescript - * import { Injectable } from '@nestjs/common'; - * import { InjectRepository } from '@nestjs/typeorm'; - * import { Repository } from 'typeorm'; - * import { Example } from '@entities/example.entity'; - * - * @Injectable() - * export class ExampleRepository { - * constructor( - * @InjectRepository(Example) - * private readonly repo: Repository, - * ) {} - * - * async create(data: Partial): Promise { - * const entity = this.repo.create(data); - * return this.repo.save(entity); - * } - * - * async findById(id: string): Promise { - * return this.repo.findOne({ where: { id } }); - * } - * } - * ``` - * - * NOTE: Repositories are NEVER exported from the module's public API. - * Services use repositories internally, but consumers only interact with services. - */ - -@Injectable() -export class ExampleRepository { - /** - * Create a new example - * @param data - Partial example data - * @returns Created example - */ - async create(data: Partial): Promise { - // Implement your database logic here - return { - id: "generated-id", - ...data, - createdAt: new Date(), - updatedAt: new Date(), - } as Example; - } - - /** - * Find example by ID - * @param id - Example identifier - * @returns Example or null if not found - */ - async findById(id: string): Promise { - // Implement your database logic here - console.log("Finding example by id:", id); - return null; - } - - /** - * Find all examples - * @returns Array of examples - */ - async findAll(): Promise { - // Implement your database logic here - return []; - } - - /** - * Update example - * @param id - Example identifier - * @param data - Partial data to update - * @returns Updated example or null if not found - */ - async update(id: string, data: Partial): Promise { - // Implement your database logic here - console.log("Updating example:", id, data); - return null; - } - - /** - * Delete example - * @param id - Example identifier - * @returns True if deleted, false if not found - */ - async delete(id: string): Promise { - // Implement your database logic here - console.log("Deleting example:", id); - return false; - } -} diff --git a/src/services/example.service.ts b/src/services/example.service.ts deleted file mode 100644 index 96e3691..0000000 --- a/src/services/example.service.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Injectable, Inject } from "@nestjs/common"; - -import { ExampleKitOptions } from "../example-kit.module"; - -/** - * Example Service - * - * Main service providing the core functionality of the module. - * This is what consumers of your module will primarily interact with. - * - * @example - * ```typescript - * constructor(private readonly exampleService: ExampleService) {} - * - * async someMethod() { - * const result = await this.exampleService.doSomething('data'); - * return result; - * } - * ``` - */ -@Injectable() -export class ExampleService { - constructor( - @Inject("EXAMPLE_KIT_OPTIONS") - private readonly _options: ExampleKitOptions, - ) {} - - /** - * Example method demonstrating service functionality - * @param data - Input data to process - * @returns Processed result - * @example - * ```typescript - * const result = await service.doSomething('test'); - * // Returns: "Processed: test" - * ``` - */ - async doSomething(data: string): Promise { - if (this._options.debug) { - console.log("[ExampleService] Processing:", data); - } - return `Processed: ${data}`; - } - - /** - * Example method for retrieving data - * @param id - Unique identifier - * @returns Retrieved data or null - */ - async findById(id: string): Promise { - // Implement your logic here - return { id, data: "example" }; - } -} From fbd765cb26ab9f4c86f8c9dafce65840fe2ac7d9 Mon Sep 17 00:00:00 2001 From: yasser Date: Fri, 10 Apr 2026 13:38:11 +0100 Subject: [PATCH 2/3] fix: use import type for InferConfig in spec --- src/define-config.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/define-config.spec.ts b/src/define-config.spec.ts index 3b7b67f..8b36d3d 100644 --- a/src/define-config.spec.ts +++ b/src/define-config.spec.ts @@ -16,7 +16,8 @@ import { z } from "zod"; -import { ConfigDefinition, defineConfig, InferConfig } from "@/define-config"; +import { ConfigDefinition, defineConfig } from "@/define-config"; +import type { InferConfig } from "@/define-config"; import { ConfigValidationError } from "@/errors/config-validation.error"; // ───────────────────────────────────────────────────────────────────────────── From d23005ab23a74a04d284e9390e12e16cd94a4ba0 Mon Sep 17 00:00:00 2001 From: yasser Date: Fri, 10 Apr 2026 13:42:00 +0100 Subject: [PATCH 3/3] fixed tag issue --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 58d33e6..f652882 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@ciscode/config-kit", - "version": "0.1.0", + "version": "0.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@ciscode/config-kit", - "version": "0.1.0", + "version": "0.3.1", "license": "MIT", "dependencies": { "class-transformer": "^0.5.1", diff --git a/package.json b/package.json index 0f308b6..6f237b2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@ciscode/config-kit", - "version": "0.3.0", + "version": "0.3.1", "description": "Typed env config for NestJS with Zod validation at startup. Fails fast on misconfiguration. Per-module namespace injection. Most foundational backend package.", "author": "CisCode", "publishConfig": {