diff --git a/packages/api/src/resolvers/function_resolvers.ts b/packages/api/src/resolvers/function_resolvers.ts index 3955b00fb..ffe7f53c8 100644 --- a/packages/api/src/resolvers/function_resolvers.ts +++ b/packages/api/src/resolvers/function_resolvers.ts @@ -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 { - const link = await getRepository(Link).findOne(article.linkId, { - relations: ['labels'], - }) - return link?.labels + async labels(article: { linkId: string }): Promise { + // retrieve labels for the link + return labelsLoader.load(article.linkId) }, }, ArticleSavingRequest: { diff --git a/packages/api/src/resolvers/labels/index.ts b/packages/api/src/resolvers/labels/index.ts index a2d369f03..53ac4012b 100644 --- a/packages/api/src/resolvers/labels/index.ts +++ b/packages/api/src/resolvers/labels/index.ts @@ -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( 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', diff --git a/packages/api/src/services/labels.ts b/packages/api/src/services/labels.ts new file mode 100644 index 000000000..2b630b373 --- /dev/null +++ b/packages/api/src/services/labels.ts @@ -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 => { + 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) diff --git a/packages/api/test/services/labels.test.ts b/packages/api/test/services/labels.test.ts new file mode 100644 index 000000000..354af4e44 --- /dev/null +++ b/packages/api/test/services/labels.test.ts @@ -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) + }) +}) diff --git a/packages/web/lib/networking/queries/useGetArticleQuery.tsx b/packages/web/lib/networking/queries/useGetArticleQuery.tsx index 2f511c61c..257edaa5c 100644 --- a/packages/web/lib/networking/queries/useGetArticleQuery.tsx +++ b/packages/web/lib/networking/queries/useGetArticleQuery.tsx @@ -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, diff --git a/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx b/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx index cafef6ae3..d6ab3ac4e 100644 --- a/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx +++ b/packages/web/lib/networking/queries/useGetLibraryItemsQuery.tsx @@ -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 = {