diff --git a/packages/api/package.json b/packages/api/package.json index 70b4ee61..6a17e314 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -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", diff --git a/packages/api/src/loaders/index.ts b/packages/api/src/loaders/index.ts new file mode 100644 index 00000000..a7cc298a --- /dev/null +++ b/packages/api/src/loaders/index.ts @@ -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 (load: (filter) => T) { + return new DataLoader(async (filters: Array) => { + // 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 + requests: DataLoader + } { + return { + lastResult: this.genericLoader>( + async (feedFullName: string) => + await this.repositories.resultRequestRepository.getLastResult( + feedFullName + ) + ), + + requests: this.genericLoader< + Promise> + >( + async (filter: { feedFullName: string; timestamp: number }) => + await this.repositories.resultRequestRepository.getFeedRequests( + filter.feedFullName, + filter.timestamp + ) + ) + } + } +} diff --git a/packages/api/src/repository/ResultRequest.ts b/packages/api/src/repository/ResultRequest.ts index b4470fbe..f282c8c6 100644 --- a/packages/api/src/repository/ResultRequest.ts +++ b/packages/api/src/repository/ResultRequest.ts @@ -68,7 +68,7 @@ export class ResultRequestRepository { async getLastResult ( feedFullName: string - ): Promise { + ): Promise { const lastResultRequest = await this.collection.findOne( { feedFullName @@ -83,6 +83,7 @@ export class ResultRequestRepository { } } ) + return this.normalizeId(lastResultRequest) } @@ -104,7 +105,7 @@ export class ResultRequestRepository { private normalizeId ( resultRequest: ResultRequestDbObject - ): ResultRequestDbObjectNormalized { + ): ResultRequestDbObjectNormalized | null { if (resultRequest?._id) { return { ...resultRequest, id: resultRequest._id.toString() } } else { diff --git a/packages/api/src/resolvers.ts b/packages/api/src/resolvers.ts index e73adf9f..5f6c958f 100644 --- a/packages/api/src/resolvers.ts +++ b/packages/api/src/resolvers.ts @@ -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 || '' diff --git a/packages/api/src/server.ts b/packages/api/src/server.ts index 97f08797..7a412010 100644 --- a/packages/api/src/server.ts +++ b/packages/api/src/server.ts @@ -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, @@ -20,7 +21,13 @@ export async function createServer ( {} ) - return { ...repositories, config: configByFullName } + const loaders = new Loaders(repositories) + + return { + ...repositories, + config: configByFullName, + loaders: loaders.getLoaders() + } } }) } diff --git a/packages/api/src/types.ts b/packages/api/src/types.ts index 2e1f4c69..f3fb2e36 100644 --- a/packages/api/src/types.ts +++ b/packages/api/src/types.ts @@ -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' @@ -15,6 +16,20 @@ export type Context = { feedRepository: FeedRepository resultRequestRepository: ResultRequestRepository config: ConfigByFullName + loaders: { + lastResult: DataLoader + requests: DataLoader< + { + feedFullName: string + timestamp: number + }, + ResultRequestDbObjectNormalized, + { + feedFullName: string + timestamp: number + } + > + } } export type ConfigByFullName = { diff --git a/packages/api/test/loaders/loaders.spec.ts b/packages/api/test/loaders/loaders.spec.ts new file mode 100644 index 00000000..4dbb3dd5 --- /dev/null +++ b/packages/api/test/loaders/loaders.spec.ts @@ -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' }) + }) + }) +})