create-nexst-monorepo 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1727 -0
- package/package.json +37 -0
- package/template/.agents/plugins/marketplace.json +21 -0
- package/template/.claude/settings.json +6 -0
- package/template/.claude/skills/frontend-design/LICENSE.txt +177 -0
- package/template/.claude/skills/frontend-design/SKILL.md +154 -0
- package/template/.claude/skills/theme-factory/SKILL.md +95 -0
- package/template/.claude/skills/theme-factory/references/schema.md +104 -0
- package/template/.claude/skills/theme-factory/scripts/theme.mjs +261 -0
- package/template/.dockerignore +43 -0
- package/template/.editorconfig +10 -0
- package/template/.github/workflows/ci.yaml +289 -0
- package/template/.github/workflows/publish-app.yaml +66 -0
- package/template/.github/workflows/publish.yaml +148 -0
- package/template/.github/workflows/snyk.yaml +34 -0
- package/template/.vscode/settings.json +8 -0
- package/template/AGENTS.md +42 -0
- package/template/Dockerfile.base +9 -0
- package/template/README.md +54 -0
- package/template/_gitignore +10 -0
- package/template/apps/api/.env.example +32 -0
- package/template/apps/api/AGENTS.md +44 -0
- package/template/apps/api/Dockerfile +69 -0
- package/template/apps/api/_gitignore +8 -0
- package/template/apps/api/knip.config.ts +22 -0
- package/template/apps/api/kustomize/base/application.properties +16 -0
- package/template/apps/api/kustomize/base/deployment.yaml +94 -0
- package/template/apps/api/kustomize/base/kustomization.yaml +17 -0
- package/template/apps/api/kustomize/base/service.yaml +10 -0
- package/template/apps/api/kustomize/overlays/prod/kustomization.yaml +17 -0
- package/template/apps/api/kustomize/overlays/staging/application.properties +1 -0
- package/template/apps/api/kustomize/overlays/staging/kustomization.yaml +21 -0
- package/template/apps/api/kustomize/secret.keys +14 -0
- package/template/apps/api/oxfmt.config.ts +8 -0
- package/template/apps/api/oxlint.config.ts +107 -0
- package/template/apps/api/package.json +79 -0
- package/template/apps/api/prisma/migrations/20260927000000_init/migration.sql +113 -0
- package/template/apps/api/prisma/migrations/migration_lock.toml +3 -0
- package/template/apps/api/prisma/schema.prisma +113 -0
- package/template/apps/api/prisma.config.ts +7 -0
- package/template/apps/api/scripts/auth.cli.ts +19 -0
- package/template/apps/api/scripts/seal-secret.sh +171 -0
- package/template/apps/api/sonar-project.properties +9 -0
- package/template/apps/api/src/app-info/app-info.dto.ts +10 -0
- package/template/apps/api/src/app-info/app-info.module.ts +7 -0
- package/template/apps/api/src/app-info/app-info.resolver.spec.ts +14 -0
- package/template/apps/api/src/app-info/app-info.resolver.ts +21 -0
- package/template/apps/api/src/app.module.spec.ts +93 -0
- package/template/apps/api/src/app.module.ts +77 -0
- package/template/apps/api/src/auth/auth.guard.spec.ts +90 -0
- package/template/apps/api/src/auth/auth.guard.ts +97 -0
- package/template/apps/api/src/auth/auth.ts +129 -0
- package/template/apps/api/src/auth/decorator/allow-anonymous.decorator.ts +17 -0
- package/template/apps/api/src/auth/decorator/is-email-verified.decorator.spec.ts +41 -0
- package/template/apps/api/src/auth/decorator/is-email-verified.decorator.ts +20 -0
- package/template/apps/api/src/auth/decorator/roles.decorator.ts +27 -0
- package/template/apps/api/src/auth/testing/make-session.ts +31 -0
- package/template/apps/api/src/auth/types/auth.types.ts +27 -0
- package/template/apps/api/src/bootstrap/register-graphql-enums.ts +12 -0
- package/template/apps/api/src/bootstrap/security.ts +24 -0
- package/template/apps/api/src/bootstrap/swagger.ts +28 -0
- package/template/apps/api/src/config/app.config.spec.ts +22 -0
- package/template/apps/api/src/config/app.config.ts +122 -0
- package/template/apps/api/src/config/graphql-depth-limit.ts +103 -0
- package/template/apps/api/src/config/graphql.config.ts +77 -0
- package/template/apps/api/src/config/resources/app.yaml +29 -0
- package/template/apps/api/src/errors/app-parse-uuid.pipe.ts +27 -0
- package/template/apps/api/src/errors/app.exception.ts +47 -0
- package/template/apps/api/src/errors/error-code.enum.ts +54 -0
- package/template/apps/api/src/errors/http-status-fallback-code.util.ts +27 -0
- package/template/apps/api/src/errors/validation-exception.factory.ts +43 -0
- package/template/apps/api/src/health/health.controller.spec.ts +82 -0
- package/template/apps/api/src/health/health.controller.ts +57 -0
- package/template/apps/api/src/health/health.module.ts +12 -0
- package/template/apps/api/src/logging/context-logger.ts +18 -0
- package/template/apps/api/src/logging/logger.config.ts +23 -0
- package/template/apps/api/src/logging/logging.module.ts +22 -0
- package/template/apps/api/src/mail/mail.module.ts +12 -0
- package/template/apps/api/src/mail/mail.service.spec.ts +47 -0
- package/template/apps/api/src/mail/mail.service.ts +123 -0
- package/template/apps/api/src/main.ts +41 -0
- package/template/apps/api/src/prisma/prisma.module.ts +12 -0
- package/template/apps/api/src/prisma/prisma.service.spec.ts +136 -0
- package/template/apps/api/src/prisma/prisma.service.ts +138 -0
- package/template/apps/api/src/queue/dto/enqueue-example.input.ts +11 -0
- package/template/apps/api/src/queue/example-queue.resolver.ts +14 -0
- package/template/apps/api/src/queue/example-queue.service.spec.ts +21 -0
- package/template/apps/api/src/queue/example-queue.service.ts +32 -0
- package/template/apps/api/src/queue/queue.module.ts +27 -0
- package/template/apps/api/src/r2/r2.module.ts +12 -0
- package/template/apps/api/src/r2/r2.service.spec.ts +117 -0
- package/template/apps/api/src/r2/r2.service.ts +131 -0
- package/template/apps/api/src/user/dto/user.dto.ts +26 -0
- package/template/apps/api/src/user/user.module.ts +10 -0
- package/template/apps/api/src/user/user.resolver.spec.ts +18 -0
- package/template/apps/api/src/user/user.resolver.ts +24 -0
- package/template/apps/api/src/utils/catch-everything-exception.filter.spec.ts +292 -0
- package/template/apps/api/src/utils/catch-everything-exception.filter.ts +70 -0
- package/template/apps/api/src/utils/error-handler.util.spec.ts +20 -0
- package/template/apps/api/src/utils/error-handler.util.ts +12 -0
- package/template/apps/api/src/utils/escape-like-pattern.util.spec.ts +29 -0
- package/template/apps/api/src/utils/escape-like-pattern.util.ts +11 -0
- package/template/apps/api/src/utils/pagination.input.ts +20 -0
- package/template/apps/api/src/utils/prisma-exception.filter.spec.ts +186 -0
- package/template/apps/api/src/utils/prisma-exception.filter.ts +45 -0
- package/template/apps/api/src/utils/prisma-graphql-exception.filter.spec.ts +216 -0
- package/template/apps/api/src/utils/prisma-graphql-exception.filter.ts +43 -0
- package/template/apps/api/tests/app.e2e-spec.ts +35 -0
- package/template/apps/api/tests/auth.e2e-spec.ts +38 -0
- package/template/apps/api/tests/helpers/api.ts +27 -0
- package/template/apps/api/tsconfig.json +11 -0
- package/template/apps/api/vitest.config.e2e.ts +16 -0
- package/template/apps/api/vitest.config.ts +40 -0
- package/template/apps/web/.env.example +7 -0
- package/template/apps/web/.storybook/decorators.tsx +38 -0
- package/template/apps/web/.storybook/main.ts +17 -0
- package/template/apps/web/.storybook/preview.ts +38 -0
- package/template/apps/web/.storybook/vitest.setup.ts +5 -0
- package/template/apps/web/AGENTS.md +29 -0
- package/template/apps/web/Dockerfile +49 -0
- package/template/apps/web/_gitignore +16 -0
- package/template/apps/web/knip.config.ts +15 -0
- package/template/apps/web/kustomize/base/application.properties +5 -0
- package/template/apps/web/kustomize/base/deployment.yaml +60 -0
- package/template/apps/web/kustomize/base/kustomization.yaml +17 -0
- package/template/apps/web/kustomize/base/service.yaml +10 -0
- package/template/apps/web/kustomize/overlays/prod/kustomization.yaml +12 -0
- package/template/apps/web/kustomize/overlays/staging/application.properties +2 -0
- package/template/apps/web/kustomize/overlays/staging/kustomization.yaml +18 -0
- package/template/apps/web/messages/de.json +124 -0
- package/template/apps/web/messages/en.json +124 -0
- package/template/apps/web/next-env.d.ts +7 -0
- package/template/apps/web/next.config.ts +39 -0
- package/template/apps/web/oxfmt.config.ts +9 -0
- package/template/apps/web/oxlint.config.ts +36 -0
- package/template/apps/web/package.json +80 -0
- package/template/apps/web/playwright.config.ts +34 -0
- package/template/apps/web/postcss.config.mjs +7 -0
- package/template/apps/web/sonar-project.properties +9 -0
- package/template/apps/web/src/app/[locale]/(auth)/recovery/page.tsx +22 -0
- package/template/apps/web/src/app/[locale]/(auth)/reset-password/page.tsx +29 -0
- package/template/apps/web/src/app/[locale]/(auth)/sign-in/page.tsx +24 -0
- package/template/apps/web/src/app/[locale]/(auth)/sign-up/page.tsx +24 -0
- package/template/apps/web/src/app/[locale]/(auth)/verify/page.tsx +24 -0
- package/template/apps/web/src/app/[locale]/dashboard/page.tsx +34 -0
- package/template/apps/web/src/app/[locale]/layout.tsx +60 -0
- package/template/apps/web/src/app/[locale]/page.tsx +23 -0
- package/template/apps/web/src/app/api/health/route.ts +3 -0
- package/template/apps/web/src/app/global-error.tsx +19 -0
- package/template/apps/web/src/app/globals.css +98 -0
- package/template/apps/web/src/app/icon.svg +1 -0
- package/template/apps/web/src/app/robots.ts +15 -0
- package/template/apps/web/src/app/sitemap.ts +24 -0
- package/template/apps/web/src/components/alert/alert.stories.tsx +25 -0
- package/template/apps/web/src/components/alert/alert.tsx +50 -0
- package/template/apps/web/src/components/avatar/avatar.stories.tsx +116 -0
- package/template/apps/web/src/components/avatar/avatar.tsx +88 -0
- package/template/apps/web/src/components/button/button-variants.ts +28 -0
- package/template/apps/web/src/components/button/button.stories.tsx +33 -0
- package/template/apps/web/src/components/button/button.tsx +32 -0
- package/template/apps/web/src/components/checkbox/checkbox.stories.tsx +312 -0
- package/template/apps/web/src/components/checkbox/checkbox.tsx +38 -0
- package/template/apps/web/src/components/dialog/danger-panel.stories.tsx +41 -0
- package/template/apps/web/src/components/dialog/danger-panel.tsx +32 -0
- package/template/apps/web/src/components/dialog/dialog.stories.tsx +239 -0
- package/template/apps/web/src/components/dialog/dialog.tsx +136 -0
- package/template/apps/web/src/components/dialog-buttons.stories.tsx +55 -0
- package/template/apps/web/src/components/dialog-buttons.tsx +48 -0
- package/template/apps/web/src/components/drawer/drawer.stories.tsx +251 -0
- package/template/apps/web/src/components/drawer/drawer.tsx +120 -0
- package/template/apps/web/src/components/dropdown-menu/dropdown-menu.stories.tsx +329 -0
- package/template/apps/web/src/components/dropdown-menu/dropdown-menu.tsx +263 -0
- package/template/apps/web/src/components/empty-state.stories.tsx +49 -0
- package/template/apps/web/src/components/empty-state.tsx +33 -0
- package/template/apps/web/src/components/field-group.stories.tsx +47 -0
- package/template/apps/web/src/components/field-group.tsx +20 -0
- package/template/apps/web/src/components/field-label.stories.tsx +34 -0
- package/template/apps/web/src/components/field-label.tsx +28 -0
- package/template/apps/web/src/components/icons.tsx +25 -0
- package/template/apps/web/src/components/input/input-otp.stories.tsx +86 -0
- package/template/apps/web/src/components/input/input-otp.tsx +184 -0
- package/template/apps/web/src/components/input/input.stories.tsx +35 -0
- package/template/apps/web/src/components/input/input.tsx +180 -0
- package/template/apps/web/src/components/popover/popover.stories.tsx +479 -0
- package/template/apps/web/src/components/popover/popover.tsx +45 -0
- package/template/apps/web/src/components/responsive-dialog/responsive-dialog.stories.tsx +307 -0
- package/template/apps/web/src/components/responsive-dialog/responsive-dialog.tsx +96 -0
- package/template/apps/web/src/components/searchbar/searchbar.stories.tsx +59 -0
- package/template/apps/web/src/components/searchbar/searchbar.tsx +64 -0
- package/template/apps/web/src/components/select/select.stories.tsx +293 -0
- package/template/apps/web/src/components/select/select.tsx +445 -0
- package/template/apps/web/src/components/skeleton/skeleton.stories.tsx +76 -0
- package/template/apps/web/src/components/skeleton/skeleton.tsx +13 -0
- package/template/apps/web/src/components/spinner/spinner.tsx +18 -0
- package/template/apps/web/src/components/switch/switch.stories.tsx +71 -0
- package/template/apps/web/src/components/switch/switch.tsx +35 -0
- package/template/apps/web/src/components/tabs/tabs.stories.tsx +209 -0
- package/template/apps/web/src/components/tabs/tabs.tsx +69 -0
- package/template/apps/web/src/components/tabs/url-tabs.stories.tsx +68 -0
- package/template/apps/web/src/components/tabs/url-tabs.tsx +42 -0
- package/template/apps/web/src/components/textarea/textarea.stories.tsx +144 -0
- package/template/apps/web/src/components/textarea/textarea.tsx +115 -0
- package/template/apps/web/src/components/toast/toaster.tsx +38 -0
- package/template/apps/web/src/components/tooltip/tooltip.stories.tsx +237 -0
- package/template/apps/web/src/components/tooltip/tooltip.tsx +49 -0
- package/template/apps/web/src/config/csp.ts +59 -0
- package/template/apps/web/src/config/routes.ts +14 -0
- package/template/apps/web/src/config/site.ts +5 -0
- package/template/apps/web/src/env.ts +15 -0
- package/template/apps/web/src/features/auth/auth-card.tsx +20 -0
- package/template/apps/web/src/features/auth/auth-divider.tsx +14 -0
- package/template/apps/web/src/features/auth/google-auth-button.tsx +49 -0
- package/template/apps/web/src/features/auth/google-one-tap.tsx +23 -0
- package/template/apps/web/src/features/auth/lib/auth-error.test.ts +22 -0
- package/template/apps/web/src/features/auth/lib/auth-error.ts +16 -0
- package/template/apps/web/src/features/auth/lib/return-to.test.ts +41 -0
- package/template/apps/web/src/features/auth/lib/return-to.ts +19 -0
- package/template/apps/web/src/features/auth/lib/validator.test.ts +45 -0
- package/template/apps/web/src/features/auth/lib/validator.ts +49 -0
- package/template/apps/web/src/features/auth/passkey-settings.tsx +106 -0
- package/template/apps/web/src/features/auth/recovery-form.tsx +61 -0
- package/template/apps/web/src/features/auth/resend-verification-button.tsx +36 -0
- package/template/apps/web/src/features/auth/reset-password-form.tsx +62 -0
- package/template/apps/web/src/features/auth/sign-in-form.stories.tsx +22 -0
- package/template/apps/web/src/features/auth/sign-in-form.tsx +116 -0
- package/template/apps/web/src/features/auth/sign-out-button.tsx +26 -0
- package/template/apps/web/src/features/auth/sign-up-form.tsx +107 -0
- package/template/apps/web/src/features/shared/provider.tsx +16 -0
- package/template/apps/web/src/features/shared/site-header.tsx +48 -0
- package/template/apps/web/src/features/shared/theme-toggle.tsx +24 -0
- package/template/apps/web/src/i18n/navigation.ts +5 -0
- package/template/apps/web/src/i18n/request.ts +21 -0
- package/template/apps/web/src/i18n/routing.ts +6 -0
- package/template/apps/web/src/lib/auth-client.ts +19 -0
- package/template/apps/web/src/lib/auth-server.ts +46 -0
- package/template/apps/web/src/lib/auth.ts +8 -0
- package/template/apps/web/src/lib/codegen.ts +30 -0
- package/template/apps/web/src/lib/graphql-client.ts +29 -0
- package/template/apps/web/src/lib/graphql-server.ts +28 -0
- package/template/apps/web/src/lib/graphql-utils.test.ts +32 -0
- package/template/apps/web/src/lib/graphql-utils.ts +76 -0
- package/template/apps/web/src/lib/hooks/use-media-query.ts +21 -0
- package/template/apps/web/src/lib/metadata.ts +17 -0
- package/template/apps/web/src/lib/network-error.test.ts +30 -0
- package/template/apps/web/src/lib/network-error.ts +17 -0
- package/template/apps/web/src/lib/proxy-utils.ts +42 -0
- package/template/apps/web/src/lib/query-client.ts +29 -0
- package/template/apps/web/src/lib/utils.ts +5 -0
- package/template/apps/web/src/proxy.ts +57 -0
- package/template/apps/web/test-results/.last-run.json +4 -0
- package/template/apps/web/tests/auth.spec.ts +17 -0
- package/template/apps/web/tests/csp-theme-script.spec.ts +37 -0
- package/template/apps/web/tests/home.spec.ts +25 -0
- package/template/apps/web/tsconfig.build.json +4 -0
- package/template/apps/web/tsconfig.json +28 -0
- package/template/apps/web/vitest.config.ts +40 -0
- package/template/apps/web/vitest.shims.d.ts +1 -0
- package/template/apps/worker/.env.example +4 -0
- package/template/apps/worker/AGENTS.md +11 -0
- package/template/apps/worker/Dockerfile +35 -0
- package/template/apps/worker/knip.config.ts +3 -0
- package/template/apps/worker/kustomize/base/application.properties +4 -0
- package/template/apps/worker/kustomize/base/deployment.yaml +62 -0
- package/template/apps/worker/kustomize/base/kustomization.yaml +16 -0
- package/template/apps/worker/kustomize/overlays/prod/kustomization.yaml +12 -0
- package/template/apps/worker/kustomize/overlays/staging/application.properties +1 -0
- package/template/apps/worker/kustomize/overlays/staging/kustomization.yaml +18 -0
- package/template/apps/worker/oxfmt.config.ts +8 -0
- package/template/apps/worker/oxlint.config.ts +15 -0
- package/template/apps/worker/package.json +33 -0
- package/template/apps/worker/sonar-project.properties +8 -0
- package/template/apps/worker/src/env.ts +13 -0
- package/template/apps/worker/src/lib/graceful-shutdown.ts +27 -0
- package/template/apps/worker/src/lib/logger.ts +20 -0
- package/template/apps/worker/src/main.ts +17 -0
- package/template/apps/worker/src/processors/example-processor.ts +15 -0
- package/template/apps/worker/src/worker/redis.ts +19 -0
- package/template/apps/worker/src/worker/worker.ts +37 -0
- package/template/apps/worker/tests/example-processor.test.ts +13 -0
- package/template/apps/worker/tests/graceful-shutdown.test.ts +34 -0
- package/template/apps/worker/tsconfig.json +4 -0
- package/template/apps/worker/vitest.config.ts +13 -0
- package/template/bun.lock +3053 -0
- package/template/bunfig.toml +2 -0
- package/template/compose.yaml +53 -0
- package/template/docker-bake.hcl +86 -0
- package/template/package.json +47 -0
- package/template/packages/lint/knip.ts +40 -0
- package/template/packages/lint/oxfmt.ts +8 -0
- package/template/packages/lint/oxlint.ts +136 -0
- package/template/packages/lint/package.json +13 -0
- package/template/packages/queues/oxfmt.config.ts +8 -0
- package/template/packages/queues/oxlint.config.ts +7 -0
- package/template/packages/queues/package.json +15 -0
- package/template/packages/queues/src/example-job.ts +11 -0
- package/template/packages/queues/src/index.ts +3 -0
- package/template/packages/queues/src/queue-names.ts +2 -0
- package/template/packages/queues/src/redis-connection.ts +27 -0
- package/template/packages/queues/tsconfig.json +4 -0
- package/template/packages/tsconfig/base.json +23 -0
- package/template/packages/tsconfig/node.json +11 -0
- package/template/packages/tsconfig/package.json +4 -0
- package/template/turbo.json +50 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { HealthCheckService, MemoryHealthIndicator } from "@nestjs/terminus";
|
|
2
|
+
import type { TestingModule } from "@nestjs/testing";
|
|
3
|
+
import { Test } from "@nestjs/testing";
|
|
4
|
+
import { vi } from "vitest";
|
|
5
|
+
import { HealthController } from "#src/health/health.controller.js";
|
|
6
|
+
import { PrismaService } from "#src/prisma/prisma.service.js";
|
|
7
|
+
|
|
8
|
+
describe("healthController", () => {
|
|
9
|
+
let controller: HealthController;
|
|
10
|
+
|
|
11
|
+
const prismaMock = {
|
|
12
|
+
isHealthy: vi.fn<() => Promise<boolean>>(),
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const healthCheckServiceMock = {
|
|
16
|
+
check: vi.fn(async (indicators: Array<() => Promise<Record<string, unknown>>>) => {
|
|
17
|
+
const results = await Promise.all(indicators.map((function_) => function_()));
|
|
18
|
+
const combined = {};
|
|
19
|
+
for (const result of results) {
|
|
20
|
+
Object.assign(combined, result);
|
|
21
|
+
}
|
|
22
|
+
return combined;
|
|
23
|
+
}),
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const memoryIndicatorMock = {};
|
|
27
|
+
|
|
28
|
+
beforeEach(async () => {
|
|
29
|
+
vi.clearAllMocks();
|
|
30
|
+
|
|
31
|
+
const module: TestingModule = await Test.createTestingModule({
|
|
32
|
+
controllers: [HealthController],
|
|
33
|
+
providers: [
|
|
34
|
+
{ provide: HealthCheckService, useValue: healthCheckServiceMock },
|
|
35
|
+
{ provide: MemoryHealthIndicator, useValue: memoryIndicatorMock },
|
|
36
|
+
{ provide: PrismaService, useValue: prismaMock },
|
|
37
|
+
],
|
|
38
|
+
}).compile();
|
|
39
|
+
|
|
40
|
+
controller = module.get<HealthController>(HealthController);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should be defined", () => {
|
|
44
|
+
expect(controller).toBeDefined();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe("liveness", () => {
|
|
48
|
+
it("returns ok with timestamp and uptime", () => {
|
|
49
|
+
const uptimeSpy = vi.spyOn(process, "uptime").mockReturnValue(123.456);
|
|
50
|
+
const res = controller.liveness();
|
|
51
|
+
|
|
52
|
+
expect(res.status).toBe("ok");
|
|
53
|
+
expect(typeof res.timestamp).toBe("string");
|
|
54
|
+
expect(() => new Date(res.timestamp)).not.toThrow();
|
|
55
|
+
expect(res.uptime).toBe(123);
|
|
56
|
+
|
|
57
|
+
uptimeSpy.mockRestore();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("check", () => {
|
|
62
|
+
it("returns database: up when prisma is healthy", async () => {
|
|
63
|
+
prismaMock.isHealthy.mockResolvedValueOnce(true);
|
|
64
|
+
const res = await controller.check();
|
|
65
|
+
|
|
66
|
+
expect(healthCheckServiceMock.check).toHaveBeenCalledTimes(1);
|
|
67
|
+
expect(res).toEqual({ database: { status: "up" } });
|
|
68
|
+
expect(prismaMock.isHealthy).toHaveBeenCalledTimes(1);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("returns database: down when prisma is NOT healthy", async () => {
|
|
72
|
+
prismaMock.isHealthy.mockResolvedValueOnce(false);
|
|
73
|
+
const res = await controller.check();
|
|
74
|
+
|
|
75
|
+
expect(healthCheckServiceMock.check).toHaveBeenCalledTimes(1);
|
|
76
|
+
expect(res).toEqual({
|
|
77
|
+
database: { status: "down" },
|
|
78
|
+
});
|
|
79
|
+
expect(prismaMock.isHealthy).toHaveBeenCalledTimes(1);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Controller, Get } from "@nestjs/common";
|
|
2
|
+
import { ApiOperation, ApiResponse, ApiTags } from "@nestjs/swagger";
|
|
3
|
+
import { HealthCheck, HealthCheckService } from "@nestjs/terminus";
|
|
4
|
+
import { AllowAnonymous } from "#src/auth/decorator/allow-anonymous.decorator.js"; // @if auth
|
|
5
|
+
import { PrismaService } from "#src/prisma/prisma.service.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Controller for health check endpoints.
|
|
9
|
+
*
|
|
10
|
+
* - Provides readiness probe with database connectivity check
|
|
11
|
+
* - Provides liveness probe for basic service status
|
|
12
|
+
* - Used by Kubernetes or Docker health checks
|
|
13
|
+
*/
|
|
14
|
+
@AllowAnonymous() // @if auth
|
|
15
|
+
@ApiTags("Health")
|
|
16
|
+
@Controller("health")
|
|
17
|
+
export class HealthController {
|
|
18
|
+
constructor(
|
|
19
|
+
private readonly health: HealthCheckService,
|
|
20
|
+
private readonly prisma: PrismaService,
|
|
21
|
+
) {}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Performs readiness check including database connectivity.
|
|
25
|
+
*
|
|
26
|
+
* @returns Health check result with database status
|
|
27
|
+
*/
|
|
28
|
+
@ApiOperation({ summary: "Health check with database and memory" })
|
|
29
|
+
@ApiResponse({ description: "Service is healthy", status: 200 })
|
|
30
|
+
@ApiResponse({ description: "Service is unhealthy", status: 503 })
|
|
31
|
+
@Get()
|
|
32
|
+
@HealthCheck()
|
|
33
|
+
check() {
|
|
34
|
+
return this.health.check([
|
|
35
|
+
async () => {
|
|
36
|
+
const isHealthy = await this.prisma.isHealthy();
|
|
37
|
+
return { database: { status: isHealthy ? "up" : "down" } };
|
|
38
|
+
},
|
|
39
|
+
]);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Performs basic liveness check.
|
|
44
|
+
*
|
|
45
|
+
* @returns Object with status, timestamp, and uptime
|
|
46
|
+
*/
|
|
47
|
+
@ApiOperation({ summary: "Liveness probe" })
|
|
48
|
+
@ApiResponse({ description: "Service is alive", status: 200 })
|
|
49
|
+
@Get("live")
|
|
50
|
+
liveness() {
|
|
51
|
+
return {
|
|
52
|
+
status: "ok",
|
|
53
|
+
timestamp: new Date().toISOString(),
|
|
54
|
+
uptime: Math.floor(process.uptime()),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Module } from "@nestjs/common";
|
|
2
|
+
import { TerminusModule } from "@nestjs/terminus";
|
|
3
|
+
import { HealthController } from "#src/health/health.controller.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Module exposing standard health and readiness endpoints.
|
|
7
|
+
*/
|
|
8
|
+
@Module({
|
|
9
|
+
controllers: [HealthController],
|
|
10
|
+
imports: [TerminusModule],
|
|
11
|
+
})
|
|
12
|
+
export class HealthModule {}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Inject, Injectable, Scope } from "@nestjs/common";
|
|
2
|
+
import { INQUIRER } from "@nestjs/core";
|
|
3
|
+
import { type Params, PARAMS_PROVIDER_TOKEN, PinoLogger } from "nestjs-pino";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A `PinoLogger` that sets its own context from the injecting class via
|
|
7
|
+
* `INQUIRER`, so consumers no longer need to call `setContext` themselves.
|
|
8
|
+
*/
|
|
9
|
+
@Injectable({ scope: Scope.TRANSIENT })
|
|
10
|
+
export class ContextLogger extends PinoLogger {
|
|
11
|
+
constructor(
|
|
12
|
+
@Inject(INQUIRER) parentClass: object,
|
|
13
|
+
@Inject(PARAMS_PROVIDER_TOKEN) params: Params,
|
|
14
|
+
) {
|
|
15
|
+
super(params);
|
|
16
|
+
this.setContext(parentClass?.constructor?.name ?? "App");
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Params } from "nestjs-pino";
|
|
2
|
+
|
|
3
|
+
export const loggerConfig: Params = {
|
|
4
|
+
pinoHttp: {
|
|
5
|
+
autoLogging: false,
|
|
6
|
+
formatters: {
|
|
7
|
+
level: (label: string) => ({ level: label.toUpperCase() }),
|
|
8
|
+
},
|
|
9
|
+
level: process.env.LOG_LEVEL ?? "debug",
|
|
10
|
+
...(process.env.NODE_ENV === "development" && {
|
|
11
|
+
transport: {
|
|
12
|
+
options: {
|
|
13
|
+
colorize: true,
|
|
14
|
+
ignore: "pid,hostname,req,res,context",
|
|
15
|
+
messageFormat: "[{context}] {msg}",
|
|
16
|
+
singleLine: true,
|
|
17
|
+
translateTime: "dd.mm.yyyy, HH:MM:ss",
|
|
18
|
+
},
|
|
19
|
+
target: "pino-pretty",
|
|
20
|
+
},
|
|
21
|
+
}),
|
|
22
|
+
},
|
|
23
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Global, Module } from "@nestjs/common";
|
|
2
|
+
import { LoggerModule } from "nestjs-pino";
|
|
3
|
+
import { ContextLogger } from "#src/logging/context-logger.js";
|
|
4
|
+
import { loggerConfig } from "#src/logging/logger.config.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The application's logging module.
|
|
8
|
+
*
|
|
9
|
+
* It wires `nestjs-pino` (pino transport + the request-scoped
|
|
10
|
+
* `Logger`/`PinoLogger` providers, configured via `loggerConfig`) and layers
|
|
11
|
+
* `ContextLogger` on top, which derives its log context from the injecting
|
|
12
|
+
* class. Feature modules inject `ContextLogger`; `main.ts` pulls `Logger` for
|
|
13
|
+
* `app.useLogger`. Nothing should import `nestjs-pino`'s `LoggerModule`
|
|
14
|
+
* directly — import this module instead.
|
|
15
|
+
*/
|
|
16
|
+
@Global()
|
|
17
|
+
@Module({
|
|
18
|
+
exports: [ContextLogger, LoggerModule],
|
|
19
|
+
imports: [LoggerModule.forRoot(loggerConfig)],
|
|
20
|
+
providers: [ContextLogger],
|
|
21
|
+
})
|
|
22
|
+
export class LoggingModule {}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Global, Module } from "@nestjs/common";
|
|
2
|
+
import { MailService } from "#src/mail/mail.service.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Global module sharing the mail-sending service.
|
|
6
|
+
*/
|
|
7
|
+
@Global()
|
|
8
|
+
@Module({
|
|
9
|
+
exports: [MailService],
|
|
10
|
+
providers: [MailService],
|
|
11
|
+
})
|
|
12
|
+
export class MailModule {}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ConfigService } from "@nestjs/config";
|
|
2
|
+
import { vi } from "vitest";
|
|
3
|
+
import type { AppConfig } from "#src/config/app.config.js";
|
|
4
|
+
import type { ContextLogger } from "#src/logging/context-logger.js";
|
|
5
|
+
import { MailService } from "#src/mail/mail.service.js";
|
|
6
|
+
|
|
7
|
+
const { sendMail } = vi.hoisted(() => ({ sendMail: vi.fn() }));
|
|
8
|
+
|
|
9
|
+
vi.mock("nodemailer", () => ({
|
|
10
|
+
createTransport: vi.fn(() => ({ sendMail })),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
describe("mailService (SMTP outside production)", () => {
|
|
14
|
+
const cfg = {
|
|
15
|
+
get: vi.fn(() => ({
|
|
16
|
+
from: "App <no-reply@example.com>",
|
|
17
|
+
smtp: { host: "localhost", port: 1025 },
|
|
18
|
+
})),
|
|
19
|
+
} as unknown as ConfigService<AppConfig, true>;
|
|
20
|
+
const logger = { debug: vi.fn() } as unknown as ContextLogger;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
sendMail.mockReset();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("sends the verification link in both the text and the HTML part", async () => {
|
|
27
|
+
const service = new MailService(cfg, logger);
|
|
28
|
+
|
|
29
|
+
await service.sendVerificationEmail("user@example.com", "https://example.com/verify?t=1");
|
|
30
|
+
|
|
31
|
+
expect(sendMail).toHaveBeenCalledTimes(1);
|
|
32
|
+
const [message] = sendMail.mock.calls[0] as [{ html: string; text: string; to: string }];
|
|
33
|
+
expect(message.to).toBe("user@example.com");
|
|
34
|
+
expect(message.text).toContain("https://example.com/verify?t=1");
|
|
35
|
+
expect(message.html).toContain("https://example.com/verify?t=1");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("sends the password reset link", async () => {
|
|
39
|
+
const service = new MailService(cfg, logger);
|
|
40
|
+
|
|
41
|
+
await service.sendPasswordResetEmail("user@example.com", "https://example.com/reset?t=2");
|
|
42
|
+
|
|
43
|
+
const [message] = sendMail.mock.calls[0] as [{ subject: string; text: string }];
|
|
44
|
+
expect(message.subject).toMatch(/reset/i);
|
|
45
|
+
expect(message.text).toContain("https://example.com/reset?t=2");
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { Injectable } from "@nestjs/common";
|
|
2
|
+
import { ConfigService } from "@nestjs/config";
|
|
3
|
+
import { createTransport } from "nodemailer";
|
|
4
|
+
import { Resend } from "resend";
|
|
5
|
+
import type { AppConfig } from "#src/config/app.config.js";
|
|
6
|
+
import { ContextLogger } from "#src/logging/context-logger.js";
|
|
7
|
+
|
|
8
|
+
const APP_NAME = "Nexst";
|
|
9
|
+
|
|
10
|
+
export type MailInput = {
|
|
11
|
+
html: string;
|
|
12
|
+
subject: string;
|
|
13
|
+
text: string;
|
|
14
|
+
to: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** Sends a single email. Lets callers depend on the capability, not the service. */
|
|
18
|
+
export type MailSender = (input: MailInput) => Promise<void>;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Service for sending authentication emails.
|
|
22
|
+
*
|
|
23
|
+
* The provider is derived from `NODE_ENV`: Resend in production and SMTP
|
|
24
|
+
* (MailHog in local development) otherwise. The sender is built once at
|
|
25
|
+
* construction and reused for every message.
|
|
26
|
+
*/
|
|
27
|
+
@Injectable()
|
|
28
|
+
export class MailService {
|
|
29
|
+
private readonly sender: MailSender;
|
|
30
|
+
|
|
31
|
+
constructor(
|
|
32
|
+
private readonly cfg: ConfigService<AppConfig, true>,
|
|
33
|
+
private readonly logger: ContextLogger,
|
|
34
|
+
) {
|
|
35
|
+
this.sender = this.createSender();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Sends an email through the configured mail provider. */
|
|
39
|
+
async send(input: MailInput): Promise<void> {
|
|
40
|
+
await this.sender(input);
|
|
41
|
+
this.logger.debug({ subject: input.subject, to: input.to }, "Email sent");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Sends the password-reset message used by Better Auth. */
|
|
45
|
+
async sendPasswordResetEmail(to: string, url: string): Promise<void> {
|
|
46
|
+
await this.send({
|
|
47
|
+
html: this.layout(
|
|
48
|
+
"Reset your password",
|
|
49
|
+
`We received a request to reset your ${APP_NAME} password. This link expires soon. If you did not request it, you can ignore this email.`,
|
|
50
|
+
url,
|
|
51
|
+
"Reset password",
|
|
52
|
+
),
|
|
53
|
+
subject: `Reset your ${APP_NAME} password`,
|
|
54
|
+
text: `Reset your ${APP_NAME} password by opening this link:\n\n${url}\n\nIf you did not request this, ignore this email.`,
|
|
55
|
+
to,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Sends the email-verification message used by Better Auth. */
|
|
60
|
+
async sendVerificationEmail(to: string, url: string): Promise<void> {
|
|
61
|
+
await this.send({
|
|
62
|
+
html: this.layout(
|
|
63
|
+
"Confirm your email",
|
|
64
|
+
`Welcome to ${APP_NAME}! Please confirm your email address to activate your account.`,
|
|
65
|
+
url,
|
|
66
|
+
"Verify email",
|
|
67
|
+
),
|
|
68
|
+
subject: `Confirm your ${APP_NAME} email`,
|
|
69
|
+
text: `Welcome to ${APP_NAME}! Confirm your email address by opening this link:\n\n${url}`,
|
|
70
|
+
to,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Builds the mail sender for the configured provider: Resend in production and
|
|
76
|
+
* SMTP (MailHog in local development) otherwise.
|
|
77
|
+
*/
|
|
78
|
+
private createSender(): MailSender {
|
|
79
|
+
const mail = this.cfg.get("mail", { infer: true });
|
|
80
|
+
const { from } = mail;
|
|
81
|
+
|
|
82
|
+
if (process.env.NODE_ENV === "production") {
|
|
83
|
+
const resend = new Resend(mail.resendApiKey);
|
|
84
|
+
|
|
85
|
+
return async (input) => {
|
|
86
|
+
const { error } = await resend.emails.send({ from, ...input });
|
|
87
|
+
if (error) {
|
|
88
|
+
throw new Error(`Resend failed to send email: ${error.message}`);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const transporter = createTransport(mail.smtp);
|
|
94
|
+
|
|
95
|
+
return async (input) => {
|
|
96
|
+
await transporter.sendMail({ from, ...input });
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private layout(heading: string, body: string, actionUrl: string, actionLabel: string): string {
|
|
101
|
+
return `<!doctype html>
|
|
102
|
+
<html>
|
|
103
|
+
<body style="margin:0;background:#f4f5f7;font-family:Arial,Helvetica,sans-serif;color:#111">
|
|
104
|
+
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="padding:32px 0">
|
|
105
|
+
<tr>
|
|
106
|
+
<td align="center">
|
|
107
|
+
<table role="presentation" width="480" cellpadding="0" cellspacing="0" style="background:#fff;border-radius:12px;padding:32px">
|
|
108
|
+
<tr><td style="font-size:20px;font-weight:700;padding-bottom:12px">${heading}</td></tr>
|
|
109
|
+
<tr><td style="font-size:15px;line-height:1.6;color:#374151;padding-bottom:24px">${body}</td></tr>
|
|
110
|
+
<tr>
|
|
111
|
+
<td>
|
|
112
|
+
<a href="${actionUrl}" style="display:inline-block;background:#2563eb;color:#fff;text-decoration:none;font-weight:600;padding:12px 24px;border-radius:8px">${actionLabel}</a>
|
|
113
|
+
</td>
|
|
114
|
+
</tr>
|
|
115
|
+
<tr><td style="font-size:12px;color:#9ca3af;padding-top:24px">If the button does not work, copy this link into your browser:<br>${actionUrl}</td></tr>
|
|
116
|
+
</table>
|
|
117
|
+
</td>
|
|
118
|
+
</tr>
|
|
119
|
+
</table>
|
|
120
|
+
</body>
|
|
121
|
+
</html>`;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ValidationPipe } from "@nestjs/common";
|
|
2
|
+
import { NestFactory } from "@nestjs/core";
|
|
3
|
+
import type { NestFastifyApplication } from "@nestjs/platform-fastify";
|
|
4
|
+
import { FastifyAdapter } from "@nestjs/platform-fastify";
|
|
5
|
+
import { Logger } from "nestjs-pino";
|
|
6
|
+
import { AppModule } from "#src/app.module.js";
|
|
7
|
+
import { registerGraphqlEnums } from "#src/bootstrap/register-graphql-enums.js"; // @if auth
|
|
8
|
+
import { addSecurity } from "#src/bootstrap/security.js";
|
|
9
|
+
import { addSwagger } from "#src/bootstrap/swagger.js";
|
|
10
|
+
import { validationExceptionFactory } from "#src/errors/validation-exception.factory.js";
|
|
11
|
+
|
|
12
|
+
async function bootstrap() {
|
|
13
|
+
const app = await NestFactory.create<NestFastifyApplication>(AppModule, new FastifyAdapter(), {
|
|
14
|
+
// @if auth
|
|
15
|
+
// Better Auth parses its own request bodies.
|
|
16
|
+
bodyParser: false,
|
|
17
|
+
// @endif
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
await addSecurity(app);
|
|
21
|
+
registerGraphqlEnums(); // @if auth
|
|
22
|
+
app.useLogger(app.get(Logger));
|
|
23
|
+
|
|
24
|
+
app.useGlobalPipes(
|
|
25
|
+
new ValidationPipe({
|
|
26
|
+
exceptionFactory: validationExceptionFactory,
|
|
27
|
+
transform: true,
|
|
28
|
+
transformOptions: { enableImplicitConversion: true },
|
|
29
|
+
whitelist: true,
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
if (process.env.NODE_ENV === "development") {
|
|
34
|
+
addSwagger(app);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
app.enableShutdownHooks();
|
|
38
|
+
await app.listen(process.env.PORT ?? 3001, "0.0.0.0");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
void bootstrap();
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Global, Module } from "@nestjs/common";
|
|
2
|
+
import { PrismaService } from "#src/prisma/prisma.service.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Global module exposing PrismaService so repositories can access the database.
|
|
6
|
+
*/
|
|
7
|
+
@Global()
|
|
8
|
+
@Module({
|
|
9
|
+
exports: [PrismaService],
|
|
10
|
+
providers: [PrismaService],
|
|
11
|
+
})
|
|
12
|
+
export class PrismaModule {}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { ConfigService } from "@nestjs/config";
|
|
2
|
+
import { vi } from "vitest";
|
|
3
|
+
import type { Mocked } from "vitest";
|
|
4
|
+
import type { ContextLogger } from "#src/logging/context-logger.js";
|
|
5
|
+
import { PrismaService } from "#src/prisma/prisma.service.js";
|
|
6
|
+
|
|
7
|
+
vi.mock("#src/generated/prisma/client.js", () => {
|
|
8
|
+
class PrismaClient {
|
|
9
|
+
$connect = vi.fn(async () => {});
|
|
10
|
+
|
|
11
|
+
$disconnect = vi.fn(async () => {});
|
|
12
|
+
$on = vi.fn();
|
|
13
|
+
$queryRaw = vi.fn();
|
|
14
|
+
}
|
|
15
|
+
return { PrismaClient };
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
vi.mock("@prisma/adapter-pg", () => ({
|
|
19
|
+
PrismaPg: vi.fn(function () {
|
|
20
|
+
return {};
|
|
21
|
+
}),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
describe("prismaService (unit)", () => {
|
|
25
|
+
let service: PrismaService;
|
|
26
|
+
let configMock: Mocked<ConfigService>;
|
|
27
|
+
let loggerMock: Mocked<ContextLogger>;
|
|
28
|
+
|
|
29
|
+
const originalAutoMigrate = process.env.PRISMA_AUTO_MIGRATE;
|
|
30
|
+
|
|
31
|
+
beforeAll(() => {
|
|
32
|
+
process.env.PRISMA_AUTO_MIGRATE = "false";
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterAll(() => {
|
|
36
|
+
if (originalAutoMigrate === undefined) {
|
|
37
|
+
delete process.env.PRISMA_AUTO_MIGRATE;
|
|
38
|
+
} else {
|
|
39
|
+
process.env.PRISMA_AUTO_MIGRATE = originalAutoMigrate;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
vi.clearAllMocks();
|
|
45
|
+
|
|
46
|
+
configMock = {
|
|
47
|
+
get: vi.fn().mockImplementation((key: string) => {
|
|
48
|
+
if (key === "db.url") {
|
|
49
|
+
return "postgresql://user:pass@localhost:5432/db";
|
|
50
|
+
}
|
|
51
|
+
}),
|
|
52
|
+
} as unknown as Mocked<ConfigService>;
|
|
53
|
+
|
|
54
|
+
loggerMock = {
|
|
55
|
+
debug: vi.fn(),
|
|
56
|
+
error: vi.fn(),
|
|
57
|
+
info: vi.fn(),
|
|
58
|
+
setContext: vi.fn(),
|
|
59
|
+
warn: vi.fn(),
|
|
60
|
+
} as unknown as Mocked<ContextLogger>;
|
|
61
|
+
|
|
62
|
+
service = new PrismaService(configMock, loggerMock);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("isHealthy", () => {
|
|
66
|
+
it("returns true when $queryRaw resolves", async () => {
|
|
67
|
+
vi.spyOn(service, "$queryRaw").mockResolvedValue([{ "?column?": 1 }]);
|
|
68
|
+
|
|
69
|
+
const result = await service.isHealthy();
|
|
70
|
+
|
|
71
|
+
expect(result).toBe(true);
|
|
72
|
+
expect(service.$queryRaw).toHaveBeenCalledTimes(1);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("returns false and logs error when $queryRaw rejects", async () => {
|
|
76
|
+
const error = new Error("boom");
|
|
77
|
+
vi.spyOn(service, "$queryRaw").mockRejectedValue(error);
|
|
78
|
+
|
|
79
|
+
const result = await service.isHealthy();
|
|
80
|
+
|
|
81
|
+
expect(result).toBe(false);
|
|
82
|
+
expect(service.$queryRaw).toHaveBeenCalledTimes(1);
|
|
83
|
+
expect(loggerMock.error).toHaveBeenCalledWith(
|
|
84
|
+
{ error: "boom" },
|
|
85
|
+
"Database health check failed",
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("onModuleDestroy", () => {
|
|
91
|
+
it('calls $disconnect and logs "closed"', async () => {
|
|
92
|
+
vi.spyOn(service, "$disconnect").mockResolvedValue();
|
|
93
|
+
|
|
94
|
+
await service.onModuleDestroy();
|
|
95
|
+
|
|
96
|
+
expect(service.$disconnect).toHaveBeenCalledTimes(1);
|
|
97
|
+
expect(loggerMock.info).toHaveBeenCalledWith("Database connection closed");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("logs error if $disconnect throws", async () => {
|
|
101
|
+
const error = new Error("cannot close");
|
|
102
|
+
vi.spyOn(service, "$disconnect").mockRejectedValue(error);
|
|
103
|
+
|
|
104
|
+
await service.onModuleDestroy();
|
|
105
|
+
|
|
106
|
+
expect(service.$disconnect).toHaveBeenCalled();
|
|
107
|
+
expect(loggerMock.error).toHaveBeenCalledWith(
|
|
108
|
+
{ error: "cannot close" },
|
|
109
|
+
"Error during database disconnect",
|
|
110
|
+
);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("onModuleInit", () => {
|
|
115
|
+
it("calls $connect and logs success", async () => {
|
|
116
|
+
vi.spyOn(service, "$connect").mockResolvedValue();
|
|
117
|
+
|
|
118
|
+
await service.onModuleInit();
|
|
119
|
+
|
|
120
|
+
expect(service.$connect).toHaveBeenCalledTimes(1);
|
|
121
|
+
expect(loggerMock.info).toHaveBeenCalledWith("Successfully connected to database");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("logs error and rethrows if $connect throws", async () => {
|
|
125
|
+
const error = new Error("cannot connect");
|
|
126
|
+
vi.spyOn(service, "$connect").mockRejectedValue(error);
|
|
127
|
+
|
|
128
|
+
await expect(service.onModuleInit()).rejects.toThrow("cannot connect");
|
|
129
|
+
expect(service.$connect).toHaveBeenCalled();
|
|
130
|
+
expect(loggerMock.error).toHaveBeenCalledWith(
|
|
131
|
+
{ error: "cannot connect" },
|
|
132
|
+
"Failed to connect to database",
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
});
|