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 <rskansing@gmail.com>
This commit is contained in:
Ronni Skansing
2025-11-21 11:40:13 +01:00
parent 5a6a15e162
commit a57c5f455f
6 changed files with 52 additions and 29 deletions
-1
View File
@@ -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<T> so we fetch the rows as needed.
offset := g.DefaultQuery("offset", "")
limit := g.DefaultQuery("limit", "")
// parse request
+26 -11
View File
@@ -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
+14 -14
View File
@@ -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,
+1
View File
@@ -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,
)
+2
View File
@@ -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,
)
@@ -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)}