@sendinel/mcp-server 1.0.146 → 1.0.148
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 +33 -32
- 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 +81 -79
- 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/lists.js
CHANGED
|
@@ -4,6 +4,7 @@ import { db } from '../db.js';
|
|
|
4
4
|
import { getProjectId } from '../project.js';
|
|
5
5
|
import { logAudit } from '../audit.js';
|
|
6
6
|
import { assertChannelCanExecute, checkApproval, currentChannelContext, requestApproval, withApproval, } from '../lib/action-approvals.js';
|
|
7
|
+
import { dbToolError } from '../lib/db-error.js';
|
|
7
8
|
const LIST_MEMBER_BATCH_SIZE = 500;
|
|
8
9
|
const txt = (text) => ({ content: [{ type: 'text', text }] });
|
|
9
10
|
const j = (data) => txt(JSON.stringify(data, null, 2));
|
|
@@ -97,14 +98,14 @@ export function registerListTools(server) {
|
|
|
97
98
|
.order('created_at', { ascending: false })
|
|
98
99
|
.limit(limit);
|
|
99
100
|
if (error)
|
|
100
|
-
return txt(`Error: ${error.
|
|
101
|
+
return txt(`Error: ${dbToolError(error).error}`);
|
|
101
102
|
const lists = data ?? [];
|
|
102
103
|
const listIds = lists.map((list) => list.id);
|
|
103
104
|
const countMap = new Map();
|
|
104
105
|
if (listIds.length > 0) {
|
|
105
106
|
const { data: counts, error: countError } = await db().rpc('get_list_member_counts', { p_list_ids: listIds });
|
|
106
107
|
if (countError)
|
|
107
|
-
return txt(`Error: ${countError.
|
|
108
|
+
return txt(`Error: ${dbToolError(countError).error}`);
|
|
108
109
|
for (const row of counts ?? []) {
|
|
109
110
|
countMap.set(row.list_id, Number(row.member_count));
|
|
110
111
|
}
|
|
@@ -135,7 +136,7 @@ export function registerListTools(server) {
|
|
|
135
136
|
.select('id, name, description, created_at')
|
|
136
137
|
.single();
|
|
137
138
|
if (error)
|
|
138
|
-
return txt(`Error: ${error.
|
|
139
|
+
return txt(`Error: ${dbToolError(error).error}`);
|
|
139
140
|
await logAudit({
|
|
140
141
|
action: 'list.created',
|
|
141
142
|
resourceType: 'list',
|
|
@@ -170,7 +171,7 @@ export function registerListTools(server) {
|
|
|
170
171
|
.select('id, name, description, updated_at')
|
|
171
172
|
.maybeSingle();
|
|
172
173
|
if (error)
|
|
173
|
-
return txt(`Error: ${error.
|
|
174
|
+
return txt(`Error: ${dbToolError(error).error}`);
|
|
174
175
|
if (!data)
|
|
175
176
|
return txt('List not found');
|
|
176
177
|
await logAudit({
|
|
@@ -194,7 +195,7 @@ export function registerListTools(server) {
|
|
|
194
195
|
.eq('id', list_id)
|
|
195
196
|
.eq('project_id', projectId);
|
|
196
197
|
if (error)
|
|
197
|
-
return txt(`Error: ${error.
|
|
198
|
+
return txt(`Error: ${dbToolError(error).error}`);
|
|
198
199
|
await logAudit({ action: 'list.deleted', resourceType: 'list', resourceId: list_id });
|
|
199
200
|
return j({ deleted: true, list_id });
|
|
200
201
|
}));
|
|
@@ -218,15 +219,15 @@ export function registerListTools(server) {
|
|
|
218
219
|
.eq('project_id', projectId)
|
|
219
220
|
.maybeSingle();
|
|
220
221
|
if (listError)
|
|
221
|
-
return j(
|
|
222
|
+
return j(dbToolError(listError));
|
|
222
223
|
if (!list)
|
|
223
224
|
return j({ error: 'List not found', code: 'LIST_NOT_FOUND' });
|
|
224
225
|
const validResult = await readValidContactIds(projectId, contact_ids);
|
|
225
226
|
if (validResult.error)
|
|
226
|
-
return j(
|
|
227
|
+
return j(dbToolError(validResult.error));
|
|
227
228
|
const memberResult = await readMemberIds(list_id, validResult.ids);
|
|
228
229
|
if (memberResult.error)
|
|
229
|
-
return j(
|
|
230
|
+
return j(dbToolError(memberResult.error));
|
|
230
231
|
const wouldAdd = difference(validResult.ids, memberResult.ids);
|
|
231
232
|
if (!confirmed) {
|
|
232
233
|
return j({
|
|
@@ -254,10 +255,10 @@ export function registerListTools(server) {
|
|
|
254
255
|
}
|
|
255
256
|
const freshValidResult = await readValidContactIds(projectId, contact_ids);
|
|
256
257
|
if (freshValidResult.error)
|
|
257
|
-
return j(
|
|
258
|
+
return j(dbToolError(freshValidResult.error));
|
|
258
259
|
const freshMemberResult = await readMemberIds(list_id, freshValidResult.ids);
|
|
259
260
|
if (freshMemberResult.error)
|
|
260
|
-
return j(
|
|
261
|
+
return j(dbToolError(freshMemberResult.error));
|
|
261
262
|
const freshWouldAdd = difference(freshValidResult.ids, freshMemberResult.ids);
|
|
262
263
|
let added = 0;
|
|
263
264
|
if (freshWouldAdd.length > 0) {
|
|
@@ -269,7 +270,7 @@ export function registerListTools(server) {
|
|
|
269
270
|
})
|
|
270
271
|
.select('id');
|
|
271
272
|
if (error)
|
|
272
|
-
return j(
|
|
273
|
+
return j(dbToolError(error));
|
|
273
274
|
added = inserted?.length ?? 0;
|
|
274
275
|
}
|
|
275
276
|
await logAudit({
|
|
@@ -300,12 +301,12 @@ export function registerListTools(server) {
|
|
|
300
301
|
.eq('project_id', projectId)
|
|
301
302
|
.maybeSingle();
|
|
302
303
|
if (listError)
|
|
303
|
-
return j(
|
|
304
|
+
return j(dbToolError(listError));
|
|
304
305
|
if (!list)
|
|
305
306
|
return j({ error: 'List not found', code: 'LIST_NOT_FOUND' });
|
|
306
307
|
const memberResult = await readMemberIds(list_id, contact_ids);
|
|
307
308
|
if (memberResult.error)
|
|
308
|
-
return j(
|
|
309
|
+
return j(dbToolError(memberResult.error));
|
|
309
310
|
if (!confirmed) {
|
|
310
311
|
return j({
|
|
311
312
|
confirmation_required: true,
|
|
@@ -330,7 +331,7 @@ export function registerListTools(server) {
|
|
|
330
331
|
}
|
|
331
332
|
const freshMemberResult = await readMemberIds(list_id, contact_ids);
|
|
332
333
|
if (freshMemberResult.error)
|
|
333
|
-
return j(
|
|
334
|
+
return j(dbToolError(freshMemberResult.error));
|
|
334
335
|
const removed = freshMemberResult.ids.length;
|
|
335
336
|
if (removed > 0) {
|
|
336
337
|
const { error } = await db()
|
|
@@ -339,7 +340,7 @@ export function registerListTools(server) {
|
|
|
339
340
|
.eq('list_id', list_id)
|
|
340
341
|
.in('contact_id', freshMemberResult.ids);
|
|
341
342
|
if (error)
|
|
342
|
-
return j(
|
|
343
|
+
return j(dbToolError(error));
|
|
343
344
|
}
|
|
344
345
|
await logAudit({
|
|
345
346
|
action: 'list.member_removed',
|
|
@@ -368,7 +369,7 @@ export function registerListTools(server) {
|
|
|
368
369
|
catch (rulesError) {
|
|
369
370
|
// #4270: invalid (or empty) filter rules add nobody and say why. Everyone is { match_all: true }.
|
|
370
371
|
if (isSegmentRulesError(rulesError))
|
|
371
|
-
return j({
|
|
372
|
+
return j({ ...dbToolError(rulesError), code: 'INVALID_SEGMENT_RULES', errors: rulesError.errors });
|
|
372
373
|
throw rulesError;
|
|
373
374
|
}
|
|
374
375
|
const approvalParams = { list_id, rules: normalizedRules };
|
|
@@ -379,13 +380,13 @@ export function registerListTools(server) {
|
|
|
379
380
|
.eq('project_id', projectId)
|
|
380
381
|
.maybeSingle();
|
|
381
382
|
if (listError)
|
|
382
|
-
return j(
|
|
383
|
+
return j(dbToolError(listError));
|
|
383
384
|
if (!list)
|
|
384
385
|
return j({ error: 'List not found', code: 'LIST_NOT_FOUND' });
|
|
385
386
|
const matchedIds = await getSegmentContactIds(normalizedRules);
|
|
386
387
|
const memberResult = await readMemberIds(list_id, matchedIds);
|
|
387
388
|
if (memberResult.error)
|
|
388
|
-
return j(
|
|
389
|
+
return j(dbToolError(memberResult.error));
|
|
389
390
|
const wouldAdd = difference(matchedIds, memberResult.ids);
|
|
390
391
|
if (!confirmed) {
|
|
391
392
|
return j({
|
|
@@ -413,7 +414,7 @@ export function registerListTools(server) {
|
|
|
413
414
|
const freshMatchedIds = await getSegmentContactIds(normalizedRules);
|
|
414
415
|
const freshMemberResult = await readMemberIds(list_id, freshMatchedIds);
|
|
415
416
|
if (freshMemberResult.error)
|
|
416
|
-
return j(
|
|
417
|
+
return j(dbToolError(freshMemberResult.error));
|
|
417
418
|
const freshWouldAdd = difference(freshMatchedIds, freshMemberResult.ids);
|
|
418
419
|
let added = 0;
|
|
419
420
|
for (let index = 0; index < freshWouldAdd.length; index += LIST_MEMBER_BATCH_SIZE) {
|
|
@@ -423,7 +424,7 @@ export function registerListTools(server) {
|
|
|
423
424
|
.upsert(rows, { onConflict: 'list_id,contact_id', ignoreDuplicates: true })
|
|
424
425
|
.select('id');
|
|
425
426
|
if (error)
|
|
426
|
-
return j(
|
|
427
|
+
return j(dbToolError(error));
|
|
427
428
|
added += inserted?.length ?? 0;
|
|
428
429
|
}
|
|
429
430
|
await logAudit({
|
|
@@ -4,6 +4,7 @@ import { stripActiveEmailContent } from '@sendinel/domain';
|
|
|
4
4
|
import { db } from '../db.js';
|
|
5
5
|
import { getProjectId } from '../project.js';
|
|
6
6
|
import { parseFutureDate } from '../lib/schedule.js';
|
|
7
|
+
import { dbToolError } from '../lib/db-error.js';
|
|
7
8
|
const txt = (text) => ({ content: [{ type: 'text', text }] });
|
|
8
9
|
const j = (data) => txt(JSON.stringify(data, null, 2));
|
|
9
10
|
const NEWSLETTER_COLUMNS = 'id, org_id, project_id, site_id, name, slug, description, sender_name, sender_email, reply_to, cadence, branding, public_signup_enabled, double_opt_in, status, subscriber_count, source, source_payload, created_at, updated_at';
|
|
@@ -53,9 +54,9 @@ async function loadSendableIssue(newsletterId, issueId, projectId) {
|
|
|
53
54
|
.maybeSingle(),
|
|
54
55
|
]);
|
|
55
56
|
if (newsletterError)
|
|
56
|
-
return {
|
|
57
|
+
return { ...dbToolError(newsletterError) };
|
|
57
58
|
if (issueError)
|
|
58
|
-
return {
|
|
59
|
+
return { ...dbToolError(issueError) };
|
|
59
60
|
if (!newsletter)
|
|
60
61
|
return { error: 'Newsletter not found', code: 'NOT_FOUND' };
|
|
61
62
|
if (!issue)
|
|
@@ -82,7 +83,7 @@ async function ensureNewsletterCampaign(newsletter, issue, projectId, scheduledA
|
|
|
82
83
|
.select('id')
|
|
83
84
|
.maybeSingle();
|
|
84
85
|
if (error)
|
|
85
|
-
return { error
|
|
86
|
+
return { ...dbToolError(error) };
|
|
86
87
|
if (!campaign)
|
|
87
88
|
campaignId = null;
|
|
88
89
|
}
|
|
@@ -98,7 +99,7 @@ async function ensureNewsletterCampaign(newsletter, issue, projectId, scheduledA
|
|
|
98
99
|
metadata,
|
|
99
100
|
}).select('id').single();
|
|
100
101
|
if (error || !campaign)
|
|
101
|
-
return { error: error
|
|
102
|
+
return { error: (error ? dbToolError(error).error : undefined) ?? 'Campaign create failed' };
|
|
102
103
|
campaignId = campaign.id;
|
|
103
104
|
}
|
|
104
105
|
const { data: firstStep, error: stepFetchError } = await db().from('campaign_steps')
|
|
@@ -107,13 +108,13 @@ async function ensureNewsletterCampaign(newsletter, issue, projectId, scheduledA
|
|
|
107
108
|
.eq('step_order', 0)
|
|
108
109
|
.maybeSingle();
|
|
109
110
|
if (stepFetchError)
|
|
110
|
-
return {
|
|
111
|
+
return { ...dbToolError(stepFetchError) };
|
|
111
112
|
if (firstStep?.id) {
|
|
112
113
|
const { error } = await db().from('campaign_steps')
|
|
113
114
|
.update({ subject: issue.subject, body_html: issue.content, body_text: issue.preheader })
|
|
114
115
|
.eq('id', firstStep.id);
|
|
115
116
|
if (error)
|
|
116
|
-
return { error
|
|
117
|
+
return { ...dbToolError(error) };
|
|
117
118
|
}
|
|
118
119
|
else {
|
|
119
120
|
const { error } = await db().from('campaign_steps').insert({
|
|
@@ -126,7 +127,7 @@ async function ensureNewsletterCampaign(newsletter, issue, projectId, scheduledA
|
|
|
126
127
|
delay_hours: 0,
|
|
127
128
|
});
|
|
128
129
|
if (error)
|
|
129
|
-
return { error
|
|
130
|
+
return { ...dbToolError(error) };
|
|
130
131
|
}
|
|
131
132
|
const { data: subscriptions, error: subscriptionError } = await db().from('newsletter_subscriptions')
|
|
132
133
|
.select('subscriber_id')
|
|
@@ -135,7 +136,7 @@ async function ensureNewsletterCampaign(newsletter, issue, projectId, scheduledA
|
|
|
135
136
|
.eq('status', 'active')
|
|
136
137
|
.not('subscriber_id', 'is', null);
|
|
137
138
|
if (subscriptionError)
|
|
138
|
-
return {
|
|
139
|
+
return { ...dbToolError(subscriptionError) };
|
|
139
140
|
const now = new Date().toISOString();
|
|
140
141
|
const nextSendAt = scheduledAt ?? now;
|
|
141
142
|
const rows = (subscriptions ?? []).map((subscription) => ({
|
|
@@ -151,14 +152,14 @@ async function ensureNewsletterCampaign(newsletter, issue, projectId, scheduledA
|
|
|
151
152
|
const { error } = await db().from('campaign_enrollments')
|
|
152
153
|
.upsert(rows, { onConflict: 'campaign_id,contact_id', ignoreDuplicates: true });
|
|
153
154
|
if (error)
|
|
154
|
-
return { error
|
|
155
|
+
return { ...dbToolError(error) };
|
|
155
156
|
}
|
|
156
157
|
const { error: statusError } = await db().from('campaigns')
|
|
157
158
|
.update({ status: 'active', updated_at: now })
|
|
158
159
|
.eq('id', campaignId)
|
|
159
160
|
.eq('project_id', projectId);
|
|
160
161
|
if (statusError)
|
|
161
|
-
return {
|
|
162
|
+
return { ...dbToolError(statusError) };
|
|
162
163
|
return { campaignId, enrolled: rows.length };
|
|
163
164
|
}
|
|
164
165
|
export function registerNewsletterTools(server) {
|
|
@@ -181,7 +182,7 @@ export function registerNewsletterTools(server) {
|
|
|
181
182
|
query = status ? query.eq('status', status) : query.neq('status', 'archived');
|
|
182
183
|
const { data, error } = await query;
|
|
183
184
|
if (error)
|
|
184
|
-
return j(
|
|
185
|
+
return j(dbToolError(error));
|
|
185
186
|
return j({ newsletters: data ?? [], count: data?.length ?? 0 });
|
|
186
187
|
});
|
|
187
188
|
server.registerTool('get_newsletter', {
|
|
@@ -197,7 +198,7 @@ export function registerNewsletterTools(server) {
|
|
|
197
198
|
}, async ({ newsletter_id }) => {
|
|
198
199
|
const { data, error } = await db().from('newsletters').select(NEWSLETTER_COLUMNS).eq('id', newsletter_id).eq('project_id', getProjectId()).maybeSingle();
|
|
199
200
|
if (error)
|
|
200
|
-
return j(
|
|
201
|
+
return j(dbToolError(error));
|
|
201
202
|
if (!data)
|
|
202
203
|
return j({ error: 'Newsletter not found', code: 'NOT_FOUND' });
|
|
203
204
|
return j(data);
|
|
@@ -256,7 +257,7 @@ export function registerNewsletterTools(server) {
|
|
|
256
257
|
if (error?.code === '23505')
|
|
257
258
|
return j({ error: 'slug already exists', code: 'conflict' });
|
|
258
259
|
if (error)
|
|
259
|
-
return j({ error
|
|
260
|
+
return j({ ...dbToolError(error), code: 'INTERNAL_ERROR' });
|
|
260
261
|
return j(data);
|
|
261
262
|
});
|
|
262
263
|
server.registerTool('update_newsletter', {
|
|
@@ -303,7 +304,7 @@ export function registerNewsletterTools(server) {
|
|
|
303
304
|
if (error?.code === '23505')
|
|
304
305
|
return j({ error: 'slug already exists', code: 'conflict' });
|
|
305
306
|
if (error)
|
|
306
|
-
return j({ error
|
|
307
|
+
return j({ ...dbToolError(error), code: 'INTERNAL_ERROR' });
|
|
307
308
|
if (!data)
|
|
308
309
|
return j({ error: 'Newsletter not found', code: 'NOT_FOUND' });
|
|
309
310
|
return j(data);
|
|
@@ -330,7 +331,7 @@ export function registerNewsletterTools(server) {
|
|
|
330
331
|
const projectId = getProjectId();
|
|
331
332
|
const { data: newsletter, error: newsletterError } = await db().from('newsletters').select('id, org_id').eq('id', newsletter_id).eq('project_id', projectId).maybeSingle();
|
|
332
333
|
if (newsletterError)
|
|
333
|
-
return j(
|
|
334
|
+
return j(dbToolError(newsletterError));
|
|
334
335
|
if (!newsletter)
|
|
335
336
|
return j({ error: 'Newsletter not found', code: 'NOT_FOUND' });
|
|
336
337
|
const { data: latest } = await db().from('newsletter_issues').select('issue_number').eq('newsletter_id', newsletter_id).order('issue_number', { ascending: false }).limit(1).maybeSingle();
|
|
@@ -346,7 +347,7 @@ export function registerNewsletterTools(server) {
|
|
|
346
347
|
source_payload: { newsletter_id, subject, content, preheader },
|
|
347
348
|
}).select(ISSUE_COLUMNS).single();
|
|
348
349
|
if (error)
|
|
349
|
-
return j(
|
|
350
|
+
return j(dbToolError(error));
|
|
350
351
|
return j(data);
|
|
351
352
|
});
|
|
352
353
|
server.registerTool('list_newsletter_issues', {
|
|
@@ -374,7 +375,7 @@ export function registerNewsletterTools(server) {
|
|
|
374
375
|
query = query.eq('status', status);
|
|
375
376
|
const { data, error } = await query;
|
|
376
377
|
if (error)
|
|
377
|
-
return j(
|
|
378
|
+
return j(dbToolError(error));
|
|
378
379
|
return j({ issues: data ?? [], count: data?.length ?? 0 });
|
|
379
380
|
});
|
|
380
381
|
server.registerTool('schedule_newsletter_issue', {
|
|
@@ -413,7 +414,7 @@ export function registerNewsletterTools(server) {
|
|
|
413
414
|
.select(ISSUE_COLUMNS)
|
|
414
415
|
.maybeSingle();
|
|
415
416
|
if (error)
|
|
416
|
-
return j(
|
|
417
|
+
return j(dbToolError(error));
|
|
417
418
|
return j({ issue: data, campaign_id: campaign.campaignId, enrolled: campaign.enrolled });
|
|
418
419
|
}));
|
|
419
420
|
server.registerTool('send_newsletter_issue', {
|
|
@@ -469,7 +470,7 @@ export function registerNewsletterTools(server) {
|
|
|
469
470
|
.select(ISSUE_COLUMNS)
|
|
470
471
|
.maybeSingle();
|
|
471
472
|
if (error)
|
|
472
|
-
return j(
|
|
473
|
+
return j(dbToolError(error));
|
|
473
474
|
return j({ issue: data, campaign_id: campaign.campaignId, enrolled: campaign.enrolled, blog_post_id: post?.id ?? null, archive_url: post?.external_url ?? null });
|
|
474
475
|
}));
|
|
475
476
|
server.registerTool('subscribe_to_newsletter', {
|
|
@@ -500,7 +501,7 @@ export function registerNewsletterTools(server) {
|
|
|
500
501
|
.eq('status', 'active')
|
|
501
502
|
.maybeSingle();
|
|
502
503
|
if (newsletterError)
|
|
503
|
-
return j(
|
|
504
|
+
return j(dbToolError(newsletterError));
|
|
504
505
|
if (!newsletter)
|
|
505
506
|
return j({ error: 'Newsletter not found', code: 'NOT_FOUND' });
|
|
506
507
|
const { data: subscriber, error: subscriberError } = await db().from('subscribers').upsert({
|
|
@@ -511,7 +512,7 @@ export function registerNewsletterTools(server) {
|
|
|
511
512
|
source,
|
|
512
513
|
}, { onConflict: 'project_id,email' }).select('id').single();
|
|
513
514
|
if (subscriberError || !subscriber)
|
|
514
|
-
return j({ error: subscriberError
|
|
515
|
+
return j({ error: (subscriberError ? dbToolError(subscriberError).error : undefined) ?? 'Subscriber upsert failed' });
|
|
515
516
|
const activeImmediately = !newsletter.double_opt_in;
|
|
516
517
|
const { data, error } = await db().from('newsletter_subscriptions').upsert({
|
|
517
518
|
newsletter_id,
|
|
@@ -527,7 +528,7 @@ export function registerNewsletterTools(server) {
|
|
|
527
528
|
unsubscribe_reason: null,
|
|
528
529
|
}, { onConflict: 'newsletter_id,email' }).select(SUBSCRIPTION_COLUMNS).single();
|
|
529
530
|
if (error)
|
|
530
|
-
return j(
|
|
531
|
+
return j(dbToolError(error));
|
|
531
532
|
return j(data);
|
|
532
533
|
});
|
|
533
534
|
server.registerTool('unsubscribe_from_newsletter', {
|
|
@@ -560,7 +561,7 @@ export function registerNewsletterTools(server) {
|
|
|
560
561
|
p_reason: reason,
|
|
561
562
|
});
|
|
562
563
|
if (error)
|
|
563
|
-
return j(
|
|
564
|
+
return j(dbToolError(error));
|
|
564
565
|
if (!data?.success)
|
|
565
566
|
return j({ error: data?.reason ?? 'Unsubscribe failed', code: 'NOT_FOUND' });
|
|
566
567
|
return j(data);
|
|
@@ -585,7 +586,7 @@ export function registerNewsletterTools(server) {
|
|
|
585
586
|
query = query.eq('status', status);
|
|
586
587
|
const { data, error } = await query;
|
|
587
588
|
if (error)
|
|
588
|
-
return j(
|
|
589
|
+
return j(dbToolError(error));
|
|
589
590
|
return j({ subscribers: data ?? [], count: data?.length ?? 0 });
|
|
590
591
|
});
|
|
591
592
|
server.registerTool('get_newsletter_stats', {
|
|
@@ -605,7 +606,7 @@ export function registerNewsletterTools(server) {
|
|
|
605
606
|
.eq('project_id', getProjectId())
|
|
606
607
|
.eq('status', 'active');
|
|
607
608
|
if (error)
|
|
608
|
-
return j(
|
|
609
|
+
return j(dbToolError(error));
|
|
609
610
|
return j({ newsletter_id, active_subscribers: count ?? 0, issue_analytics_status: 'pending_phase_4' });
|
|
610
611
|
});
|
|
611
612
|
}
|
package/build/tools/org.js
CHANGED
|
@@ -5,6 +5,7 @@ import { adminDb, db } from '../db.js';
|
|
|
5
5
|
import { getProjectId } from '../project.js';
|
|
6
6
|
import { assertChannelCanExecute, checkApproval, currentChannelContext, requestApproval, withApproval } from '../lib/action-approvals.js';
|
|
7
7
|
import { callSendinelV1 } from '../lib/v1-client.js';
|
|
8
|
+
import { dbToolError } from '../lib/db-error.js';
|
|
8
9
|
const j = (data) => ({ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] });
|
|
9
10
|
const PROVIDER_SWITCH_V1_PROVIDERS = ['resend', 'ses', 'elastic-email'];
|
|
10
11
|
async function callProviderSwitchApi(path, init) {
|
|
@@ -60,7 +61,7 @@ async function resolveCurrentOrgId(requestedOrgId) {
|
|
|
60
61
|
.eq('id', getProjectId())
|
|
61
62
|
.maybeSingle();
|
|
62
63
|
if (error)
|
|
63
|
-
return { ok: false, error
|
|
64
|
+
return { ok: false, ...dbToolError(error) };
|
|
64
65
|
if (!project?.org_id)
|
|
65
66
|
return { ok: false, error: 'Project has no organization', code: 'NO_ORG' };
|
|
66
67
|
if (requestedOrgId && requestedOrgId !== project.org_id) {
|
|
@@ -89,7 +90,7 @@ async function getCurrentProjectOrg() {
|
|
|
89
90
|
.eq('id', getProjectId())
|
|
90
91
|
.maybeSingle();
|
|
91
92
|
if (error)
|
|
92
|
-
return { ok: false, error
|
|
93
|
+
return { ok: false, ...dbToolError(error) };
|
|
93
94
|
if (!data?.org_id)
|
|
94
95
|
return { ok: false, error: 'Project organization not found', code: 'NOT_FOUND' };
|
|
95
96
|
return { ok: true, project: data };
|
|
@@ -123,7 +124,7 @@ async function upsertWhiteLabelSettings(orgId, payload) {
|
|
|
123
124
|
if (error) {
|
|
124
125
|
if (error.code === '23505')
|
|
125
126
|
return { ok: false, error: 'Custom domain is already claimed', code: 'CONFLICT' };
|
|
126
|
-
return { ok: false, error
|
|
127
|
+
return { ok: false, ...dbToolError(error) };
|
|
127
128
|
}
|
|
128
129
|
return { ok: true, settings: formatWhiteLabelSettings(data) };
|
|
129
130
|
}
|
|
@@ -141,21 +142,23 @@ export function registerOrgTools(server) {
|
|
|
141
142
|
}, async ({ org_id }) => {
|
|
142
143
|
let orgId = org_id;
|
|
143
144
|
if (!orgId) {
|
|
144
|
-
const { data: project } = await db().from('projects').select('org_id').eq('id', getProjectId()).maybeSingle();
|
|
145
|
+
const { data: project, error } = await db().from('projects').select('org_id').eq('id', getProjectId()).maybeSingle();
|
|
146
|
+
if (error)
|
|
147
|
+
return j(dbToolError(error));
|
|
145
148
|
orgId = project?.org_id;
|
|
146
149
|
}
|
|
147
150
|
if (!orgId)
|
|
148
151
|
return j({ members: [] });
|
|
149
152
|
const { data, error } = await db().from('org_members').select('user_id, role, created_at').eq('org_id', orgId);
|
|
150
153
|
if (error)
|
|
151
|
-
return j(
|
|
154
|
+
return j(dbToolError(error));
|
|
152
155
|
const membersWithUsers = await Promise.all((data ?? []).map(async (member) => {
|
|
153
156
|
const { data: userData, error: userError } = await adminDb().auth.admin.getUserById(member.user_id);
|
|
154
157
|
return { member, user: userData?.user ?? null, error: userError };
|
|
155
158
|
}));
|
|
156
159
|
const userError = membersWithUsers.find((result) => result.error)?.error;
|
|
157
160
|
if (userError)
|
|
158
|
-
return j(
|
|
161
|
+
return j(dbToolError(userError));
|
|
159
162
|
return j({
|
|
160
163
|
members: membersWithUsers.map(({ member, user }) => ({
|
|
161
164
|
user_id: member.user_id,
|
|
@@ -214,7 +217,7 @@ export function registerOrgTools(server) {
|
|
|
214
217
|
.eq('user_id', user_id)
|
|
215
218
|
.maybeSingle();
|
|
216
219
|
if (memberErr)
|
|
217
|
-
return j(
|
|
220
|
+
return j(dbToolError(memberErr));
|
|
218
221
|
if (!member)
|
|
219
222
|
return j({ error: 'Member not found', code: 'NOT_FOUND' });
|
|
220
223
|
const approvalParams = { user_id, org_id: context.orgId };
|
|
@@ -256,7 +259,7 @@ export function registerOrgTools(server) {
|
|
|
256
259
|
.eq('org_id', context.orgId)
|
|
257
260
|
.eq('user_id', user_id);
|
|
258
261
|
if (error)
|
|
259
|
-
return j(
|
|
262
|
+
return j(dbToolError(error));
|
|
260
263
|
void logAudit({
|
|
261
264
|
action: 'remove_member',
|
|
262
265
|
resourceType: 'org_member',
|
|
@@ -292,7 +295,7 @@ export function registerOrgTools(server) {
|
|
|
292
295
|
.eq('user_id', user_id)
|
|
293
296
|
.maybeSingle();
|
|
294
297
|
if (memberErr)
|
|
295
|
-
return j(
|
|
298
|
+
return j(dbToolError(memberErr));
|
|
296
299
|
if (!member)
|
|
297
300
|
return j({ error: 'Member not found', code: 'NOT_FOUND' });
|
|
298
301
|
const approvalParams = { user_id, role, org_id: context.orgId };
|
|
@@ -327,7 +330,7 @@ export function registerOrgTools(server) {
|
|
|
327
330
|
.select('user_id, role')
|
|
328
331
|
.maybeSingle();
|
|
329
332
|
if (error)
|
|
330
|
-
return j(
|
|
333
|
+
return j(dbToolError(error));
|
|
331
334
|
void logAudit({
|
|
332
335
|
action: 'update_member_role',
|
|
333
336
|
resourceType: 'org_member',
|
|
@@ -362,7 +365,7 @@ export function registerOrgTools(server) {
|
|
|
362
365
|
.eq('user_id', user_id)
|
|
363
366
|
.maybeSingle();
|
|
364
367
|
if (membershipErr)
|
|
365
|
-
return j(
|
|
368
|
+
return j(dbToolError(membershipErr));
|
|
366
369
|
if (!membership)
|
|
367
370
|
return j({ error: 'Member not found', code: 'NOT_FOUND' });
|
|
368
371
|
if (membership.role === 'owner') {
|
|
@@ -373,7 +376,7 @@ export function registerOrgTools(server) {
|
|
|
373
376
|
.select('id')
|
|
374
377
|
.eq('org_id', context.orgId);
|
|
375
378
|
if (orgProjectsErr)
|
|
376
|
-
return j(
|
|
379
|
+
return j(dbToolError(orgProjectsErr));
|
|
377
380
|
const validIds = new Set((orgProjects ?? []).map((project) => project.id));
|
|
378
381
|
const assignmentMap = new Map();
|
|
379
382
|
for (const project of projects)
|
|
@@ -391,7 +394,7 @@ export function registerOrgTools(server) {
|
|
|
391
394
|
.eq('user_id', user_id)
|
|
392
395
|
.in('project_id', idsToRemove);
|
|
393
396
|
if (deleteErr)
|
|
394
|
-
return j(
|
|
397
|
+
return j(dbToolError(deleteErr));
|
|
395
398
|
}
|
|
396
399
|
if (requestedProjects.length > 0) {
|
|
397
400
|
const rows = requestedProjects.map((project) => ({ user_id, project_id: project.project_id, role: project.role }));
|
|
@@ -399,7 +402,7 @@ export function registerOrgTools(server) {
|
|
|
399
402
|
.from('user_project_access')
|
|
400
403
|
.upsert(rows, { onConflict: 'user_id,project_id' });
|
|
401
404
|
if (upsertErr)
|
|
402
|
-
return j(
|
|
405
|
+
return j(dbToolError(upsertErr));
|
|
403
406
|
}
|
|
404
407
|
void logAudit({
|
|
405
408
|
action: 'set_member_project_access',
|
|
@@ -430,7 +433,7 @@ export function registerOrgTools(server) {
|
|
|
430
433
|
.is('accepted_at', null)
|
|
431
434
|
.gt('expires_at', new Date().toISOString());
|
|
432
435
|
if (error)
|
|
433
|
-
return j(
|
|
436
|
+
return j(dbToolError(error));
|
|
434
437
|
return j({ invitations: data ?? [] });
|
|
435
438
|
});
|
|
436
439
|
server.registerTool('resend_invitation', {
|
|
@@ -459,7 +462,7 @@ export function registerOrgTools(server) {
|
|
|
459
462
|
.gt('expires_at', new Date().toISOString())
|
|
460
463
|
.maybeSingle();
|
|
461
464
|
if (invitationErr)
|
|
462
|
-
return j(
|
|
465
|
+
return j(dbToolError(invitationErr));
|
|
463
466
|
if (!invitation)
|
|
464
467
|
return j({ error: 'Invitation not found', code: 'NOT_FOUND' });
|
|
465
468
|
const expiresAt = new Date(Date.now() + 7 * 86400000).toISOString();
|
|
@@ -472,7 +475,7 @@ export function registerOrgTools(server) {
|
|
|
472
475
|
.select('id')
|
|
473
476
|
.maybeSingle();
|
|
474
477
|
if (updateErr)
|
|
475
|
-
return j(
|
|
478
|
+
return j(dbToolError(updateErr));
|
|
476
479
|
if (!updated)
|
|
477
480
|
return j({ error: 'Invitation not found', code: 'NOT_FOUND' });
|
|
478
481
|
const appUrl = (process.env.NEXT_PUBLIC_APP_URL ?? 'https://sendinel.ai').replace(/\/$/, '');
|
|
@@ -509,7 +512,7 @@ export function registerOrgTools(server) {
|
|
|
509
512
|
.eq('org_id', context.orgId)
|
|
510
513
|
.maybeSingle();
|
|
511
514
|
if (invitationErr)
|
|
512
|
-
return j(
|
|
515
|
+
return j(dbToolError(invitationErr));
|
|
513
516
|
if (!invitation || invitation.accepted_at)
|
|
514
517
|
return j({ error: 'Invitation not found', code: 'NOT_FOUND' });
|
|
515
518
|
const { error } = await db()
|
|
@@ -518,7 +521,7 @@ export function registerOrgTools(server) {
|
|
|
518
521
|
.eq('id', invitation_id)
|
|
519
522
|
.eq('org_id', context.orgId);
|
|
520
523
|
if (error)
|
|
521
|
-
return j(
|
|
524
|
+
return j(dbToolError(error));
|
|
522
525
|
void logAudit({
|
|
523
526
|
action: 'invitation.revoked',
|
|
524
527
|
resourceType: 'org_invitation',
|
|
@@ -613,7 +616,7 @@ export function registerOrgTools(server) {
|
|
|
613
616
|
query = query.lte('created_at', `${date_to}T23:59:59.999Z`);
|
|
614
617
|
const { data, count, error } = await query;
|
|
615
618
|
if (error)
|
|
616
|
-
return j(
|
|
619
|
+
return j(dbToolError(error));
|
|
617
620
|
return j({ entries: data ?? [], total: count ?? 0, limit: effectiveLimit, offset: effectiveOffset });
|
|
618
621
|
});
|
|
619
622
|
server.registerTool('get_audit_stats', {
|
|
@@ -720,10 +723,10 @@ export function registerOrgTools(server) {
|
|
|
720
723
|
? await adminDb().from('model_registry').select(columns).eq('org_id', org_id)
|
|
721
724
|
: { data: [], error: null };
|
|
722
725
|
if (scopedRows.error)
|
|
723
|
-
return j(
|
|
726
|
+
return j(dbToolError(scopedRows.error));
|
|
724
727
|
const globalRows = await adminDb().from('model_registry').select(columns).is('org_id', null);
|
|
725
728
|
if (globalRows.error)
|
|
726
|
-
return j(
|
|
729
|
+
return j(dbToolError(globalRows.error));
|
|
727
730
|
return j({
|
|
728
731
|
overrides: [
|
|
729
732
|
...(scopedRows.data ?? []),
|
|
@@ -751,7 +754,7 @@ export function registerOrgTools(server) {
|
|
|
751
754
|
.eq('org_id', context.project.org_id)
|
|
752
755
|
.maybeSingle();
|
|
753
756
|
if (error)
|
|
754
|
-
return j(
|
|
757
|
+
return j(dbToolError(error));
|
|
755
758
|
return j({
|
|
756
759
|
org_id: context.project.org_id,
|
|
757
760
|
plan: context.project.plan ?? 'free',
|
package/build/tools/segments.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { dbErrorResult } from '../lib/db-errors.js';
|
|
2
1
|
import { withApproval } from '../lib/action-approvals.js';
|
|
3
2
|
import { z } from 'zod';
|
|
4
3
|
import Anthropic from '@anthropic-ai/sdk';
|
|
5
4
|
import { db } from '../db.js';
|
|
6
5
|
import { getProjectId } from '../project.js';
|
|
7
|
-
import { dbToolError, toolError } from '../lib/db-error.js';
|
|
6
|
+
import { dbErrorResult, dbToolError, toolError } from '../lib/db-error.js';
|
|
8
7
|
import { getAnthropicModelId } from '../anthropic-model.js';
|
|
9
8
|
import { AI_MAX_TOKENS, outputConfig, anthropicCheckedText } from '@sendinel/domain';
|
|
10
9
|
import { applySegmentRules, buildSegmentBaseQuery, evaluateSegmentRules, hydrateEmailSendsContacts, isSegmentRulesError, normalizeSegmentRulesToTree, prepareSegmentRulesForStorage, readAllRows, } from '@sendinel/domain';
|
|
@@ -132,7 +131,7 @@ export function registerSegmentTools(server) {
|
|
|
132
131
|
.eq('project_id', getProjectId())
|
|
133
132
|
.order('created_at', { ascending: false });
|
|
134
133
|
if (error)
|
|
135
|
-
return txt(`Error: ${error.
|
|
134
|
+
return txt(`Error: ${dbToolError(error).error}`);
|
|
136
135
|
const segments = [];
|
|
137
136
|
for (const seg of data ?? []) {
|
|
138
137
|
let ids;
|
|
@@ -218,7 +217,7 @@ export function registerSegmentTools(server) {
|
|
|
218
217
|
.eq('id', segment_id)
|
|
219
218
|
.eq('project_id', getProjectId());
|
|
220
219
|
if (error)
|
|
221
|
-
return txt(`Error: ${error.
|
|
220
|
+
return txt(`Error: ${dbToolError(error).error}`);
|
|
222
221
|
return j({ deleted: true, segment_id });
|
|
223
222
|
}));
|
|
224
223
|
server.registerTool('preview_segment', {
|
|
@@ -336,7 +335,7 @@ export function registerSegmentTools(server) {
|
|
|
336
335
|
.select('id, name, description, rules, created_at')
|
|
337
336
|
.single();
|
|
338
337
|
if (error)
|
|
339
|
-
return txt(`Error: ${error.
|
|
338
|
+
return txt(`Error: ${dbToolError(error).error}`);
|
|
340
339
|
return j({ ...data, estimated_contacts: ids.length });
|
|
341
340
|
});
|
|
342
341
|
}
|