nestjs-graphql-dataloader
nestjs-graphql-dataloader copied to clipboard
NestJS 8.x support
I was recently trying to upgrade to NestJS 8, and when I do, Nest complains about being unable to load ModuleRef. When running on 7.x, this same code seemed to work fine. Any ideas if this is something with my solution or if the library needs upgrading to support the newest version.
Error: Nest can't resolve dependencies of the DataLoaderInterceptor (?). Please make sure that the argument ModuleRef at index [0] is available in the AppModule context.
Potential solutions:
- If ModuleRef is a provider, is it part of the current AppModule?
- If ModuleRef is exported from a separate @Module, is that module imported within AppModule?
@Module({
imports: [ /* the Module containing ModuleRef */ ]
})
at Injector.lookupComponentInParentModules (/Users/steve/Documents/GitHub/load/node_modules/@nestjs/core/injector/injector.js:193:19)
at processTicksAndRejections (internal/process/task_queues.js:93:5)
at Injector.resolveComponentInstance (/Users/steve/Documents/GitHub/load/node_modules/@nestjs/core/injector/injector.js:149:33)
at resolveParam (/Users/steve/Documents/GitHub/load/node_modules/@nestjs/core/injector/injector.js:103:38)
at async Promise.all (index 0)
at Injector.resolveConstructorParams (/Users/steve/Documents/GitHub/load/node_modules/@nestjs/core/injector/injector.js:118:27)
at Injector.loadInstance (/Users/steve/Documents/GitHub/load/node_modules/@nestjs/core/injector/injector.js:47:9)
at Injector.loadProvider (/Users/steve/Documents/GitHub/load/node_modules/@nestjs/core/injector/injector.js:69:9)
at async Promise.all (index 3)
at InstanceLoader.createInstancesOfProviders (/Users/steve/Documents/GitHub/load/node_modules/@nestjs/core/injector/instance-loader.js:44:9)
@Module({
imports: [
...
],
controllers: [],
providers: [
{
provide: APP_INTERCEPTOR,
useClass: DataLoaderInterceptor,
},
],
})
export class AppModule {}
It's because this module uses constructor name as injection token, which seems no longer work in nest 8.
/**
* The decorator to be used within your graphql method.
*/
export const Loader = createParamDecorator(
(data: string | Function, context: ExecutionContext) => {
const name = typeof data === "string" ? data : data?.name;
// ....
// here we should pass original type instead of it's name
return ctx[NEST_LOADER_CONTEXT_KEY].getLoader(name);
}
);
Here is my implementation (warning, it's not 1 to 1 replacement for this library)
import {
CallHandler,
createParamDecorator,
ExecutionContext,
Injectable,
InternalServerErrorException,
NestInterceptor,
Type,
} from '@nestjs/common';
import { APP_INTERCEPTOR, ContextIdFactory, ModuleRef } from '@nestjs/core';
import { GqlContextType, GqlExecutionContext } from '@nestjs/graphql';
import DataLoader from 'dataloader';
import { Observable } from 'rxjs';
import { GraphQlRequestContext } from './graphql-request-context';
type DataLoaderInjectionToken = { name: string, getFactory: () => Type<NestDataLoaderFactory<any, any>> }
/**
* Context key where get loader function will be stored.
* This class should be added to your module providers like so:
* {
* provide: APP_INTERCEPTOR,
* useClass: DataLoaderInterceptor,
* },
*/
const NEST_LOADER_CONTEXT_KEY = 'NEST_LOADER_CONTEXT_KEY';
@Injectable()
export class DataLoaderInterceptor implements NestInterceptor {
constructor(
private readonly moduleRef: ModuleRef,
) {}
public intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
if (context.getType<GqlContextType>() !== 'graphql') {
return next.handle();
}
const ctx = GqlExecutionContext.create(context).getContext();
if (ctx[NEST_LOADER_CONTEXT_KEY] === undefined) {
const contextId = ContextIdFactory.create();
const loaders = {};
ctx[NEST_LOADER_CONTEXT_KEY] = {
loaders,
getLoader: (type: DataLoaderInjectionToken, _ctx: GraphQlRequestContext): Promise<NestDataLoaderFactory<any, any>> => {
const name = type.name.replace('Factory', '');
if (loaders[name] === undefined) {
loaders[name] = (async () => {
try {
return (
await this.moduleRef.resolve<NestDataLoaderFactory<any, any>>(
type.getFactory(),
contextId,
{ strict: false },
)
).generateDataLoader(_ctx);
} catch (e) {
throw new InternalServerErrorException(
`The loader ${type.name} is not provided` + e,
);
}
})();
}
return loaders[name];
},
};
}
return next.handle();
}
}
/**
* The decorator to be used within your graphql method.
*/
export const Loader = createParamDecorator(
// tslint:disable-next-line: ban-types
(type: DataLoaderInjectionToken, context: ExecutionContext) => {
const name = type?.name;
if (!name) {
throw new InternalServerErrorException(
`Invalid name provider to @Loader ('${name}')`,
);
}
if (context.getType<GqlContextType>() !== 'graphql') {
throw new InternalServerErrorException(
'@Loader should only be used within the GraphQL context',
);
}
const ctx = GqlExecutionContext.create(context).getContext();
if (!name || !ctx[NEST_LOADER_CONTEXT_KEY]) {
throw new InternalServerErrorException(
`You should provide interceptor ${DataLoaderInterceptor.name} globally with ${APP_INTERCEPTOR}`,
);
}
return ctx[NEST_LOADER_CONTEXT_KEY].getLoader(type, ctx);
},
);
// https://github.com/graphql/dataloader/issues/66#issuecomment-386252044
export const ensureOrder = (options) => {
const {
docs,
keys,
prop,
error = (key) => `Document does not exist (${key})`,
} = options;
// Put documents (docs) into a map where key is a document's ID or some
// property (prop) of a document and value is a document.
const docsMap = new Map();
docs.forEach((doc) => docsMap.set(doc[prop], doc));
// Loop through the keys and for each one retrieve proper document. For not
// existing documents generate an error.
return keys.map((key) => {
return (
docsMap.get(key) ||
new Error(typeof error === 'function' ? error(key) : error)
);
});
};
export interface INestDataLoaderFactoryOptions<ID, Type> {
propertyKey?: string;
query: (keys: readonly ID[]) => Promise<Type[]>;
typeName?: string;
dataloaderConfig?: DataLoader.Options<ID, Type>;
}
export abstract class NestDataLoaderFactory<ID, Type> {
protected abstract getOptions: (context: GraphQlRequestContext) => INestDataLoaderFactoryOptions<ID, Type>;
public generateDataLoader(context: GraphQlRequestContext) {
return this.createLoader(this.getOptions(context));
}
protected createLoader(options: INestDataLoaderFactoryOptions<ID, Type>): DataLoader<ID, Type> {
const defaultTypeName = this.constructor.name.replace('LoaderFactory', '');
return new DataLoader<ID, Type>(async (keys) => {
return ensureOrder({
docs: await options.query(keys),
keys,
prop: options.propertyKey || 'id',
error: (keyValue) => `${options.typeName || defaultTypeName} does not exist (${keyValue})`,
});
}, {
...options.dataloaderConfig,
});
}
}
Usage examples:
// Loader Factory
import { Injectable } from '@nestjs/common';
import { NestDataLoaderFactory } from '../../infrastructure/nest-data-loader';
import { GraphQlRequestContext } from '../../infrastructure/graphql-request-context';
import DataLoader from 'dataloader';
import { getRequestedLang } from '../../infrastructure/get-requested-lang';
import { DbPoi, PoiRepository } from './poi.repository';
export class PoiLoader extends DataLoader<string, DbPoi> {
public static getFactory() {
return PoiLoaderFactory;
}
}
@Injectable()
export class PoiLoaderFactory extends NestDataLoaderFactory<string, DbPoi> {
constructor(
private readonly poiRepository: PoiRepository,
) {
super();
}
protected getOptions = (context: GraphQlRequestContext) => {
const lang = getRequestedLang(context.req);
return ({
query: (ids: string[]) => {
console.log('request poi ids', ids);
return this.poiRepository.getByIds(ids, lang);
},
});
};
}
Usage in resolver:
@ResolveField()
public async details(
@Loader(PoiLoader) poiLoader: PoiLoader,
) {}