Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
afcf4dd
chore(config): add @interfaces/* and @indicators/* path aliases
SAAD-MOUMOU Mar 31, 2026
2e42a1d
chore(config): rename eslint.config.js to .mjs to fix ESM loading
SAAD-MOUMOU Mar 31, 2026
f856137
chore(git): fix husky pre-commit hook for v10 compatibility
SAAD-MOUMOU Mar 31, 2026
c74b8ae
feat(interfaces): add IHealthIndicator, HealthStatus, HealthIndicator…
SAAD-MOUMOU Mar 31, 2026
41d7af3
feat(indicators): add PostgresHealthIndicator (SELECT 1 + timeout)
SAAD-MOUMOU Mar 31, 2026
8f9af5e
test(indicators): add PostgresHealthIndicator unit tests (success/err…
SAAD-MOUMOU Mar 31, 2026
5dff0e2
feat(indicators): add RedisHealthIndicator (PING + timeout)
SAAD-MOUMOU Mar 31, 2026
af6f235
test(indicators): add RedisHealthIndicator unit tests (success/error/…
SAAD-MOUMOU Mar 31, 2026
a5d8377
feat(indicators): add HttpHealthIndicator (GET + 2xx check + timeout)
SAAD-MOUMOU Mar 31, 2026
914f341
test(indicators): add HttpHealthIndicator unit tests (2xx/non-2xx/net…
SAAD-MOUMOU Mar 31, 2026
60eedae
chore(deps): update package-lock after npm install
SAAD-MOUMOU Mar 31, 2026
7891e1e
fix(interfaces): add optional details field to HealthIndicatorResult
SAAD-MOUMOU Apr 2, 2026
b9e39f6
chore(config): set module to CommonJS and moduleResolution to Node fo…
SAAD-MOUMOU Apr 1, 2026
dbe9e52
chore(package): rename to @ciscode/health-kit
SAAD-MOUMOU Apr 1, 2026
801e648
chore(deps): update package-lock
SAAD-MOUMOU Apr 1, 2026
2e5d559
feat(indicators): add MongoHealthIndicator with ping command and timeout
SAAD-MOUMOU Apr 1, 2026
83cd1d6
test(indicators): add MongoHealthIndicator unit tests (success/error/…
SAAD-MOUMOU Apr 1, 2026
0b633fa
feat(services): add HealthService with Promise.allSettled orchestration
SAAD-MOUMOU Apr 1, 2026
10b4423
test(services): add HealthService unit tests (liveness/readiness/conc…
SAAD-MOUMOU Apr 1, 2026
4fdefba
feat(controllers): add HealthController factory (GET live/ready, plat…
SAAD-MOUMOU Apr 1, 2026
bbb1f18
test(controllers): add HealthController unit tests (200 ok / 503 Serv…
SAAD-MOUMOU Apr 1, 2026
80cdcaf
feat(module): add HealthKitModule.register() dynamic module
SAAD-MOUMOU Apr 1, 2026
e29be84
feat(exports): update public API exports for health-kit
SAAD-MOUMOU Apr 1, 2026
eff4978
fix(services): rename indicators to results in HealthCheckResult resp…
SAAD-MOUMOU Apr 2, 2026
a73cb21
feat(indicators): add createIndicator inline factory with timeout sup…
SAAD-MOUMOU Apr 2, 2026
7898821
test(indicators): add createIndicator unit tests (true/false/void/thr…
SAAD-MOUMOU Apr 2, 2026
543a07a
feat(indicators): add BaseHealthIndicator abstract class with result(…
SAAD-MOUMOU Apr 2, 2026
63c1336
test(indicators): add BaseHealthIndicator unit tests
SAAD-MOUMOU Apr 2, 2026
f8e8e01
feat(decorators): add @HealthIndicator decorator for auto-registratio…
SAAD-MOUMOU Apr 2, 2026
d4947d7
test(decorators): add @HealthIndicator decorator unit tests
SAAD-MOUMOU Apr 2, 2026
36a99ab
feat(module): extend HealthKitModule.register() with indicators[] opt…
SAAD-MOUMOU Apr 2, 2026
15c05f7
feat(exports): export createIndicator, BaseHealthIndicator, @HealthIn…
SAAD-MOUMOU Apr 2, 2026
042c48c
chore(package): update description to mention MongoDB
SAAD-MOUMOU Apr 2, 2026
f63633f
chore(indicators): remove MongoHealthIndicator
SAAD-MOUMOU Apr 2, 2026
052cecf
feat(module): make path optional with default 'health', add registerA…
SAAD-MOUMOU Apr 2, 2026
86f470c
feat(exports): remove MongoHealthIndicator, export HealthModuleAsyncO…
SAAD-MOUMOU Apr 2, 2026
8eae15f
chore(package): add @nestjs/terminus peer dep, fix description
SAAD-MOUMOU Apr 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,4 +1 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npx lint-staged
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hook no longer has a shebang or Husky bootstrap (.husky.sh). Git executes hooks as programs; without #!/usr/bin/env sh this commonly fails with an "exec format" error, and without the Husky bootstrap you may lose environment setup. Restore the standard Husky header and then run npx lint-staged.

Copilot uses AI. Check for mistakes.
80 changes: 0 additions & 80 deletions eslint.config.js

This file was deleted.

76 changes: 57 additions & 19 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -1,42 +1,80 @@
// @ts-check
import eslint from "@eslint/js";
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
import globals from "globals";
import importPlugin from "eslint-plugin-import";
import tseslint from "typescript-eslint";

export default tseslint.config(
export default [
{
ignores: ["eslint.config.mjs"],
ignores: [
"dist/**",
"coverage/**",
"node_modules/**",
// Ignore all example files for CSR architecture
"src/example-kit.*",
"src/controllers/example.controller.ts",
"src/services/example.service.ts",
"src/entities/example.entity.ts",
"src/repositories/example.repository.ts",
"src/guards/example.guard.ts",
"src/decorators/example.decorator.ts",
"src/dto/create-example.dto.ts",
"src/dto/update-example.dto.ts",
],
},

eslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
eslintPluginPrettierRecommended,

// TypeScript ESLint (includes recommended rules)
...tseslint.configs.recommended,

// Base TS rules (all TS files)
{
files: ["**/*.ts"],
languageOptions: {
globals: {
...globals.node,
...globals.jest,
},
sourceType: "commonjs",
parser: tseslint.parser,
parserOptions: {
projectService: true,
project: "./tsconfig.eslint.json",
tsconfigRootDir: import.meta.dirname,
ecmaVersion: "latest",
sourceType: "module",
},
globals: { ...globals.node, ...globals.jest },
},
plugins: {
"@typescript-eslint": tseslint.plugin,
import: importPlugin,
},
rules: {
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
"@typescript-eslint/consistent-type-imports": ["error", { prefer: "type-imports" }],

"import/no-duplicates": "error",
"import/order": [
"error",
{
"newlines-between": "always",
alphabetize: { order: "asc", caseInsensitive: true },
},
],
},
},

