use dataloader to fetch all labels of a list of linkIds in a single q… (#133)

* use dataloader to fetch all labels of a list of linkIds in a single query and cached

* add labels in GQL query in frontend
This commit is contained in:
Hongbo Wu
2022-02-28 12:13:26 +08:00
committed by GitHub
parent 64ceca0e63
commit 7bf454ae91
6 changed files with 92 additions and 8 deletions

View File

@ -79,9 +79,8 @@ import {
generateDownloadSignedUrl,
generateUploadFilePathName,
} from '../utils/uploads'
import { getRepository } from 'typeorm'
import { Link } from '../entity/link'
import { Label } from '../entity/label'
import { labelsLoader } from '../services/labels'
/* eslint-disable @typescript-eslint/naming-convention */
type ResultResolveType = {
@ -428,11 +427,9 @@ export const functionResolvers = {
ctx.models
)
},
async labels(article: { linkId: string }): Promise<Label[] | undefined> {
const link = await getRepository(Link).findOne(article.linkId, {
relations: ['labels'],
})
return link?.labels
async labels(article: { linkId: string }): Promise<Label[]> {
// retrieve labels for the link
return labelsLoader.load(article.linkId)
},
},
ArticleSavingRequest: {

View File

@ -24,6 +24,7 @@ import { getManager, getRepository, ILike } from 'typeorm'
import { setClaims } from '../../entity/utils'
import { Link } from '../../entity/link'
import { LinkLabel } from '../../entity/link_label'
import { labelsLoader } from '../../services/labels'
export const labelsResolver = authorized<LabelsSuccess, LabelsError>(
async (_obj, _params, { claims: { uid }, log }) => {
@ -173,7 +174,7 @@ export const deleteLabelResolver = authorized<
label,
}
} catch (error) {
log.error(error)
log.error('error', error)
return {
errorCodes: [DeleteLabelErrorCode.BadRequest],
}
@ -226,6 +227,9 @@ export const setLabelsResolver = authorized<
.save(labels.map((label) => ({ link, label })))
})
// clear cache
labelsLoader.clear(linkId)
analytics.track({
userId: uid,
event: 'setLabels',

View File

@ -0,0 +1,19 @@
import DataLoader from 'dataloader'
import { Label } from '../entity/label'
import { getRepository, In } from 'typeorm'
import { Link } from '../entity/link'
const batchGetLabelsFromLinkIds = async (
linkIds: readonly string[]
): Promise<Label[][]> => {
const links = await getRepository(Link).find({
where: { id: In(linkIds as string[]) },
relations: ['labels'],
})
return linkIds.map(
(linkId) => links.find((link) => link.id === linkId)?.labels || []
)
}
export const labelsLoader = new DataLoader(batchGetLabelsFromLinkIds)

View File

@ -0,0 +1,55 @@
import 'mocha'
import { expect } from 'chai'
import 'chai/register-should'
import { labelsLoader } from '../../src/services/labels'
import {
createTestLabel,
createTestLink,
createTestPage,
createTestUser,
deleteTestUser,
} from '../db'
import { getRepository } from 'typeorm'
import { LinkLabel } from '../../src/entity/link_label'
import { Label } from '../../src/entity/label'
import { Link } from '../../src/entity/link'
describe('batch get labels from linkIds', () => {
let username = 'testUser'
let labels: Label[] = []
let link: Link
before(async () => {
// create test user
const user = await createTestUser(username)
// Create some test links
const page = await createTestPage()
link = await createTestLink(user, page)
for (let i = 0; i < 3; i++) {
// create testing labels
const label = await createTestLabel(user, `label_${i}`, '#d55757')
// set label to a link
await getRepository(LinkLabel).save({
link: link,
label: label,
})
labels.push(label)
}
})
after(async () => {
// clean up
await deleteTestUser(username)
})
it('should return a list of label from one link', async () => {
const result = await labelsLoader.load(link.id)
expect(result).length(3)
expect(result[0].id).to.eql(labels[0].id)
expect(result[1].id).to.eql(labels[1].id)
expect(result[2].id).to.eql(labels[2].id)
})
})

View File

@ -76,6 +76,9 @@ const query = gql`
highlights(input: { includeFriends: $includeFriendsHighlights }) {
...HighlightFields
}
labels {
...LabelFields
}
}
}
... on ArticleError {
@ -85,6 +88,7 @@ const query = gql`
}
${articleFragment}
${highlightFragment}
${labelFragment}
`
export const cacheArticle = (
mutate: ScopedMutator,

View File

@ -6,6 +6,7 @@ import { articleFragment } from '../fragments/articleFragment'
import { setLinkArchivedMutation } from '../mutations/setLinkArchivedMutation'
import { deleteLinkMutation } from '../mutations/deleteLinkMutation'
import { articleReadingProgressMutation } from '../mutations/articleReadingProgressMutation'
import { labelFragment } from '../fragments/labelFragment'
export type LibraryItemsQueryInput = {
limit: number
@ -89,6 +90,9 @@ export function useGetLibraryItemsQuery({
cursor
node {
...ArticleFields
labels {
...LabelFields
}
originalArticleUrl
}
}
@ -106,6 +110,7 @@ export function useGetLibraryItemsQuery({
}
}
${articleFragment}
${labelFragment}
`
const variables = {