diff --git a/config.schema.json b/config.schema.json index 42e6143..780c2fc 100644 --- a/config.schema.json +++ b/config.schema.json @@ -35,6 +35,40 @@ }, "nullable": false, "x-typia-jsDocTags": [] + }, + "servers": { + "x-typia-required": false, + "type": "object", + "properties": { + "graphql": { + "x-typia-required": false, + "type": "object", + "properties": { + "path": { + "type": "string", + "nullable": false, + "x-typia-required": false + } + }, + "nullable": false, + "x-typia-jsDocTags": [] + }, + "trpc": { + "x-typia-required": false, + "type": "object", + "properties": { + "path": { + "type": "string", + "nullable": false, + "x-typia-required": false + } + }, + "nullable": false, + "x-typia-jsDocTags": [] + } + }, + "nullable": false, + "x-typia-jsDocTags": [] } }, "nullable": false, diff --git a/src/album/album.module.ts b/src/album/album.module.ts index c60fc46..2eb729b 100644 --- a/src/album/album.module.ts +++ b/src/album/album.module.ts @@ -7,7 +7,7 @@ import { AlbumService } from "@album/album.service"; import { AlbumResolver } from "@album/album.resolver"; @Module({ - imports: [MetadataModule, ConfigModule], + imports: [MetadataModule, ConfigModule.forFeature()], providers: [AlbumResolver, AlbumService], exports: [AlbumService], }) diff --git a/src/album/album.resolver.spec.ts b/src/album/album.resolver.spec.ts index 2c934ff..302904a 100644 --- a/src/album/album.resolver.spec.ts +++ b/src/album/album.resolver.spec.ts @@ -1,11 +1,11 @@ import { Test, TestingModule } from "@nestjs/testing"; import { MetadataModule } from "@metadata/metadata.module"; -import { ConfigModule } from "@config/config.module"; import { AlbumResolver } from "@album/album.resolver"; import { AlbumService } from "@album/album.service"; +import { installMockedConfig } from "@test/utils/installMockedConfig"; import { installMetadataMock, MockResolver } from "@test/utils/installMetadataMock"; describe("AlbumResolver", () => { @@ -14,7 +14,7 @@ describe("AlbumResolver", () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - imports: [MetadataModule, ConfigModule], + imports: [MetadataModule, installMockedConfig()], providers: [AlbumResolver, AlbumService], }).compile(); diff --git a/src/album/album.service.spec.ts b/src/album/album.service.spec.ts index ad433d7..1e1bc7e 100644 --- a/src/album/album.service.spec.ts +++ b/src/album/album.service.spec.ts @@ -1,11 +1,11 @@ import { Test, TestingModule } from "@nestjs/testing"; import { MetadataModule } from "@metadata/metadata.module"; -import { ConfigModule } from "@config/config.module"; import { AlbumService } from "@album/album.service"; import { installMetadataMock, MockResolver } from "@test/utils/installMetadataMock"; +import { installMockedConfig } from "@test/utils/installMockedConfig"; describe("AlbumService", () => { let service: AlbumService; @@ -13,7 +13,7 @@ describe("AlbumService", () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - imports: [MetadataModule, ConfigModule], + imports: [MetadataModule, installMockedConfig()], providers: [AlbumService], }).compile(); diff --git a/src/album/album.service.ts b/src/album/album.service.ts index 6bca296..c3c8669 100644 --- a/src/album/album.service.ts +++ b/src/album/album.service.ts @@ -4,15 +4,17 @@ import { Album } from "@common/album.dto"; import { MetadataService } from "@metadata/metadata.service"; +import { ConfigData } from "@config/config.module"; +import { InjectConfig } from "@config/config.decorator"; + import { ObjectService } from "@common/object.service"; -import { ConfigService } from "@config/config.service"; @Injectable() export class AlbumService extends ObjectService { public constructor( @Inject(MetadataService) private readonly metadataService: MetadataService, - @Inject(ConfigService) private readonly configService: ConfigService, + @InjectConfig() private readonly configData: ConfigData, ) { - super(metadataService, configService, ({ resolver, ...input }) => resolver.searchAlbum(input)); + super(metadataService, configData, ({ resolver, ...input }) => resolver.searchAlbum(input)); } } diff --git a/src/app.module.ts b/src/app.module.ts index 9a2cf6e..36f9d04 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -4,7 +4,9 @@ import { Module } from "@nestjs/common"; import { GraphQLModule } from "@nestjs/graphql"; import { ApolloDriver, ApolloDriverConfig } from "@nestjs/apollo"; -import { ConfigModule } from "@config/config.module"; +import { ConfigData, ConfigModule } from "@config/config.module"; +import { CONFIG_DATA } from "@config/config.decorator"; + import { MetadataModule } from "@metadata/metadata.module"; import { AlbumModule } from "@album/album.module"; import { TrackModule } from "@track/track.module"; @@ -13,12 +15,16 @@ import { TRPCServerModule } from "@trpc-server/trpc-server.module"; @Module({ imports: [ - ConfigModule, - GraphQLModule.forRoot({ + ConfigModule.forRoot(), + GraphQLModule.forRootAsync({ driver: ApolloDriver, - playground: process.env.NODE_ENV === "development", - autoSchemaFile: path.join(process.cwd(), "schema.graphqls"), - path: "/", + imports: [ConfigModule.forFeature()], + inject: [CONFIG_DATA], + useFactory: async (configData: ConfigData) => ({ + playground: process.env.NODE_ENV === "development", + autoSchemaFile: path.join(process.cwd(), "schema.graphqls"), + path: configData?.servers?.graphql?.path || "/", + }), }), TRPCServerModule, MetadataModule, diff --git a/src/artist/artist.module.ts b/src/artist/artist.module.ts index 7852693..760e542 100644 --- a/src/artist/artist.module.ts +++ b/src/artist/artist.module.ts @@ -7,7 +7,7 @@ import { ArtistService } from "@artist/artist.service"; import { ArtistResolver } from "@artist/artist.resolver"; @Module({ - imports: [MetadataModule, ConfigModule], + imports: [MetadataModule, ConfigModule.forFeature()], providers: [ArtistResolver, ArtistService], exports: [ArtistService], }) diff --git a/src/artist/artist.resolver.spec.ts b/src/artist/artist.resolver.spec.ts index e61e557..0dd9946 100644 --- a/src/artist/artist.resolver.spec.ts +++ b/src/artist/artist.resolver.spec.ts @@ -1,12 +1,12 @@ import { Test, TestingModule } from "@nestjs/testing"; import { MetadataModule } from "@metadata/metadata.module"; -import { ConfigModule } from "@config/config.module"; import { ArtistResolver } from "@artist/artist.resolver"; import { ArtistService } from "@artist/artist.service"; import { installMetadataMock, MockResolver } from "@test/utils/installMetadataMock"; +import { installMockedConfig } from "@test/utils/installMockedConfig"; describe("ArtistResolver", () => { let resolver: ArtistResolver; @@ -14,7 +14,7 @@ describe("ArtistResolver", () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - imports: [MetadataModule, ConfigModule], + imports: [MetadataModule, installMockedConfig()], providers: [ArtistResolver, ArtistService], }).compile(); diff --git a/src/artist/artist.service.spec.ts b/src/artist/artist.service.spec.ts index 795e9c6..92e5efc 100644 --- a/src/artist/artist.service.spec.ts +++ b/src/artist/artist.service.spec.ts @@ -1,11 +1,11 @@ import { Test, TestingModule } from "@nestjs/testing"; import { MetadataModule } from "@metadata/metadata.module"; -import { ConfigModule } from "@config/config.module"; import { ArtistService } from "@artist/artist.service"; import { installMetadataMock, MockResolver } from "@test/utils/installMetadataMock"; +import { installMockedConfig } from "@test/utils/installMockedConfig"; describe("ArtistService", () => { let service: ArtistService; @@ -13,7 +13,7 @@ describe("ArtistService", () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - imports: [MetadataModule, ConfigModule], + imports: [MetadataModule, installMockedConfig()], providers: [ArtistService], }).compile(); diff --git a/src/artist/artist.service.ts b/src/artist/artist.service.ts index 3d21518..318a78d 100644 --- a/src/artist/artist.service.ts +++ b/src/artist/artist.service.ts @@ -1,7 +1,9 @@ import { Inject, Injectable } from "@nestjs/common"; import { MetadataService } from "@metadata/metadata.service"; -import { ConfigService } from "@config/config.service"; + +import { ConfigData } from "@config/config.module"; +import { InjectConfig } from "@config/config.decorator"; import { ObjectService } from "@common/object.service"; import { Artist } from "@common/artist.dto"; @@ -10,8 +12,8 @@ import { Artist } from "@common/artist.dto"; export class ArtistService extends ObjectService { public constructor( @Inject(MetadataService) private readonly metadataService: MetadataService, - @Inject(ConfigService) private readonly configService: ConfigService, + @InjectConfig() private readonly configData: ConfigData, ) { - super(metadataService, configService, ({ resolver, ...input }) => resolver.searchArtist(input)); + super(metadataService, configData, ({ resolver, ...input }) => resolver.searchArtist(input)); } } diff --git a/src/common/object.service.spec.ts b/src/common/object.service.spec.ts index 5bec5b7..3e28a6f 100644 --- a/src/common/object.service.spec.ts +++ b/src/common/object.service.spec.ts @@ -1,11 +1,11 @@ import { ObjectService } from "@common/object.service"; -import { ConfigService } from "@config/config.service"; +import { ConfigData, DEFAULT_CONFIG } from "@config/config.module"; import { MetadataServiceMock } from "@test/utils/metadata.service.mock"; class MockService extends ObjectService { - public constructor() { - super(new MetadataServiceMock(), new ConfigService(), async () => [{ test: "value" }]); + public constructor(config: ConfigData = DEFAULT_CONFIG) { + super(new MetadataServiceMock(), config, async () => [{ test: "value" }]); } } @@ -22,11 +22,7 @@ describe("ObjectService", () => { }); it("should set cache TTL on module initialization", async () => { - const service = new MockService(); - Object.defineProperty(service["config"], "getConfig", { - value: () => ({ cacheTTL: 1000 }), - }); - + const service = new MockService({ cacheTTL: 1000, resolvers: {} }); await service.onModuleInit(); expect(service["searchCache"]["timeToLive"]).toBe(1000); @@ -34,10 +30,6 @@ describe("ObjectService", () => { it("should not set cache TTL on module initialization if it is not defined", async () => { const service = new MockService(); - Object.defineProperty(service["config"], "getConfig", { - value: () => ({}), - }); - await service.onModuleInit(); expect(service["searchCache"]["timeToLive"]).toBe(3600); diff --git a/src/common/object.service.ts b/src/common/object.service.ts index f3fc047..8b77425 100644 --- a/src/common/object.service.ts +++ b/src/common/object.service.ts @@ -1,12 +1,14 @@ +import { OnModuleInit } from "@nestjs/common"; + import { MetadataService } from "@metadata/metadata.service"; import BaseResolver from "@metadata/resolvers/base"; +import { ConfigData } from "@config/config.module"; + import { SearchInput } from "@common/search-input.dto"; import { CacheStorage } from "@utils/cache"; import { AsyncFn } from "@utils/types"; -import { ConfigService } from "@config/config.service"; -import { OnModuleInit } from "@nestjs/common"; interface SearchInputData extends SearchInput { resolver: BaseResolver; @@ -19,12 +21,12 @@ export class ObjectService implements OnModuleInit { constructor( private readonly metadata: MetadataService, - private readonly config: ConfigService, + private readonly config: ConfigData, private readonly searchFn: AsyncFn, ) {} public onModuleInit() { - const config = this.config.getConfig(); + const config = this.config; if (typeof config.cacheTTL !== "number") { return; } diff --git a/src/config/config.decorator.ts b/src/config/config.decorator.ts new file mode 100644 index 0000000..262f77c --- /dev/null +++ b/src/config/config.decorator.ts @@ -0,0 +1,7 @@ +import { Inject } from "@nestjs/common"; + +export const CONFIG_DATA = "CONFIG_DATA"; + +export function InjectConfig() { + return Inject(CONFIG_DATA); +} diff --git a/src/config/config.service.spec.ts b/src/config/config.module.spec.ts similarity index 66% rename from src/config/config.service.spec.ts rename to src/config/config.module.spec.ts index a34b37d..4a4fea5 100644 --- a/src/config/config.service.spec.ts +++ b/src/config/config.module.spec.ts @@ -1,29 +1,34 @@ import * as fs from "fs-extra"; -import { Test, TestingModule } from "@nestjs/testing"; - -import { ConfigData, ConfigService, DEFAULT_CONFIG } from "@config/config.service"; +import { Test } from "@nestjs/testing"; +import { ConfigData, ConfigModule, DEFAULT_CONFIG } from "@config/config.module"; +import { InjectConfig } from "@config/config.decorator"; jest.mock("fs-extra"); const mockedFs = fs as jest.Mocked; -describe("ConfigService", () => { - let service: ConfigService; +async function createMockedModule() { + class MockService { + public constructor(@InjectConfig() public readonly config: ConfigData) {} + } - beforeEach(async () => { - const module: TestingModule = await Test.createTestingModule({ - providers: [ConfigService], - }).compile(); + const module = await Test.createTestingModule({ + imports: [ConfigModule.forFeature()], + providers: [MockService], + }).compile(); - service = module.get(ConfigService); - }); + const mockService = module.get(MockService); + + return { module, mockService }; +} +describe("ConfigModule", () => { afterEach(() => { jest.resetAllMocks(); }); it("should be defined", () => { - expect(service).toBeDefined(); + expect(module).toBeDefined(); }); it("should be able to read config file from disk", async () => { @@ -39,10 +44,10 @@ describe("ConfigService", () => { mockedFs.existsSync.mockReturnValue(true); mockedFs.readJson.mockResolvedValue(mockedConfig); - await service.onModuleInit(); + const { mockService } = await createMockedModule(); expect(mockedFs.readJson).toBeCalledTimes(1); - expect(service.getConfig()).toEqual(mockedConfig); + expect(mockService.config).toEqual(mockedConfig); }); it("should write default config to disk if config file does not exist", async () => { @@ -50,20 +55,20 @@ describe("ConfigService", () => { mockedFs.writeJson.mockResolvedValue(); mockedFs.readJson.mockResolvedValue(DEFAULT_CONFIG); - await service.onModuleInit(); + const { mockService } = await createMockedModule(); expect(mockedFs.writeJson).toBeCalledTimes(1); expect(mockedFs.writeJson).toBeCalledWith(expect.stringContaining("config.json"), DEFAULT_CONFIG, { spaces: 4, }); - expect(service.getConfig()).toEqual(DEFAULT_CONFIG); + expect(mockService.config).toEqual(DEFAULT_CONFIG); }); it("should throw error if config file exists but is invalid", async () => { mockedFs.existsSync.mockReturnValue(true); mockedFs.readJson.mockResolvedValue({}); - await expect(service.onModuleInit()).rejects.toThrowError("config file is invalid."); + await expect(createMockedModule()).rejects.toThrowError("config file is invalid."); }); it("should write schema definition to disk in development mode", async () => { @@ -75,7 +80,7 @@ describe("ConfigService", () => { return; }); - await service.onModuleInit(); + await createMockedModule(); expect(mockedFs.writeJson).toBeCalledTimes(1); expect(mockedFs.writeJson).toBeCalledWith(expect.stringContaining("config.schema.json"), expect.anything(), { @@ -84,8 +89,4 @@ describe("ConfigService", () => { process.env.NODE_ENV = "test"; }); - - it("should throw error when getConfig is called before onModuleInit", () => { - expect(() => service.getConfig()).toThrowError("Config service is not initialized yet"); - }); }); diff --git a/src/config/config.module.ts b/src/config/config.module.ts index 013986c..1c380d9 100644 --- a/src/config/config.module.ts +++ b/src/config/config.module.ts @@ -1,9 +1,94 @@ -import { Module } from "@nestjs/common"; +import * as fs from "fs-extra"; +import path from "path"; +import betterAjvErrors from "better-ajv-errors"; +import { application } from "typia"; +import { PartialDeep } from "type-fest"; +import Ajv from "ajv"; -import { ConfigService } from "@config/config.service"; +import { dereference } from "@apidevtools/json-schema-ref-parser"; -@Module({ - providers: [ConfigService], - exports: [ConfigService], -}) -export class ConfigModule {} +import { DynamicModule, Logger, Module, Provider } from "@nestjs/common"; +import { ResolverOptionsMap } from "@metadata/resolvers"; + +import { CONFIG_DATA } from "@config/config.decorator"; + +export type ServerTypes = "graphql" | "trpc"; +export type ServerConfig = { + [key in ServerTypes]: { + path: string; + }; +}; + +export type ConfigData = { + cacheTTL?: number; + resolvers: Partial; + servers?: PartialDeep; +}; + +const ajv = new Ajv({ allErrors: true, strict: false }); +export const DEFAULT_CONFIG: ConfigData = { + resolvers: {}, +}; + +@Module({}) +export class ConfigModule { + private static readonly CONFIG_FILE_PATH = path.join(process.cwd(), "config.json"); + private static readonly CONFIG_SCHEMA = application<[ConfigData]>(); + private static readonly logger = new Logger(ConfigModule.name); + + public static forRoot(): DynamicModule { + return { + module: ConfigModule, + providers: [], + exports: [], + }; + } + + public static forFeature(): DynamicModule { + const configDataProvider: Provider = { + provide: CONFIG_DATA, + useFactory: async () => { + return ConfigModule.loadConfigData(); + }, + }; + + return { + module: ConfigModule, + providers: [configDataProvider], + exports: [configDataProvider], + }; + } + + private static async loadConfigData(): Promise { + if (!fs.existsSync(ConfigModule.CONFIG_FILE_PATH)) { + ConfigModule.logger.warn(`Config file does not exist. we will make a new default config file for you.`); + await fs.writeJson(ConfigModule.CONFIG_FILE_PATH, DEFAULT_CONFIG, { + spaces: 4, + }); + } + + const schema = await dereference(ConfigModule.CONFIG_SCHEMA); + if (process.env.NODE_ENV === "development") { + ConfigModule.logger.debug("Write config file schema to ./config.schema.json"); + + await fs.writeJson(path.join(process.cwd(), "config.schema.json"), schema["schemas"][0], { + spaces: 4, + }); + } + + const data: ConfigData = await fs.readJson(ConfigModule.CONFIG_FILE_PATH); + const validate = ajv.compile(schema["schemas"][0]); + const valid = validate(data); + if (!valid && validate.errors) { + const output = betterAjvErrors(schema, data, validate.errors, { + format: "cli", + indent: 4, + }); + + throw new Error(`config file is invalid.\n${output}`); + } + + ConfigModule.logger.log(`Configuration successfully loaded`); + return data; + } +} diff --git a/src/config/config.service.ts b/src/config/config.service.ts deleted file mode 100644 index 9b3c415..0000000 --- a/src/config/config.service.ts +++ /dev/null @@ -1,71 +0,0 @@ -import * as _ from "lodash"; -import * as fs from "fs-extra"; -import * as path from "path"; -import Ajv from "ajv"; -import { application } from "typia"; -import betterAjvErrors from "better-ajv-errors"; - -import { dereference } from "@apidevtools/json-schema-ref-parser"; - -import { Injectable, Logger, OnModuleInit } from "@nestjs/common"; -import { ResolverOptionsMap } from "@metadata/resolvers"; - -export type ConfigData = { - cacheTTL?: number; - resolvers: Partial; -}; - -const ajv = new Ajv({ allErrors: true, strict: false }); -export const DEFAULT_CONFIG: ConfigData = { - resolvers: {}, -}; - -@Injectable() -export class ConfigService implements OnModuleInit { - private static readonly CONFIG_FILE_PATH = path.join(process.cwd(), "config.json"); - private static readonly CONFIG_SCHEMA = application<[ConfigData]>(); - - private readonly logger: Logger = new Logger(ConfigService.name); - private appConfig: ConfigData | null = null; - - public async onModuleInit(): Promise { - if (!fs.existsSync(ConfigService.CONFIG_FILE_PATH)) { - this.logger.warn(`Config file does not exist. we will make a new default config file for you.`); - await fs.writeJson(ConfigService.CONFIG_FILE_PATH, DEFAULT_CONFIG, { - spaces: 4, - }); - } - - const schema = await dereference(ConfigService.CONFIG_SCHEMA); - if (process.env.NODE_ENV === "development") { - this.logger.debug("Write config file schema to ./config.schema.json"); - - await fs.writeJson(path.join(process.cwd(), "config.schema.json"), schema["schemas"][0], { - spaces: 4, - }); - } - - const data: ConfigData = await fs.readJson(ConfigService.CONFIG_FILE_PATH); - const validate = ajv.compile(schema["schemas"][0]); - const valid = validate(data); - if (!valid && validate.errors) { - const output = betterAjvErrors(schema, data, validate.errors, { - format: "cli", - indent: 4, - }); - - throw new Error(`config file is invalid.\n${output}`); - } - - this.appConfig = data; - this.logger.log(`Configuration successfully loaded`); - } - - public getConfig(): ConfigData { - if (!this.appConfig) { - throw new Error("Config service is not initialized yet"); - } - - return _.cloneDeep(this.appConfig); - } -} diff --git a/src/main.ts b/src/main.ts index 1d9d421..c979ff2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -11,7 +11,6 @@ import { AppModule } from "@root/app.module"; async function bootstrap() { const app = await NestFactory.create(AppModule); const trpcService = app.get(TRPCServerService); - app.useGlobalPipes(new ValidationPipe()); trpcService.applyMiddleware(app); diff --git a/src/metadata/metadata.module.ts b/src/metadata/metadata.module.ts index c401616..e448919 100644 --- a/src/metadata/metadata.module.ts +++ b/src/metadata/metadata.module.ts @@ -5,7 +5,7 @@ import { ConfigModule } from "@config/config.module"; import { MetadataService } from "@metadata/metadata.service"; @Module({ - imports: [ConfigModule], + imports: [ConfigModule.forFeature()], providers: [MetadataService], exports: [MetadataService], }) diff --git a/src/metadata/metadata.service.spec.ts b/src/metadata/metadata.service.spec.ts index 4b91513..b3dc78c 100644 --- a/src/metadata/metadata.service.spec.ts +++ b/src/metadata/metadata.service.spec.ts @@ -1,22 +1,19 @@ import { Test, TestingModule } from "@nestjs/testing"; -import { ConfigModule } from "@config/config.module"; - import { MetadataService } from "@metadata/metadata.service"; -import { installConfigMock } from "@test/utils/installConfigMock"; +import { installMockedConfig } from "@test/utils/installMockedConfig"; describe("MetadataService", () => { let service: MetadataService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - imports: [ConfigModule], + imports: [installMockedConfig()], providers: [MetadataService], }).compile(); service = module.get(MetadataService); - installConfigMock(service["configService"]); }); afterEach(() => { diff --git a/src/metadata/metadata.service.ts b/src/metadata/metadata.service.ts index cb0bab0..00308c4 100644 --- a/src/metadata/metadata.service.ts +++ b/src/metadata/metadata.service.ts @@ -1,18 +1,19 @@ -import { Inject, Injectable, Logger, OnModuleInit } from "@nestjs/common"; +import { Injectable, Logger, OnModuleInit } from "@nestjs/common"; -import { createResolver, ResolverOptions, ResolverPair, ResolverTypes } from "@metadata/resolvers"; +import { ConfigData } from "@config/config.module"; +import { InjectConfig } from "@config/config.decorator"; -import { ConfigService } from "@config/config.service"; +import { createResolver, ResolverOptions, ResolverPair, ResolverTypes } from "@metadata/resolvers"; @Injectable() export class MetadataService implements OnModuleInit { private readonly logger = new Logger(MetadataService.name); private readonly resolvers: ResolverPair[] = []; - public constructor(@Inject(ConfigService) private readonly configService: ConfigService) {} + public constructor(@InjectConfig() private readonly config: ConfigData) {} public async onModuleInit() { - const { resolvers } = this.configService.getConfig(); + const { resolvers } = this.config; for (const [name, options] of Object.entries(resolvers) as [ResolverTypes, ResolverOptions][]) { const resolver = createResolver(name, options); diff --git a/src/track/track.module.ts b/src/track/track.module.ts index 9f6c59c..d8de708 100644 --- a/src/track/track.module.ts +++ b/src/track/track.module.ts @@ -7,7 +7,7 @@ import { TrackService } from "@track/track.service"; import { TrackResolver } from "@track/track.resolver"; @Module({ - imports: [MetadataModule, ConfigModule], + imports: [MetadataModule, ConfigModule.forFeature()], providers: [TrackResolver, TrackService], exports: [TrackService], }) diff --git a/src/track/track.resolver.spec.ts b/src/track/track.resolver.spec.ts index 0714288..7a05028 100644 --- a/src/track/track.resolver.spec.ts +++ b/src/track/track.resolver.spec.ts @@ -1,12 +1,12 @@ import { Test, TestingModule } from "@nestjs/testing"; import { MetadataModule } from "@metadata/metadata.module"; -import { ConfigModule } from "@config/config.module"; import { TrackResolver } from "@track/track.resolver"; import { TrackService } from "@track/track.service"; import { installMetadataMock, MockResolver } from "@test/utils/installMetadataMock"; +import { installMockedConfig } from "@test/utils/installMockedConfig"; describe("TrackResolver", () => { let resolver: TrackResolver; @@ -14,7 +14,7 @@ describe("TrackResolver", () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - imports: [MetadataModule, ConfigModule], + imports: [MetadataModule, installMockedConfig()], providers: [TrackResolver, TrackService], }).compile(); diff --git a/src/track/track.service.spec.ts b/src/track/track.service.spec.ts index 4c1ff8e..1478b4b 100644 --- a/src/track/track.service.spec.ts +++ b/src/track/track.service.spec.ts @@ -1,11 +1,11 @@ import { Test, TestingModule } from "@nestjs/testing"; import { MetadataModule } from "@metadata/metadata.module"; -import { ConfigModule } from "@config/config.module"; import { TrackService } from "@track/track.service"; import { installMetadataMock, MockResolver } from "@test/utils/installMetadataMock"; +import { installMockedConfig } from "@test/utils/installMockedConfig"; describe("TrackService", () => { let service: TrackService; @@ -13,7 +13,7 @@ describe("TrackService", () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - imports: [MetadataModule, ConfigModule], + imports: [MetadataModule, installMockedConfig()], providers: [TrackService], }).compile(); diff --git a/src/track/track.service.ts b/src/track/track.service.ts index 70f8bae..77dbb43 100644 --- a/src/track/track.service.ts +++ b/src/track/track.service.ts @@ -2,8 +2,10 @@ import { Inject, Injectable } from "@nestjs/common"; import { MetadataService } from "@metadata/metadata.service"; +import { ConfigData } from "@config/config.module"; +import { InjectConfig } from "@config/config.decorator"; + import { ObjectService } from "@common/object.service"; -import { ConfigService } from "@config/config.service"; import { Track } from "@common/track.dto"; @@ -11,8 +13,8 @@ import { Track } from "@common/track.dto"; export class TrackService extends ObjectService { public constructor( @Inject(MetadataService) private readonly metadataService: MetadataService, - @Inject(ConfigService) private readonly configService: ConfigService, + @InjectConfig() private readonly configData: ConfigData, ) { - super(metadataService, configService, ({ resolver, ...input }) => resolver.searchTrack(input)); + super(metadataService, configData, ({ resolver, ...input }) => resolver.searchTrack(input)); } } diff --git a/src/trpc-server/trpc-server.module.ts b/src/trpc-server/trpc-server.module.ts index 987c4b5..3521773 100644 --- a/src/trpc-server/trpc-server.module.ts +++ b/src/trpc-server/trpc-server.module.ts @@ -3,11 +3,12 @@ import { Module } from "@nestjs/common"; import { TrackModule } from "@track/track.module"; import { AlbumModule } from "@album/album.module"; import { ArtistModule } from "@artist/artist.module"; +import { ConfigModule } from "@config/config.module"; import { TRPCServerService } from "@trpc-server/trpc-server.service"; @Module({ - imports: [TrackModule, AlbumModule, ArtistModule], + imports: [TrackModule, AlbumModule, ArtistModule, ConfigModule.forFeature()], providers: [TRPCServerService], exports: [TRPCServerService], }) diff --git a/src/trpc-server/trpc-server.service.spec.ts b/src/trpc-server/trpc-server.service.spec.ts index 4765b75..170da53 100644 --- a/src/trpc-server/trpc-server.service.spec.ts +++ b/src/trpc-server/trpc-server.service.spec.ts @@ -6,12 +6,14 @@ import { ArtistModule } from "@artist/artist.module"; import { TRPCServerService } from "@trpc-server/trpc-server.service"; +import { installMockedConfig } from "@test/utils/installMockedConfig"; + describe("TRPCService", () => { let service: TRPCServerService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - imports: [TrackModule, AlbumModule, ArtistModule], + imports: [installMockedConfig(), TrackModule, AlbumModule, ArtistModule], providers: [TRPCServerService], }).compile(); diff --git a/src/trpc-server/trpc-server.service.ts b/src/trpc-server/trpc-server.service.ts index a4f0613..d1e2352 100644 --- a/src/trpc-server/trpc-server.service.ts +++ b/src/trpc-server/trpc-server.service.ts @@ -9,6 +9,9 @@ import { TrackService } from "@track/track.service"; import { AlbumService } from "@album/album.service"; import { ArtistService } from "@artist/artist.service"; +import { ConfigData } from "@config/config.module"; +import { InjectConfig } from "@config/config.decorator"; + import { SearchInput } from "@common/search-input.dto"; import { RouteArgs } from "@trpc-server/types"; @@ -23,14 +26,18 @@ export class TRPCServerService { }); public constructor( + @InjectConfig() private readonly configData: ConfigData, @Inject(TrackService) private readonly trackService: TrackService, @Inject(AlbumService) private readonly albumService: AlbumService, @Inject(ArtistService) private readonly artistService: ArtistService, ) {} public applyMiddleware(app: NestExpressApplication) { + const { servers = {} } = this.configData; + const { trpc = {} } = servers; + app.use( - "/trpc", + trpc.path || "/trpc", createExpressMiddleware({ router: this.appRouter, }), diff --git a/test/utils/installConfigMock.ts b/test/utils/installConfigMock.ts deleted file mode 100644 index 06be507..0000000 --- a/test/utils/installConfigMock.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ConfigData, ConfigService } from "@config/config.service"; -import * as Resolvers from "@metadata/resolvers"; - -import { MockResolver } from "@test/utils/installMetadataMock"; - -export function installConfigMock(configService: ConfigService) { - jest.spyOn(configService, "getConfig").mockImplementation(() => { - return { - resolvers: { - mocked: {}, - }, - } as ConfigData; - }); - - jest.spyOn(Resolvers, "createResolver").mockImplementation(() => { - return new MockResolver(); - }); -} diff --git a/test/utils/installMockedConfig.ts b/test/utils/installMockedConfig.ts new file mode 100644 index 0000000..3cfc690 --- /dev/null +++ b/test/utils/installMockedConfig.ts @@ -0,0 +1,37 @@ +import path from "path"; + +import { DynamicModule, Provider } from "@nestjs/common"; + +import { ConfigData, ConfigModule } from "@config/config.module"; +import { CONFIG_DATA } from "@config/config.decorator"; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +require("dotenv").config({ + path: path.join(process.cwd(), "./.env.test"), +}); + +export function installMockedConfig(): DynamicModule { + const configDataProvider: Provider = { + provide: CONFIG_DATA, + useFactory: (): ConfigData => { + if (!process.env.SPOTIFY_CLIENT_ID || !process.env.SPOTIFY_CLIENT_SECRET) { + throw new Error("SPOTIFY_CLIENT_ID and SPOTIFY_CLIENT_SECRET must be set"); + } + + return { + resolvers: { + spotify: { + clientId: process.env.SPOTIFY_CLIENT_ID, + clientSecret: process.env.SPOTIFY_CLIENT_SECRET, + }, + }, + }; + }, + }; + + return { + module: ConfigModule, + providers: [configDataProvider], + exports: [configDataProvider], + }; +}