Conversation
…ckages - Replace git tag --list strategy with package.json-driven tag validation in all 16 publish workflows; use git rev-parse to verify the exact tag exists rather than guessing the latest repo-wide tag - Update error guidance to reflect feat/** → develop → master flow - Standardize dependabot to npm-only, grouped, monthly cadence across all 16 packages; remove github-actions ecosystem updates - Add missing dependabot.yml to AuthKit-UI, ChartKit-UI, HealthKit, HooksKit, paymentkit, StorageKit
* feat(COMPT-55): add ICacheStore port and Redis/InMemory adapters * feat(COMPT-56): add CacheModule, CacheService, and DI tokens * style: fix Prettier formatting across all files * style: fix Prettier formatting after develop merge * fix(lint): fix import order and replace any types with proper NestJS types
* feat(COMPT-55): add ICacheStore port and Redis/InMemory adapters * feat(COMPT-56): add CacheModule, CacheService, and DI tokens * style: fix Prettier formatting across all files * style: fix Prettier formatting after develop merge * fix(lint): fix import order and replace any types with proper NestJS types * feat(COMPT-57): add @Cacheable and @CacheEvict decorators with key interpolation
* feat(COMPT-55): add ICacheStore port and Redis/InMemory adapters * feat(COMPT-56): add CacheModule, CacheService, and DI tokens * style: fix Prettier formatting across all files * style: fix Prettier formatting after develop merge * fix(lint): fix import order and replace any types with proper NestJS types * feat(COMPT-57): add @Cacheable and @CacheEvict decorators with key interpolation * test(COMPT-58): add full test suite with 95%+ coverage across all adapters, service, and decorators * fix(lint): fix import/order and no-require-imports violations in spec files
…#5) * docs(COMPT-59): add README, update peer deps, create v0.1.0 changeset * style: fix Prettier formatting across all files
…clean up index exports (#6)
* fix(ci): fix SonarCloud coverage — use src/**/*.spec.ts instead of test/ * chore: set version to 0.0.1 for initial publish
* fix(ci): fix SonarCloud coverage — use src/**/*.spec.ts instead of test/ * Develop (#7) * ops: UPDATED publish workflow and dependabot PR limits * ops (ci): standardize publish validation and dependabot across all packages - Replace git tag --list strategy with package.json-driven tag validation in all 16 publish workflows; use git rev-parse to verify the exact tag exists rather than guessing the latest repo-wide tag - Update error guidance to reflect feat/** → develop → master flow - Standardize dependabot to npm-only, grouped, monthly cadence across all 16 packages; remove github-actions ecosystem updates - Add missing dependabot.yml to AuthKit-UI, ChartKit-UI, HealthKit, HooksKit, paymentkit, StorageKit * security: added CODEOWNER file for branches security * ops: updated relese check workflow# * feat(COMPT-55): add ICacheStore port and Redis/InMemory adapters (#1) * Feature/compt 56 cache module service (#2) * feat(COMPT-55): add ICacheStore port and Redis/InMemory adapters * feat(COMPT-56): add CacheModule, CacheService, and DI tokens * style: fix Prettier formatting across all files * style: fix Prettier formatting after develop merge * fix(lint): fix import order and replace any types with proper NestJS types * Feature/compt 57 cacheable cacheevict decorators (#3) * feat(COMPT-55): add ICacheStore port and Redis/InMemory adapters * feat(COMPT-56): add CacheModule, CacheService, and DI tokens * style: fix Prettier formatting across all files * style: fix Prettier formatting after develop merge * fix(lint): fix import order and replace any types with proper NestJS types * feat(COMPT-57): add @Cacheable and @CacheEvict decorators with key interpolation * Feature/compt 58 test suite (#4) * feat(COMPT-55): add ICacheStore port and Redis/InMemory adapters * feat(COMPT-56): add CacheModule, CacheService, and DI tokens * style: fix Prettier formatting across all files * style: fix Prettier formatting after develop merge * fix(lint): fix import order and replace any types with proper NestJS types * feat(COMPT-57): add @Cacheable and @CacheEvict decorators with key interpolation * test(COMPT-58): add full test suite with 95%+ coverage across all adapters, service, and decorators * fix(lint): fix import/order and no-require-imports violations in spec files * docs(COMPT-59): add README, update peer deps, create v0.1.0 changeset (#5) * docs(COMPT-59): add README, update peer deps, create v0.1.0 changeset * style: fix Prettier formatting across all files * improvement: replace KEYS with SCAN, fix @Cacheable null-return bug, clean up index exports (#6) * ci: update release check workflow * fix(ci): fix SonarCloud coverage — use src/**/*.spec.ts instead of test/ (#8) * ops: updated release check jobs ] --------- Co-authored-by: Zaiidmo <zaiidmoumnii@gmail.com> * chore: set version to 0.0.1 for initial publish --------- Co-authored-by: Zaiidmo <zaiidmoumnii@gmail.com>
* fix(ci): fix SonarCloud coverage — use src/**/*.spec.ts instead of test/ * Develop (#7) * ops: UPDATED publish workflow and dependabot PR limits * ops (ci): standardize publish validation and dependabot across all packages - Replace git tag --list strategy with package.json-driven tag validation in all 16 publish workflows; use git rev-parse to verify the exact tag exists rather than guessing the latest repo-wide tag - Update error guidance to reflect feat/** → develop → master flow - Standardize dependabot to npm-only, grouped, monthly cadence across all 16 packages; remove github-actions ecosystem updates - Add missing dependabot.yml to AuthKit-UI, ChartKit-UI, HealthKit, HooksKit, paymentkit, StorageKit * security: added CODEOWNER file for branches security * ops: updated relese check workflow# * feat(COMPT-55): add ICacheStore port and Redis/InMemory adapters (#1) * Feature/compt 56 cache module service (#2) * feat(COMPT-55): add ICacheStore port and Redis/InMemory adapters * feat(COMPT-56): add CacheModule, CacheService, and DI tokens * style: fix Prettier formatting across all files * style: fix Prettier formatting after develop merge * fix(lint): fix import order and replace any types with proper NestJS types * Feature/compt 57 cacheable cacheevict decorators (#3) * feat(COMPT-55): add ICacheStore port and Redis/InMemory adapters * feat(COMPT-56): add CacheModule, CacheService, and DI tokens * style: fix Prettier formatting across all files * style: fix Prettier formatting after develop merge * fix(lint): fix import order and replace any types with proper NestJS types * feat(COMPT-57): add @Cacheable and @CacheEvict decorators with key interpolation * Feature/compt 58 test suite (#4) * feat(COMPT-55): add ICacheStore port and Redis/InMemory adapters * feat(COMPT-56): add CacheModule, CacheService, and DI tokens * style: fix Prettier formatting across all files * style: fix Prettier formatting after develop merge * fix(lint): fix import order and replace any types with proper NestJS types * feat(COMPT-57): add @Cacheable and @CacheEvict decorators with key interpolation * test(COMPT-58): add full test suite with 95%+ coverage across all adapters, service, and decorators * fix(lint): fix import/order and no-require-imports violations in spec files * docs(COMPT-59): add README, update peer deps, create v0.1.0 changeset (#5) * docs(COMPT-59): add README, update peer deps, create v0.1.0 changeset * style: fix Prettier formatting across all files * improvement: replace KEYS with SCAN, fix @Cacheable null-return bug, clean up index exports (#6) * ci: update release check workflow * fix(ci): fix SonarCloud coverage — use src/**/*.spec.ts instead of test/ (#8) * ops: updated release check jobs ] --------- Co-authored-by: Zaiidmo <zaiidmoumnii@gmail.com> * chore: set version to 0.0.1 for initial publish --------- Co-authored-by: Zaiidmo <zaiidmoumnii@gmail.com>
* chore: align version to 1.0.0 to match master * chore: revert version to 0.0.1 as required
There was a problem hiding this comment.
Pull request overview
This PR transitions the repository from the template “ExampleKit” shape into an initial public release of @ciscode/cachekit: a NestJS caching module with pluggable store adapters (memory/Redis), an injectable CacheService, and method decorators for transparent caching/eviction.
Changes:
- Added CacheKit core:
CacheModule(dynamic module),CacheService, ports/adapters, and@Cacheable/@CacheEvictdecorators. - Added comprehensive Jest test coverage for the new module/service/adapters/utilities and increased global coverage thresholds.
- Updated packaging, docs, TypeScript/Jest alias configuration, and CI/CD workflows (release checks, publish pipeline, dependabot/codeowners).
Reviewed changes
Copilot reviewed 28 out of 30 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| tsconfig.json | Adds new path aliases for @ports/* and @adapters/* (and updates utils alias). |
| src/utils/resolve-cache-key.util.ts | Implements {n} placeholder interpolation for cache key templates. |
| src/utils/resolve-cache-key.util.spec.ts | Unit tests for cache key interpolation behavior and edge cases. |
| src/utils/cache-service-ref.ts | Adds a module-scoped singleton reference used by decorators to access CacheService. |
| src/utils/cache-service-ref.spec.ts | Tests singleton behavior and error when accessed before initialization. |
| src/services/cache.service.ts | Implements primary caching API (get/set/delete/clear/has/wrap) over ICacheStore. |
| src/services/cache.service.spec.ts | Tests core service behavior including TTL resolution and wrap() semantics. |
| src/ports/cache-store.port.ts | Defines the ICacheStore abstraction used by adapters and the service. |
| src/index.ts | Updates public exports to CacheKit module/service/decorators/adapters/tokens/port types. |
| src/decorators/cacheable.decorator.ts | Adds cache-aside method decorator with key templating and envelope handling. |
| src/decorators/cacheable.decorator.spec.ts | Tests caching behavior (hit/miss), interpolation, TTL forwarding, and sync/async support. |
| src/decorators/cache-evict.decorator.ts | Adds eviction decorator that deletes a resolved key after successful execution. |
| src/decorators/cache-evict.decorator.spec.ts | Tests eviction behavior, error path (no eviction), interpolation, sync/async support. |
| src/constants.ts | Introduces exported DI tokens (CACHE_STORE, CACHE_MODULE_OPTIONS). |
| src/cache-kit.module.ts | Implements CacheModule.register() / registerAsync() and adapter wiring logic. |
| src/cache-kit.module.spec.ts | Tests module wiring, async registration, and CacheServiceRef initialization. |
| src/adapters/redis-cache-store.adapter.ts | Adds ioredis-backed store adapter with prefix support and SCAN-based clear. |
| src/adapters/redis-cache-store.adapter.spec.ts | Tests Redis adapter using ioredis-mock (including prefix clear semantics). |
| src/adapters/in-memory-cache-store.adapter.ts | Adds Map-backed adapter with lazy TTL eviction and JSON serialization. |
| src/adapters/in-memory-cache-store.adapter.spec.ts | Tests in-memory adapter contract, TTL expiry, and parse-error behavior. |
| README.md | Rewrites README for CacheKit usage, configuration, and API overview. |
| package.json | Renames package/versioning intent, adjusts deps/peers, and adds ioredis-mock dev dependency. |
| package-lock.json | Updates lockfile to reflect dependency/peer changes and added dev deps. |
| jest.config.ts | Updates coverage thresholds and module mappers; adds forceExit. |
| .github/workflows/release-check.yml | Restructures CI into quality/test/build/sonar jobs and adds status reporting. |
| .github/workflows/publish.yml | Switches publish trigger to master push and adds tag/version validation + provenance publish. |
| .github/workflows/pr-validation.yml | Updates Node version used in PR validation. |
| .github/dependabot.yml | Adds dependabot configuration for npm dependencies. |
| .github/CODEOWNERS | Adds repository-wide codeowner rule. |
| .changeset/compt-59-v0-1-0.md | Adds changeset describing the initial public release contents. |
Comments suppressed due to low confidence (2)
README.md:287
- The README still contains the old template sections starting at "### 3. Define DTOs" (ExampleKitModule, class-validator DTOs, etc.). This conflicts with the new CacheKit API and the updated dependencies; remove or replace this leftover template content to avoid misleading consumers.
## 📄 License
MIT © [CisCode](https://github.com/CISCODE-MA)
### 3. Define DTOs
```typescript
// src/dto/create-example.dto.ts
import { IsString, IsNotEmpty } from "class-validator";
export class CreateExampleDto {
@IsString()
@IsNotEmpty()
name: string;
}
**.github/workflows/publish.yml:88**
* The workflow publishes twice (two "Publish to NPM" steps). The second publish will fail with "previously published" (or could race) and makes the job non-idempotent. Keep a single publish step (with provenance if desired).
- name: Publish to NPM
run: npm publish --access public --provenance
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish to NPM
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
</details>
| const factoryClass = (options.useClass ?? options.useExisting)!; | ||
|
|
||
| const factoryProvider: Provider = options.useClass | ||
| ? // useClass: let NestJS instantiate a new instance of this class | ||
| { provide: factoryClass, useClass: factoryClass } | ||
| : // useExisting: reuse a token already registered elsewhere in the module tree | ||
| { provide: factoryClass, useExisting: options.useExisting }; | ||
|
|
||
| return [ | ||
| factoryProvider, | ||
| { | ||
| // Call createCacheOptions() on the factory instance to get the options | ||
| provide: CACHE_MODULE_OPTIONS, | ||
| useFactory: (factory: { | ||
| createCacheOptions(): Promise<CacheModuleOptions> | CacheModuleOptions; | ||
| }) => factory.createCacheOptions(), | ||
| inject: [factoryClass], |
There was a problem hiding this comment.
createAsyncProviders()'s useExisting branch registers { provide: factoryClass, useExisting: options.useExisting } where factoryClass is options.useExisting, resulting in a self-referential provider (provide: X, useExisting: X). This will break registerAsync({ useExisting: ... }) at runtime; the factory instance should be injected from the existing token without re-providing it (or the provide token should differ).
| const factoryClass = (options.useClass ?? options.useExisting)!; | |
| const factoryProvider: Provider = options.useClass | |
| ? // useClass: let NestJS instantiate a new instance of this class | |
| { provide: factoryClass, useClass: factoryClass } | |
| : // useExisting: reuse a token already registered elsewhere in the module tree | |
| { provide: factoryClass, useExisting: options.useExisting }; | |
| return [ | |
| factoryProvider, | |
| { | |
| // Call createCacheOptions() on the factory instance to get the options | |
| provide: CACHE_MODULE_OPTIONS, | |
| useFactory: (factory: { | |
| createCacheOptions(): Promise<CacheModuleOptions> | CacheModuleOptions; | |
| }) => factory.createCacheOptions(), | |
| inject: [factoryClass], | |
| if (options.useClass) { | |
| return [ | |
| { | |
| // useClass: let NestJS instantiate a new instance of this class | |
| provide: options.useClass, | |
| useClass: options.useClass, | |
| }, | |
| { | |
| // Call createCacheOptions() on the factory instance to get the options | |
| provide: CACHE_MODULE_OPTIONS, | |
| useFactory: (factory: { | |
| createCacheOptions(): Promise<CacheModuleOptions> | CacheModuleOptions; | |
| }) => factory.createCacheOptions(), | |
| inject: [options.useClass], | |
| }, | |
| { | |
| // Build the store adapter from the resolved options | |
| provide: CACHE_STORE, | |
| useFactory: (resolvedOptions: CacheModuleOptions): ICacheStore => | |
| createStoreFromOptions(resolvedOptions), | |
| inject: [CACHE_MODULE_OPTIONS], | |
| }, | |
| ]; | |
| } | |
| return [ | |
| { | |
| // useExisting: inject a token already registered elsewhere in the module tree | |
| provide: CACHE_MODULE_OPTIONS, | |
| useFactory: (factory: { | |
| createCacheOptions(): Promise<CacheModuleOptions> | CacheModuleOptions; | |
| }) => factory.createCacheOptions(), | |
| inject: [options.useExisting!], |
| * Factory function type used by registerAsync's useFactory. | ||
| * May return the options synchronously or as a Promise. | ||
| */ | ||
| export type CacheModuleOptionsFactory = () => Promise<CacheModuleOptions> | CacheModuleOptions; | ||
|
|
||
| /** | ||
| * Asynchronous configuration options for CacheModule.registerAsync(). | ||
| * Supports three patterns: | ||
| * - useFactory — inline factory function (most common) | ||
| * - useClass — instantiate a config class per module | ||
| * - useExisting — reuse an already-provided config class | ||
| */ | ||
| export interface CacheModuleAsyncOptions { | ||
| /** Providers whose tokens are passed as arguments to useFactory. */ | ||
| inject?: Array<InjectionToken | OptionalFactoryDependency>; | ||
|
|
||
| /** Inline factory that resolves to CacheModuleOptions. */ | ||
| useFactory?: (...args: unknown[]) => Promise<CacheModuleOptions> | CacheModuleOptions; | ||
|
|
||
| /** | ||
| * Class that the module will instantiate to obtain the options. | ||
| * The class must implement CacheModuleOptionsFactory. | ||
| */ | ||
| useClass?: Type<{ createCacheOptions(): Promise<CacheModuleOptions> | CacheModuleOptions }>; | ||
|
|
||
| /** | ||
| * Re-use an already-provided token (class or value) as the options factory. | ||
| * The resolved instance must implement CacheModuleOptionsFactory. | ||
| */ | ||
| useExisting?: Type<{ createCacheOptions(): Promise<CacheModuleOptions> | CacheModuleOptions }>; |
There was a problem hiding this comment.
The CacheModuleOptionsFactory type is defined as a function signature, but useClass/useExisting expect an object with createCacheOptions(). This mismatch is confusing for consumers implementing async config classes; align the type alias and the interface expectations (and update the comments) so the contract is unambiguous.
| * Factory function type used by registerAsync's useFactory. | |
| * May return the options synchronously or as a Promise. | |
| */ | |
| export type CacheModuleOptionsFactory = () => Promise<CacheModuleOptions> | CacheModuleOptions; | |
| /** | |
| * Asynchronous configuration options for CacheModule.registerAsync(). | |
| * Supports three patterns: | |
| * - useFactory — inline factory function (most common) | |
| * - useClass — instantiate a config class per module | |
| * - useExisting — reuse an already-provided config class | |
| */ | |
| export interface CacheModuleAsyncOptions { | |
| /** Providers whose tokens are passed as arguments to useFactory. */ | |
| inject?: Array<InjectionToken | OptionalFactoryDependency>; | |
| /** Inline factory that resolves to CacheModuleOptions. */ | |
| useFactory?: (...args: unknown[]) => Promise<CacheModuleOptions> | CacheModuleOptions; | |
| /** | |
| * Class that the module will instantiate to obtain the options. | |
| * The class must implement CacheModuleOptionsFactory. | |
| */ | |
| useClass?: Type<{ createCacheOptions(): Promise<CacheModuleOptions> | CacheModuleOptions }>; | |
| /** | |
| * Re-use an already-provided token (class or value) as the options factory. | |
| * The resolved instance must implement CacheModuleOptionsFactory. | |
| */ | |
| useExisting?: Type<{ createCacheOptions(): Promise<CacheModuleOptions> | CacheModuleOptions }>; | |
| * Contract implemented by async configuration providers used with | |
| * registerAsync({ useClass }) and registerAsync({ useExisting }). | |
| * The provider may return the options synchronously or as a Promise. | |
| */ | |
| export interface CacheModuleOptionsFactory { | |
| createCacheOptions(): Promise<CacheModuleOptions> | CacheModuleOptions; | |
| } | |
| /** | |
| * Asynchronous configuration options for CacheModule.registerAsync(). | |
| * Supports three patterns: | |
| * - useFactory — inline factory function that returns CacheModuleOptions | |
| * - useClass — instantiate a config class implementing CacheModuleOptionsFactory | |
| * - useExisting — reuse an already-provided provider implementing CacheModuleOptionsFactory | |
| */ | |
| export interface CacheModuleAsyncOptions { | |
| /** Providers whose tokens are passed as arguments to useFactory. */ | |
| inject?: Array<InjectionToken | OptionalFactoryDependency>; | |
| /** Inline factory function that resolves to CacheModuleOptions. */ | |
| useFactory?: (...args: unknown[]) => Promise<CacheModuleOptions> | CacheModuleOptions; | |
| /** | |
| * Class that the module will instantiate to obtain the options. | |
| * The instantiated class must implement CacheModuleOptionsFactory. | |
| */ | |
| useClass?: Type<CacheModuleOptionsFactory>; | |
| /** | |
| * Re-use an already-provided token (class or value) as the options factory. | |
| * The resolved instance must implement CacheModuleOptionsFactory. | |
| */ | |
| useExisting?: Type<CacheModuleOptionsFactory>; |
| ttl: 300, | ||
| redis: { | ||
| client: "redis://localhost:6379", | ||
| keyPrefix: "myapp:", |
There was a problem hiding this comment.
README examples use keyPrefix: "myapp:" / default "app:", but RedisCacheStore.buildKey() always inserts its own : separator. With a trailing colon this produces double-colons (e.g. myapp::user:1). Update the docs (or normalize in code) to clarify whether keyPrefix should include a trailing :.
| keyPrefix: "myapp:", | |
| keyPrefix: "myapp", |
| - name: Lint | ||
| run: npm run lint --if-present 2>/dev/null || true | ||
|
|
||
| - name: Run tests (if present) | ||
| run: npm test --if-present | ||
| continue-on-error: false | ||
| - name: Test | ||
| run: npm test --if-present 2>/dev/null || true |
There was a problem hiding this comment.
npm run lint is effectively ignored (|| true), so the workflow may publish even when lint fails. If lint is intended to be non-blocking, make that explicit; otherwise remove the || true so publishing is gated on lint success.
| // forceExit closes open ioredis handles left by ioredis-mock after tests finish | ||
| forceExit: true, |
There was a problem hiding this comment.
forceExit: true makes Jest exit even when there are open handles, which can mask real resource leaks and flaky tests. Prefer cleaning up the ioredis-mock instances (e.g., disconnect/quit in afterEach/afterAll) and keep forceExit off if possible.
| // forceExit closes open ioredis handles left by ioredis-mock after tests finish | |
| forceExit: true, |
| // ── Cache hit ────────────────────────────────────────────────────── | ||
| // Read the envelope. null from get() means cache miss; a present envelope | ||
| // means hit — even when the stored return value was null or undefined. | ||
| const envelope = await cacheService.get<CacheEnvelope>(resolvedKey); | ||
| if (envelope !== null && isEnvelope(envelope)) { | ||
| return envelope.__v; | ||
| } | ||
|
|
||
| // ── Cache miss ───────────────────────────────────────────────────── | ||
| // Call the original method; wrap in Promise.resolve() to handle both | ||
| // sync methods (returns a plain value) and async methods (returns a Promise) | ||
| const result = await Promise.resolve(originalMethod.apply(this, args)); | ||
|
|
||
| // Wrap in an envelope before storing so null/undefined returns are | ||
| // preserved and distinguishable from a cache miss on the next call. | ||
| await cacheService.set<CacheEnvelope>(resolvedKey, { __v: result }, ttlSeconds); | ||
|
|
There was a problem hiding this comment.
@Cacheable claims it can preserve cached undefined values (see the comments and CacheEnvelope doc), but both adapters serialize via JSON.stringify. JSON.stringify({ __v: undefined }) becomes {}, so the __v property is lost and the next read is treated as a cache miss. Either disallow/normalize undefined return values (e.g., convert to null + sentinel) or change the envelope encoding so undefined can round-trip, and add a test for the undefined case.
|



Summary
Why
Checklist
npm run lintpassesnpm run typecheckpassesnpm testpassesnpm run buildpassesnpx changeset) if this affects consumersNotes