Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api): add dataloader to avoid refetching db #146

Merged
merged 1 commit into from
Mar 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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>) => {
// load data from all
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,6 +83,7 @@ export class ResultRequestRepository {
}
}
)

return this.normalizeId(lastResultRequest)
}

Expand All @@ -104,7 +105,7 @@ export class ResultRequestRepository {

private normalizeId (
resultRequest: ResultRequestDbObject
): ResultRequestDbObjectNormalized {
): ResultRequestDbObjectNormalized | null {
if (resultRequest?._id) {
return { ...resultRequest, id: resultRequest._id.toString() }
} else {
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' })
})
})
})