generic-dataloader
Using DataLoader in Common Stack
In Common Stack, DataLoader is used to optimize data fetching by batching and caching requests. This document explains how to set up and use DataLoader, specifically focusing on AccountUserDataLoader
and its integration with a GraphQL resolver.
You can reader further about DataLoaders here
1. BulkDataLoader Implementation
The BulkDataLoader
class is a generic implementation of DataLoader, designed to work with any service that implements the IBaseService
interface.
import DataLoader from 'dataloader';
import { injectable, unmanaged } from 'inversify';
import { DataLoaderOptions, IBaseService, IDataLoader } from '../interfaces';
@injectable()
export class BulkDataLoader<T extends { id: string }> extends DataLoader<string, T | null> implements IDataLoader<T> {
constructor(@unmanaged() private readonly service: IBaseService<T>) {
super(async (ids: string[]) => {
const data = await this.service.getByIds(ids);
return ids.map((id) => data.find((record) => record.id === id) || null);
});
}
withOptions = new DataLoader<DataLoaderOptions<T>, T[]>(async (options) => {
const [{ searchKey, comparator, ...rest }] = options;
const ids = options.map((option) => option.id);
const results = await this.service.getAll({
...rest,
criteria: {
...rest.criteria,
[searchKey]: { $in: ids },
},
});
return ids.map((modelId) =>
results.filter((item) => {
if (typeof comparator === 'function') return comparator(modelId, item);
return item[searchKey].toString() === modelId.toString();
}),
);
});
}
2. DataLoader Interface
The IDataLoader
interface extends the basic DataLoader functionality to include additional options.
import DataLoader from 'dataloader';
import { IBaseService } from './base-service';
export type DataLoaderOptions<T> = Parameters<IBaseService<T>['getAll']>[0] & {
id: string;
searchKey: keyof T;
comparator?: (source: unknown, target: T) => boolean;
};
export type IDataLoader<T> = DataLoader<string, T> & {
withOptions: DataLoader<DataLoaderOptions<T>, T[]>;
};
3. DataLoader with Service
Creating a DataLoader is easy, all you have to do is
@injectable()
export class AccountUserDataLoader extends BulkDataLoader<IUserAccount> {
constructor(
@inject(TYPES.IAccountService)
accountService: IAccountService,
) {
super(accountService);
}
}
@injectable()
export class TeamsDataLoader extends BulkDataLoader<ITeam> {
constructor(
@inject(TYPES.ITeamService)
teamService: ITeamService,
) {
super(teamService);
}
}
and you will have a fully functional data loader with advanced functionality.
Thanks to the BulkDataLoader
parent class
4. GraphQL Resolver Integration
You can use these data loaders in your field resolvers to batch requests, the below example will explain both simple and advanced use cases of account and teams data loaders.
export const resolvers: (options: IResolverOptions) => IResolvers<IContext & MyContext> = (options) => ({
OrgUser: {
// # This will batch all ids and make a single call to db with all the ids
user: (root, args, { accountUserDataLoader }) => accountUserDataLoader.load(root.userId),
// # If you have a use case where you need to fetch other than the Id you use `withOptions`
// method, where you can define any key and pass additonal citeria
// comparator function is optional, which will help filter related result for the specific resolver
teamNames: async (root, args, { teamsDataLoader }) => {
const teams = await teamsDataLoader.withOptions.load({
id: root.orgName,
searchKey: 'orgName',
criteria: {
'teamMembers.userId': root.userId,
},
comparator(userId: string, team) {
return (
team.orgName === root.orgName &&
team.teamMembers.findIndex((i) => i.userId === root.userId) >= 0
);
},
});
return teams.map((team) => (team as ITeam)?.name);
},
},
});
This document outlines the setup and usage of DataLoader in the Common Stack, focusing on the BulkDataLoader
class, its interface, a specific implementation for user accounts, and its integration into GraphQL resolvers.