Skip to content

Commit

Permalink
feat(api): add dataloader to avoid refetching db
Browse files Browse the repository at this point in the history
  • Loading branch information
Tommytrg committed Mar 9, 2022
1 parent 29f433c commit e8d33c3
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 20 deletions.
1 change: 1 addition & 0 deletions packages/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
},
"dependencies": {
"apollo-server": "^2.22.1",
"dataloader": "2.0.0",
"dotenv": "^10.0.0",
"graphql": "^15.5.0",
"jest": "^27.0.1",
Expand Down
53 changes: 53 additions & 0 deletions packages/api/src/loaders/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import DataLoader from 'dataloader'
import { Repositories, ResultRequestDbObjectNormalized } from '../types'

export class Loaders {
repositories: Repositories

constructor (repositories: Repositories) {
this.repositories = repositories
}
// returns a loader that fetches data using the given function
private genericLoader<T> (load: (filter) => T) {
return new DataLoader(async (filters: Array<string>) => {
console.log('here')
const data = await Promise.all(
await filters.map(async (filter, index) => ({
data: await load(filter),
index
}))
)
// ensure they are sorted
const fetchedDataByIndex = data.reduce((acc, val) => {
acc[val.index] = val.data

return acc
}, {})
return filters.map((_, index) => fetchedDataByIndex[index] || null)
})
}

getLoaders (): {
lastResult: DataLoader<string, ResultRequestDbObjectNormalized, string>
requests: DataLoader<string, ResultRequestDbObjectNormalized, string>
} {
return {
lastResult: this.genericLoader<Promise<ResultRequestDbObjectNormalized>>(
async (feedFullName: string) =>
await this.repositories.resultRequestRepository.getLastResult(
feedFullName
)
),

requests: this.genericLoader<
Promise<Array<ResultRequestDbObjectNormalized>>
>(
async (filter: { feedFullName: string; timestamp: number }) =>
await this.repositories.resultRequestRepository.getFeedRequests(
filter.feedFullName,
filter.timestamp
)
)
}
}
}
5 changes: 3 additions & 2 deletions packages/api/src/repository/ResultRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export class ResultRequestRepository {

async getLastResult (
feedFullName: string
): Promise<ResultRequestDbObjectNormalized> {
): Promise<ResultRequestDbObjectNormalized | null> {
const lastResultRequest = await this.collection.findOne(
{
feedFullName
Expand All @@ -83,7 +83,8 @@ export class ResultRequestRepository {
}
}
)
return this.normalizeId(lastResultRequest)

return lastResultRequest ? this.normalizeId(lastResultRequest) : null
}

async insert (
Expand Down
26 changes: 9 additions & 17 deletions packages/api/src/resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,17 @@ const resolvers = {
}
},
Feed: {
requests: async (parent, args, { resultRequestRepository }: Context) => {
return await resultRequestRepository.getFeedRequests(
parent.feedFullName,
args.timestamp
)
requests: async (parent, args, { loaders }: Context) => {
return await loaders.requests.load({
feedFullName: parent.feedFullName,
timestamp: args.timestamp
})
},
lastResult: async (parent, _args, { resultRequestRepository }: Context) => {
// FIXME: add dataloader library to avoid overfetching
return (await resultRequestRepository.getLastResult(parent.feedFullName))
?.result
lastResult: async (parent, _args, { loaders }: Context) => {
return (await loaders.lastResult.load(parent.feedFullName))?.result
},
lastResultTimestamp: async (
parent,
_args,
{ resultRequestRepository }: Context
) => {
// FIXME: add dataloader library to avoid overfetching
return (await resultRequestRepository.getLastResult(parent.feedFullName))
?.timestamp
lastResultTimestamp: async (parent, _args, { loaders }: Context) => {
return (await loaders.lastResult.load(parent.feedFullName))?.timestamp
},
color: async (parent, _args, { config }: Context) => {
return config[parent.feedFullName]?.color || ''
Expand Down
9 changes: 8 additions & 1 deletion packages/api/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import typeDefs from './typeDefs'
import { DIRECTIVES } from '@graphql-codegen/typescript-mongodb'
import resolvers from './resolvers'
import { ConfigByFullName, FeedInfo, Repositories } from './types'
import { Loaders } from './loaders'

export async function createServer (
repositories: Repositories,
Expand All @@ -20,7 +21,13 @@ export async function createServer (
{}
)

return { ...repositories, config: configByFullName }
const loaders = new Loaders(repositories)

return {
...repositories,
config: configByFullName,
loaders: loaders.getLoaders()
}
}
})
}
15 changes: 15 additions & 0 deletions packages/api/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Contract } from 'web3-eth-contract'

import { FeedRepository } from './repository/Feed'
import { ResultRequestRepository } from './repository/ResultRequest'
import DataLoader from 'dataloader'

export * from './generated/types'
export { AbiItem } from 'web3-utils'
Expand All @@ -15,6 +16,20 @@ export type Context = {
feedRepository: FeedRepository
resultRequestRepository: ResultRequestRepository
config: ConfigByFullName
loaders: {
lastResult: DataLoader<string, ResultRequestDbObjectNormalized, string>
requests: DataLoader<
{
feedFullName: string
timestamp: number
},
ResultRequestDbObjectNormalized,
{
feedFullName: string
timestamp: number
}
>
}
}

export type ConfigByFullName = {
Expand Down
95 changes: 95 additions & 0 deletions packages/api/test/loaders/loaders.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { Loaders } from '../../src/loaders'

describe('loaders', () => {
describe('lastResult', () => {
it('lastResult loader should call getLastResult', async () => {
const getLastResultMock = jest.fn(() => ({ feedFullName: 'name' }))
const loaders = new Loaders({
resultRequestRepository: {
getLastResult: getLastResultMock
}
} as any)

await loaders.getLoaders().lastResult.load('feedName')

expect(getLastResultMock).toHaveBeenCalledWith('feedName')
})

it('lastResult loader should call getLastResult the same amount of times than filters provided', async () => {
const getLastResultMock = jest.fn(() => ({ feedFullName: 'name' }))
const loaders = new Loaders({
resultRequestRepository: {
getLastResult: getLastResultMock
}
} as any)

await loaders.getLoaders().lastResult.load('feedName1')
await loaders.getLoaders().lastResult.load('feedName2')

expect(getLastResultMock).toHaveBeenNthCalledWith(1, 'feedName1')
expect(getLastResultMock).toHaveBeenNthCalledWith(2, 'feedName2')
})

it('lastResult loader should return the result of calling getLastResult', async () => {
const getLastResultMock = jest.fn(() => ({ feedFullName: 'name' }))
const loaders = new Loaders({
resultRequestRepository: {
getLastResult: getLastResultMock
}
} as any)

const result = await loaders.getLoaders().lastResult.load('feedName')

expect(result).toStrictEqual({ feedFullName: 'name' })
})
})

describe('getRequests', () => {
it('should call getFeedRequests', async () => {
const getFeedRequestsMock = jest.fn(() => ({ feedFullName: 'name' }))
const loaders = new Loaders({
resultRequestRepository: {
getFeedRequests: getFeedRequestsMock
}
} as any)

await loaders
.getLoaders()
.requests.load({ feedFullName: 'feedName', timestamp: 1 } as any)

expect(getFeedRequestsMock).toHaveBeenCalledWith('feedName', 1)
})

it('should call getFeedRequests the same amount of times than filters provided', async () => {
const getFeedRequestsMock = jest.fn(() => ({ feedFullName: 'name' }))
const loaders = new Loaders({
resultRequestRepository: {
getFeedRequests: getFeedRequestsMock
}
} as any)

await loaders
.getLoaders()
.requests.load({ feedFullName: 'feedName1', timestamp: 1 } as any)
await loaders
.getLoaders()
.requests.load({ feedFullName: 'feedName2', timestamp: 2 } as any)

expect(getFeedRequestsMock).toHaveBeenNthCalledWith(1, 'feedName1', 1)
expect(getFeedRequestsMock).toHaveBeenNthCalledWith(2, 'feedName2', 2)
})

it('should return the result of calling getFeedRequests', async () => {
const getFeedRequestsMock = jest.fn(() => ({ feedFullName: 'name' }))
const loaders = new Loaders({
resultRequestRepository: {
getFeedRequests: getFeedRequestsMock
}
} as any)

const result = await loaders.getLoaders().requests.load('feedName')

expect(result).toStrictEqual({ feedFullName: 'name' })
})
})
})

0 comments on commit e8d33c3

Please sign in to comment.