// Architecture boundary: core must not import Nest
{
files: ["src/core/**/*.ts"],
rules: {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-floating-promises": "warn",
"@typescript-eslint/no-unsafe-argument": "warn",
"@typescript-eslint/no-unused-vars": [
"no-restricted-imports": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
patterns: [
{
group: ["@nestjs/*"],
message: "Do not import NestJS in core/. Keep core framework-free.",
},
],
},
],
"no-unused-vars": "off",
},
},
);
];
2 changes: 2 additions & 0 deletions jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ const config: Config = {
"^@filters/(.*)$": "<rootDir>/src/filters/$1",
"^@middleware/(.*)$": "<rootDir>/src/middleware/$1",
"^@utils/(.*)$": "<rootDir>/src/utils/$1",
"^@interfaces/(.*)$": "<rootDir>/src/interfaces/$1",
"^@indicators/(.*)$": "<rootDir>/src/indicators/$1",
},
};

Expand Down
File renamed without changes.
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 8 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@ciscode/nestjs-developerkit",
"name": "@ciscode/health-kit",
"version": "1.0.0",
"description": "Template for NestJS developer kits (npm packages).",
"description": "NestJS health-check module — liveness & readiness probes with built-in PostgreSQL, Redis, and HTTP indicators.",
"author": "CisCode",
Comment on lines 1 to 5
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an existing changeset under .changeset/ that still targets the old package name (@ciscode/nestjs-developerkit). With the package rename to @ciscode/health-kit, changesets should be updated/added so versioning/changelog automation continues to work for the new package name.

