From b39ba496e345829fcfae5e6191c396274e139d6c Mon Sep 17 00:00:00 2001 From: Julia Wegmayr Date: Tue, 21 Jan 2025 16:32:32 +0100 Subject: [PATCH 1/4] add check for validFolderId --- .../brevo-api/brevo-api-folders.service.ts | 34 +++++++++++++++++++ .../api/src/brevo-api/brevo-api.module.ts | 5 +-- .../api/src/brevo-api/dto/brevo-api-folder.ts | 10 ++++++ .../src/brevo-config/brevo-config.resolver.ts | 22 ++++++++++++ 4 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 packages/api/src/brevo-api/brevo-api-folders.service.ts create mode 100644 packages/api/src/brevo-api/dto/brevo-api-folder.ts diff --git a/packages/api/src/brevo-api/brevo-api-folders.service.ts b/packages/api/src/brevo-api/brevo-api-folders.service.ts new file mode 100644 index 00000000..8be808d9 --- /dev/null +++ b/packages/api/src/brevo-api/brevo-api-folders.service.ts @@ -0,0 +1,34 @@ +import * as Brevo from "@getbrevo/brevo"; +import { Inject, Injectable } from "@nestjs/common"; +import { EmailCampaignScopeInterface } from "src/types"; + +import { BrevoModuleConfig } from "../config/brevo-module.config"; +import { BREVO_MODULE_CONFIG } from "../config/brevo-module.constants"; +import { BrevoApiFolder } from "./dto/brevo-api-folder"; + +@Injectable() +export class BrevoApiFoldersService { + private readonly contactsApi: Brevo.ContactsApi; + + constructor(@Inject(BREVO_MODULE_CONFIG) private readonly config: BrevoModuleConfig) { + this.contactsApi = new Brevo.ContactsApi(); + } + + public async getFolders(scope: EmailCampaignScopeInterface): Promise | undefined> { + const apiKey = this.config.brevo.resolveConfig(scope).apiKey; + this.contactsApi.setApiKey(Brevo.ContactsApiApiKeys.apiKey, apiKey); + + // Limit set by Brevo + const limit = 50; + + const offset = 0; + + const { response, body } = await this.contactsApi.getFolders(limit, offset); + + if (response.statusCode !== 200) { + throw new Error("Failed to get folders"); + } + + return body.folders as BrevoApiFolder[]; + } +} diff --git a/packages/api/src/brevo-api/brevo-api.module.ts b/packages/api/src/brevo-api/brevo-api.module.ts index 5ee4542b..85a53fee 100644 --- a/packages/api/src/brevo-api/brevo-api.module.ts +++ b/packages/api/src/brevo-api/brevo-api.module.ts @@ -5,12 +5,13 @@ import { Module } from "@nestjs/common"; import { ConfigModule } from "../config/config.module"; import { BrevoApiCampaignsService } from "./brevo-api-campaigns.service"; import { BrevoApiContactsService } from "./brevo-api-contact.service"; +import { BrevoApiFoldersService } from "./brevo-api-folders.service"; import { BrevoApiSenderService } from "./brevo-api-sender.service"; import { BrevoTransactionalMailsService } from "./brevo-api-transactional-mails.service"; @Module({ imports: [ConfigModule, CacheModule.register({ ttl: 1000 * 60 }), MikroOrmModule.forFeature(["BrevoConfig"])], - providers: [BrevoApiContactsService, BrevoApiCampaignsService, BrevoTransactionalMailsService, BrevoApiSenderService], - exports: [BrevoApiContactsService, BrevoApiCampaignsService, BrevoTransactionalMailsService, BrevoApiSenderService], + providers: [BrevoApiContactsService, BrevoApiCampaignsService, BrevoTransactionalMailsService, BrevoApiSenderService, BrevoApiFoldersService], + exports: [BrevoApiContactsService, BrevoApiCampaignsService, BrevoTransactionalMailsService, BrevoApiSenderService, BrevoApiFoldersService], }) export class BrevoApiModule {} diff --git a/packages/api/src/brevo-api/dto/brevo-api-folder.ts b/packages/api/src/brevo-api/dto/brevo-api-folder.ts new file mode 100644 index 00000000..5815eb10 --- /dev/null +++ b/packages/api/src/brevo-api/dto/brevo-api-folder.ts @@ -0,0 +1,10 @@ +import { Field, ID, ObjectType } from "@nestjs/graphql"; + +@ObjectType() +export class BrevoApiFolder { + @Field(() => ID) + id: number; + + @Field(() => String) + name: string; +} diff --git a/packages/api/src/brevo-config/brevo-config.resolver.ts b/packages/api/src/brevo-config/brevo-config.resolver.ts index c04ab617..c303be7d 100644 --- a/packages/api/src/brevo-config/brevo-config.resolver.ts +++ b/packages/api/src/brevo-config/brevo-config.resolver.ts @@ -4,6 +4,7 @@ import { InjectRepository } from "@mikro-orm/nestjs"; import { Type } from "@nestjs/common"; import { Args, ID, Mutation, Query, Resolver } from "@nestjs/graphql"; +import { BrevoApiFoldersService } from "../brevo-api/brevo-api-folders.service"; import { BrevoApiSenderService } from "../brevo-api/brevo-api-sender.service"; import { BrevoTransactionalMailsService } from "../brevo-api/brevo-api-transactional-mails.service"; import { BrevoApiEmailTemplate } from "../brevo-api/dto/brevo-api-email-templates-list"; @@ -26,6 +27,7 @@ export function createBrevoConfigResolver({ constructor( private readonly entityManager: EntityManager, private readonly brevoSenderApiService: BrevoApiSenderService, + private readonly brevoFolderIdService: BrevoApiFoldersService, private readonly brevoTransactionalEmailsApiService: BrevoTransactionalMailsService, @InjectRepository(BrevoConfig) private readonly repository: EntityRepository, ) {} @@ -50,6 +52,16 @@ export function createBrevoConfigResolver({ return false; } + private async isValidFolderId({ folderId }: { folderId: number }): Promise { + const folders = await this.brevoFolderIdService.getFolders(Scope); + + if (folders && folders.some((folder) => folder.id === folderId)) { + return true; + } + + return false; + } + @RequiredPermission(["brevo-newsletter-config"], { skipScopeCheck: true }) @Query(() => [BrevoApiSender], { nullable: true }) async senders( @@ -91,6 +103,10 @@ export function createBrevoConfigResolver({ throw new Error("Template ID is not valid. "); } + if (!(await this.isValidFolderId({ folderId: input.folderId }))) { + throw new Error("Folder ID is not valid. "); + } + const brevoConfig = this.repository.create({ ...input, scope, @@ -121,6 +137,12 @@ export function createBrevoConfigResolver({ } } + if (input.folderId) { + if (!(await this.isValidFolderId({ folderId: input.folderId }))) { + throw new Error("Folder ID is not valid. "); + } + } + if (lastUpdatedAt) { validateNotModified(brevoConfig, lastUpdatedAt); } From 7d3bf9b6b4c707625386951aa826ffa8a97a4695 Mon Sep 17 00:00:00 2001 From: Julia Wegmayr Date: Tue, 11 Feb 2025 15:55:19 +0100 Subject: [PATCH 2/4] add loop to fetch all folders if there are more than 50 --- .../brevo-api/brevo-api-folders.service.ts | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/packages/api/src/brevo-api/brevo-api-folders.service.ts b/packages/api/src/brevo-api/brevo-api-folders.service.ts index 8be808d9..ca9748c5 100644 --- a/packages/api/src/brevo-api/brevo-api-folders.service.ts +++ b/packages/api/src/brevo-api/brevo-api-folders.service.ts @@ -4,6 +4,7 @@ import { EmailCampaignScopeInterface } from "src/types"; import { BrevoModuleConfig } from "../config/brevo-module.config"; import { BREVO_MODULE_CONFIG } from "../config/brevo-module.constants"; +import { handleBrevoError } from "./brevo-api.utils"; import { BrevoApiFolder } from "./dto/brevo-api-folder"; @Injectable() @@ -14,21 +15,33 @@ export class BrevoApiFoldersService { this.contactsApi = new Brevo.ContactsApi(); } - public async getFolders(scope: EmailCampaignScopeInterface): Promise | undefined> { + async *getAllBrevoFolders(scope: EmailCampaignScopeInterface): AsyncGenerator { const apiKey = this.config.brevo.resolveConfig(scope).apiKey; this.contactsApi.setApiKey(Brevo.ContactsApiApiKeys.apiKey, apiKey); // Limit set by Brevo const limit = 50; + let offset = 0; - const offset = 0; + while (true) { + try { + const { response, body } = await this.contactsApi.getFolders(limit, offset); - const { response, body } = await this.contactsApi.getFolders(limit, offset); + if (response.statusCode !== 200) { + throw new Error("Failed to get folders"); + } - if (response.statusCode !== 200) { - throw new Error("Failed to get folders"); - } + const folders = body.folders ?? []; + if (folders.length === 0) { + break; + } + + yield* folders; - return body.folders as BrevoApiFolder[]; + offset += limit; + } catch (error) { + handleBrevoError(error); + } + } } } From 778ef16543c666cb1c6f6b63eb0f185606efcfee Mon Sep 17 00:00:00 2001 From: Julia Wegmayr Date: Tue, 11 Feb 2025 15:55:51 +0100 Subject: [PATCH 3/4] adapt resolver to use getAllBrevoFolders and add error --- .../api/src/brevo-config/brevo-config.resolver.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/api/src/brevo-config/brevo-config.resolver.ts b/packages/api/src/brevo-config/brevo-config.resolver.ts index c303be7d..1e7c8032 100644 --- a/packages/api/src/brevo-config/brevo-config.resolver.ts +++ b/packages/api/src/brevo-config/brevo-config.resolver.ts @@ -53,12 +53,13 @@ export function createBrevoConfigResolver({ } private async isValidFolderId({ folderId }: { folderId: number }): Promise { - const folders = await this.brevoFolderIdService.getFolders(Scope); - - if (folders && folders.some((folder) => folder.id === folderId)) { - return true; + for await (const folder of this.brevoFolderIdService.getAllBrevoFolders(Scope)) { + if (folder.id === folderId) { + return true; + } else { + throw new Error("Folder id is not valid. "); + } } - return false; } From 06e5338625e8e0ad80a4607b4dad4385e3a37510 Mon Sep 17 00:00:00 2001 From: Julia Wegmayr Date: Fri, 14 Feb 2025 11:00:15 +0100 Subject: [PATCH 4/4] correct isValidFolderId function --- packages/api/src/brevo-config/brevo-config.resolver.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/api/src/brevo-config/brevo-config.resolver.ts b/packages/api/src/brevo-config/brevo-config.resolver.ts index 1e7c8032..7facd416 100644 --- a/packages/api/src/brevo-config/brevo-config.resolver.ts +++ b/packages/api/src/brevo-config/brevo-config.resolver.ts @@ -56,8 +56,6 @@ export function createBrevoConfigResolver({ for await (const folder of this.brevoFolderIdService.getAllBrevoFolders(Scope)) { if (folder.id === folderId) { return true; - } else { - throw new Error("Folder id is not valid. "); } } return false;