Skip to content

Commit 23191da

Browse files
author
hirsch88
committed
Add data-loaders
1 parent 1e72adc commit 23191da

File tree

9 files changed

+178
-42
lines changed

9 files changed

+178
-42
lines changed

src/api/models/Pet.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm';
1+
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
22
import { IsNotEmpty } from 'class-validator';
33
import { User } from './User';
44

@@ -17,7 +17,11 @@ export class Pet {
1717
@Column()
1818
public age: number;
1919

20+
@Column()
21+
public userId: number;
22+
2023
@ManyToOne(type => User, user => user.pets)
24+
@JoinColumn({ name: 'userId' })
2125
public user: User;
2226

2327
public toString(): string {

src/api/queries/GetPetsQuery.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { GraphQLFieldConfig, GraphQLList } from 'graphql';
2+
import { Query, AbstractQuery, GraphQLContext } from './../../lib/graphql';
3+
import { PetService } from '../services/PetService';
4+
import { PetType } from './../types/PetType';
5+
import { Logger } from '../../core/Logger';
6+
import { Pet } from '../models/Pet';
7+
8+
@Query()
9+
export class GetPetsQuery extends AbstractQuery<GraphQLContext<any, any>, Pet[], any> implements GraphQLFieldConfig {
10+
public type = new GraphQLList(PetType);
11+
public allow = [];
12+
public args = {};
13+
14+
private log = new Logger(__filename);
15+
16+
public async run(root: any, args: any, context: GraphQLContext<any, any>): Promise<Pet[]> {
17+
const pets = await context.container.get<PetService>(PetService).find();
18+
this.log.info(`Found ${pets.length} pets`);
19+
return pets;
20+
}
21+
22+
}

src/api/repositories/PetRepository.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@ import { Repository, EntityRepository } from 'typeorm';
22
import { Pet } from '../models/Pet';
33

44
@EntityRepository(Pet)
5-
export class PetRepository extends Repository<Pet> {
5+
export class PetRepository extends Repository<Pet> {
6+
7+
/**
8+
* Find by userId is used for our data-loader to get all needed pets in one query.
9+
*/
10+
public findByUserIds(ids: string[]): Promise<Pet[]> {
11+
return this.createQueryBuilder()
12+
.select()
13+
.where(`pet.userId IN (${ids.map(id => `'${id}'`).join(', ')})`)
14+
.getMany();
15+
}
616

717
}

src/api/types/PetType.ts

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,43 @@ import {
33
GraphQLString,
44
GraphQLInt,
55
GraphQLObjectType,
6+
GraphQLFieldConfigMap,
67
} from 'graphql';
8+
import { merge } from 'lodash';
9+
import { OwnerType } from './UserType';
10+
import { Pet } from '../models/Pet';
11+
import { GraphQLContext } from '../../lib/graphql';
12+
13+
const PetFields: GraphQLFieldConfigMap = {
14+
id: {
15+
type: GraphQLID,
16+
description: 'The ID',
17+
},
18+
name: {
19+
type: GraphQLString,
20+
description: 'The name of the pet.',
21+
},
22+
age: {
23+
type: GraphQLInt,
24+
description: 'The age of the pet in years.',
25+
},
26+
};
27+
28+
export const PetOfUserType = new GraphQLObjectType({
29+
name: 'PetOfUser',
30+
description: 'A users pet',
31+
fields: () => merge<GraphQLFieldConfigMap, GraphQLFieldConfigMap>(PetFields, {}),
32+
});
733

834
export const PetType = new GraphQLObjectType({
935
name: 'Pet',
1036
description: 'A single pet.',
11-
fields: {
12-
id: {
13-
type: GraphQLID,
14-
description: 'The ID',
15-
},
16-
name: {
17-
type: GraphQLString,
18-
description: 'The name of the pet.',
37+
fields: () => merge<GraphQLFieldConfigMap, GraphQLFieldConfigMap>(PetFields, {
38+
owner: {
39+
type: OwnerType,
40+
description: 'The owner of the pet',
41+
resolve: (pet: Pet, args: any, context: GraphQLContext<any, any>) =>
42+
context.dataLoaders.users.load(pet.userId),
1943
},
20-
age: {
21-
type: GraphQLInt,
22-
description: 'The age of the pet in years.',
23-
},
24-
},
44+
}),
2545
});

src/api/types/UserType.ts

Lines changed: 37 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,52 @@
1-
import { PetService } from './../services/PetService';
21
import {
32
GraphQLID,
43
GraphQLString,
54
GraphQLObjectType,
5+
GraphQLFieldConfigMap,
66
GraphQLList,
77
} from 'graphql';
8-
import { PetType } from './PetType';
9-
import { User } from '../models/User';
8+
import { merge } from 'lodash';
109
import { GraphQLContext } from '../../lib/graphql';
10+
import { PetOfUserType } from './PetType';
11+
import { User } from '../models/User';
12+
13+
const UserFields: GraphQLFieldConfigMap = {
14+
id: {
15+
type: GraphQLID,
16+
description: 'The ID',
17+
},
18+
firstName: {
19+
type: GraphQLString,
20+
description: 'The first name of the user.',
21+
},
22+
lastName: {
23+
type: GraphQLString,
24+
description: 'The last name of the user.',
25+
},
26+
email: {
27+
type: GraphQLString,
28+
description: 'The email of this user.',
29+
},
30+
};
1131

1232
export const UserType = new GraphQLObjectType({
1333
name: 'User',
1434
description: 'A single user.',
15-
fields: {
16-
id: {
17-
type: GraphQLID,
18-
description: 'The ID',
19-
},
20-
firstName: {
21-
type: GraphQLString,
22-
description: 'The first name of the user.',
23-
},
24-
lastName: {
25-
type: GraphQLString,
26-
description: 'The last name of the user.',
27-
},
28-
email: {
29-
type: GraphQLString,
30-
description: 'The email of this user.',
31-
},
35+
fields: () => merge<GraphQLFieldConfigMap, GraphQLFieldConfigMap>(UserFields, {
3236
pets: {
33-
type: new GraphQLList(PetType),
37+
type: new GraphQLList(PetOfUserType),
3438
description: 'The pets of a user',
35-
resolve: (user: User, args: any, context: GraphQLContext<any, any>) =>
36-
context.container.get<PetService>(PetService).findByUser(user),
39+
resolve: async (user: User, args: any, context: GraphQLContext<any, any>) =>
40+
// We use data-loaders to save db queries
41+
context.dataLoaders.petByUserIds.loadMany([user.id]),
42+
// This would be the case with a normal service, but not very fast
43+
// context.container.get<PetService>(PetService).findByUser(user),
3744
},
38-
},
45+
}),
46+
});
47+
48+
export const OwnerType = new GraphQLObjectType({
49+
name: 'Owner',
50+
description: 'The owner of a pet',
51+
fields: () => merge<GraphQLFieldConfigMap, GraphQLFieldConfigMap>(UserFields, {}),
3952
});

src/lib/graphql/GraphQLContext.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
import * as express from 'express';
2+
import * as DataLoader from 'dataloader';
23
import { Container } from 'typedi';
34

45
export interface GraphQLContext<TData, TResolveArgs> {
56
container: typeof Container;
67
request: express.Request;
78
response: express.Response;
9+
dataLoaders: GraphQLContextDataLoader;
810
resolveArgs?: TResolveArgs;
911
data?: TData;
1012
}
13+
14+
export interface GraphQLContextDataLoader {
15+
[key: string]: DataLoader<number | string, any>;
16+
}

src/lib/graphql/dataloader.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
export interface Identifiable {
2+
id?: number | number;
3+
}
4+
5+
export function ensureInputOrder<T extends Identifiable>(ids: number[] | string[], result: T[], key: string): T[] {
6+
// For the dataloader batching to work, the results must be in the same order and of the
7+
// same length as the ids. See: https://github.com/facebook/dataloader#batch-function
8+
const orderedResult: T[] = [];
9+
for (const id of ids) {
10+
const item = result.find(t => t[key] === id);
11+
if (item) {
12+
orderedResult.push(item);
13+
} else {
14+
/* tslint:disable */
15+
// @ts-ignore
16+
orderedResult.push(null);
17+
/* tslint:enable */
18+
}
19+
}
20+
return orderedResult;
21+
}

src/lib/graphql/index.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import * as express from 'express';
22
import * as GraphQLHTTP from 'express-graphql';
3+
import * as DataLoader from 'dataloader';
34
import { GraphQLSchema, GraphQLObjectType } from 'graphql';
4-
import { Container as container } from 'typedi';
5+
import { Container as container, ObjectType } from 'typedi';
56

6-
import { GraphQLContext } from './GraphQLContext';
7+
import { GraphQLContext, GraphQLContextDataLoader } from './GraphQLContext';
78
import { MetadataArgsStorage } from './MetadataArgsStorage';
89
import { importClassesFromDirectories } from './importClassesFromDirectories';
910
import { handlingErrors, getErrorCode, getErrorMessage } from './graphql-error-handling';
11+
import { ensureInputOrder } from './dataloader';
12+
import { Repository, getCustomRepository, getRepository } from 'typeorm';
1013

1114
// -------------------------------------------------------------------------
1215
// Main exports
@@ -24,13 +27,41 @@ export * from './graphql-error-handling';
2427
// Main Functions
2528
// -------------------------------------------------------------------------
2629

30+
/**
31+
* Creates a new dataloader wiht the typorm repository
32+
*/
33+
export function createDataLoader<T>(obj: ObjectType<T>, method?: string, key?: string): DataLoader<any, any> {
34+
let repository;
35+
try {
36+
repository = getCustomRepository<Repository<any>>(obj);
37+
} catch (errorRepo) {
38+
try {
39+
repository = getRepository(obj);
40+
} catch (errorModel) {
41+
throw new Error('Could not create a dataloader, because obj is nether model or repository!');
42+
}
43+
}
44+
45+
return new DataLoader(async (ids: number[]) => {
46+
let items = [];
47+
if (method) {
48+
items = await repository[method](ids);
49+
} else {
50+
items = await repository.findByIds(ids);
51+
}
52+
53+
return ensureInputOrder(ids, items, key || 'id');
54+
});
55+
}
56+
2757
/**
2858
* Defines the options to create a GraphQLServer
2959
*/
3060
export interface GraphQLServerOptions<TData> {
3161
queries: string[];
3262
mutations: string[];
3363
route?: string;
64+
dataLoaders?: GraphQLContextDataLoader;
3465
editorEnabled?: boolean;
3566
contextData?: TData;
3667
}
@@ -56,6 +87,7 @@ export function createGraphQLServer<TData>(expressApp: express.Application, opti
5687
container,
5788
request,
5889
response,
90+
dataLoaders: options.dataLoaders || {},
5991
resolveArgs: {},
6092
data: options.contextData,
6193
};

src/loaders/graphqlLoader.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { MicroframeworkSettings, MicroframeworkLoader } from 'microframework';
2-
import { createGraphQLServer } from '../lib/graphql';
2+
import { createGraphQLServer, createDataLoader } from '../lib/graphql';
33
import { env } from '../core/env';
4+
import { PetRepository } from './../api/repositories/PetRepository';
5+
import { Pet } from './../api/models/Pet';
6+
import { UserRepository } from './../api/repositories/UserRepository';
47

58

69
export const graphqlLoader: MicroframeworkLoader = (settings: MicroframeworkSettings | undefined) => {
@@ -12,6 +15,11 @@ export const graphqlLoader: MicroframeworkLoader = (settings: MicroframeworkSett
1215
editorEnabled: env.graphql.enabled,
1316
queries: env.app.dirs.queries,
1417
mutations: env.app.dirs.queries,
18+
dataLoaders: {
19+
users: createDataLoader(UserRepository),
20+
pets: createDataLoader(Pet),
21+
petByUserIds: createDataLoader(PetRepository, 'findByUserIds', 'userId'),
22+
},
1523
});
1624

1725
}

0 commit comments

Comments
 (0)