Copilot uses AI. Check for mistakes.
"publishConfig": {
"access": "public"
Expand Down Expand Up @@ -45,10 +45,15 @@
"peerDependencies": {
"@nestjs/common": "^10 || ^11",
"@nestjs/core": "^10 || ^11",
"@nestjs/platform-express": "^10 || ^11",
"@nestjs/terminus": "^10 || ^11",
"reflect-metadata": "^0.2.2",
"rxjs": "^7"
},
"peerDependenciesMeta": {
"@nestjs/terminus": {
"optional": true
}
},
Comment on lines +48 to +56
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nestjs/terminus is added as an (optional) peer dependency, but there are no imports/usages of it in this package. Unused peer deps can confuse consumers and generate unnecessary install warnings; either remove it or add the intended integration that requires it.

Suggested change
"@nestjs/terminus": "^10 || ^11",
"reflect-metadata": "^0.2.2",
"rxjs": "^7"
},
"peerDependenciesMeta": {
"@nestjs/terminus": {
"optional": true
}
},
"reflect-metadata": "^0.2.2",
"rxjs": "^7"
},

Copilot uses AI. Check for mistakes.
"dependencies": {
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1"
Expand Down
62 changes: 62 additions & 0 deletions src/controllers/health.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { ServiceUnavailableException } from "@nestjs/common";
import type { TestingModule } from "@nestjs/testing";
import { Test } from "@nestjs/testing";
import { HealthService } from "@services/health.service";
import type { HealthCheckResult } from "@services/health.service";

import { createHealthController } from "./health.controller";

// ── Helpers ──────────────────────────────────────────────────────────────────

const makeService = (liveness: "ok" | "error", readiness: "ok" | "error") =>
({
checkLiveness: jest.fn().mockResolvedValue({ status: liveness, indicators: [] }),
checkReadiness: jest.fn().mockResolvedValue({ status: readiness, indicators: [] }),
Comment on lines +13 to +14
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This spec constructs mock HealthService results with an indicators field, but HealthCheckResult exported by HealthService uses results. Align the mocked return shape with the exported type to avoid mismatched behavior and to keep the tests meaningful.

Suggested change
checkLiveness: jest.fn().mockResolvedValue({ status: liveness, indicators: [] }),
checkReadiness: jest.fn().mockResolvedValue({ status: readiness, indicators: [] }),
checkLiveness: jest.fn().mockResolvedValue({ status: liveness, results: [] }),
checkReadiness: jest.fn().mockResolvedValue({ status: readiness, results: [] }),

Copilot uses AI. Check for mistakes.
}) as unknown as HealthService;

interface HealthControllerInstance {
live(): Promise<HealthCheckResult>;
ready(): Promise<HealthCheckResult>;
}

async function buildController(
liveness: "ok" | "error",
readiness: "ok" | "error",
): Promise<HealthControllerInstance> {
const HealthController = createHealthController("health");
const moduleRef: TestingModule = await Test.createTestingModule({
controllers: [HealthController],
providers: [{ provide: HealthService, useValue: makeService(liveness, readiness) }],
}).compile();
return moduleRef.get<HealthControllerInstance>(HealthController as never);
}

// ── Tests ─────────────────────────────────────────────────────────────────────

describe("HealthController (factory)", () => {
describe("GET /health/live", () => {
it("returns result when all liveness indicators are up", async () => {
const controller = await buildController("ok", "ok");
const result = await controller.live();
expect(result.status).toBe("ok");
});

it("throws ServiceUnavailableException (503) when any liveness indicator is down", async () => {
const controller = await buildController("error", "ok");
await expect(controller.live()).rejects.toThrow(ServiceUnavailableException);
});
});

describe("GET /health/ready", () => {
it("returns result when all readiness indicators are up", async () => {
const controller = await buildController("ok", "ok");
const result = await controller.ready();
expect(result.status).toBe("ok");
});

it("throws ServiceUnavailableException (503) when any readiness indicator is down", async () => {
const controller = await buildController("ok", "error");
await expect(controller.ready()).rejects.toThrow(ServiceUnavailableException);
});
});
});
43 changes: 43 additions & 0 deletions src/controllers/health.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {
Controller,
Get,
HttpCode,
HttpStatus,
ServiceUnavailableException,
Type,
} from "@nestjs/common";
Comment on lines +7 to +8
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@typescript-eslint/consistent-type-imports is enabled in this repo; Type is only used as a type here, so importing it as a value will violate lint rules. Switch to import type { Type } ... (and keep runtime imports separate).

Suggested change
Type,
} from "@nestjs/common";
} from "@nestjs/common";
import type { Type } from "@nestjs/common";

