From a57c5f455f35fee211b621bf42a82083c44a3302 Mon Sep 17 00:00:00 2001 From: Ronni Skansing Date: Fri, 21 Nov 2025 11:40:13 +0100 Subject: [PATCH] Fix pagination has next page on campaign page. Fix campaign recipients return standard T[] result. Fix anonymization affected recipient outside scope. Signed-off-by: Ronni Skansing --- backend/controller/campaign.go | 1 - backend/repository/campaignRecipient.go | 37 +++++++++++++------ backend/service/campaign.go | 28 +++++++------- backend/service/recipient.go | 1 + backend/service/recipientGroup.go | 2 + .../src/routes/campaign/[id]/+page.svelte | 12 ++++-- 6 files changed, 52 insertions(+), 29 deletions(-) diff --git a/backend/controller/campaign.go b/backend/controller/campaign.go index 9d398b2..69b015a 100644 --- a/backend/controller/campaign.go +++ b/backend/controller/campaign.go @@ -735,7 +735,6 @@ func (c *Campaign) GetRecipientsByCampaignID(g *gin.Context) { } // endpoints is handled a bit differently and allows to // fetch an unlimited amount of rows if no offset and limit is set. - // TODO this endpoint should be changed to a Result so we fetch the rows as needed. offset := g.DefaultQuery("offset", "") limit := g.DefaultQuery("limit", "") // parse request diff --git a/backend/repository/campaignRecipient.go b/backend/repository/campaignRecipient.go index 0491ebe..a9b4f4c 100644 --- a/backend/repository/campaignRecipient.go +++ b/backend/repository/campaignRecipient.go @@ -159,11 +159,11 @@ func (r *CampaignRecipient) GetByCampaignID( ctx context.Context, campaignID *uuid.UUID, options *CampaignRecipientOption, -) ([]*model.CampaignRecipient, error) { - recps := []*model.CampaignRecipient{} +) (*model.Result[model.CampaignRecipient], error) { + result := model.NewEmptyResult[model.CampaignRecipient]() db, err := useQuery(r.DB, database.CAMPAIGN_TABLE, options.QueryArgs, allowedCampaignRecipientColumns...) if err != nil { - return recps, errs.Wrap(err) + return result, errs.Wrap(err) } db = r.preload(db, options) var dbCampaignRecipients []database.CampaignRecipient @@ -176,16 +176,23 @@ func (r *CampaignRecipient) GetByCampaignID( Find(&dbCampaignRecipients) if res.Error != nil { - return recps, res.Error + return result, res.Error } + + hasNextPage, err := useHasNextPage(db, database.CAMPAIGN_RECIPIENT_TABLE_NAME, options.QueryArgs, allowedCampaignRecipientColumns...) + if err != nil { + return result, errs.Wrap(err) + } + result.HasNextPage = hasNextPage + for _, dbCampaignRecipient := range dbCampaignRecipients { r, err := ToCampaignRecipient(&dbCampaignRecipient) if err != nil { - return recps, nil + return result, nil } - recps = append(recps, r) + result.Rows = append(result.Rows, r) } - return recps, nil + return result, nil } // GetByID gets a campaign recipient by id @@ -414,6 +421,7 @@ func (c *CampaignRecipient) UpdateByID( // Anonymize adds an anonymized id to a campaign recipient func (r *CampaignRecipient) Anonymize( ctx context.Context, + campaignID *uuid.UUID, recipientID *uuid.UUID, anonymizedID *uuid.UUID, ) error { @@ -421,10 +429,17 @@ func (r *CampaignRecipient) Anonymize( "anonymized_id": anonymizedID.String(), } AddUpdatedAt(row) - res := r.DB. - Model(&database.CampaignRecipient{}). - Where("recipient_id = ?", recipientID). - Updates(row) + db := r.DB.Model(&database.CampaignRecipient{}) + + // if campaignID is nil, anonymize across all campaigns (e.g., when deleting recipient) + // otherwise, only anonymize for the specific campaign + if campaignID != nil { + db = db.Where("campaign_id = ? AND recipient_id = ?", campaignID, recipientID) + } else { + db = db.Where("recipient_id = ?", recipientID) + } + + res := db.Updates(row) if res.Error != nil { return res.Error diff --git a/backend/service/campaign.go b/backend/service/campaign.go index 90594dc..451fc82 100644 --- a/backend/service/campaign.go +++ b/backend/service/campaign.go @@ -871,15 +871,15 @@ func (c *Campaign) GetRecipientsByCampaignID( session *model.Session, campaignID *uuid.UUID, options *repository.CampaignRecipientOption, -) ([]*model.CampaignRecipient, error) { +) (*model.Result[model.CampaignRecipient], error) { ae := NewAuditEvent("Campaign.GetRecipientsByCampaignID", session) ae.Details["campaignId"] = campaignID.String() - recipients := []*model.CampaignRecipient{} + result := model.NewEmptyResult[model.CampaignRecipient]() // check permissions isAuthorized, err := IsAuthorized(session, data.PERMISSION_ALLOW_GLOBAL) if err != nil && !errors.Is(err, errs.ErrAuthorizationFailed) { c.LogAuthError(err) - return recipients, errs.Wrap(err) + return result, errs.Wrap(err) } if !isAuthorized { c.AuditLogNotAuthorized(ae) @@ -889,17 +889,17 @@ func (c *Campaign) GetRecipientsByCampaignID( if options.OrderBy == "" { options.OrderBy = "campaign_recipients.sent_at" } - recipients, err = c.CampaignRecipientRepository.GetByCampaignID( + result, err = c.CampaignRecipientRepository.GetByCampaignID( ctx, campaignID, options, ) if err != nil { c.Logger.Errorw("failed to get recipients by campaign id", "error", err) - return recipients, errs.Wrap(err) + return result, errs.Wrap(err) } // no audit on read - return recipients, nil + return result, nil } // GetEventsByCampaignID gets all events for a campaign @@ -3206,7 +3206,7 @@ func (c *Campaign) AnonymizeByID( // assign a anonymized ID to each campaign recipient and make a map between // their ID and the anonymized ID, this is a itermidiate step to anonymize the events // where campaign receipients have both a anonymized ID and the recipient ID - campaignRecipients, err := c.CampaignRecipientRepository.GetByCampaignID( + campaignRecipientsResult, err := c.CampaignRecipientRepository.GetByCampaignID( ctx, id, &repository.CampaignRecipientOption{}, @@ -3215,7 +3215,7 @@ func (c *Campaign) AnonymizeByID( c.Logger.Errorw("failed to get campaign recipients by campaign id", "error", err) return errs.Wrap(err) } - for _, cr := range campaignRecipients { + for _, cr := range campaignRecipientsResult.Rows { if cr.RecipientID.IsNull() { c.Logger.Debug("skipping anonymization of campaign recipient without recipient") continue @@ -3224,17 +3224,17 @@ func (c *Campaign) AnonymizeByID( anonymizedID := uuid.New() cr.AnonymizedID = nullable.NewNullableWithValue(anonymizedID) recipientID := cr.RecipientID.MustGet() - err := c.CampaignRecipientRepository.Anonymize(ctx, &recipientID, &anonymizedID) - if err != nil { - c.Logger.Errorw("failed to add anonymized ID to campaign recipient", "error", err) - return errs.Wrap(err) - } - // anonymize events and assign each anonymized ID so the events can still be tracked campaignID, err := cr.CampaignID.Get() if err != nil { c.Logger.Debug("Recipient removed or anonymized, skipping in anonymization") continue } + err = c.CampaignRecipientRepository.Anonymize(ctx, &campaignID, &recipientID, &anonymizedID) + if err != nil { + c.Logger.Errorw("failed to add anonymized ID to campaign recipient", "error", err) + return errs.Wrap(err) + } + // anonymize events and assign each anonymized ID so the events can still be tracked err = c.CampaignRepository.AnonymizeCampaignEvent( ctx, &campaignID, diff --git a/backend/service/recipient.go b/backend/service/recipient.go index b08d73b..9b07518 100644 --- a/backend/service/recipient.go +++ b/backend/service/recipient.go @@ -620,6 +620,7 @@ func (r *Recipient) DeleteByID( anonymizedID := uuid.New() err = r.CampaignRecipientRepository.Anonymize( ctx, + nil, // nil campaignID means anonymize across all campaigns id, &anonymizedID, ) diff --git a/backend/service/recipientGroup.go b/backend/service/recipientGroup.go index b8879ff..35b9510 100644 --- a/backend/service/recipientGroup.go +++ b/backend/service/recipientGroup.go @@ -470,6 +470,7 @@ func (r *RecipientGroup) RemoveRecipients( anonymizedID := uuid.New() err = r.RecipientService.CampaignRecipientRepository.Anonymize( ctx, + nil, // nil campaignID means anonymize across all campaigns recpID, &anonymizedID, ) @@ -547,6 +548,7 @@ func (r *RecipientGroup) DeleteByID( } err = r.RecipientService.CampaignRecipientRepository.Anonymize( ctx, + nil, // nil campaignID means anonymize across all campaigns &recpID, &anonymizedID, ) diff --git a/frontend/src/routes/campaign/[id]/+page.svelte b/frontend/src/routes/campaign/[id]/+page.svelte index 3fa04f1..b8282c7 100644 --- a/frontend/src/routes/campaign/[id]/+page.svelte +++ b/frontend/src/routes/campaign/[id]/+page.svelte @@ -75,6 +75,8 @@ }; let allowedFilter = null; let campaignRecipients = []; + let campaignRecipientsHasNextPage = false; + let campaignEventsHasNextPage = false; let recipientEventsRecipient = { name: null, id: null @@ -280,7 +282,7 @@ if (!res.success) { throw res.error; } - const events = res.data.map((v) => ({ + const events = (res.data?.rows ?? []).map((v) => ({ createdAt: v.sendAt, eventName: 'campaign_recipient_scheduled', recipient: v.recipient @@ -732,7 +734,8 @@ if (!res.success) { throw res.error; } - campaignRecipients = res.data; + campaignRecipients = res.data?.rows ?? []; + campaignRecipientsHasNextPage = res.data?.hasNextPage ?? false; } catch (e) { addToast('Failed to load recipients', 'Error'); console.error('failed to load recipients', e); @@ -759,7 +762,8 @@ eventsTableURLParams ); if (res.success) { - campaign = { ...campaign, events: res.data.rows }; + campaign = { ...campaign, events: res.data?.rows ?? [] }; + campaignEventsHasNextPage = res.data?.hasNextPage ?? false; } } catch (e) { addToast('Failed to load events', 'Error'); @@ -1518,6 +1522,7 @@ pagination={eventsTableURLParams} plural="events" hasData={!!campaign.events.length} + hasNextPage={campaignEventsHasNextPage} hasActions={false} isGhost={isEventTableLoading} > @@ -1603,6 +1608,7 @@ pagination={recipientTableUrlParams} plural="recipients" hasData={!!campaignRecipients.length} + hasNextPage={campaignRecipientsHasNextPage} isGhost={isRecipientTableLoading} > {#each campaignRecipients as recp (recp.id)}