From f7c0fdde9714ab3fcb4c2084279916b0b55f3c94 Mon Sep 17 00:00:00 2001 From: Thomas Dax Date: Tue, 20 Aug 2024 10:45:31 +0200 Subject: [PATCH] Make it possible to select multiple target groups for one campaign (#72) --- .changeset/tall-berries-hope.md | 6 +++ demo/api/schema.gql | 6 +-- .../src/emailCampaigns/EmailCampaignsGrid.tsx | 11 ++-- .../form/EmailCampaignForm.gql.ts | 3 +- .../emailCampaigns/form/EmailCampaignForm.tsx | 23 ++++---- .../form/SendManagerFields.gql.ts | 14 +++-- .../emailCampaigns/form/SendManagerFields.tsx | 43 ++++++++------- packages/api/generate-schema.ts | 2 +- packages/api/schema.gql | 6 +-- .../brevo-api/brevo-api-campaigns.service.ts | 8 +-- .../dto/email-campaign-input.factory.ts | 9 ++-- .../email-campaign/email-campaign.resolver.ts | 15 +++--- .../email-campaign/email-campaigns.service.ts | 52 ++++++++++--------- .../entities/email-campaign-entity.factory.ts | 10 ++-- .../migrations/Migration20240819214939.ts | 25 +++++++++ .../src/mikro-orm/migrations/migrations.ts | 2 + .../entity/target-group-entity.factory.ts | 7 ++- 17 files changed, 148 insertions(+), 94 deletions(-) create mode 100644 .changeset/tall-berries-hope.md create mode 100644 packages/api/src/mikro-orm/migrations/Migration20240819214939.ts diff --git a/.changeset/tall-berries-hope.md b/.changeset/tall-berries-hope.md new file mode 100644 index 00000000..76523b2a --- /dev/null +++ b/.changeset/tall-berries-hope.md @@ -0,0 +1,6 @@ +--- +"@comet/brevo-admin": minor +"@comet/brevo-api": minor +--- + +Allow sending a campaign to multiple target groups diff --git a/demo/api/schema.gql b/demo/api/schema.gql index 6ed0fca6..143e5c3d 100644 --- a/demo/api/schema.gql +++ b/demo/api/schema.gql @@ -436,7 +436,7 @@ type EmailCampaign implements DocumentInterface { subject: String! brevoId: Int scheduledAt: DateTime - targetGroup: TargetGroup + targetGroups: [TargetGroup!]! content: EmailCampaignContentBlockData! scope: EmailCampaignContentScope! sendingState: SendingState! @@ -896,7 +896,7 @@ input EmailCampaignInput { title: String! subject: String! scheduledAt: DateTime - targetGroup: ID + targetGroups: [ID!]! content: EmailCampaignContentBlockInput! } @@ -907,7 +907,7 @@ input EmailCampaignUpdateInput { title: String subject: String scheduledAt: DateTime - targetGroup: ID + targetGroups: [ID!] content: EmailCampaignContentBlockInput } diff --git a/packages/admin/src/emailCampaigns/EmailCampaignsGrid.tsx b/packages/admin/src/emailCampaigns/EmailCampaignsGrid.tsx index b7e3a509..2fa765da 100644 --- a/packages/admin/src/emailCampaigns/EmailCampaignsGrid.tsx +++ b/packages/admin/src/emailCampaigns/EmailCampaignsGrid.tsx @@ -46,7 +46,7 @@ const emailCampaignsFragment = gql` scheduledAt brevoId content - targetGroup { + targetGroups { id title } @@ -155,10 +155,10 @@ export function EmailCampaignsGrid({ width: 200, }, { - field: "targetGroup", - headerName: intl.formatMessage({ id: "cometBrevoModule.emailCampaign.targetGroup", defaultMessage: "Target group" }), + field: "targetGroups", + headerName: intl.formatMessage({ id: "cometBrevoModule.emailCampaign.targetGroups", defaultMessage: "Target groups" }), width: 150, - renderCell: ({ value }) => (value ? value.title : "-"), + renderCell: ({ value }) => value.map((value: { title: string }) => value.title).join(", "), filterable: false, sortable: false, }, @@ -195,12 +195,13 @@ export function EmailCampaignsGrid({ title: row.title, subject: row.subject, content: EmailCampaignContentBlock.state2Output(EmailCampaignContentBlock.input2State(row.content)), + targetGroups: row.targetGroups.map((targetGroup) => targetGroup.id), }; }} onPaste={async ({ input }) => { await client.mutate({ mutation: createEmailCampaignMutation, - variables: { scope, input }, + variables: { scope, input: { ...input } }, }); }} onDelete={ diff --git a/packages/admin/src/emailCampaigns/form/EmailCampaignForm.gql.ts b/packages/admin/src/emailCampaigns/form/EmailCampaignForm.gql.ts index 346c1999..e65f7556 100644 --- a/packages/admin/src/emailCampaigns/form/EmailCampaignForm.gql.ts +++ b/packages/admin/src/emailCampaigns/form/EmailCampaignForm.gql.ts @@ -7,8 +7,9 @@ export const emailCampaignFormFragment = gql` scheduledAt content sendingState - targetGroup { + targetGroups { id + title } } `; diff --git a/packages/admin/src/emailCampaigns/form/EmailCampaignForm.tsx b/packages/admin/src/emailCampaigns/form/EmailCampaignForm.tsx index 9d04071c..222e4593 100644 --- a/packages/admin/src/emailCampaigns/form/EmailCampaignForm.tsx +++ b/packages/admin/src/emailCampaigns/form/EmailCampaignForm.tsx @@ -67,9 +67,9 @@ export function EmailCampaignForm({ id, EmailCampaignContentBlock, scope, previe content: EmailCampaignContentBlock, }; - type EmailCampaignState = Omit & { + type EmailCampaignState = Omit & { [key in keyof typeof rootBlocks]: BlockState<(typeof rootBlocks)[key]>; - } & { targetGroup?: string }; + }; const stackApi = useStackApi(); const stackSwitchApi = useStackSwitchApi(); @@ -103,14 +103,15 @@ export function EmailCampaignForm({ id, EmailCampaignContentBlock, scope, previe content: EmailCampaignContentBlock.input2State(emailCampaign.content), scheduledAt: emailCampaign?.scheduledAt ? new Date(emailCampaign.scheduledAt) : null, sendingState: emailCampaign?.sendingState, - targetGroup: emailCampaign?.targetGroup?.id, + targetGroups: emailCampaign?.targetGroups, }; }, state2Output: (state) => ({ ...state, content: EmailCampaignContentBlock.state2Output(state.content), - scheduledAt: state.targetGroup ? state.scheduledAt ?? null : null, + scheduledAt: state.targetGroups.length > 0 ? state.scheduledAt ?? null : null, sendingState: undefined, + targetGroups: state.targetGroups.map((targetGroup) => targetGroup.id), }), defaultState: { title: "", @@ -118,6 +119,7 @@ export function EmailCampaignForm({ id, EmailCampaignContentBlock, scope, previe content: EmailCampaignContentBlock.defaultValues(), sendingState: "DRAFT", scheduledAt: undefined, + targetGroups: [], }, }); @@ -168,6 +170,7 @@ export function EmailCampaignForm({ id, EmailCampaignContentBlock, scope, previe if (!id) { throw new Error("Missing id in edit mode"); } + const { data: mutationResponse } = await client.mutate({ mutation: updateEmailCampaignMutation, variables: { id, input: { ...output }, lastUpdatedAt: query.data?.emailCampaign?.updatedAt }, @@ -181,7 +184,7 @@ export function EmailCampaignForm({ id, EmailCampaignContentBlock, scope, previe } else { const { data: mutationResponse } = await client.mutate({ mutation: createEmailCampaignMutation, - variables: { scope, input: { ...output, targetGroup: output.targetGroup } }, + variables: { scope, input: { ...output, targetGroups: output.targetGroups } }, }); if (!mutationResponse) { @@ -215,7 +218,7 @@ export function EmailCampaignForm({ id, EmailCampaignContentBlock, scope, previe }; const isScheduledDateInPast = state.scheduledAt != undefined && isBefore(new Date(state.scheduledAt), new Date()); - const isSchedulingDisabled = state.sendingState === "SENT" || mode === "add" || !state.targetGroup || isScheduledDateInPast; + const isSchedulingDisabled = state.sendingState === "SENT" || mode === "add" || state.targetGroups.length === 0 || isScheduledDateInPast; return ( @@ -283,19 +286,19 @@ export function EmailCampaignForm({ id, EmailCampaignContentBlock, scope, previe ), content: ( setState({ ...state, scheduledAt: values.scheduledAt, targetGroup: values.targetGroup })} + onSubmit={(values) => setState({ ...state, scheduledAt: values.scheduledAt, targetGroups: values.targetGroups })} initialValues={{ - targetGroup: state.targetGroup, + targetGroups: state.targetGroups, scheduledAt: state.scheduledAt, }} > - + ), }, diff --git a/packages/admin/src/emailCampaigns/form/SendManagerFields.gql.ts b/packages/admin/src/emailCampaigns/form/SendManagerFields.gql.ts index 08c6538c..3a9af8de 100644 --- a/packages/admin/src/emailCampaigns/form/SendManagerFields.gql.ts +++ b/packages/admin/src/emailCampaigns/form/SendManagerFields.gql.ts @@ -1,14 +1,22 @@ import { gql } from "@apollo/client"; +const targetGroupSelectFragment = gql` + fragment TargetGroupSelect on TargetGroup { + id + title + } +`; + export const targetGroupsSelectQuery = gql` query TargetGroupsSelect($scope: EmailCampaignContentScopeInput!) { - targetGroups(scope: $scope) { + targetGroups(scope: $scope, limit: 100) { nodes { - id - title + ...TargetGroupSelect } } } + + ${targetGroupSelectFragment} `; export const sendEmailCampaignNowMutation = gql` diff --git a/packages/admin/src/emailCampaigns/form/SendManagerFields.tsx b/packages/admin/src/emailCampaigns/form/SendManagerFields.tsx index c567875a..9bdc03aa 100644 --- a/packages/admin/src/emailCampaigns/form/SendManagerFields.tsx +++ b/packages/admin/src/emailCampaigns/form/SendManagerFields.tsx @@ -1,10 +1,10 @@ -import { useMutation, useQuery } from "@apollo/client"; -import { Field, FinalFormSelect, SaveButton, useStackSwitchApi } from "@comet/admin"; +import { useApolloClient, useMutation } from "@apollo/client"; +import { Field, FinalFormSelect, SaveButton, useAsyncOptionsProps, useStackSwitchApi } from "@comet/admin"; import { FinalFormDateTimePicker } from "@comet/admin-date-time"; import { Newsletter } from "@comet/admin-icons"; import { AdminComponentPaper, AdminComponentSectionGroup } from "@comet/blocks-admin"; import { ContentScopeInterface } from "@comet/cms-admin"; -import { Card, MenuItem } from "@mui/material"; +import { Card } from "@mui/material"; import * as React from "react"; import { FormattedMessage } from "react-intl"; @@ -13,6 +13,7 @@ import { sendEmailCampaignNowMutation, targetGroupsSelectQuery } from "./SendMan import { GQLSendEmailCampaignNowMutation, GQLSendEmailCampaignNowMutationVariables, + GQLTargetGroupSelectFragment, GQLTargetGroupsSelectQuery, GQLTargetGroupsSelectQueryVariables, } from "./SendManagerFields.gql.generated"; @@ -39,12 +40,18 @@ const validateScheduledAt = (value: Date, now: Date) => { export const SendManagerFields = ({ isSchedulingDisabled, scope, id, isSendable }: SendManagerFieldsProps) => { const stackSwitchApi = useStackSwitchApi(); + const apolloClient = useApolloClient(); const [isSendEmailCampaignNowDialogOpen, setIsSendEmailCampaignNowDialogOpen] = React.useState(false); - const { data: targetGroups } = useQuery(targetGroupsSelectQuery, { - variables: { scope }, - fetchPolicy: "network-only", + const selectAsyncMultipleProps = useAsyncOptionsProps(async () => { + return ( + await apolloClient.query({ + query: targetGroupsSelectQuery, + variables: { scope }, + fetchPolicy: "network-only", + }) + ).data.targetGroups.nodes; }); const [sendEmailCampaignNow, { loading: sendEmailCampaignNowLoading, error: sendEmailCampaignNowError }] = useMutation< @@ -69,21 +76,19 @@ export const SendManagerFields = ({ isSchedulingDisabled, scope, id, isSendable validate={(value) => (isSchedulingDisabled ? undefined : validateScheduledAt(value, now))} componentsProps={{ datePicker: { placeholder: "DD.MM.YYYY", minDate: now }, timePicker: { placeholder: "HH:mm" } }} /> + } - name="targetGroup" + component={FinalFormSelect} + getOptionLabel={(option: GQLTargetGroupSelectFragment) => option.title} + getOptionSelected={(option: GQLTargetGroupSelectFragment, value: string) => { + return option.id === value; + }} + {...selectAsyncMultipleProps} + name="targetGroups" + label={} + multiple fullWidth - > - {(props) => ( - - {targetGroups?.targetGroups.nodes.map((option) => ( - - {option.title} - - ))} - - )} - + /> { const gqlSchemaFactory = app.get(GraphQLSchemaFactory); const BrevoContact = BrevoContactFactory.create({}); - const [BrevoContactInput, BrevoContactUpdateInput] = BrevoContactInputFactory.create({}); + const [BrevoContactInput, BrevoContactUpdateInput] = BrevoContactInputFactory.create({ Scope: EmailCampaignScope }); const BrevoContactSubscribeInput = SubscribeInputFactory.create({ Scope: EmailCampaignScope }); const BrevoContactResolver = createBrevoContactResolver({ BrevoContact, diff --git a/packages/api/schema.gql b/packages/api/schema.gql index bbb8dcd5..3777d1b4 100644 --- a/packages/api/schema.gql +++ b/packages/api/schema.gql @@ -140,7 +140,7 @@ type EmailCampaign implements DocumentInterface { subject: String! brevoId: Int scheduledAt: DateTime - targetGroup: TargetGroup + targetGroups: [TargetGroup!]! content: EmailCampaignContentBlockData! scope: EmailCampaignContentScope! sendingState: SendingState! @@ -303,7 +303,7 @@ input EmailCampaignInput { title: String! subject: String! scheduledAt: DateTime - targetGroup: ID + targetGroups: [ID!]! content: EmailCampaignContentBlockInput! } @@ -314,7 +314,7 @@ input EmailCampaignUpdateInput { title: String subject: String scheduledAt: DateTime - targetGroup: ID + targetGroups: [ID!] content: EmailCampaignContentBlockInput } diff --git a/packages/api/src/brevo-api/brevo-api-campaigns.service.ts b/packages/api/src/brevo-api/brevo-api-campaigns.service.ts index 8a9bd19b..b73716a3 100644 --- a/packages/api/src/brevo-api/brevo-api-campaigns.service.ts +++ b/packages/api/src/brevo-api/brevo-api-campaigns.service.ts @@ -54,14 +54,14 @@ export class BrevoApiCampaignsService { htmlContent: string; scheduledAt?: Date; }): Promise { - const targetGroup = await campaign.targetGroup?.load(); + const targetGroups = await campaign.targetGroups.loadItems(); const { sender } = this.config.brevo.resolveConfig(campaign.scope); const emailCampaign = { name: campaign.title, subject: campaign.subject, sender: { name: sender.name, email: sender.email }, - recipients: { listIds: targetGroup ? [targetGroup?.brevoId] : [] }, + recipients: { listIds: targetGroups.map((targetGroup) => targetGroup.brevoId) }, htmlContent, scheduledAt: scheduledAt?.toISOString(), }; @@ -81,14 +81,14 @@ export class BrevoApiCampaignsService { htmlContent: string; scheduledAt?: Date; }): Promise { - const targetGroup = await campaign.targetGroup?.load(); + const targetGroups = await campaign.targetGroups.loadItems(); const { sender } = this.config.brevo.resolveConfig(campaign.scope); const emailCampaign = { name: campaign.title, subject: campaign.subject, sender: { name: sender.name, mail: sender.email }, - recipients: { listIds: targetGroup ? [targetGroup?.brevoId] : [] }, + recipients: { listIds: targetGroups.map((targetGroup) => targetGroup.brevoId) }, htmlContent, scheduledAt: scheduledAt?.toISOString(), }; diff --git a/packages/api/src/email-campaign/dto/email-campaign-input.factory.ts b/packages/api/src/email-campaign/dto/email-campaign-input.factory.ts index d13d491a..2ddd251e 100644 --- a/packages/api/src/email-campaign/dto/email-campaign-input.factory.ts +++ b/packages/api/src/email-campaign/dto/email-campaign-input.factory.ts @@ -10,7 +10,7 @@ export interface EmailCampaignInputInterface { subject: string; scheduledAt?: Date | null; content: BlockInputInterface; - targetGroup?: string; + targetGroups: string[]; } export class EmailCampaignInputFactory { @@ -38,10 +38,9 @@ export class EmailCampaignInputFactory { @Field(() => Date, { nullable: true }) scheduledAt?: Date | null; - @IsUndefinable() - @Field(() => ID, { nullable: true }) - @IsUUID() - targetGroup?: string; + @Field(() => [ID]) + @IsUUID(4, { each: true }) + targetGroups: string[]; @Field(() => RootBlockInputScalar(EmailCampaignContentBlock)) @Transform(({ value }) => (isBlockInputInterface(value) ? value : EmailCampaignContentBlock.blockInputFactory(value)), { diff --git a/packages/api/src/email-campaign/email-campaign.resolver.ts b/packages/api/src/email-campaign/email-campaign.resolver.ts index a7f0dbfc..aeddb446 100644 --- a/packages/api/src/email-campaign/email-campaign.resolver.ts +++ b/packages/api/src/email-campaign/email-campaign.resolver.ts @@ -1,5 +1,5 @@ import { AffectedEntity, extractGraphqlFields, PaginatedResponseFactory, RequiredPermission, validateNotModified } from "@comet/cms-api"; -import { EntityManager, EntityRepository, FindOptions, Reference, wrap } from "@mikro-orm/core"; +import { EntityManager, EntityRepository, FindOptions, wrap } from "@mikro-orm/core"; import { InjectRepository } from "@mikro-orm/nestjs"; import { Type } from "@nestjs/common"; import { Args, ArgsType, ID, Info, Mutation, ObjectType, Parent, Query, ResolveField, Resolver } from "@nestjs/graphql"; @@ -66,8 +66,8 @@ export function createEmailCampaignsResolver({ const fields = extractGraphqlFields(info, { root: "nodes" }); const populate: string[] = []; - if (fields.includes("targetGroup")) { - populate.push("targetGroup"); + if (fields.includes("targetGroups")) { + populate.push("targetGroups"); } // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -94,12 +94,9 @@ export function createEmailCampaignsResolver({ scope: typeof Scope, @Args("input", { type: () => EmailCampaignInput }, new DynamicDtoValidationPipe(EmailCampaignInput)) input: EmailCampaignInputInterface, ): Promise { - const { targetGroup: targetGroupInput } = input; - const campaign = this.repository.create({ ...input, scope, - targetGroup: targetGroupInput ? Reference.create(await this.targetGroupRepository.findOneOrFail(targetGroupInput)) : undefined, content: input.content.transformToBlockData(), scheduledAt: input.scheduledAt ?? undefined, sendingState: input.scheduledAt ? SendingState.SCHEDULED : SendingState.DRAFT, @@ -240,9 +237,9 @@ export function createEmailCampaignsResolver({ return campaign.sendingState; } - @ResolveField(() => TargetGroup, { nullable: true }) - async targetGroup(@Parent() emailCampaign: EmailCampaignInterface): Promise { - return emailCampaign.targetGroup?.load(); + @ResolveField(() => [TargetGroup]) + async targetGroups(@Parent() emailCampaign: EmailCampaignInterface): Promise { + return emailCampaign.targetGroups.loadItems(); } } diff --git a/packages/api/src/email-campaign/email-campaigns.service.ts b/packages/api/src/email-campaign/email-campaigns.service.ts index 3edf55a1..5c90f732 100644 --- a/packages/api/src/email-campaign/email-campaigns.service.ts +++ b/packages/api/src/email-campaign/email-campaigns.service.ts @@ -118,32 +118,34 @@ export class EmailCampaignsService { public async sendEmailCampaignNow(campaign: EmailCampaignInterface): Promise { const brevoCampaign = await this.saveEmailCampaignInBrevo(campaign); - const targetGroup = await brevoCampaign.targetGroup?.load(); - - if (targetGroup?.brevoId) { - let currentOffset = 0; - let totalContacts = 0; - const limit = 50; - do { - const [contacts, total] = await this.brevoApiContactsService.findContactsByListId( - targetGroup.brevoId, - limit, - currentOffset, - campaign.scope, - ); - const emails = contacts.map((contact) => contact.email).filter((email): email is string => email !== undefined); - const containedEmails = await this.ecgRtrListService.getContainedEcgRtrListEmails(emails); - - if (containedEmails.length > 0) { - await this.brevoApiContactsService.blacklistMultipleContacts(containedEmails, campaign.scope); + const targetGroups = await brevoCampaign.targetGroups.loadItems(); + + for (const targetGroup of targetGroups) { + if (targetGroup?.brevoId) { + let currentOffset = 0; + let totalContacts = 0; + const limit = 50; + do { + const [contacts, total] = await this.brevoApiContactsService.findContactsByListId( + targetGroup.brevoId, + limit, + currentOffset, + campaign.scope, + ); + const emails = contacts.map((contact) => contact.email).filter((email): email is string => email !== undefined); + const containedEmails = await this.ecgRtrListService.getContainedEcgRtrListEmails(emails); + + if (containedEmails.length > 0) { + await this.brevoApiContactsService.blacklistMultipleContacts(containedEmails, campaign.scope); + } + + currentOffset += limit; + totalContacts = total; + } while (currentOffset < totalContacts); + + if (brevoCampaign.brevoId) { + return this.brevoApiCampaignService.sendBrevoCampaign(brevoCampaign); } - - currentOffset += limit; - totalContacts = total; - } while (currentOffset < totalContacts); - - if (brevoCampaign.brevoId) { - return this.brevoApiCampaignService.sendBrevoCampaign(brevoCampaign); } } diff --git a/packages/api/src/email-campaign/entities/email-campaign-entity.factory.ts b/packages/api/src/email-campaign/entities/email-campaign-entity.factory.ts index 01ab161c..dc4d8a7c 100644 --- a/packages/api/src/email-campaign/entities/email-campaign-entity.factory.ts +++ b/packages/api/src/email-campaign/entities/email-campaign-entity.factory.ts @@ -1,6 +1,6 @@ import { Block, BlockDataInterface, RootBlock } from "@comet/blocks-api"; import { DocumentInterface, RootBlockDataScalar, RootBlockType } from "@comet/cms-api"; -import { Embedded, Entity, Enum, ManyToOne, OptionalProps, PrimaryKey, Property, Ref } from "@mikro-orm/core"; +import { Collection, Embedded, Entity, Enum, ManyToMany, OptionalProps, PrimaryKey, Property } from "@mikro-orm/core"; import { Type } from "@nestjs/common"; import { Field, ID, Int, ObjectType } from "@nestjs/graphql"; import { v4 } from "uuid"; @@ -21,7 +21,7 @@ export interface EmailCampaignInterface { content: BlockDataInterface; scope: EmailCampaignScopeInterface; sendingState: SendingState; - targetGroup?: Ref; + targetGroups: Collection; } export class EmailCampaignEntityFactory { @@ -77,9 +77,9 @@ export class EmailCampaignEntityFactory { @Field(() => Date, { nullable: true }) scheduledAt?: Date; - @ManyToOne(() => TargetGroup, { nullable: true, ref: true }) - @Field(() => TargetGroup, { nullable: true }) - targetGroup?: Ref = undefined; + @ManyToMany(() => TargetGroup, (targetGroup) => targetGroup.campaigns, { owner: true }) + @Field(() => [TargetGroup]) + targetGroups = new Collection(this); @RootBlock(EmailCampaignContentBlock) @Property({ customType: new RootBlockType(EmailCampaignContentBlock) }) diff --git a/packages/api/src/mikro-orm/migrations/Migration20240819214939.ts b/packages/api/src/mikro-orm/migrations/Migration20240819214939.ts new file mode 100644 index 00000000..4e15f60b --- /dev/null +++ b/packages/api/src/mikro-orm/migrations/Migration20240819214939.ts @@ -0,0 +1,25 @@ +import { Migration } from "@mikro-orm/migrations"; + +export class Migration20240819214939 extends Migration { + async up(): Promise { + this.addSql('alter table "EmailCampaign" drop constraint "EmailCampaign_targetGroup_foreign";'); + + this.addSql( + 'create table "EmailCampaign_targetGroups" ("emailCampaign" uuid not null, "targetGroup" uuid not null, constraint "EmailCampaign_targetGroups_pkey" primary key ("emailCampaign", "targetGroup"));', + ); + + this.addSql( + 'alter table "EmailCampaign_targetGroups" add constraint "EmailCampaign_targetGroups_emailCampaign_foreign" foreign key ("emailCampaign") references "EmailCampaign" ("id") on update cascade on delete cascade;', + ); + this.addSql( + 'alter table "EmailCampaign_targetGroups" add constraint "EmailCampaign_targetGroups_targetGroup_foreign" foreign key ("targetGroup") references "TargetGroup" ("id") on update cascade on delete cascade;', + ); + + this.addSql(` + INSERT INTO "EmailCampaign_targetGroups" ("emailCampaign", "targetGroup") + SELECT "id", "targetGroup" FROM "EmailCampaign" WHERE "targetGroup" IS NOT NULL; + `); + + this.addSql('alter table "EmailCampaign" drop column "targetGroup";'); + } +} diff --git a/packages/api/src/mikro-orm/migrations/migrations.ts b/packages/api/src/mikro-orm/migrations/migrations.ts index ce2b9b7a..f3282bdf 100644 --- a/packages/api/src/mikro-orm/migrations/migrations.ts +++ b/packages/api/src/mikro-orm/migrations/migrations.ts @@ -7,6 +7,7 @@ import { Migration20240527112204 } from "./Migration20240527112204"; import { Migration20240619092554 } from "./Migration20240619092554"; import { Migration20240619145217 } from "./Migration20240619145217"; import { Migration20240621102349 } from "./Migration20240621102349"; +import { Migration20240819214939 } from "./Migration20240819214939"; export const migrationsList: MigrationObject[] = [ { name: "Migration20240115095733", class: Migration20240115095733 }, @@ -16,4 +17,5 @@ export const migrationsList: MigrationObject[] = [ { name: "Migration20240619092554", class: Migration20240619092554 }, { name: "Migration20240619145217", class: Migration20240619145217 }, { name: "Migration20240621102349", class: Migration20240621102349 }, + { name: "Migration20240819214939", class: Migration20240819214939 }, ]; diff --git a/packages/api/src/target-group/entity/target-group-entity.factory.ts b/packages/api/src/target-group/entity/target-group-entity.factory.ts index af85f723..ff94c6f1 100644 --- a/packages/api/src/target-group/entity/target-group-entity.factory.ts +++ b/packages/api/src/target-group/entity/target-group-entity.factory.ts @@ -1,9 +1,10 @@ import { DocumentInterface } from "@comet/cms-api"; -import { Embedded, Entity, OptionalProps, PrimaryKey, Property } from "@mikro-orm/core"; +import { Collection, Embedded, Entity, ManyToMany, OptionalProps, PrimaryKey, Property } from "@mikro-orm/core"; import { Type } from "@nestjs/common"; import { Field, ID, Int, ObjectType } from "@nestjs/graphql"; import { v4 } from "uuid"; +import { EmailCampaignInterface } from "../../email-campaign/entities/email-campaign-entity.factory"; import { BrevoContactFilterAttributesInterface, EmailCampaignScopeInterface } from "../../types"; export interface TargetGroupInterface { @@ -19,6 +20,7 @@ export interface TargetGroupInterface { scope: EmailCampaignScopeInterface; filters?: BrevoContactFilterAttributesInterface; assignedContactsTargetGroupBrevoId?: number; + campaigns: Collection; } export class TargetGroupEntityFactory { @@ -74,6 +76,9 @@ export class TargetGroupEntityFactory { @Property({ columnType: "int", nullable: true }) @Field(() => Int, { nullable: true }) assignedContactsTargetGroupBrevoId?: number; + + @ManyToMany("EmailCampaign", "targetGroups") + campaigns = new Collection(this); } if (BrevoFilterAttributes) { @Entity()