Copilot uses AI. Check for mistakes.
import { HealthService } from "@services/health.service";
import type { HealthCheckResult } from "@services/health.service";

/**
* Factory that returns a NestJS controller class configured with the
* caller-supplied `path` prefix (e.g. `"health"`).
*
* Platform-agnostic — works with Express and Fastify.
* Returns 200 when all indicators are "up",
* throws ServiceUnavailableException (503) when any indicator is "down".
*/
export function createHealthController(path: string): Type<unknown> {
@Controller(path)
class HealthController {
constructor(private readonly healthService: HealthService) {}

@Get("live")
@HttpCode(HttpStatus.OK)
async live(): Promise<HealthCheckResult> {
const result = await this.healthService.checkLiveness();
if (result.status === "error") throw new ServiceUnavailableException(result);
return result;
}

@Get("ready")
@HttpCode(HttpStatus.OK)
async ready(): Promise<HealthCheckResult> {
const result = await this.healthService.checkReadiness();
if (result.status === "error") throw new ServiceUnavailableException(result);
return result;
}
}

return HealthController;
}
36 changes: 36 additions & 0 deletions src/decorators/health-indicator.decorator.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import "reflect-metadata";
import { HEALTH_INDICATOR_METADATA, HealthIndicator } from "./health-indicator.decorator";

class SomeIndicator {}

Check warning on line 4 in src/decorators/health-indicator.decorator.spec.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected empty class.

See more on https://sonarcloud.io/project/issues?id=CISCODE-MA_HealthKit&issues=AZ1ObvMI-_giqCLYy8KR&open=AZ1ObvMI-_giqCLYy8KR&pullRequest=16
class AnotherIndicator {}

Check warning on line 5 in src/decorators/health-indicator.decorator.spec.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected empty class.

See more on https://sonarcloud.io/project/issues?id=CISCODE-MA_HealthKit&issues=AZ1ObvMI-_giqCLYy8KS&open=AZ1ObvMI-_giqCLYy8KS&pullRequest=16
class UndecotratedIndicator {}

Check warning on line 6 in src/decorators/health-indicator.decorator.spec.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Unexpected empty class.

See more on https://sonarcloud.io/project/issues?id=CISCODE-MA_HealthKit&issues=AZ1ObvMI-_giqCLYy8KT&open=AZ1ObvMI-_giqCLYy8KT&pullRequest=16

Comment on lines +4 to +7
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in class name: UndecotratedIndicatorUndecoratedIndicator. While test-only, this makes the code harder to read/search.

Copilot uses AI. Check for mistakes.
@HealthIndicator("liveness")
class LivenessIndicator extends SomeIndicator {}

@HealthIndicator("readiness")
class ReadinessIndicator extends AnotherIndicator {}

describe("@HealthIndicator decorator", () => {
it("attaches liveness metadata to the target class", () => {
const scope = Reflect.getMetadata(HEALTH_INDICATOR_METADATA, LivenessIndicator);
expect(scope).toBe("liveness");
});

it("attaches readiness metadata to the target class", () => {
const scope = Reflect.getMetadata(HEALTH_INDICATOR_METADATA, ReadinessIndicator);
expect(scope).toBe("readiness");
});

it("returns undefined for undecorated classes", () => {
const scope = Reflect.getMetadata(HEALTH_INDICATOR_METADATA, UndecotratedIndicator);
expect(scope).toBeUndefined();
});

it("does not affect other classes when decorating one", () => {
const livScope = Reflect.getMetadata(HEALTH_INDICATOR_METADATA, LivenessIndicator);
const readScope = Reflect.getMetadata(HEALTH_INDICATOR_METADATA, ReadinessIndicator);
expect(livScope).toBe("liveness");
expect(readScope).toBe("readiness");
});
});
Loading
Loading