From 4125e56735e02c413d0276b1ed48cf320481e958 Mon Sep 17 00:00:00 2001 From: Gabriel Juchault Date: Wed, 10 Jan 2024 14:12:50 +0100 Subject: [PATCH] feat(dependency-store): implement dependency store with new sdk --- .vscode/tasks.json | 5 ++ package-lock.json | 8 +- package.json | 2 +- .../__tests__/get-healthcheck.test.ts | 46 ++++++---- .../healthcheck/__tests__/get-users.test.ts | 9 +- .../healthcheck/get-healthcheck.ts | 19 +++-- src/application/healthcheck/get-users.ts | 11 ++- src/application/healthcheck/index.ts | 19 +---- src/index.ts | 30 +++---- src/infrastructure/date.ts | 9 -- src/presentation/http/index.ts | 8 +- .../http/routes/healthcheck/index.ts | 10 ++- .../__tests__/get-healthcheck.test.ts | 22 +++-- src/repository/healthcheck/index.ts | 23 +++-- src/repository/index.ts | 12 ++- .../user/__tests__/bulk-add.test.ts | 20 ++--- src/repository/user/__tests__/get.test.ts | 16 ++-- src/repository/user/index.ts | 14 +-- src/store.ts | 19 +++++ src/test-helpers/mock.ts | 85 +++++++++++++++++++ 20 files changed, 238 insertions(+), 149 deletions(-) delete mode 100644 src/infrastructure/date.ts create mode 100644 src/store.ts create mode 100644 src/test-helpers/mock.ts diff --git a/.vscode/tasks.json b/.vscode/tasks.json index bbdf8179..1426c5bc 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -6,6 +6,11 @@ "type": "shell", "command": "${workspaceRoot}/node_modules/tsx/dist/cli.mjs", "args": ["--test", "${relativeFile}"], + "presentation": { + "clear": true, + "showReuseMessage": false, + "echo": false + }, "group": { "kind": "test", "isDefault": true diff --git a/package-lock.json b/package-lock.json index 325db926..9ad6561a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "@fastify/multipart": "^8.1.0", "@fastify/rate-limit": "^9.1.0", "@fastify/under-pressure": "^8.3.0", - "@gjuchault/typescript-service-sdk": "^6.0.1", + "@gjuchault/typescript-service-sdk": "^6.3.0", "@opentelemetry/api": "^1.7.0", "@opentelemetry/core": "^1.19.0", "@opentelemetry/exporter-prometheus": "^0.46.0", @@ -1045,9 +1045,9 @@ } }, "node_modules/@gjuchault/typescript-service-sdk": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@gjuchault/typescript-service-sdk/-/typescript-service-sdk-6.0.1.tgz", - "integrity": "sha512-PFdyjx+Zw75uMF2+jEi5T22emoAuNg18vReflUXldcwRDwgrbbxJdHbPVzBfZjNzPo8s1D6ZKF+xxfysv2E/gg==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@gjuchault/typescript-service-sdk/-/typescript-service-sdk-6.3.0.tgz", + "integrity": "sha512-lMBIy2UkSVlj6U9i/FI4m+Ka0s4wbA/4AnmeMt6GteKEs1IjQCTJf6Atu557A+fadsL7dN1iq0tuUJkKMsMmQg==", "engines": { "node": "^21.2.0", "npm": "^10.2.3" diff --git a/package.json b/package.json index 24a9a111..ecec5eab 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "@fastify/multipart": "^8.1.0", "@fastify/rate-limit": "^9.1.0", "@fastify/under-pressure": "^8.3.0", - "@gjuchault/typescript-service-sdk": "^6.0.1", + "@gjuchault/typescript-service-sdk": "^6.3.0", "@opentelemetry/api": "^1.7.0", "@opentelemetry/core": "^1.19.0", "@opentelemetry/exporter-prometheus": "^0.46.0", diff --git a/src/application/healthcheck/__tests__/get-healthcheck.test.ts b/src/application/healthcheck/__tests__/get-healthcheck.test.ts index 6f748c2b..e781c086 100644 --- a/src/application/healthcheck/__tests__/get-healthcheck.test.ts +++ b/src/application/healthcheck/__tests__/get-healthcheck.test.ts @@ -2,8 +2,10 @@ import * as assert from "node:assert/strict"; import { before, describe, it, mock } from "node:test"; import type { Redis } from "ioredis"; +import { err, ok } from "neverthrow"; import type { HealthcheckRepository } from "~/repository/healthcheck/index.js"; +import { buildMockDependencyStore } from "~/test-helpers/mock.js"; import { getHealthcheck, GetHealthcheckResult } from "../get-healthcheck.js"; @@ -16,23 +18,25 @@ const mockUnhealthyCache = { } as unknown as Redis; const mockHealthyRepository: HealthcheckRepository = { - getHealthcheck: mock.fn(() => Promise.resolve({ outcome: "healthy" })), + getHealthcheck: mock.fn(() => Promise.resolve(ok("healthy"))), }; const mockUnhealthyRepository: HealthcheckRepository = { - getHealthcheck: mock.fn(() => Promise.resolve({ outcome: "unhealthy" })), + getHealthcheck: mock.fn(() => Promise.resolve(err("databaseError"))), }; await describe("getHealthcheck()", async () => { await describe("given a healthy cache and database", async () => { + const dependencyStore = buildMockDependencyStore({ + cache: mockHealthyCache, + repository: { healthcheck: mockHealthyRepository }, + }); + await describe("when called", async () => { let result: GetHealthcheckResult; before(async () => { - result = await getHealthcheck({ - cache: mockHealthyCache, - healthcheckRepository: mockHealthyRepository, - }); + result = await getHealthcheck({ dependencyStore }); }); await it("returns healthy", () => { @@ -49,14 +53,16 @@ await describe("getHealthcheck()", async () => { }); await describe("given an unhealthy cache and healthy database", async () => { + const dependencyStore = buildMockDependencyStore({ + cache: mockUnhealthyCache, + repository: { healthcheck: mockHealthyRepository }, + }); + await describe("when called", async () => { let result: GetHealthcheckResult; before(async () => { - result = await getHealthcheck({ - cache: mockUnhealthyCache, - healthcheckRepository: mockHealthyRepository, - }); + result = await getHealthcheck({ dependencyStore }); }); await it("returns unhealthy cache, healthy database", () => { @@ -73,14 +79,16 @@ await describe("getHealthcheck()", async () => { }); await describe("given a healthy cache and unhealthy database", async () => { + const dependencyStore = buildMockDependencyStore({ + cache: mockHealthyCache, + repository: { healthcheck: mockUnhealthyRepository }, + }); + await describe("when called", async () => { let result: GetHealthcheckResult; before(async () => { - result = await getHealthcheck({ - cache: mockHealthyCache, - healthcheckRepository: mockUnhealthyRepository, - }); + result = await getHealthcheck({ dependencyStore }); }); await it("returns unhealthy cache, healthy database", () => { @@ -97,14 +105,16 @@ await describe("getHealthcheck()", async () => { }); await describe("given a healthy cache and database", async () => { + const dependencyStore = buildMockDependencyStore({ + cache: mockUnhealthyCache, + repository: { healthcheck: mockUnhealthyRepository }, + }); + await describe("when called", async () => { let result: GetHealthcheckResult; before(async () => { - result = await getHealthcheck({ - cache: mockUnhealthyCache, - healthcheckRepository: mockUnhealthyRepository, - }); + result = await getHealthcheck({ dependencyStore }); }); await it("returns unhealthy cache, healthy database", () => { diff --git a/src/application/healthcheck/__tests__/get-users.test.ts b/src/application/healthcheck/__tests__/get-users.test.ts index 133a4c1b..c2413347 100644 --- a/src/application/healthcheck/__tests__/get-users.test.ts +++ b/src/application/healthcheck/__tests__/get-users.test.ts @@ -5,6 +5,7 @@ import { ok } from "neverthrow"; import type { UserEmail, UserId, UserName } from "~/domain/user.js"; import type { UserRepository } from "~/repository/user/index.js"; +import { buildMockDependencyStore } from "~/test-helpers/mock.js"; import { getUsers, GetUsersResult } from "../get-users.js"; @@ -24,13 +25,17 @@ const mockRepository: UserRepository = { }; await describe("getUsers()", async () => { - await describe("given a healthy cache and database", async () => { + await describe("given a repository", async () => { + const dependencyStore = buildMockDependencyStore({ + repository: { user: mockRepository }, + }); + await describe("when called", async () => { let result: GetUsersResult; before(async () => { result = await getUsers({ - userRepository: mockRepository, + dependencyStore, }); }); diff --git a/src/application/healthcheck/get-healthcheck.ts b/src/application/healthcheck/get-healthcheck.ts index 6d8ee42a..c2c0636f 100644 --- a/src/application/healthcheck/get-healthcheck.ts +++ b/src/application/healthcheck/get-healthcheck.ts @@ -1,9 +1,7 @@ import os from "node:os"; import v8 from "node:v8"; -import type { Redis } from "ioredis"; - -import type { HealthcheckRepository } from "~/repository/healthcheck/index.js"; +import { DependencyStore } from "~/store"; export interface GetHealthcheckResult { database: "healthy" | "unhealthy"; @@ -13,12 +11,14 @@ export interface GetHealthcheckResult { } export async function getHealthcheck({ - healthcheckRepository, - cache, + dependencyStore, }: { - healthcheckRepository: HealthcheckRepository; - cache: Redis; + dependencyStore: DependencyStore; }): Promise { + const cache = dependencyStore.get("cache"); + const { healthcheck: healthcheckRepository } = + dependencyStore.get("repository"); + const databaseResult = await healthcheckRepository.getHealthcheck(); let cacheResult: "healthy" | "unhealthy" = "healthy"; @@ -36,7 +36,10 @@ export async function getHealthcheck({ v8HeapStatistics.total_heap_size / v8HeapStatistics.heap_size_limit; return { - database: databaseResult.outcome, + database: databaseResult.match( + () => "healthy", + () => "unhealthy", + ), cache: cacheResult, systemMemory: systemMemoryUsage > 0.8 ? "unhealthy" : "healthy", processMemory: processMemoryUsage > 0.8 ? "unhealthy" : "healthy", diff --git a/src/application/healthcheck/get-users.ts b/src/application/healthcheck/get-users.ts index 7ddc1e95..fa305916 100644 --- a/src/application/healthcheck/get-users.ts +++ b/src/application/healthcheck/get-users.ts @@ -2,10 +2,8 @@ import type { NonEmptyArray } from "@gjuchault/typescript-service-sdk"; import type { Result } from "neverthrow"; import type { User } from "~/domain/user.js"; -import type { - GetUsersError as GetUsersRepositoryError, - UserRepository, -} from "~/repository/user/index.js"; +import type { GetUsersError as GetUsersRepositoryError } from "~/repository/user/index.js"; +import { DependencyStore } from "~/store"; export type GetUsersResult = Result< NonEmptyArray, @@ -13,10 +11,11 @@ export type GetUsersResult = Result< >; export async function getUsers({ - userRepository, + dependencyStore, }: { - userRepository: UserRepository; + dependencyStore: DependencyStore; }): Promise { + const { user: userRepository } = dependencyStore.get("repository"); const users = await userRepository.get(); return users; diff --git a/src/application/healthcheck/index.ts b/src/application/healthcheck/index.ts index c1d1f249..679a0ff4 100644 --- a/src/application/healthcheck/index.ts +++ b/src/application/healthcheck/index.ts @@ -1,10 +1,4 @@ -import type { - Cache, - DependencyStore, - TaskScheduling, -} from "@gjuchault/typescript-service-sdk"; - -import type { Repository } from "~/repository/index.js"; +import { DependencyStore } from "~/store.js"; import type { GetHealthcheckResult } from "./get-healthcheck.js"; import { getHealthcheck } from "./get-healthcheck.js"; @@ -18,11 +12,7 @@ export async function createHealthcheckApplication({ }: { dependencyStore: DependencyStore; }): Promise { - const taskScheduling = - dependencyStore.retrieve("taskScheduling"); - const cache = dependencyStore.retrieve("cache"); - const { healthcheck: healthcheckRepository } = - dependencyStore.retrieve("repository"); + const taskScheduling = dependencyStore.get("taskScheduling"); const enqueueSomeTask = await taskScheduling.createTask<{ id: string }>( "someTask", @@ -35,10 +25,7 @@ export async function createHealthcheckApplication({ return { async getHealthcheck() { - return getHealthcheck({ - cache, - healthcheckRepository, - }); + return getHealthcheck({ dependencyStore }); }, }; } diff --git a/src/index.ts b/src/index.ts index 2ac92aa1..0f1d146a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,7 +12,7 @@ import type { import { createCacheStorage, createDatabase, - createDependencyStore, + createDateProvider, createHttpServer, createLoggerProvider, createShutdownManager, @@ -25,18 +25,16 @@ import { config } from "~/config.js"; import { createAppRouter } from "~/presentation/http/index.js"; import { createRepository } from "~/repository/index.js"; -import { dateProvider } from "./infrastructure/date"; +import { dependencyStore } from "./store"; export async function startApp() { - const dependencyStore = createDependencyStore(); - const createLogger = createLoggerProvider({ config }); - dependencyStore.provide("logger", createLogger); + const logger = createLogger("app"); - const telemetry = createTelemetry({ config, dependencyStore }); - dependencyStore.provide("telemetry", telemetry); + dependencyStore.set("logger", createLogger); - const logger = createLogger("app"); + const telemetry = createTelemetry({ config, dependencyStore }); + dependencyStore.set("telemetry", telemetry); const appStartedTimestamp = Date.now(); logger.info(`starting service ${config.name}...`, { @@ -46,7 +44,7 @@ export async function startApp() { platform: process.platform, }); - dependencyStore.provide("date", dateProvider); + dependencyStore.set("date", createDateProvider()); let database: Database; let cache: Cache; @@ -57,21 +55,21 @@ export async function startApp() { try { cache = await createCacheStorage({ config, dependencyStore }); - dependencyStore.provide("cache", cache); + dependencyStore.set("cache", cache); taskScheduling = createTaskScheduling({ config, dependencyStore }); - dependencyStore.provide("taskScheduling", taskScheduling); + dependencyStore.set("taskScheduling", taskScheduling); database = await createDatabase({ config, dependencyStore }); - dependencyStore.provide("database", database); + dependencyStore.set("database", database); - const repository = createRepository({ database }); - dependencyStore.provide("repository", repository); + const repository = createRepository({ dependencyStore }); + dependencyStore.set("repository", repository); const healthcheckApplication = await createHealthcheckApplication({ dependencyStore, }); - dependencyStore.provide("healthcheckApplication", healthcheckApplication); + dependencyStore.set("healthcheckApplication", healthcheckApplication); const appRouter = createAppRouter({ dependencyStore }); @@ -80,7 +78,7 @@ export async function startApp() { dependencyStore, appRouter, }); - dependencyStore.provide("httpServer", httpServer); + dependencyStore.set("httpServer", httpServer); shutdown = createShutdownManager({ config, diff --git a/src/infrastructure/date.ts b/src/infrastructure/date.ts deleted file mode 100644 index 5cee11c6..00000000 --- a/src/infrastructure/date.ts +++ /dev/null @@ -1,9 +0,0 @@ -export type DateProvider = { - now: () => number; -}; - -export function dateProvider() { - return { - now: () => Date.now(), - }; -} diff --git a/src/presentation/http/index.ts b/src/presentation/http/index.ts index 0f546502..7fc4bfa2 100644 --- a/src/presentation/http/index.ts +++ b/src/presentation/http/index.ts @@ -1,8 +1,7 @@ -import { DependencyStore } from "@gjuchault/typescript-service-sdk"; import { initContract } from "@ts-rest/core"; import { initServer } from "@ts-rest/fastify"; -import type { HealthcheckApplication } from "~/application/healthcheck/index.js"; +import { DependencyStore } from "~/store.js"; import { bindHealthcheckRoutes, @@ -25,11 +24,8 @@ export function createAppRouter({ }: { dependencyStore: DependencyStore; }) { - const healthcheckApplication = - dependencyStore.retrieve("healthcheckApplication"); - const healthcheckRouter = bindHealthcheckRoutes({ - healthcheckApplication, + dependencyStore, }); return s.router(contract, { diff --git a/src/presentation/http/routes/healthcheck/index.ts b/src/presentation/http/routes/healthcheck/index.ts index 660cb6f5..62b9c4dc 100644 --- a/src/presentation/http/routes/healthcheck/index.ts +++ b/src/presentation/http/routes/healthcheck/index.ts @@ -4,8 +4,8 @@ import type { } from "@ts-rest/core"; import { z } from "zod"; -import { GetHealthcheckResult } from "~/application/healthcheck/get-healthcheck.js"; -import type { HealthcheckApplication } from "~/application/healthcheck/index.js"; +import type { GetHealthcheckResult } from "~/application/healthcheck/get-healthcheck.js"; +import type { DependencyStore } from "~/store"; export type RouterGetHealthcheckResult = ServerInferResponses< (typeof healthcheckRouterContract)["getHealthcheck"] @@ -40,10 +40,12 @@ export const healthcheckRouterContract = { } as const; export function bindHealthcheckRoutes({ - healthcheckApplication, + dependencyStore, }: { - healthcheckApplication: HealthcheckApplication; + dependencyStore: DependencyStore; }) { + const healthcheckApplication = dependencyStore.get("healthcheckApplication"); + return { async getHealthcheck(): Promise { const healthcheck = await healthcheckApplication.getHealthcheck(); diff --git a/src/repository/healthcheck/__tests__/get-healthcheck.test.ts b/src/repository/healthcheck/__tests__/get-healthcheck.test.ts index fbd68c8f..9aa0eead 100644 --- a/src/repository/healthcheck/__tests__/get-healthcheck.test.ts +++ b/src/repository/healthcheck/__tests__/get-healthcheck.test.ts @@ -3,15 +3,15 @@ import { before, describe, it } from "node:test"; import { slonikHelpers } from "@gjuchault/typescript-service-sdk"; +import { buildMockDependencyStore } from "~/test-helpers/mock.js"; + import { createHealthcheckRepository, GetHealthcheckResult } from "../index.js"; await describe("getHealthcheck()", async () => { await describe("given a healthy database", async () => { const { query, database } = slonikHelpers.createMockDatabase([]); - - const repository = createHealthcheckRepository({ - database, - }); + const dependencyStore = buildMockDependencyStore({ database }); + const repository = createHealthcheckRepository({ dependencyStore }); await describe("when called", async () => { let result: GetHealthcheckResult; @@ -20,8 +20,8 @@ await describe("getHealthcheck()", async () => { result = await repository.getHealthcheck(); }); - await it("returns outcome healthy", () => { - assert.equal(result.outcome, "healthy"); + await it("returns ok", () => { + assert.equal(result.isOk(), true); }); await it("called the database with the appropriate query", () => { @@ -33,10 +33,8 @@ await describe("getHealthcheck()", async () => { await describe("given an unhealthy database", async () => { const { query, database } = slonikHelpers.createFailingQueryMockDatabase(); - - const repository = createHealthcheckRepository({ - database, - }); + const dependencyStore = buildMockDependencyStore({ database }); + const repository = createHealthcheckRepository({ dependencyStore }); await describe("when called", async () => { let result: GetHealthcheckResult; @@ -45,8 +43,8 @@ await describe("getHealthcheck()", async () => { result = await repository.getHealthcheck(); }); - await it("returns outcome unhealthy", () => { - assert.equal(result.outcome, "unhealthy"); + await it("returns err", () => { + assert.equal(result.isErr(), true); }); await it("called the database with the appropriate query", () => { diff --git a/src/repository/healthcheck/index.ts b/src/repository/healthcheck/index.ts index d5f67efd..84ad9ff9 100644 --- a/src/repository/healthcheck/index.ts +++ b/src/repository/healthcheck/index.ts @@ -1,30 +1,29 @@ -import { DatabasePool, sql } from "slonik"; +import { err, ok, Result } from "neverthrow"; +import { sql } from "slonik"; import { z } from "zod"; +import { DependencyStore } from "~/store"; + export interface HealthcheckRepository { getHealthcheck(): Promise; } -export type GetHealthcheckResult = - | { outcome: "healthy" } - | { outcome: "unhealthy" }; +export type GetHealthcheckResult = Result<"healthy", "databaseError">; export function createHealthcheckRepository({ - database, + dependencyStore, }: { - database: DatabasePool; + dependencyStore: DependencyStore; }): HealthcheckRepository { async function getHealthcheck(): Promise { + const database = dependencyStore.get("database"); + try { await database.query(sql.type(z.unknown())`select 1`); - return { - outcome: "healthy", - }; + return ok("healthy"); } catch { - return { - outcome: "unhealthy", - }; + return err("databaseError"); } } diff --git a/src/repository/index.ts b/src/repository/index.ts index 5d7e388f..354fc362 100644 --- a/src/repository/index.ts +++ b/src/repository/index.ts @@ -1,20 +1,24 @@ -import type { Database } from "@gjuchault/typescript-service-sdk"; +import { DependencyStore } from "~/store.js"; import type { HealthcheckRepository } from "./healthcheck/index.js"; import { createHealthcheckRepository } from "./healthcheck/index.js"; +import type { UserRepository } from "./user/index.js"; +import { createUserRepository } from "./user/index.js"; export type Repository = { healthcheck: HealthcheckRepository; + user: UserRepository; }; export function createRepository({ - database, + dependencyStore, }: { - database: Database; + dependencyStore: DependencyStore; }): Repository { return { healthcheck: createHealthcheckRepository({ - database, + dependencyStore, }), + user: createUserRepository({ dependencyStore }), }; } diff --git a/src/repository/user/__tests__/bulk-add.test.ts b/src/repository/user/__tests__/bulk-add.test.ts index 5fcdd151..f6a48c00 100644 --- a/src/repository/user/__tests__/bulk-add.test.ts +++ b/src/repository/user/__tests__/bulk-add.test.ts @@ -1,16 +1,14 @@ import * as assert from "node:assert/strict"; import { before, describe, it } from "node:test"; -import { - createMockLogger, - slonikHelpers, -} from "@gjuchault/typescript-service-sdk"; +import { slonikHelpers } from "@gjuchault/typescript-service-sdk"; import { userEmailSchema, userIdSchema, userNameSchema, } from "~/domain/user.js"; +import { buildMockDependencyStore } from "~/test-helpers/mock.js"; import type { BulkAddResult } from "../index.js"; import { createUserRepository } from "../index.js"; @@ -18,11 +16,8 @@ import { createUserRepository } from "../index.js"; await describe("bulkAdd()", async () => { await describe("given a database with users", async () => { const { query, database } = slonikHelpers.createMockDatabase([]); - - const repository = createUserRepository({ - database, - logger: createMockLogger(), - }); + const dependencyStore = buildMockDependencyStore({ database }); + const repository = createUserRepository({ dependencyStore }); await describe("when called", async () => { let result: BulkAddResult; @@ -86,11 +81,8 @@ await describe("bulkAdd()", async () => { await describe("given an erroring database", async () => { const { database } = slonikHelpers.createFailingQueryMockDatabase(); - - const repository = createUserRepository({ - database, - logger: createMockLogger(), - }); + const dependencyStore = buildMockDependencyStore({ database }); + const repository = createUserRepository({ dependencyStore }); await describe("when called with no filters", async () => { let result: BulkAddResult; diff --git a/src/repository/user/__tests__/get.test.ts b/src/repository/user/__tests__/get.test.ts index 728f9f45..39976801 100644 --- a/src/repository/user/__tests__/get.test.ts +++ b/src/repository/user/__tests__/get.test.ts @@ -2,7 +2,6 @@ import * as assert from "node:assert/strict"; import { before, describe, it } from "node:test"; import { - createMockLogger, nonEmptyArray, slonikHelpers, } from "@gjuchault/typescript-service-sdk"; @@ -12,6 +11,7 @@ import { userIdSchema, userNameSchema, } from "~/domain/user.js"; +import { buildMockDependencyStore } from "~/test-helpers/mock.js"; import type { GetResult } from "../index.js"; import { createUserRepository } from "../index.js"; @@ -30,11 +30,8 @@ await describe("get()", async () => { email: userEmailSchema.parse("doe@mail.com"), }, ]); - - const repository = createUserRepository({ - database, - logger: createMockLogger(), - }); + const dependencyStore = buildMockDependencyStore({ database }); + const repository = createUserRepository({ dependencyStore }); await describe("when called with no filters", async () => { let result: GetResult; @@ -110,11 +107,8 @@ await describe("get()", async () => { await describe("given an erroring database", async () => { const { database } = slonikHelpers.createFailingQueryMockDatabase(); - - const repository = createUserRepository({ - database, - logger: createMockLogger(), - }); + const dependencyStore = buildMockDependencyStore({ database }); + const repository = createUserRepository({ dependencyStore }); await describe("when called with no filters", async () => { let result: GetResult; diff --git a/src/repository/user/index.ts b/src/repository/user/index.ts index e3e8fb8a..8d5fede6 100644 --- a/src/repository/user/index.ts +++ b/src/repository/user/index.ts @@ -1,16 +1,16 @@ import { - Logger, NonEmptyArray, nonEmptyArray, PrepareBulkInsertError, slonikHelpers, } from "@gjuchault/typescript-service-sdk"; import { err, fromPromise, ok, Result } from "neverthrow"; -import { DatabasePool, SlonikError, sql } from "slonik"; +import { SlonikError, sql } from "slonik"; import { z } from "zod"; import type { User } from "~/domain/user.js"; import { userSchema } from "~/domain/user.js"; +import { DependencyStore } from "~/store"; export interface UserRepository { get(filters?: GetUsersFilters): Promise; @@ -47,12 +47,14 @@ const nonEmptyUserArraySchema = const databaseUserSchema = userSchema; export function createUserRepository({ - database, - logger, + dependencyStore, }: { - database: DatabasePool; - logger: Logger; + dependencyStore: DependencyStore; }): UserRepository { + const createLogger = dependencyStore.get("logger"); + const logger = createLogger("repository/user"); + const database = dependencyStore.get("database"); + async function get(filters?: GetUsersFilters): Promise { const idsFragment = filters?.ids === undefined diff --git a/src/store.ts b/src/store.ts new file mode 100644 index 00000000..d0743897 --- /dev/null +++ b/src/store.ts @@ -0,0 +1,19 @@ +import { + createDependencyStore, + DependencyStore as SdkDependencyStore, +} from "@gjuchault/typescript-service-sdk"; + +import type { HealthcheckApplication } from "./application/healthcheck"; +import type { Repository } from "./repository"; + +export const createAppDependencyStore = createDependencyStore<{ + repository: Repository; + healthcheckApplication: HealthcheckApplication; +}>; + +export const dependencyStore = createAppDependencyStore(); + +export type DependencyStore = SdkDependencyStore<{ + repository: Repository; + healthcheckApplication: HealthcheckApplication; +}>; diff --git a/src/test-helpers/mock.ts b/src/test-helpers/mock.ts new file mode 100644 index 00000000..ecbccbc9 --- /dev/null +++ b/src/test-helpers/mock.ts @@ -0,0 +1,85 @@ +// eslint-disable-next-line eslint-comments/disable-enable-pair +/* eslint-disable security/detect-object-injection */ +import { mock } from "node:test"; + +import type { + Cache, + Database, + DateProvider, + HttpServer, + TaskScheduling, + Telemetry, +} from "@gjuchault/typescript-service-sdk"; +import { createMockLoggerProvider } from "@gjuchault/typescript-service-sdk"; + +import type { HealthcheckApplication } from "~/application/healthcheck"; +import type { Repository } from "~/repository"; +import { createAppDependencyStore } from "~/store"; + +export function buildMock(initialImplementation: Partial = {}): T { + const store: Partial = {}; + + for (const key of Object.keys(initialImplementation) as (keyof T)[]) { + const implementation = initialImplementation[key]; + + store[key] = + typeof implementation === "function" + ? mock.fn(implementation) + : implementation; + } + + const proxy = new Proxy(store, { + get(_target, propertyName) { + const function2 = Reflect.get(store, propertyName); + if (!function2 && propertyName !== "then") { + const mockFunction = mock.fn(); + Reflect.set(store, propertyName, mockFunction); + return mockFunction; + } + return function2; + }, + + set(_target, propertyName, propertyValue) { + return Reflect.set(store, propertyName, propertyValue); + }, + }); + + return proxy as T; +} + +export function buildMockDependencyStore({ + cache, + database, + date, + healthcheckApplication, + httpServer, + repository, + taskScheduling, + telemetry, +}: { + cache?: Partial; + database?: Partial; + date?: Partial; + healthcheckApplication?: Partial; + httpServer?: Partial; + repository?: Partial; + taskScheduling?: Partial; + telemetry?: Partial; +}) { + return createAppDependencyStore({ + cache: buildMock(cache), + database: buildMock(database), + date: buildMock( + date ?? { + nowAsDate: () => new Date("2020-05-20"), + nowAsNumber: () => new Date("2020-05-20").getTime(), + }, + ), + healthcheckApplication: buildMock(healthcheckApplication), + httpServer: buildMock(httpServer), + logger: createMockLoggerProvider(), + repository: buildMock(repository), + taskScheduling: buildMock(taskScheduling), + telemetry: buildMock(telemetry), + }); +}