Skip to content

Commit

Permalink
feat: convert to full Nest.js module
Browse files Browse the repository at this point in the history
  • Loading branch information
robertrossmann committed Jun 23, 2024
1 parent 409990a commit d83bfa0
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 71 deletions.
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.validate.enable": true,
// Allow VS Code ESLint plugin to use the flat config
"eslint.experimental.useFlatConfig": true,
"eslint.useFlatConfig": true,
"eslint.options": {
"cacheLocation": ".cache/",
},
Expand Down
73 changes: 4 additions & 69 deletions src/Dataloader.interceptor.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,11 @@
import {
Injectable,
Type,
createParamDecorator,
type CallHandler,
type ExecutionContext,
type NestInterceptor,
} from '@nestjs/common'
import { ModuleRef } from '@nestjs/core'
import { GqlExecutionContext, type GqlContextType } from '@nestjs/graphql'
import type DataLoader from 'dataloader'
import { DataloaderFactory } from './DataloaderFactory.js'

/**
* DataloaderFactory constructor type
* @private
*/
type Factory = Type<DataloaderFactory<unknown, unknown>>

/**
* A key that uniquely identifies a request. This is used to store the dataloaders in a WeakMap.
* @private
*/
type RequestKey = object

/** @private */
interface InventoryItem {
/** ModuleRef is used by the `@Loader()` decorator to pull the Factory instance from Nest's DI container */
moduleRef: ModuleRef
/** Dataloaders already constructed by a given Factory for this request. */
dataloaders: Map<Factory, DataLoader<unknown, unknown>>
}

/**
* A weak map that tracks the requests and the dataloaders created for those requests
* @private
*/
const inventory = new WeakMap<RequestKey, InventoryItem>()

/**
* Given Nestjs execution context, obtain something that is scoped to the lifetime of the current request and does not
* change (same instance).
* @private
*/
function ctxkey(context: ExecutionContext) {
const type = context.getType<GqlContextType>()

switch (type) {
case 'graphql': return GqlExecutionContext.create(context).getContext<{ req: RequestKey }>().req
case 'http': return context.switchToHttp().getRequest<RequestKey>()
// Support for other request types can be added later, we just did not need them yet.
default: throw new Error(`Unknown or unsupported context type: ${type}`)
}
}
import { ctxkey, store } from './internal.js'

/**
* Interceptor that keeps a weak reference to all requests in order for the `@Loader()` param decorator to manage
Expand All @@ -62,6 +16,8 @@ function ctxkey(context: ExecutionContext) {
* request. Usually, this is the `req` object itself.
* - we want to lazily construct instances of Dataloaders when they are needed, and for that we need to save the
* `moduleRef` instance in order to have access to Nest's dependency injection container from the `@Loader` decorator.
*
* @private
*/
@Injectable()
class DataloaderInterceptor implements NestInterceptor<unknown, unknown> {
Expand All @@ -73,7 +29,7 @@ class DataloaderInterceptor implements NestInterceptor<unknown, unknown> {
}

intercept(context: ExecutionContext, next: CallHandler<unknown>) {
inventory.set(ctxkey(context), {
store.set(ctxkey(context), {
moduleRef: this.#moduleRef,
dataloaders: new Map(),
})
Expand All @@ -82,27 +38,6 @@ class DataloaderInterceptor implements NestInterceptor<unknown, unknown> {
}
}

/**
* Load a `Dataloader` factory into the decorated parameter
* The factory class must be an implementation of the `DataloaderFactory` abstract class.
*/
const Loader = createParamDecorator((Factory: Factory, context: ExecutionContext) => {
const item = inventory.get(ctxkey(context))

if (!item) {
throw new Error('DataLoaderInterceptor not registered in this Nest.js application')
}

if (!item.dataloaders.has(Factory)) {
// We don't have this dataloader created yet for this request, let's instantiate it and save it
const factory = item.moduleRef.get(Factory, { strict: false })
item.dataloaders.set(Factory, factory.create(context))
}

return item.dataloaders.get(Factory)
})

export {
DataloaderInterceptor,
Loader,
}
20 changes: 20 additions & 0 deletions src/Dataloader.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { DynamicModule, Module } from '@nestjs/common'
import { APP_INTERCEPTOR } from '@nestjs/core'
import { DataloaderInterceptor } from './Dataloader.interceptor.js'

@Module({
providers: [
{ provide: APP_INTERCEPTOR, useClass: DataloaderInterceptor },
],
})
class DataloaderModule {
static forRoot(): DynamicModule {
return {
module: DataloaderModule,
}
}
}

export {
DataloaderModule,
}
26 changes: 26 additions & 0 deletions src/Loader.decorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { createParamDecorator, type ExecutionContext } from '@nestjs/common'
import { ctxkey, store, type Factory } from './internal.js'

/**
* Load a `Dataloader` factory into the decorated parameter
* The factory class must be an implementation of the `DataloaderFactory` abstract class.
*/
const Loader = createParamDecorator((Factory: Factory, context: ExecutionContext) => {
const item = store.get(ctxkey(context))

if (!item) {
throw new Error('DataLoaderInterceptor not registered in this Nest.js application')
}

if (!item.dataloaders.has(Factory)) {
// We don't have this dataloader created yet for this request, let's instantiate it and save it
const factory = item.moduleRef.get(Factory, { strict: false })
item.dataloaders.set(Factory, factory.create(context))
}

return item.dataloaders.get(Factory)
})

export {
Loader,
}
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { DataloaderInterceptor, Loader } from './Dataloader.interceptor.js'
export { Loader } from './Loader.decorator.js'
export { DataloaderFactory, LoaderFrom, Aggregated } from './DataloaderFactory.js'
export { DataloaderModule } from './Dataloader.module.js'
47 changes: 47 additions & 0 deletions src/internal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { type ExecutionContext, type Type } 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 './DataloaderFactory.js'

/**
* DataloaderFactory constructor type
* @private
*/
type Factory = Type<DataloaderFactory<unknown, unknown>>

/** @private */
interface StoreItem {
/** ModuleRef is used by the `@Loader()` decorator to pull the Factory instance from Nest's DI container */
moduleRef: ModuleRef
/** Dataloaders already constructed by a given Factory for this request. */
dataloaders: Map<Factory, DataLoader<unknown, unknown>>
}

/**
* A weak map that tracks the requests and the dataloaders created for those requests
* @private
*/
const store = new WeakMap<object, StoreItem>()

/**
* Given Nestjs execution context, obtain something that is scoped to the lifetime of the current request and does not
* change (same instance).
* @private
*/
function ctxkey(context: ExecutionContext) {
const type = context.getType<GqlContextType>()

switch (type) {
case 'graphql': return GqlExecutionContext.create(context).getContext<{ req: object }>().req
case 'http': return context.switchToHttp().getRequest<object>()
// Support for other request types can be added later, we just did not need them yet.
default: throw new Error(`Unknown or unsupported context type: ${type}`)
}
}

export {
store,
Factory,
ctxkey,
}

0 comments on commit d83bfa0

Please sign in to comment.