Skip to content

Commit 33af3fb

Browse files
Merge pull request #2 from strvcom:feat/dataloadermodule-forfeature
feat: implement `DataloaderModule.forFeature()` 💪
2 parents d74f04c + 21c6985 commit 33af3fb

9 files changed

+129
-52
lines changed

readme.md

+23-2
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,6 @@ export {
5252

5353
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.
5454

55-
> ⚠️ 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.
56-
5755
> 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.
5856
5957
```ts
@@ -124,10 +122,33 @@ export {
124122
}
125123
```
126124

125+
### Export the factory
126+
127+
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.
128+
129+
```ts
130+
// authors.module.ts
131+
import { Module } from '@nestjs/common'
132+
import { DataloaderModule } from '@strv/nestjs-dataloader'
133+
import { BooksService } from './books.service.js'
134+
import { AuthorBooksLoaderFactory } from './AuthorBooksLoader.factory.js'
135+
136+
@Module({
137+
imports:[
138+
DataloaderModule.forFeature([AuthorBooksLoaderFactory]),
139+
],
140+
providers: [BooksService],
141+
exports: [DataloaderModule],
142+
})
143+
class AuthorsModule {}
144+
```
145+
127146
### Inject a Dataloader
128147

129148
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.
130149

150+
> 💡 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.
151+
131152
```ts
132153
// author.resolver.ts
133154
import { Resolver, ResolveField } from '@nestjs/graphql'

src/Dataloader.module.ts

+14-24
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,33 @@
1-
import { DynamicModule, type FactoryProvider, Module } from '@nestjs/common'
2-
import { APP_INTERCEPTOR } from '@nestjs/core'
3-
import { DataloaderInterceptor } from './Dataloader.interceptor.js'
4-
import { OPTIONS_TOKEN } from './internal.js'
5-
import { type DataloaderOptions } from './types.js'
1+
import { type DynamicModule, Module } from '@nestjs/common'
2+
import { type DataloaderModuleOptions, type Factory, type DataloaderOptions } from './types.js'
3+
import { DataloaderCoreModule } from './DataloaderCore.module.js'
64

7-
@Module({
8-
providers: [
9-
{ provide: APP_INTERCEPTOR, useClass: DataloaderInterceptor },
10-
],
11-
})
5+
@Module({})
126
class DataloaderModule {
137
static forRoot(options?: DataloaderOptions): DynamicModule {
148
return {
159
module: DataloaderModule,
16-
providers: [{
17-
provide: OPTIONS_TOKEN,
18-
useValue: options,
19-
}],
10+
imports: [DataloaderCoreModule.forRoot(options)],
2011
}
2112
}
2213

2314
static forRootAsync(options: DataloaderModuleOptions): DynamicModule {
2415
return {
2516
module: DataloaderModule,
26-
imports: options.imports ?? [],
27-
providers: [{
28-
provide: OPTIONS_TOKEN,
29-
inject: options.inject ?? [],
30-
useFactory: options.useFactory,
31-
}],
17+
imports: [DataloaderCoreModule.forRootAsync(options)],
3218
}
3319
}
34-
}
3520

36-
/** Dataloader module options for async configuration */
37-
type DataloaderModuleOptions = Omit<FactoryProvider<DataloaderOptions>, 'provide'> & Pick<DynamicModule, 'imports'>
21+
static forFeature(loaders: Factory[]): DynamicModule {
22+
return {
23+
module: DataloaderModule,
24+
providers: loaders,
25+
exports: loaders,
26+
}
27+
}
28+
}
3829

3930

4031
export {
4132
DataloaderModule,
42-
DataloaderModuleOptions,
4333
}

src/DataloaderCore.module.ts

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { type Provider, type DynamicModule, Module } from '@nestjs/common'
2+
import { APP_INTERCEPTOR } from '@nestjs/core'
3+
import { DataloaderInterceptor } from './Dataloader.interceptor.js'
4+
import { OPTIONS_TOKEN } from './internal.js'
5+
import { type DataloaderModuleOptions, type DataloaderOptions } from './types.js'
6+
7+
/** @private */
8+
const interceptor: Provider = { provide: APP_INTERCEPTOR, useClass: DataloaderInterceptor }
9+
10+
/** @private */
11+
@Module({})
12+
class DataloaderCoreModule {
13+
static forRoot(options?: DataloaderOptions): DynamicModule {
14+
return {
15+
module: DataloaderCoreModule,
16+
providers: [
17+
interceptor,
18+
{
19+
provide: OPTIONS_TOKEN,
20+
useValue: options,
21+
},
22+
],
23+
}
24+
}
25+
26+
static forRootAsync(options: DataloaderModuleOptions): DynamicModule {
27+
return {
28+
module: DataloaderCoreModule,
29+
imports: options.imports ?? [],
30+
providers: [
31+
interceptor,
32+
{
33+
provide: OPTIONS_TOKEN,
34+
inject: options.inject ?? [],
35+
useFactory: options.useFactory,
36+
},
37+
],
38+
}
39+
}
40+
}
41+
42+
export {
43+
DataloaderCoreModule,
44+
}

src/Loader.decorator.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createParamDecorator, type ExecutionContext } from '@nestjs/common'
2-
import { lifetimeKey, store, type Factory } from './internal.js'
3-
import { type LifetimeKeyFn } from './types.js'
2+
import { lifetimeKey, store } from './internal.js'
3+
import { type Factory, type LifetimeKeyFn } from './types.js'
44
import { DataloaderException } from './DataloaderException.js'
55

66
/**

src/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
export { LifetimeKeyFn, DataloaderOptions } from './types.js'
1+
export { LifetimeKeyFn, DataloaderOptions, DataloaderModuleOptions } from './types.js'
22
export { DataloaderException } from './DataloaderException.js'
33
export { Loader, createLoaderDecorator } from './Loader.decorator.js'
44
export { DataloaderFactory, LoaderFrom, Aggregated } from './Dataloader.factory.js'
5-
export { DataloaderModule, DataloaderModuleOptions } from './Dataloader.module.js'
5+
export { DataloaderModule } from './Dataloader.module.js'

src/internal.ts

+2-10
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,13 @@
1-
import { type ExecutionContext, type Type } from '@nestjs/common'
1+
import { type ExecutionContext } from '@nestjs/common'
22
import { type ModuleRef } from '@nestjs/core'
33
import { GqlExecutionContext, type GqlContextType } from '@nestjs/graphql'
44
import type DataLoader from 'dataloader'
5-
import { type DataloaderFactory } from './Dataloader.factory.js'
6-
import { type LifetimeKeyFn } from './types.js'
5+
import { type Factory, type LifetimeKeyFn } from './types.js'
76
import { DataloaderException } from './DataloaderException.js'
87

98
/** @private */
109
const OPTIONS_TOKEN = Symbol('DataloaderModuleOptions')
1110

12-
/**
13-
* DataloaderFactory constructor type
14-
* @private
15-
*/
16-
type Factory = Type<DataloaderFactory<unknown, unknown>>
17-
1811
/** @private */
1912
interface StoreItem {
2013
/** 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) => {
4841
export {
4942
OPTIONS_TOKEN,
5043
store,
51-
Factory,
5244
lifetimeKey,
5345
}

src/types.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { type ExecutionContext } from '@nestjs/common'
1+
import { type Type, type ExecutionContext, type FactoryProvider, type DynamicModule } from '@nestjs/common'
2+
import { type DataloaderFactory } from './Dataloader.factory.js'
3+
4+
/** DataloaderFactory constructor type */
5+
type Factory = Type<DataloaderFactory<unknown, unknown>>
26

37
/**
48
* Given an execution context, extract a value out of it that is
@@ -22,7 +26,12 @@ interface DataloaderOptions {
2226
lifetime?: LifetimeKeyFn
2327
}
2428

29+
/** Dataloader module options for async configuration */
30+
type DataloaderModuleOptions = Omit<FactoryProvider<DataloaderOptions>, 'provide'> & Pick<DynamicModule, 'imports'>
31+
2532
export {
33+
Factory,
2634
LifetimeKeyFn,
2735
DataloaderOptions,
36+
DataloaderModuleOptions,
2837
}

test/DataloaderModule.test.ts

+25-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { describe } from 'vitest'
22
import { Test, TestingModule } from '@nestjs/testing'
3-
import { DataloaderModule } from '@strv/nestjs-dataloader'
3+
import { Injectable } from '@nestjs/common'
4+
import { DataloaderFactory, DataloaderModule } from '@strv/nestjs-dataloader'
45

56
describe('DataloaderModule', it => {
67
it('exists', t => {
@@ -26,4 +27,27 @@ describe('DataloaderModule', it => {
2627

2728
t.expect(app).toBeInstanceOf(TestingModule)
2829
})
30+
31+
it('.forFeatre()', async t => {
32+
@Injectable()
33+
class SampleLoaderFactory extends DataloaderFactory<unknown, unknown> {
34+
load = async (keys: unknown[]) => await Promise.resolve(keys)
35+
id = (key: unknown) => key
36+
}
37+
38+
const provider = DataloaderModule.forFeature([SampleLoaderFactory])
39+
const module = Test.createTestingModule({ imports: [
40+
DataloaderModule.forRoot(),
41+
provider,
42+
] })
43+
const app = await module.compile()
44+
t.onTestFinished(async () => await app.close())
45+
46+
t.expect(app).toBeInstanceOf(TestingModule)
47+
48+
t.expect(provider).toBeDefined()
49+
t.expect(provider.module).toBe(DataloaderModule)
50+
t.expect(provider.providers).toEqual([SampleLoaderFactory])
51+
t.expect(provider.exports).toEqual([SampleLoaderFactory])
52+
})
2953
})

test/Loader.test.ts

+7-10
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,8 @@ describe('@Loader()', it => {
1010
it('injects the dataloader instance into the request handler', async t => {
1111
@Injectable()
1212
class SampleLoaderFactory extends DataloaderFactory<unknown, unknown> {
13-
async load(keys: unknown[]) {
14-
return await Promise.resolve(keys)
15-
}
16-
17-
id(key: unknown) {
18-
return key
19-
}
13+
load = async (keys: unknown[]) => await Promise.resolve(keys)
14+
id = (key: unknown) => key
2015
}
2116

2217
@Controller()
@@ -28,10 +23,12 @@ describe('@Loader()', it => {
2823
}
2924

3025
const module = await Test.createTestingModule({
31-
imports: [DataloaderModule.forRoot()],
26+
imports: [
27+
DataloaderModule.forRoot(),
28+
DataloaderModule.forFeature([SampleLoaderFactory]),
29+
],
3230
controllers: [TestController],
33-
providers: [SampleLoaderFactory],
34-
exports: [SampleLoaderFactory],
31+
exports: [DataloaderModule],
3532
}).compile()
3633
const app = await module.createNestApplication<NestExpressApplication>().init()
3734
t.onTestFinished(async () => await app.close())

0 commit comments

Comments
 (0)