@sendinel/mcp-server 1.0.146 → 1.0.147
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/lib/action-approvals.js +3 -1
- package/build/lib/db-error.d.ts +39 -6
- package/build/lib/db-error.js +221 -11
- package/build/server.js +5 -3
- package/build/tools/ab-testing.js +4 -3
- package/build/tools/advisor.js +2 -2
- package/build/tools/ai-work.js +14 -13
- package/build/tools/analytics.js +26 -26
- package/build/tools/approvals.js +3 -1
- package/build/tools/automations.js +12 -11
- package/build/tools/brand-kit.js +4 -3
- package/build/tools/campaign-sets.js +11 -10
- package/build/tools/campaigns.js +73 -74
- package/build/tools/compound.js +7 -6
- package/build/tools/connector.js +4 -3
- package/build/tools/contacts.js +44 -45
- package/build/tools/content.js +16 -16
- package/build/tools/convert-imported.js +8 -7
- package/build/tools/data-proposals.js +8 -7
- package/build/tools/data.js +39 -38
- package/build/tools/delivery-ops.js +32 -32
- package/build/tools/drafts.js +10 -9
- package/build/tools/forms.js +5 -4
- package/build/tools/gdpr.js +10 -9
- package/build/tools/landing-pages.js +3 -3
- package/build/tools/lists.js +21 -20
- package/build/tools/newsletters.js +26 -25
- package/build/tools/org.js +26 -23
- package/build/tools/segments.js +4 -5
- package/build/tools/short-links.js +7 -6
- package/build/tools/signatures.js +10 -9
- package/build/tools/sites.js +9 -8
- package/build/tools/sms.js +14 -13
- package/build/tools/social-posts.js +20 -19
- package/build/tools/social-replies.js +7 -6
- package/build/tools/surveys.js +10 -9
- package/build/tools/templates.js +14 -13
- package/build/tools/warmup.js +5 -4
- package/build/tools/webhooks.js +5 -4
- package/package.json +1 -1
- package/build/lib/db-errors.d.ts +0 -12
- package/build/lib/db-errors.js +0 -20
package/build/tools/campaigns.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { dbErrorResult } from '../lib/db-errors.js';
|
|
2
1
|
import { resolveCampaignReentry, readCampaignPeriods, campaignPeriodFields } from "@sendinel/domain";
|
|
3
2
|
import { CAMPAIGN_BRANCH_CONDITIONS, campaignBranchConditionValue, isContactPropertyConditionList, STEP_PROPERTY_CONDITIONS_ERROR, validateStepPropertyRules } from "@sendinel/domain";
|
|
4
3
|
import { z } from 'zod';
|
|
@@ -13,7 +12,7 @@ import { ensureReplyActionSecretConfigured, mintReplyAction, } from '@sendinel/d
|
|
|
13
12
|
import { finishCampaignEdit, resumeCampaignEdit, TRIGGER_EDIT_SELECT } from '../lib/campaign-trigger-approval.js';
|
|
14
13
|
import { adminDb, db, requestContext } from '../db.js';
|
|
15
14
|
import { getProjectId } from '../project.js';
|
|
16
|
-
import { dbToolError, toolError } from '../lib/db-error.js';
|
|
15
|
+
import { dbErrorResult, dbToolError, toolError } from '../lib/db-error.js';
|
|
17
16
|
import { checkPlanLimit } from '../plan-limits.js';
|
|
18
17
|
import { buildVoicePrompt, buildEmailSystemPrompt, deriveTemplateVariables, validateTemplate } from '../lib/voice.js';
|
|
19
18
|
import { fireWebhookEvent } from '../lib/webhook-events.js';
|
|
@@ -296,7 +295,7 @@ export async function applyCampaignTargetingFields(updates, input, projectId, ex
|
|
|
296
295
|
.eq('project_id', projectId)
|
|
297
296
|
.maybeSingle();
|
|
298
297
|
if (error)
|
|
299
|
-
return error.
|
|
298
|
+
return dbToolError(error).error;
|
|
300
299
|
if (!segment)
|
|
301
300
|
return 'Segment not found';
|
|
302
301
|
updates.segment_id = input.segment_id;
|
|
@@ -436,7 +435,7 @@ async function executeBroadcast(input) {
|
|
|
436
435
|
.order('created_at', { ascending: false })
|
|
437
436
|
.limit(1);
|
|
438
437
|
if (matchErr) {
|
|
439
|
-
return { ok: false,
|
|
438
|
+
return { ok: false, ...dbToolError(matchErr), code: 'ONE_SEND_LOOKUP_FAILED' };
|
|
440
439
|
}
|
|
441
440
|
const match = (matches ?? [])[0];
|
|
442
441
|
if (match)
|
|
@@ -463,7 +462,7 @@ async function executeBroadcast(input) {
|
|
|
463
462
|
.select('id')
|
|
464
463
|
.single();
|
|
465
464
|
if (campaignErr || !campaign) {
|
|
466
|
-
return { ok: false, error: campaignErr
|
|
465
|
+
return { ok: false, error: (campaignErr ? dbToolError(campaignErr).error : undefined) ?? 'Failed to create campaign', code: 'CAMPAIGN_CREATE_FAILED' };
|
|
467
466
|
}
|
|
468
467
|
const { data: step, error: stepErr } = await db()
|
|
469
468
|
.from('campaign_steps')
|
|
@@ -480,7 +479,7 @@ async function executeBroadcast(input) {
|
|
|
480
479
|
.select('id')
|
|
481
480
|
.single();
|
|
482
481
|
if (stepErr || !step) {
|
|
483
|
-
return { ok: false, error: stepErr
|
|
482
|
+
return { ok: false, error: (stepErr ? dbToolError(stepErr).error : undefined) ?? 'Campaign created but failed to add broadcast step', code: 'STEP_CREATE_FAILED', campaign_id: campaign.id };
|
|
484
483
|
}
|
|
485
484
|
await recordBrandAssetUsage({
|
|
486
485
|
client: db(),
|
|
@@ -504,7 +503,7 @@ async function executeBroadcast(input) {
|
|
|
504
503
|
.from('campaign_enrollments')
|
|
505
504
|
.upsert(batch, { onConflict: 'campaign_id,contact_id', ignoreDuplicates: true });
|
|
506
505
|
if (enrollErr) {
|
|
507
|
-
return { ok: false,
|
|
506
|
+
return { ok: false, ...dbToolError(enrollErr), code: 'ENROLLMENT_FAILED', campaign_id: campaign.id, enrolled_before_failure: i };
|
|
508
507
|
}
|
|
509
508
|
}
|
|
510
509
|
const { error: activateErr } = await db()
|
|
@@ -513,7 +512,7 @@ async function executeBroadcast(input) {
|
|
|
513
512
|
.eq('id', campaign.id)
|
|
514
513
|
.eq('project_id', input.projectId);
|
|
515
514
|
if (activateErr) {
|
|
516
|
-
return { ok: false,
|
|
515
|
+
return { ok: false, ...dbToolError(activateErr), code: 'ACTIVATION_FAILED', campaign_id: campaign.id, enrolled: input.contactIds.length };
|
|
517
516
|
}
|
|
518
517
|
return { ok: true, campaignId: campaign.id, stepId: step.id, enrolled: input.contactIds.length, nextSendAt };
|
|
519
518
|
}
|
|
@@ -532,7 +531,7 @@ async function attachBroadcastToExisting(input, match) {
|
|
|
532
531
|
// One enrollment per campaign/contact, and ids has at most 200 entries.
|
|
533
532
|
.limit(200);
|
|
534
533
|
if (enrolledErr)
|
|
535
|
-
return { ok: false,
|
|
534
|
+
return { ok: false, ...dbToolError(enrolledErr), code: 'ONE_SEND_LOOKUP_FAILED', campaign_id: match.id };
|
|
536
535
|
for (const row of enrolledRows ?? [])
|
|
537
536
|
skip.add(row.contact_id);
|
|
538
537
|
let logRows;
|
|
@@ -565,7 +564,7 @@ async function attachBroadcastToExisting(input, match) {
|
|
|
565
564
|
.upsert(batch, { onConflict: 'campaign_id,contact_id', ignoreDuplicates: true })
|
|
566
565
|
.select('contact_id');
|
|
567
566
|
if (enrollErr) {
|
|
568
|
-
return { ok: false,
|
|
567
|
+
return { ok: false, ...dbToolError(enrollErr), code: 'ENROLLMENT_FAILED', campaign_id: match.id, enrolled_before_failure: enrolled };
|
|
569
568
|
}
|
|
570
569
|
enrolled += (inserted ?? []).length;
|
|
571
570
|
}
|
|
@@ -585,7 +584,7 @@ async function attachBroadcastToExisting(input, match) {
|
|
|
585
584
|
.eq('id', match.id)
|
|
586
585
|
.eq('project_id', input.projectId);
|
|
587
586
|
if (updateErr) {
|
|
588
|
-
return { ok: false,
|
|
587
|
+
return { ok: false, ...dbToolError(updateErr), code: 'ACTIVATION_FAILED', campaign_id: match.id, enrolled };
|
|
589
588
|
}
|
|
590
589
|
}
|
|
591
590
|
const { data: steps } = await db()
|
|
@@ -674,7 +673,7 @@ export function registerCampaignTools(server) {
|
|
|
674
673
|
query = query.eq('site_id', site_id);
|
|
675
674
|
const { data, error } = await query;
|
|
676
675
|
if (error)
|
|
677
|
-
return j(
|
|
676
|
+
return j(dbToolError(error));
|
|
678
677
|
const days = {};
|
|
679
678
|
for (const campaign of data ?? []) {
|
|
680
679
|
if (campaign.type === 'triggered') {
|
|
@@ -745,7 +744,7 @@ export function registerCampaignTools(server) {
|
|
|
745
744
|
q = q.eq('status', status);
|
|
746
745
|
const { data, error } = await q;
|
|
747
746
|
if (error)
|
|
748
|
-
return { content: [{ type: 'text', text: `Error: ${error.
|
|
747
|
+
return { content: [{ type: 'text', text: `Error: ${dbToolError(error).error}` }] };
|
|
749
748
|
// total_opened / total_clicked stay as the raw event counters (unchanged); unique_* are people
|
|
750
749
|
// counts that cannot exceed total_sent. null = the people-count lookup failed (unknown, not 0).
|
|
751
750
|
const rows = data ?? [];
|
|
@@ -791,7 +790,7 @@ export function registerCampaignTools(server) {
|
|
|
791
790
|
.eq('campaigns.site_id', site_id)
|
|
792
791
|
.order('next_send_at', { ascending: true });
|
|
793
792
|
if (error)
|
|
794
|
-
return txt(`Error: ${error.
|
|
793
|
+
return txt(`Error: ${dbToolError(error).error}`);
|
|
795
794
|
const campaigns = groupScheduleByCampaign((data ?? []));
|
|
796
795
|
const total_sends_scheduled = campaigns.reduce((sum, campaign) => sum + campaign.total_sends_scheduled, 0);
|
|
797
796
|
return j({
|
|
@@ -862,7 +861,7 @@ export function registerCampaignTools(server) {
|
|
|
862
861
|
.select()
|
|
863
862
|
.single();
|
|
864
863
|
if (error)
|
|
865
|
-
return { content: [{ type: 'text', text: `Error: ${error.
|
|
864
|
+
return { content: [{ type: 'text', text: `Error: ${dbToolError(error).error}` }] };
|
|
866
865
|
return { content: [{ type: 'text', text: JSON.stringify({ ...data, requires_approval_description: ACTIVATION_APPROVAL_DESCRIPTION, fires_on: buildCampaignFiresOn(data), trigger_diagnostics: buildCampaignTriggerDiagnostics(data) }, null, 2) }] };
|
|
867
866
|
});
|
|
868
867
|
const propertyConditions = z.array(z.union([
|
|
@@ -980,7 +979,7 @@ export function registerCampaignTools(server) {
|
|
|
980
979
|
.is('delay_days', null)
|
|
981
980
|
.is('delay_hours', null);
|
|
982
981
|
if (stepsErr)
|
|
983
|
-
return j({ error: `Could not check the campaign's emails: ${stepsErr.
|
|
982
|
+
return j({ error: `Could not check the campaign's emails: ${dbToolError(stepsErr).error}`, code: 'STEP_LOOKUP_FAILED' });
|
|
984
983
|
if (unsetSteps?.length) {
|
|
985
984
|
return j({
|
|
986
985
|
error: 'Some emails have no wait set. Set how long to wait before each one, then try again.',
|
|
@@ -996,7 +995,7 @@ export function registerCampaignTools(server) {
|
|
|
996
995
|
.eq('project_id', getProjectId())
|
|
997
996
|
.maybeSingle();
|
|
998
997
|
if (campErr)
|
|
999
|
-
return j({ error: `Could not check the campaign: ${campErr.
|
|
998
|
+
return j({ error: `Could not check the campaign: ${dbToolError(campErr).error}`, code: 'CAMPAIGN_LOOKUP_FAILED' });
|
|
1000
999
|
if (campRow?.type === 'triggered' && campaignTriggerKind(campRow) === 'manual' && !campRow.segment_id) {
|
|
1001
1000
|
return j({ error: 'This triggered campaign has no trigger. Set an event, tag, date, contact event, or segment before activating it.', code: 'validation_failed' });
|
|
1002
1001
|
}
|
|
@@ -1007,7 +1006,7 @@ export function registerCampaignTools(server) {
|
|
|
1007
1006
|
.select('campaign_id, step_order, subject, body_html, body_text')
|
|
1008
1007
|
.eq('campaign_id', campaign_id);
|
|
1009
1008
|
if (contentErr)
|
|
1010
|
-
return j({ error: `Could not check the campaign's emails: ${contentErr.
|
|
1009
|
+
return j({ error: `Could not check the campaign's emails: ${dbToolError(contentErr).error}`, code: 'STEP_LOOKUP_FAILED' });
|
|
1011
1010
|
const readiness = await checkCampaignContent(getProjectId(), [campRow], contentSteps ?? []);
|
|
1012
1011
|
if (readiness.blocking.length)
|
|
1013
1012
|
return j(contentNotReadyResponse(readiness.blocking));
|
|
@@ -1049,7 +1048,7 @@ export function registerCampaignTools(server) {
|
|
|
1049
1048
|
.select()
|
|
1050
1049
|
.single();
|
|
1051
1050
|
if (error)
|
|
1052
|
-
return { content: [{ type: 'text', text: `Error: ${error.
|
|
1051
|
+
return { content: [{ type: 'text', text: `Error: ${dbToolError(error).error}` }] };
|
|
1053
1052
|
// Fire webhook for status changes
|
|
1054
1053
|
const eventMap = { active: 'campaign.activated', completed: 'campaign.completed', paused: 'campaign.paused' };
|
|
1055
1054
|
if (eventMap[status]) {
|
|
@@ -1084,7 +1083,7 @@ export function registerCampaignTools(server) {
|
|
|
1084
1083
|
.eq('project_id', getProjectId())
|
|
1085
1084
|
.maybeSingle();
|
|
1086
1085
|
if (contactError)
|
|
1087
|
-
return txt(`Error: ${contactError.
|
|
1086
|
+
return txt(`Error: ${dbToolError(contactError).error}`);
|
|
1088
1087
|
if (!contact)
|
|
1089
1088
|
return j({ error: 'Contact not found', code: 'NOT_FOUND' });
|
|
1090
1089
|
if (contact.global_unsubscribed)
|
|
@@ -1097,7 +1096,7 @@ export function registerCampaignTools(server) {
|
|
|
1097
1096
|
.select()
|
|
1098
1097
|
.single();
|
|
1099
1098
|
if (error)
|
|
1100
|
-
return { content: [{ type: 'text', text: `Error: ${error.
|
|
1099
|
+
return { content: [{ type: 'text', text: `Error: ${dbToolError(error).error}` }] };
|
|
1101
1100
|
// Update campaign enrolled counter
|
|
1102
1101
|
const { data: campaign } = await db().from('campaigns').select('total_enrolled').eq('id', campaign_id).maybeSingle();
|
|
1103
1102
|
if (campaign) {
|
|
@@ -1135,9 +1134,9 @@ export function registerCampaignTools(server) {
|
|
|
1135
1134
|
.maybeSingle(),
|
|
1136
1135
|
]);
|
|
1137
1136
|
if (campaignRes.error)
|
|
1138
|
-
return txt(`Error: ${campaignRes.error.
|
|
1137
|
+
return txt(`Error: ${dbToolError(campaignRes.error).error}`);
|
|
1139
1138
|
if (contactRes.error)
|
|
1140
|
-
return txt(`Error: ${contactRes.error.
|
|
1139
|
+
return txt(`Error: ${dbToolError(contactRes.error).error}`);
|
|
1141
1140
|
if (!campaignRes.data || !contactRes.data)
|
|
1142
1141
|
return j({ error: 'Campaign or contact not found', code: 'NOT_FOUND' });
|
|
1143
1142
|
const { data: enrollment, error } = await db()
|
|
@@ -1147,7 +1146,7 @@ export function registerCampaignTools(server) {
|
|
|
1147
1146
|
.eq('contact_id', contact_id)
|
|
1148
1147
|
.maybeSingle();
|
|
1149
1148
|
if (error)
|
|
1150
|
-
return txt(`Error: ${error.
|
|
1149
|
+
return txt(`Error: ${dbToolError(error).error}`);
|
|
1151
1150
|
if (!enrollment) {
|
|
1152
1151
|
return j({ enrolled: false, enrollment: null, campaign: campaignRes.data, contact: contactRes.data });
|
|
1153
1152
|
}
|
|
@@ -1169,9 +1168,9 @@ export function registerCampaignTools(server) {
|
|
|
1169
1168
|
.maybeSingle(),
|
|
1170
1169
|
]);
|
|
1171
1170
|
if (stepsSentError)
|
|
1172
|
-
return txt(`Error: ${stepsSentError.
|
|
1171
|
+
return txt(`Error: ${dbToolError(stepsSentError).error}`);
|
|
1173
1172
|
if (currentStepResult.error)
|
|
1174
|
-
return txt(`Error: ${currentStepResult.error.
|
|
1173
|
+
return txt(`Error: ${dbToolError(currentStepResult.error).error}`);
|
|
1175
1174
|
return j({
|
|
1176
1175
|
enrolled: true,
|
|
1177
1176
|
campaign: campaignRes.data,
|
|
@@ -1207,7 +1206,7 @@ export function registerCampaignTools(server) {
|
|
|
1207
1206
|
.eq('site_id', site_id)
|
|
1208
1207
|
.eq('subscribed', true);
|
|
1209
1208
|
if (subErr)
|
|
1210
|
-
return { content: [{ type: 'text', text: `Error: ${subErr.
|
|
1209
|
+
return { content: [{ type: 'text', text: `Error: ${dbToolError(subErr).error}` }] };
|
|
1211
1210
|
if (!subs?.length)
|
|
1212
1211
|
return { content: [{ type: 'text', text: 'No active subscribers for this site.' }] };
|
|
1213
1212
|
const ids = subs.map((s) => s.contact_id);
|
|
@@ -1227,7 +1226,7 @@ export function registerCampaignTools(server) {
|
|
|
1227
1226
|
.from('campaign_enrollments')
|
|
1228
1227
|
.upsert(rows, { onConflict: 'campaign_id,contact_id', ignoreDuplicates: true });
|
|
1229
1228
|
if (enrollErr)
|
|
1230
|
-
return { content: [{ type: 'text', text: `Error: ${enrollErr.
|
|
1229
|
+
return { content: [{ type: 'text', text: `Error: ${dbToolError(enrollErr).error}` }] };
|
|
1231
1230
|
// Update campaign enrolled counter
|
|
1232
1231
|
const { data: campaign } = await db().from('campaigns').select('total_enrolled').eq('id', campaign_id).maybeSingle();
|
|
1233
1232
|
if (campaign) {
|
|
@@ -1275,7 +1274,7 @@ export function registerCampaignTools(server) {
|
|
|
1275
1274
|
.eq('project_id', projectId)
|
|
1276
1275
|
.maybeSingle();
|
|
1277
1276
|
if (siteErr || !site)
|
|
1278
|
-
return j({ error: siteErr
|
|
1277
|
+
return j({ error: (siteErr ? dbToolError(siteErr).error : undefined) ?? 'Site not found', code: 'SITE_NOT_FOUND' });
|
|
1279
1278
|
const { data: segment, error: segmentErr } = await db()
|
|
1280
1279
|
.from('segments')
|
|
1281
1280
|
.select('id, name, rules, site_id')
|
|
@@ -1283,7 +1282,7 @@ export function registerCampaignTools(server) {
|
|
|
1283
1282
|
.eq('project_id', projectId)
|
|
1284
1283
|
.maybeSingle();
|
|
1285
1284
|
if (segmentErr || !segment)
|
|
1286
|
-
return j({ error: segmentErr
|
|
1285
|
+
return j({ error: (segmentErr ? dbToolError(segmentErr).error : undefined) ?? 'Segment not found', code: 'SEGMENT_NOT_FOUND' });
|
|
1287
1286
|
const rules = scopeSegmentRulesToSite(segment.rules, segment.site_id);
|
|
1288
1287
|
// #3669: the same audience rules as send_broadcast and the dashboard: paged, signed up to this
|
|
1289
1288
|
// sender, consent gate applied. The count shown is the count enrolled.
|
|
@@ -1295,7 +1294,7 @@ export function registerCampaignTools(server) {
|
|
|
1295
1294
|
// #4270: a malformed segment enrolls nobody and says why.
|
|
1296
1295
|
if (isSegmentRulesError(error))
|
|
1297
1296
|
return j({ error: error.message, code: 'INVALID_SEGMENT_RULES', errors: error.errors });
|
|
1298
|
-
return j({ error: `Could not load this audience: ${error instanceof Error ? error.
|
|
1297
|
+
return j({ error: `Could not load this audience: ${error instanceof Error ? dbToolError(error).error : String(error)}`, code: 'AUDIENCE_LOOKUP_FAILED' });
|
|
1299
1298
|
}
|
|
1300
1299
|
const contactIds = audience.contactIds;
|
|
1301
1300
|
if (!confirmed) {
|
|
@@ -1437,7 +1436,7 @@ export function registerCampaignTools(server) {
|
|
|
1437
1436
|
.eq('action', 'send_broadcast')
|
|
1438
1437
|
.maybeSingle();
|
|
1439
1438
|
if (rowErr)
|
|
1440
|
-
return j({
|
|
1439
|
+
return j({ ...dbToolError(rowErr), code: 'APPROVAL_LOOKUP_FAILED' });
|
|
1441
1440
|
if (!row)
|
|
1442
1441
|
return txt(`Approval ${approval_id} is not_found. Request a new approval.`);
|
|
1443
1442
|
const stored = (row.params ?? {});
|
|
@@ -1517,7 +1516,7 @@ export function registerCampaignTools(server) {
|
|
|
1517
1516
|
.select('id, name, from_email')
|
|
1518
1517
|
.eq('project_id', projectId);
|
|
1519
1518
|
if (sitesErr)
|
|
1520
|
-
return j({
|
|
1519
|
+
return j({ ...dbToolError(sitesErr), code: 'SITE_LOOKUP_FAILED' });
|
|
1521
1520
|
const site = site_id ? (sites ?? []).find((s) => s.id === site_id) : (sites?.length === 1 ? sites[0] : null);
|
|
1522
1521
|
if (!site) {
|
|
1523
1522
|
return j({
|
|
@@ -1540,7 +1539,7 @@ export function registerCampaignTools(server) {
|
|
|
1540
1539
|
.eq('project_id', projectId)
|
|
1541
1540
|
.maybeSingle();
|
|
1542
1541
|
if (segmentErr)
|
|
1543
|
-
return j({
|
|
1542
|
+
return j({ ...dbToolError(segmentErr), code: 'SEGMENT_LOOKUP_FAILED' });
|
|
1544
1543
|
if (!segment)
|
|
1545
1544
|
return j({ error: 'Segment not found', code: 'SEGMENT_NOT_FOUND' });
|
|
1546
1545
|
rules = scopeSegmentRulesToSite(segment.rules, segment.site_id);
|
|
@@ -1551,7 +1550,7 @@ export function registerCampaignTools(server) {
|
|
|
1551
1550
|
rules = await parseAudienceRules(anthropic, audience);
|
|
1552
1551
|
}
|
|
1553
1552
|
catch (error) {
|
|
1554
|
-
return j({ error: error instanceof Error ? error.
|
|
1553
|
+
return j({ error: error instanceof Error ? dbToolError(error).error : String(error), code: 'AUDIENCE_NOT_UNDERSTOOD' });
|
|
1555
1554
|
}
|
|
1556
1555
|
segmentName = audience;
|
|
1557
1556
|
}
|
|
@@ -1564,7 +1563,7 @@ export function registerCampaignTools(server) {
|
|
|
1564
1563
|
// #4270: malformed segment or parsed audience rules send to nobody and say why.
|
|
1565
1564
|
if (isSegmentRulesError(error))
|
|
1566
1565
|
return j({ error: error.message, code: 'INVALID_SEGMENT_RULES', errors: error.errors, rules });
|
|
1567
|
-
return j({ error: `Could not load this audience: ${error instanceof Error ? error.
|
|
1566
|
+
return j({ error: `Could not load this audience: ${error instanceof Error ? dbToolError(error).error : String(error)}`, code: 'AUDIENCE_LOOKUP_FAILED' });
|
|
1568
1567
|
}
|
|
1569
1568
|
if (resolved.contactIds.length === 0) {
|
|
1570
1569
|
await logRefusedSend('send_broadcast', site.id, sendClass, resolved, { audience: segment_id ? { segment_id } : { inline_rules: true } });
|
|
@@ -1577,7 +1576,7 @@ export function registerCampaignTools(server) {
|
|
|
1577
1576
|
content = { subject: subject ?? generated.subject, body_html: generated.body_html, body_text: generated.body_text, blocks: generated.blocks };
|
|
1578
1577
|
}
|
|
1579
1578
|
catch (error) {
|
|
1580
|
-
return j({ error: error instanceof Error ? error.
|
|
1579
|
+
return j({ error: error instanceof Error ? dbToolError(error).error : String(error), code: 'DRAFT_FAILED' });
|
|
1581
1580
|
}
|
|
1582
1581
|
}
|
|
1583
1582
|
else {
|
|
@@ -1717,7 +1716,7 @@ export function registerCampaignTools(server) {
|
|
|
1717
1716
|
query = query.eq('category', category);
|
|
1718
1717
|
const { data, error } = await query;
|
|
1719
1718
|
if (error)
|
|
1720
|
-
return txt(`Error: ${error.
|
|
1719
|
+
return txt(`Error: ${dbToolError(error).error}`);
|
|
1721
1720
|
return j(data ?? []);
|
|
1722
1721
|
});
|
|
1723
1722
|
server.registerTool('clone_sequence_template', {
|
|
@@ -1742,7 +1741,7 @@ export function registerCampaignTools(server) {
|
|
|
1742
1741
|
.eq('id', template_id)
|
|
1743
1742
|
.maybeSingle();
|
|
1744
1743
|
if (templateError)
|
|
1745
|
-
return txt(`Error: ${templateError.
|
|
1744
|
+
return txt(`Error: ${dbToolError(templateError).error}`);
|
|
1746
1745
|
if (!template)
|
|
1747
1746
|
return txt('Error: Template not found');
|
|
1748
1747
|
const limit = await checkPlanLimit('active_campaigns');
|
|
@@ -1757,7 +1756,7 @@ export function registerCampaignTools(server) {
|
|
|
1757
1756
|
.eq('project_id', projectId)
|
|
1758
1757
|
.maybeSingle();
|
|
1759
1758
|
if (targetSiteError)
|
|
1760
|
-
return txt(`Error: ${targetSiteError.
|
|
1759
|
+
return txt(`Error: ${dbToolError(targetSiteError).error}`);
|
|
1761
1760
|
if (!targetSite)
|
|
1762
1761
|
return txt('Error: Site not found for this project');
|
|
1763
1762
|
}
|
|
@@ -1771,7 +1770,7 @@ export function registerCampaignTools(server) {
|
|
|
1771
1770
|
.limit(1)
|
|
1772
1771
|
.maybeSingle();
|
|
1773
1772
|
if (siteError)
|
|
1774
|
-
return txt(`Error: ${siteError.
|
|
1773
|
+
return txt(`Error: ${dbToolError(siteError).error}`);
|
|
1775
1774
|
targetSiteId = site?.id ?? null;
|
|
1776
1775
|
}
|
|
1777
1776
|
if (!targetSiteId) {
|
|
@@ -1790,7 +1789,7 @@ export function registerCampaignTools(server) {
|
|
|
1790
1789
|
.select()
|
|
1791
1790
|
.single();
|
|
1792
1791
|
if (campaignError)
|
|
1793
|
-
return txt(`Error creating campaign: ${campaignError.
|
|
1792
|
+
return txt(`Error creating campaign: ${dbToolError(campaignError).error}`);
|
|
1794
1793
|
const steps = Array.isArray(template.template?.steps) ? template.template.steps : [];
|
|
1795
1794
|
if (steps.length > 0) {
|
|
1796
1795
|
const { error: stepsError } = await db()
|
|
@@ -1807,7 +1806,7 @@ export function registerCampaignTools(server) {
|
|
|
1807
1806
|
condition_lookback_hours: step.condition_lookback_hours ?? null,
|
|
1808
1807
|
})));
|
|
1809
1808
|
if (stepsError)
|
|
1810
|
-
return txt(`Campaign created but steps failed: ${stepsError.
|
|
1809
|
+
return txt(`Campaign created but steps failed: ${dbToolError(stepsError).error}`);
|
|
1811
1810
|
}
|
|
1812
1811
|
await logAudit({
|
|
1813
1812
|
action: 'sequence_template.cloned',
|
|
@@ -2026,7 +2025,7 @@ Adapt your tone accordingly:
|
|
|
2026
2025
|
.select('id')
|
|
2027
2026
|
.single();
|
|
2028
2027
|
if (segErr)
|
|
2029
|
-
return txt(`Error creating segment: ${segErr.
|
|
2028
|
+
return txt(`Error creating segment: ${dbToolError(segErr).error}`);
|
|
2030
2029
|
segId = seg.id;
|
|
2031
2030
|
}
|
|
2032
2031
|
const campLimit = await checkPlanLimit('active_campaigns');
|
|
@@ -2038,7 +2037,7 @@ Adapt your tone accordingly:
|
|
|
2038
2037
|
.select('id')
|
|
2039
2038
|
.single();
|
|
2040
2039
|
if (campErr)
|
|
2041
|
-
return txt(`Error creating campaign: ${campErr.
|
|
2040
|
+
return txt(`Error creating campaign: ${dbToolError(campErr).error}`);
|
|
2042
2041
|
const steps = [];
|
|
2043
2042
|
for (let i = 0; i < plan.steps.length; i++) {
|
|
2044
2043
|
const planned = plan.steps[i];
|
|
@@ -2053,7 +2052,7 @@ Adapt your tone accordingly:
|
|
|
2053
2052
|
.select('id, step_order, subject')
|
|
2054
2053
|
.single();
|
|
2055
2054
|
if (stepErr)
|
|
2056
|
-
return txt(`Error adding step ${i}: ${stepErr.
|
|
2055
|
+
return txt(`Error adding step ${i}: ${dbToolError(stepErr).error}`);
|
|
2057
2056
|
steps.push(step);
|
|
2058
2057
|
}
|
|
2059
2058
|
const enrollRows = plan.contact_ids.map((id) => ({ campaign_id: campaign.id, contact_id: id, next_send_at: new Date().toISOString() }));
|
|
@@ -2087,7 +2086,7 @@ Adapt your tone accordingly:
|
|
|
2087
2086
|
.eq('action', 'launch_campaign')
|
|
2088
2087
|
.maybeSingle();
|
|
2089
2088
|
if (rowErr)
|
|
2090
|
-
return txt(`Error reading approval ${approval_id}: ${rowErr.
|
|
2089
|
+
return txt(`Error reading approval ${approval_id}: ${dbToolError(rowErr).error}`);
|
|
2091
2090
|
if (!row)
|
|
2092
2091
|
return txt(`Approval ${approval_id} is not_found. Request a new approval.`);
|
|
2093
2092
|
const stored = (row.params ?? {});
|
|
@@ -2682,7 +2681,7 @@ Do not include body_html in your response; Sendinel renders body_html from block
|
|
|
2682
2681
|
ensureReplyActionSecretConfigured();
|
|
2683
2682
|
}
|
|
2684
2683
|
catch (err) {
|
|
2685
|
-
return txt(`Error: ${err instanceof Error ? err.
|
|
2684
|
+
return txt(`Error: ${err instanceof Error ? dbToolError(err).error : 'Reply action secret is not configured'}`);
|
|
2686
2685
|
}
|
|
2687
2686
|
// Suppression check
|
|
2688
2687
|
const { data: suppression } = await db()
|
|
@@ -2732,7 +2731,7 @@ Do not include body_html in your response; Sendinel renders body_html from block
|
|
|
2732
2731
|
.eq('project_id', projectId)
|
|
2733
2732
|
.maybeSingle();
|
|
2734
2733
|
if (templateError)
|
|
2735
|
-
return txt(`Error: ${templateError.
|
|
2734
|
+
return txt(`Error: ${dbToolError(templateError).error}`);
|
|
2736
2735
|
if (!template)
|
|
2737
2736
|
return txt('Error: Template not found');
|
|
2738
2737
|
const { data: project } = await adminDb().from('projects').select('brand_settings').eq('id', projectId).maybeSingle();
|
|
@@ -2814,7 +2813,7 @@ Do not include body_html in your response; Sendinel renders body_html from block
|
|
|
2814
2813
|
}
|
|
2815
2814
|
}
|
|
2816
2815
|
if (logError || !logRow?.id) {
|
|
2817
|
-
return txt(`Error: ${logError
|
|
2816
|
+
return txt(`Error: ${(logError ? dbToolError(logError).error : undefined) ?? 'Failed to create email log row'}`);
|
|
2818
2817
|
}
|
|
2819
2818
|
let minted;
|
|
2820
2819
|
try {
|
|
@@ -2853,7 +2852,7 @@ Do not include body_html in your response; Sendinel renders body_html from block
|
|
|
2853
2852
|
}
|
|
2854
2853
|
catch (error) {
|
|
2855
2854
|
await db().from('email_log').update({ status: 'failed' }).eq('id', logRow.id);
|
|
2856
|
-
return txt(`Error: Send failed — ${error instanceof Error ? error.
|
|
2855
|
+
return txt(`Error: Send failed — ${error instanceof Error ? dbToolError(error).error : 'unknown provider error'}`);
|
|
2857
2856
|
}
|
|
2858
2857
|
await db().from('email_log').update({
|
|
2859
2858
|
status: 'sent',
|
|
@@ -2896,7 +2895,7 @@ Do not include body_html in your response; Sendinel renders body_html from block
|
|
|
2896
2895
|
.limit(1)
|
|
2897
2896
|
.maybeSingle();
|
|
2898
2897
|
if (error)
|
|
2899
|
-
return txt(`Error: ${error.
|
|
2898
|
+
return txt(`Error: ${dbToolError(error).error}`);
|
|
2900
2899
|
if (!data)
|
|
2901
2900
|
return j({ found: false, correlation_id });
|
|
2902
2901
|
const outcome = data.status === 'pending' ? 'awaiting'
|
|
@@ -2929,7 +2928,7 @@ Do not include body_html in your response; Sendinel renders body_html from block
|
|
|
2929
2928
|
.order('created_at', { ascending: false })
|
|
2930
2929
|
.limit(limit);
|
|
2931
2930
|
if (error)
|
|
2932
|
-
return txt(`Error: ${error.
|
|
2931
|
+
return txt(`Error: ${dbToolError(error).error}`);
|
|
2933
2932
|
const items = (data ?? []).map((row) => ({
|
|
2934
2933
|
...row,
|
|
2935
2934
|
outcome: row.status === 'pending' ? 'awaiting'
|
|
@@ -3230,7 +3229,7 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3230
3229
|
.select()
|
|
3231
3230
|
.single();
|
|
3232
3231
|
if (campErr)
|
|
3233
|
-
return txt(`Failed to create campaign: ${campErr.
|
|
3232
|
+
return txt(`Failed to create campaign: ${dbToolError(campErr).error}`);
|
|
3234
3233
|
// 2. Add steps
|
|
3235
3234
|
const stepInserts = steps.map((s, i) => ({
|
|
3236
3235
|
campaign_id: campaign.id,
|
|
@@ -3246,7 +3245,7 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3246
3245
|
.from('campaign_steps')
|
|
3247
3246
|
.insert(stepInserts);
|
|
3248
3247
|
if (stepErr)
|
|
3249
|
-
return txt(`Campaign created (${campaign.id}) but failed to add steps: ${stepErr.
|
|
3248
|
+
return txt(`Campaign created (${campaign.id}) but failed to add steps: ${dbToolError(stepErr).error}`);
|
|
3250
3249
|
// 3. Enroll segment if provided
|
|
3251
3250
|
let enrolled = 0;
|
|
3252
3251
|
if (segment_id) {
|
|
@@ -3335,14 +3334,14 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3335
3334
|
.eq('project_id', projectId)
|
|
3336
3335
|
.maybeSingle();
|
|
3337
3336
|
if (campaignErr || !campaign)
|
|
3338
|
-
return j({ error: campaignErr
|
|
3337
|
+
return j({ error: (campaignErr ? dbToolError(campaignErr).error : undefined) ?? 'Campaign not found', code: 'NOT_FOUND' });
|
|
3339
3338
|
const { data, error } = await db()
|
|
3340
3339
|
.from('campaign_steps')
|
|
3341
3340
|
.select('id, step_order, subject, body_html, body_text, blocks, delay_days, delay_hours, skip_if_event, send_only_if_event, skip_if_property, send_only_if_property, created_at, updated_at')
|
|
3342
3341
|
.eq('campaign_id', campaign_id)
|
|
3343
3342
|
.order('step_order', { ascending: true });
|
|
3344
3343
|
if (error)
|
|
3345
|
-
return j(
|
|
3344
|
+
return j(dbToolError(error));
|
|
3346
3345
|
return j({ steps: data ?? [], count: data?.length ?? 0 });
|
|
3347
3346
|
});
|
|
3348
3347
|
server.registerTool('update_campaign_step', {
|
|
@@ -3391,7 +3390,7 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3391
3390
|
.eq('id', step_id)
|
|
3392
3391
|
.maybeSingle();
|
|
3393
3392
|
if (stepErr || !step)
|
|
3394
|
-
return j({ error: stepErr
|
|
3393
|
+
return j({ error: (stepErr ? dbToolError(stepErr).error : undefined) ?? 'Step not found', code: 'NOT_FOUND' });
|
|
3395
3394
|
const { data: campaign } = await db()
|
|
3396
3395
|
.from('campaigns')
|
|
3397
3396
|
.select('id')
|
|
@@ -3424,7 +3423,7 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3424
3423
|
.select('id, step_order, subject, delay_days, delay_hours, skip_if_event, send_only_if_event, skip_if_property, send_only_if_property, updated_at')
|
|
3425
3424
|
.single();
|
|
3426
3425
|
if (error)
|
|
3427
|
-
return j(
|
|
3426
|
+
return j(dbToolError(error));
|
|
3428
3427
|
await logAudit({ action: 'campaign.step_updated', resourceType: 'campaign_step', resourceId: data.id, details: { property_rules: properties.rules } });
|
|
3429
3428
|
return j({ step: data, ok: true });
|
|
3430
3429
|
});
|
|
@@ -3463,7 +3462,7 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3463
3462
|
.delete()
|
|
3464
3463
|
.eq('id', step_id);
|
|
3465
3464
|
if (error)
|
|
3466
|
-
return j(
|
|
3465
|
+
return j(dbToolError(error));
|
|
3467
3466
|
return j({ ok: true, deleted: step_id });
|
|
3468
3467
|
}));
|
|
3469
3468
|
server.registerTool('delete_campaign', {
|
|
@@ -3495,7 +3494,7 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3495
3494
|
.eq('id', campaign_id)
|
|
3496
3495
|
.eq('project_id', projectId);
|
|
3497
3496
|
if (error)
|
|
3498
|
-
return j(
|
|
3497
|
+
return j(dbToolError(error));
|
|
3499
3498
|
return j({ success: true, deleted: campaign.name });
|
|
3500
3499
|
}));
|
|
3501
3500
|
server.registerTool('set_campaign_trigger', {
|
|
@@ -3524,7 +3523,7 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3524
3523
|
.select(TRIGGER_EDIT_SELECT)
|
|
3525
3524
|
.eq('id', args.campaign_id).eq('project_id', projectId).maybeSingle();
|
|
3526
3525
|
if (loadError)
|
|
3527
|
-
return j(
|
|
3526
|
+
return j(dbToolError(loadError));
|
|
3528
3527
|
if (!existing)
|
|
3529
3528
|
return j({ error: 'Campaign not found', code: 'NOT_FOUND' });
|
|
3530
3529
|
if (!['draft', 'active', 'paused'].includes(existing.status)) {
|
|
@@ -3618,7 +3617,7 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3618
3617
|
if (send_at !== undefined && send_at !== null) {
|
|
3619
3618
|
const { data: existing, error: fetchError } = await getExisting();
|
|
3620
3619
|
if (fetchError)
|
|
3621
|
-
return j(
|
|
3620
|
+
return j(dbToolError(fetchError));
|
|
3622
3621
|
if (!existing)
|
|
3623
3622
|
return j({ error: 'Campaign not found', code: 'NOT_FOUND' });
|
|
3624
3623
|
campaignType = existing.type;
|
|
@@ -3626,7 +3625,7 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3626
3625
|
if (type !== undefined) {
|
|
3627
3626
|
const { data: existing, error: fetchError } = await getExisting();
|
|
3628
3627
|
if (fetchError)
|
|
3629
|
-
return j(
|
|
3628
|
+
return j(dbToolError(fetchError));
|
|
3630
3629
|
if (!existing)
|
|
3631
3630
|
return j({ error: 'Campaign not found', code: 'NOT_FOUND' });
|
|
3632
3631
|
if (type !== existing.type) {
|
|
@@ -3641,7 +3640,7 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3641
3640
|
if (hasTriggerUpdate) {
|
|
3642
3641
|
const { data: existing, error: fetchError } = await getExisting();
|
|
3643
3642
|
if (fetchError)
|
|
3644
|
-
return j(
|
|
3643
|
+
return j(dbToolError(fetchError));
|
|
3645
3644
|
if (!existing)
|
|
3646
3645
|
return j({ error: 'Campaign not found', code: 'NOT_FOUND' });
|
|
3647
3646
|
existingCampaignForTargeting = existing;
|
|
@@ -3697,7 +3696,7 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3697
3696
|
const result = await db().from('campaigns').update({ ...updates, updated_at: new Date().toISOString() })
|
|
3698
3697
|
.eq('id', campaign_id).eq('project_id', projectId).select().single();
|
|
3699
3698
|
if (result.error)
|
|
3700
|
-
return j(
|
|
3699
|
+
return j(dbToolError(result.error));
|
|
3701
3700
|
data = result.data;
|
|
3702
3701
|
await logAudit({ action: 'campaign.updated', resourceType: 'campaign', resourceId: campaign_id, details: { updates } });
|
|
3703
3702
|
}
|
|
@@ -3730,7 +3729,7 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3730
3729
|
.eq('project_id', projectId)
|
|
3731
3730
|
.maybeSingle();
|
|
3732
3731
|
if (campErr || !campaign)
|
|
3733
|
-
return j({ error: campErr
|
|
3732
|
+
return j({ error: (campErr ? dbToolError(campErr).error : undefined) ?? 'Campaign not found', code: 'NOT_FOUND' });
|
|
3734
3733
|
const [{ data: steps, error: stepsError }, { count: enrollCount, error: enrollError }, { count: sentCount, error: sentError }, { count: queuedEmailCount, error: queuedEmailError }, { count: failedSendCount, error: failedSendError }, { data: queueRows, error: queueError }, { count: dlqUnresolvedCount, error: dlqError },] = await Promise.all([
|
|
3735
3734
|
db()
|
|
3736
3735
|
.from('campaign_steps')
|
|
@@ -3775,7 +3774,7 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3775
3774
|
]);
|
|
3776
3775
|
const visibilityError = stepsError ?? enrollError ?? sentError ?? queuedEmailError ?? failedSendError ?? queueError ?? dlqError;
|
|
3777
3776
|
if (visibilityError)
|
|
3778
|
-
return j({
|
|
3777
|
+
return j({ ...dbToolError(visibilityError), code: 'CAMPAIGN_VISIBILITY_QUERY_FAILED' });
|
|
3779
3778
|
const periods = await readCampaignPeriods(db(), projectId, campaign);
|
|
3780
3779
|
const outstandingRows = queueRows ?? [];
|
|
3781
3780
|
const pendingOrRetryRows = outstandingRows.filter((row) => row.status !== 'processing');
|
|
@@ -3840,7 +3839,7 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3840
3839
|
.eq('campaign_id', campaign_id)
|
|
3841
3840
|
.eq('contact_id', contact_id);
|
|
3842
3841
|
if (error)
|
|
3843
|
-
return j(
|
|
3842
|
+
return j(dbToolError(error));
|
|
3844
3843
|
return j({ success: true });
|
|
3845
3844
|
});
|
|
3846
3845
|
// #3348 - campaign branching and goals were fully built (schema, engine, dashboard API)
|
|
@@ -3869,7 +3868,7 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3869
3868
|
query = query.eq('from_step_id', step_id);
|
|
3870
3869
|
const { data, error } = await query;
|
|
3871
3870
|
if (error)
|
|
3872
|
-
return j(
|
|
3871
|
+
return j(dbToolError(error));
|
|
3873
3872
|
return j({ campaign_id, branches: data ?? [], count: (data ?? []).length });
|
|
3874
3873
|
});
|
|
3875
3874
|
server.registerTool('add_campaign_branch', {
|
|
@@ -3941,7 +3940,7 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3941
3940
|
.eq('id', branch_id)
|
|
3942
3941
|
.eq('campaign_id', campaign_id);
|
|
3943
3942
|
if (error)
|
|
3944
|
-
return j(
|
|
3943
|
+
return j(dbToolError(error));
|
|
3945
3944
|
await logAudit({ action: 'remove_campaign_branch', resourceType: 'campaign', resourceId: campaign_id, details: { branch_id } }).catch(() => { });
|
|
3946
3945
|
return j({ success: true, removed: branch_id });
|
|
3947
3946
|
});
|
|
@@ -3959,7 +3958,7 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3959
3958
|
.eq('campaign_id', campaign_id)
|
|
3960
3959
|
.order('created_at', { ascending: true });
|
|
3961
3960
|
if (error)
|
|
3962
|
-
return j(
|
|
3961
|
+
return j(dbToolError(error));
|
|
3963
3962
|
return j({ campaign_id, goals: data ?? [], count: (data ?? []).length });
|
|
3964
3963
|
});
|
|
3965
3964
|
server.registerTool('set_campaign_goal', {
|
|
@@ -3995,7 +3994,7 @@ Return ONLY valid JSON with this exact shape:
|
|
|
3995
3994
|
.select('id, goal_type, goal_value, action, target_campaign_id')
|
|
3996
3995
|
.single();
|
|
3997
3996
|
if (error)
|
|
3998
|
-
return j(
|
|
3997
|
+
return j(dbToolError(error));
|
|
3999
3998
|
await logAudit({ action: 'set_campaign_goal', resourceType: 'campaign', resourceId: campaign_id, details: { goal_id: data?.id, goal_type, goal_action: resolvedAction } }).catch(() => { });
|
|
4000
3999
|
return j({ success: true, goal: data });
|
|
4001
4000
|
});
|
|
@@ -4013,7 +4012,7 @@ Return ONLY valid JSON with this exact shape:
|
|
|
4013
4012
|
.eq('id', goal_id)
|
|
4014
4013
|
.eq('campaign_id', campaign_id);
|
|
4015
4014
|
if (error)
|
|
4016
|
-
return j(
|
|
4015
|
+
return j(dbToolError(error));
|
|
4017
4016
|
await logAudit({ action: 'remove_campaign_goal', resourceType: 'campaign', resourceId: campaign_id, details: { goal_id } }).catch(() => { });
|
|
4018
4017
|
return j({ success: true, removed: goal_id });
|
|
4019
4018
|
});
|