diff --git a/readme.md b/readme.md index e108e79..399797b 100644 --- a/readme.md +++ b/readme.md @@ -52,8 +52,6 @@ export { A Factory is responsible for creating new instances of Dataloader. Each factory creates only one type of Dataloader so for each relation you will need to define a Factory. You define a Factory by subclassing the provided `DataloaderFactory` and implemneting `load()` and `id()` methods on it, at minimum. -> ⚠️ Each `DataloaderFactory` implementation must be added to your module's `providers: []` and `exports: []` sections in order to make it available to other parts of your application. - > Each Factory can be considered global in the dependency graph, you do not need to import the module that provides the Factory in order to use it elsewhere in your application. ```ts @@ -124,10 +122,33 @@ export { } ``` +### Export the factory + +Each Dataloader factory you create must be added to Nest.js DI container via `DataloaderModule.forFeature()`. Don't forget to also export the `DataloaderModule` to make the Dataloader factory available to other modules. + +```ts +// authors.module.ts +import { Module } from '@nestjs/common' +import { DataloaderModule } from '@strv/nestjs-dataloader' +import { BooksService } from './books.service.js' +import { AuthorBooksLoaderFactory } from './AuthorBooksLoader.factory.js' + +@Module({ + imports:[ + DataloaderModule.forFeature([AuthorBooksLoaderFactory]), + ], + providers: [BooksService], + exports: [DataloaderModule], +}) +class AuthorsModule {} +``` + ### Inject a Dataloader Now that we have a Dataloader factory defined and available in the DI container, it's time to put it to some use! To obtain a Dataloader instance, you can use the provided `@Loader()` param decorator in your GraphQL resolvers. +> 💡 It's possible to use the `@Loader()` param decorator also in REST controllers although the benefits of using Dataloaders in REST APIs are not that tangible as in GraphQL. However, if your app provides both GraphQL and REST interfaces this might be a good way to share some logic between the two. + ```ts // author.resolver.ts import { Resolver, ResolveField } from '@nestjs/graphql' diff --git a/src/Dataloader.module.ts b/src/Dataloader.module.ts index 9a37008..a0506bb 100644 --- a/src/Dataloader.module.ts +++ b/src/Dataloader.module.ts @@ -1,43 +1,33 @@ -import { DynamicModule, type FactoryProvider, Module } from '@nestjs/common' -import { APP_INTERCEPTOR } from '@nestjs/core' -import { DataloaderInterceptor } from './Dataloader.interceptor.js' -import { OPTIONS_TOKEN } from './internal.js' -import { type DataloaderOptions } from './types.js' +import { type DynamicModule, Module } from '@nestjs/common' +import { type DataloaderModuleOptions, type Factory, type DataloaderOptions } from './types.js' +import { DataloaderCoreModule } from './DataloaderCore.module.js' -@Module({ - providers: [ - { provide: APP_INTERCEPTOR, useClass: DataloaderInterceptor }, - ], -}) +@Module({}) class DataloaderModule { static forRoot(options?: DataloaderOptions): DynamicModule { return { module: DataloaderModule, - providers: [{ - provide: OPTIONS_TOKEN, - useValue: options, - }], + imports: [DataloaderCoreModule.forRoot(options)], } } static forRootAsync(options: DataloaderModuleOptions): DynamicModule { return { module: DataloaderModule, - imports: options.imports ?? [], - providers: [{ - provide: OPTIONS_TOKEN, - inject: options.inject ?? [], - useFactory: options.useFactory, - }], + imports: [DataloaderCoreModule.forRootAsync(options)], } } -} -/** Dataloader module options for async configuration */ -type DataloaderModuleOptions = Omit, 'provide'> & Pick + static forFeature(loaders: Factory[]): DynamicModule { + return { + module: DataloaderModule, + providers: loaders, + exports: loaders, + } + } +} export { DataloaderModule, - DataloaderModuleOptions, } diff --git a/src/DataloaderCore.module.ts b/src/DataloaderCore.module.ts new file mode 100644 index 0000000..67e6c23 --- /dev/null +++ b/src/DataloaderCore.module.ts @@ -0,0 +1,44 @@ +import { type Provider, type DynamicModule, Module } from '@nestjs/common' +import { APP_INTERCEPTOR } from '@nestjs/core' +import { DataloaderInterceptor } from './Dataloader.interceptor.js' +import { OPTIONS_TOKEN } from './internal.js' +import { type DataloaderModuleOptions, type DataloaderOptions } from './types.js' + +/** @private */ +const interceptor: Provider = { provide: APP_INTERCEPTOR, useClass: DataloaderInterceptor } + +/** @private */ +@Module({}) +class DataloaderCoreModule { + static forRoot(options?: DataloaderOptions): DynamicModule { + return { + module: DataloaderCoreModule, + providers: [ + interceptor, + { + provide: OPTIONS_TOKEN, + useValue: options, + }, + ], + } + } + + static forRootAsync(options: DataloaderModuleOptions): DynamicModule { + return { + module: DataloaderCoreModule, + imports: options.imports ?? [], + providers: [ + interceptor, + { + provide: OPTIONS_TOKEN, + inject: options.inject ?? [], + useFactory: options.useFactory, + }, + ], + } + } +} + +export { + DataloaderCoreModule, +} diff --git a/src/Loader.decorator.ts b/src/Loader.decorator.ts index 6ed9b3f..0382794 100644 --- a/src/Loader.decorator.ts +++ b/src/Loader.decorator.ts @@ -1,6 +1,6 @@ import { createParamDecorator, type ExecutionContext } from '@nestjs/common' -import { lifetimeKey, store, type Factory } from './internal.js' -import { type LifetimeKeyFn } from './types.js' +import { lifetimeKey, store } from './internal.js' +import { type Factory, type LifetimeKeyFn } from './types.js' import { DataloaderException } from './DataloaderException.js' /** diff --git a/src/index.ts b/src/index.ts index 2ae9a1f..916599f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ -export { LifetimeKeyFn, DataloaderOptions } from './types.js' +export { LifetimeKeyFn, DataloaderOptions, DataloaderModuleOptions } from './types.js' export { DataloaderException } from './DataloaderException.js' export { Loader, createLoaderDecorator } from './Loader.decorator.js' export { DataloaderFactory, LoaderFrom, Aggregated } from './Dataloader.factory.js' -export { DataloaderModule, DataloaderModuleOptions } from './Dataloader.module.js' +export { DataloaderModule } from './Dataloader.module.js' diff --git a/src/internal.ts b/src/internal.ts index cfe2a36..62e216e 100644 --- a/src/internal.ts +++ b/src/internal.ts @@ -1,20 +1,13 @@ -import { type ExecutionContext, type Type } from '@nestjs/common' +import { type ExecutionContext } from '@nestjs/common' import { type ModuleRef } from '@nestjs/core' import { GqlExecutionContext, type GqlContextType } from '@nestjs/graphql' import type DataLoader from 'dataloader' -import { type DataloaderFactory } from './Dataloader.factory.js' -import { type LifetimeKeyFn } from './types.js' +import { type Factory, type LifetimeKeyFn } from './types.js' import { DataloaderException } from './DataloaderException.js' /** @private */ const OPTIONS_TOKEN = Symbol('DataloaderModuleOptions') -/** - * DataloaderFactory constructor type - * @private - */ -type Factory = Type> - /** @private */ interface StoreItem { /** ModuleRef is used by the `@Loader()` decorator to pull the Factory instance from Nest's DI container */ @@ -48,6 +41,5 @@ const lifetimeKey: LifetimeKeyFn = (context: ExecutionContext) => { export { OPTIONS_TOKEN, store, - Factory, lifetimeKey, } diff --git a/src/types.ts b/src/types.ts index c3233ac..51cb535 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,4 +1,8 @@ -import { type ExecutionContext } from '@nestjs/common' +import { type Type, type ExecutionContext, type FactoryProvider, type DynamicModule } from '@nestjs/common' +import { type DataloaderFactory } from './Dataloader.factory.js' + +/** DataloaderFactory constructor type */ +type Factory = Type> /** * Given an execution context, extract a value out of it that is @@ -22,7 +26,12 @@ interface DataloaderOptions { lifetime?: LifetimeKeyFn } +/** Dataloader module options for async configuration */ +type DataloaderModuleOptions = Omit, 'provide'> & Pick + export { + Factory, LifetimeKeyFn, DataloaderOptions, + DataloaderModuleOptions, } diff --git a/test/DataloaderModule.test.ts b/test/DataloaderModule.test.ts index 3291e32..6050193 100644 --- a/test/DataloaderModule.test.ts +++ b/test/DataloaderModule.test.ts @@ -1,6 +1,7 @@ import { describe } from 'vitest' import { Test, TestingModule } from '@nestjs/testing' -import { DataloaderModule } from '@strv/nestjs-dataloader' +import { Injectable } from '@nestjs/common' +import { DataloaderFactory, DataloaderModule } from '@strv/nestjs-dataloader' describe('DataloaderModule', it => { it('exists', t => { @@ -26,4 +27,27 @@ describe('DataloaderModule', it => { t.expect(app).toBeInstanceOf(TestingModule) }) + + it('.forFeatre()', async t => { + @Injectable() + class SampleLoaderFactory extends DataloaderFactory { + load = async (keys: unknown[]) => await Promise.resolve(keys) + id = (key: unknown) => key + } + + const provider = DataloaderModule.forFeature([SampleLoaderFactory]) + const module = Test.createTestingModule({ imports: [ + DataloaderModule.forRoot(), + provider, + ] }) + const app = await module.compile() + t.onTestFinished(async () => await app.close()) + + t.expect(app).toBeInstanceOf(TestingModule) + + t.expect(provider).toBeDefined() + t.expect(provider.module).toBe(DataloaderModule) + t.expect(provider.providers).toEqual([SampleLoaderFactory]) + t.expect(provider.exports).toEqual([SampleLoaderFactory]) + }) }) diff --git a/test/Loader.test.ts b/test/Loader.test.ts index 25639f0..4fef91c 100644 --- a/test/Loader.test.ts +++ b/test/Loader.test.ts @@ -10,13 +10,8 @@ describe('@Loader()', it => { it('injects the dataloader instance into the request handler', async t => { @Injectable() class SampleLoaderFactory extends DataloaderFactory { - async load(keys: unknown[]) { - return await Promise.resolve(keys) - } - - id(key: unknown) { - return key - } + load = async (keys: unknown[]) => await Promise.resolve(keys) + id = (key: unknown) => key } @Controller() @@ -28,10 +23,12 @@ describe('@Loader()', it => { } const module = await Test.createTestingModule({ - imports: [DataloaderModule.forRoot()], + imports: [ + DataloaderModule.forRoot(), + DataloaderModule.forFeature([SampleLoaderFactory]), + ], controllers: [TestController], - providers: [SampleLoaderFactory], - exports: [SampleLoaderFactory], + exports: [DataloaderModule], }).compile() const app = await module.createNestApplication().init() t.onTestFinished(async () => await app.close())