conduyt 1.42.0 → 1.44.0
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/dist/index.js +253 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -180,6 +180,123 @@ contacts
|
|
|
180
180
|
return client.del(`/api/v1/contacts/${encodeURIComponent(id)}`);
|
|
181
181
|
}));
|
|
182
182
|
// ---- deals ----
|
|
183
|
+
contacts
|
|
184
|
+
.command("delete-matching")
|
|
185
|
+
.description("Soft-delete EVERY contact a list filter matches, across pages (#73): the contacts page's \"Select all N matching\" action. Give the same filter you would give `contacts list` (or --filter with a JSON object of list query parameters) plus --expected, the total that filter showed; the server re-counts and refuses (409) when the set moved, so you never delete rows you did not look at. Contacts with an upcoming appointment are kept and reported. The command repeats the call until the server reports done. Requires contacts:delete.")
|
|
186
|
+
.option("--search <text>", "free-text search")
|
|
187
|
+
.option("--tag <name>", "one tag")
|
|
188
|
+
.option("--tags <names>", "comma-separated tags")
|
|
189
|
+
.option("--source <source>", "lead source")
|
|
190
|
+
.option("--assigned-to <userId>", "assignee user id")
|
|
191
|
+
.option("--smart-view <id>", "Smart View id")
|
|
192
|
+
.option("--smart-list <id>", "Smart List id")
|
|
193
|
+
.option("--import-id <id>", "import job id")
|
|
194
|
+
.option("--master-status <statuses>", "comma-separated lead statuses to include")
|
|
195
|
+
.option("--filter <json>", "a JSON object of list query parameters (merged over the flags above)")
|
|
196
|
+
.requiredOption("--expected <n>", "the total the list showed for this filter")
|
|
197
|
+
.option("--yes", "skip the typed confirmation (scripts)")
|
|
198
|
+
.action(run(async (client, opts) => {
|
|
199
|
+
const filter = {};
|
|
200
|
+
if (typeof opts.search === "string")
|
|
201
|
+
filter.search = opts.search;
|
|
202
|
+
if (typeof opts.tag === "string")
|
|
203
|
+
filter.tag = opts.tag;
|
|
204
|
+
if (typeof opts.tags === "string")
|
|
205
|
+
filter.tags = opts.tags;
|
|
206
|
+
if (typeof opts.source === "string")
|
|
207
|
+
filter.source = opts.source;
|
|
208
|
+
if (typeof opts.assignedTo === "string")
|
|
209
|
+
filter.assigned_to = opts.assignedTo;
|
|
210
|
+
if (typeof opts.smartView === "string")
|
|
211
|
+
filter.smart_view = opts.smartView;
|
|
212
|
+
if (typeof opts.smartList === "string")
|
|
213
|
+
filter.smartListId = opts.smartList;
|
|
214
|
+
if (typeof opts.importId === "string")
|
|
215
|
+
filter.import_id = opts.importId;
|
|
216
|
+
if (typeof opts.masterStatus === "string")
|
|
217
|
+
filter.master_status = opts.masterStatus.split(",").map((s) => s.trim()).filter(Boolean);
|
|
218
|
+
if (typeof opts.filter === "string") {
|
|
219
|
+
let parsed;
|
|
220
|
+
try {
|
|
221
|
+
parsed = JSON.parse(opts.filter);
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
throw new Error("--filter must be a JSON object of list query parameters.");
|
|
225
|
+
}
|
|
226
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
227
|
+
throw new Error("--filter must be a JSON object of list query parameters.");
|
|
228
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
229
|
+
if (typeof v === "string" || (Array.isArray(v) && v.every((x) => typeof x === "string")))
|
|
230
|
+
filter[k] = v;
|
|
231
|
+
else
|
|
232
|
+
throw new Error(`--filter.${k} must be a string or an array of strings.`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const hasValue = Object.values(filter).some((v) => (Array.isArray(v) ? v.some((x) => x.trim()) : v.trim()));
|
|
236
|
+
if (!hasValue)
|
|
237
|
+
throw new Error("Narrow the delete with at least one filter value; deleting every contact is not a bulk action.");
|
|
238
|
+
const expected = Number(opts.expected);
|
|
239
|
+
if (!Number.isInteger(expected) || expected < 1)
|
|
240
|
+
throw new Error("--expected must be the positive whole number the list showed for this filter.");
|
|
241
|
+
if (!opts.yes) {
|
|
242
|
+
const { createInterface } = await import("node:readline/promises");
|
|
243
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
244
|
+
const answer = (await rl.question(`Delete all ${expected} contacts matching this filter? Type the number ${expected} to confirm: `)).trim();
|
|
245
|
+
rl.close();
|
|
246
|
+
if (answer !== String(expected))
|
|
247
|
+
throw new Error("Cancelled: the typed number did not match.");
|
|
248
|
+
}
|
|
249
|
+
// The first call freezes the set on the server (409 when the count moved or when shared rows are not
|
|
250
|
+
// yours to delete); every later pass sends back the ids the server returned, never the filter again.
|
|
251
|
+
let body = { filter, expectedTotal: expected };
|
|
252
|
+
let deleted = 0;
|
|
253
|
+
let skipped = 0;
|
|
254
|
+
let kept = 0;
|
|
255
|
+
let last = {};
|
|
256
|
+
// A continuation that fails for a retryable reason (429 honouring retryAfter, a 5xx, a network error) is
|
|
257
|
+
// retried with the SAME ids so committed progress is never stranded.
|
|
258
|
+
const postWithRetry = async (payload) => {
|
|
259
|
+
for (let attempt = 0;; attempt++) {
|
|
260
|
+
try {
|
|
261
|
+
return (await client.post("/api/v1/bulk/contacts/delete-matching", payload));
|
|
262
|
+
}
|
|
263
|
+
catch (err) {
|
|
264
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
265
|
+
const status = Number(/^Conduyt API (\d{3})/.exec(msg)?.[1] ?? 0);
|
|
266
|
+
const retryable = "ids" in payload && (status === 429 || status >= 500 || status === 0);
|
|
267
|
+
if (!retryable || attempt >= 5)
|
|
268
|
+
throw err;
|
|
269
|
+
const retryAfter = Number(/"retryAfter":(\d+)/.exec(msg)?.[1] ?? 0);
|
|
270
|
+
await new Promise((r) => setTimeout(r, status === 429 ? Math.max(5, Math.min(60, retryAfter)) * 1000 : 2000 * (attempt + 1)));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
for (let pass = 0; pass < 200; pass++) {
|
|
275
|
+
const res = await postWithRetry(body);
|
|
276
|
+
const d = (res?.data ?? res);
|
|
277
|
+
last = d;
|
|
278
|
+
deleted += Number(d.deleted ?? 0);
|
|
279
|
+
skipped += Number(d.skipped ?? 0);
|
|
280
|
+
kept += Number(d.keptWithAppointments ?? 0);
|
|
281
|
+
const remainingIds = Array.isArray(d.remainingIds) ? d.remainingIds : [];
|
|
282
|
+
if (d.done || remainingIds.length === 0)
|
|
283
|
+
break;
|
|
284
|
+
if (!d.busy && Number(d.deleted ?? 0) === 0)
|
|
285
|
+
break;
|
|
286
|
+
if (d.busy)
|
|
287
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
288
|
+
body = { ids: remainingIds, continuation: d.continuation ?? null };
|
|
289
|
+
}
|
|
290
|
+
const remaining = Number(last.remaining ?? 0);
|
|
291
|
+
return {
|
|
292
|
+
deleted,
|
|
293
|
+
skipped,
|
|
294
|
+
keptWithAppointments: kept,
|
|
295
|
+
done: last.done === true,
|
|
296
|
+
remaining,
|
|
297
|
+
...(remaining > 0 ? { note: "the run stopped before finishing; run the same command again (with the new --expected) to delete the rest" } : {}),
|
|
298
|
+
};
|
|
299
|
+
}));
|
|
183
300
|
const deals = program.command("deals").description("Manage deals");
|
|
184
301
|
deals
|
|
185
302
|
.command("list")
|
|
@@ -1188,6 +1305,142 @@ automations
|
|
|
1188
1305
|
return client.get(path);
|
|
1189
1306
|
}));
|
|
1190
1307
|
// ---- companies ----
|
|
1308
|
+
// ─── Run Batch (#80) ───────────────────────────────────────────────────────
|
|
1309
|
+
const batch = program.command("batch").description("Run Batch: one logged task on many contacts (enroll, delete, add-tag, remove-tag, assign, distribute, set-source, set-status), with progress and cancel; admins and owners only");
|
|
1310
|
+
batch
|
|
1311
|
+
.command("create")
|
|
1312
|
+
.description("Queue a Run Batch. Pick the contacts with --ids, or freeze EVERY contact a list filter matches with the same flags as `contacts list` plus --expected (the total the list showed; the server refuses with 409 when the set moved). One task per run: enroll (--workflow), delete, add-tag/remove-tag (--tag-id), assign (--user), distribute (--users, round robin), set-source (--source), set-status (--status). No undo; `batch cancel` stops a run.")
|
|
1313
|
+
.requiredOption("--name <name>", "a name for the run")
|
|
1314
|
+
.requiredOption("--task <task>", "enroll | delete | add-tag | remove-tag | assign | distribute | set-source | set-status")
|
|
1315
|
+
.option("--workflow <automationId>", "enroll: the published workflow id")
|
|
1316
|
+
.option("--tag-id <tagId>", "add-tag / remove-tag: the tag id")
|
|
1317
|
+
.option("--user <userId>", "assign: one rep")
|
|
1318
|
+
.option("--users <userIds>", "distribute: comma-separated rep ids (2–50), rotated in order")
|
|
1319
|
+
.option("--source <source>", "set-source: the new lead source (overwrites)")
|
|
1320
|
+
.option("--status <status>", "set-status: one of the account's lead statuses")
|
|
1321
|
+
.option("--comment <text>", "why this run (stored with it)")
|
|
1322
|
+
.option("--ids <contactIds>", "comma-separated contact ids (instead of a filter)")
|
|
1323
|
+
.option("--search <text>", "filter: free-text search")
|
|
1324
|
+
.option("--tag <name>", "filter: one tag")
|
|
1325
|
+
.option("--tags <names>", "filter: comma-separated tags")
|
|
1326
|
+
.option("--filter-source <source>", "filter: lead source")
|
|
1327
|
+
.option("--assigned-to <userId>", "filter: assignee user id")
|
|
1328
|
+
.option("--smart-view <id>", "filter: Smart View id")
|
|
1329
|
+
.option("--smart-list <id>", "filter: Smart List id")
|
|
1330
|
+
.option("--import-id <id>", "filter: import job id")
|
|
1331
|
+
.option("--master-status <statuses>", "filter: comma-separated lead statuses to include")
|
|
1332
|
+
.option("--filter <json>", "filter: a JSON object of list query parameters (merged over the filter flags)")
|
|
1333
|
+
.option("--expected <n>", "filter: the total the list showed for this filter (required with a filter)")
|
|
1334
|
+
.action(run(async (client, opts) => {
|
|
1335
|
+
const TASKS = { enroll: "enroll", delete: "delete", "add-tag": "add_tag", "remove-tag": "remove_tag", assign: "assign", distribute: "assign", "set-source": "set_source", "set-status": "set_status" };
|
|
1336
|
+
const task = String(opts.task ?? "");
|
|
1337
|
+
const operation = TASKS[task];
|
|
1338
|
+
if (!operation)
|
|
1339
|
+
throw new Error("--task must be one of: enroll, delete, add-tag, remove-tag, assign, distribute, set-source, set-status");
|
|
1340
|
+
const params = {};
|
|
1341
|
+
if (task === "add-tag" || task === "remove-tag") {
|
|
1342
|
+
if (!opts.tagId)
|
|
1343
|
+
throw new Error(`--tag-id is required for ${task}`);
|
|
1344
|
+
params.tagId = opts.tagId;
|
|
1345
|
+
}
|
|
1346
|
+
else if (task === "assign") {
|
|
1347
|
+
if (!opts.user)
|
|
1348
|
+
throw new Error("--user is required for assign (use distribute with --users for a pool)");
|
|
1349
|
+
params.userId = opts.user;
|
|
1350
|
+
}
|
|
1351
|
+
else if (task === "distribute") {
|
|
1352
|
+
const users = csvArg(opts.users ?? "", "--users");
|
|
1353
|
+
if (users.length < 2)
|
|
1354
|
+
throw new Error("--users needs at least 2 rep ids for distribute");
|
|
1355
|
+
params.userIds = users;
|
|
1356
|
+
}
|
|
1357
|
+
else if (task === "set-source") {
|
|
1358
|
+
if (!opts.source?.trim())
|
|
1359
|
+
throw new Error("--source is required for set-source");
|
|
1360
|
+
params.source = opts.source.trim();
|
|
1361
|
+
}
|
|
1362
|
+
else if (task === "set-status") {
|
|
1363
|
+
if (!opts.status?.trim())
|
|
1364
|
+
throw new Error("--status is required for set-status");
|
|
1365
|
+
params.status = opts.status.trim();
|
|
1366
|
+
}
|
|
1367
|
+
else if (task === "enroll" && !opts.workflow) {
|
|
1368
|
+
throw new Error("--workflow is required for enroll");
|
|
1369
|
+
}
|
|
1370
|
+
const body = { name: opts.name, operation, ...(task === "enroll" ? { automationId: opts.workflow } : {}), ...(Object.keys(params).length ? { params } : {}), ...(opts.comment ? { comment: opts.comment } : {}) };
|
|
1371
|
+
if (opts.ids) {
|
|
1372
|
+
body.contactIds = csvArg(opts.ids, "--ids");
|
|
1373
|
+
}
|
|
1374
|
+
else {
|
|
1375
|
+
const filter = {};
|
|
1376
|
+
if (typeof opts.search === "string")
|
|
1377
|
+
filter.search = opts.search;
|
|
1378
|
+
if (typeof opts.tag === "string")
|
|
1379
|
+
filter.tag = opts.tag;
|
|
1380
|
+
if (typeof opts.tags === "string")
|
|
1381
|
+
filter.tags = opts.tags;
|
|
1382
|
+
if (typeof opts.filterSource === "string")
|
|
1383
|
+
filter.source = opts.filterSource;
|
|
1384
|
+
if (typeof opts.assignedTo === "string")
|
|
1385
|
+
filter.assigned_to = opts.assignedTo;
|
|
1386
|
+
if (typeof opts.smartView === "string")
|
|
1387
|
+
filter.smart_view = opts.smartView;
|
|
1388
|
+
if (typeof opts.smartList === "string")
|
|
1389
|
+
filter.smartListId = opts.smartList;
|
|
1390
|
+
if (typeof opts.importId === "string")
|
|
1391
|
+
filter.import_id = opts.importId;
|
|
1392
|
+
if (typeof opts.masterStatus === "string")
|
|
1393
|
+
filter.master_status = opts.masterStatus.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1394
|
+
if (typeof opts.filter === "string") {
|
|
1395
|
+
const parsed = jsonArg(opts.filter, "--filter");
|
|
1396
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
1397
|
+
if (typeof v === "string" || (Array.isArray(v) && v.every((x) => typeof x === "string")))
|
|
1398
|
+
filter[k] = v;
|
|
1399
|
+
else
|
|
1400
|
+
throw new Error(`--filter.${k} must be a string or an array of strings.`);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
const hasValue = Object.values(filter).some((v) => (Array.isArray(v) ? v.some((x) => x.trim()) : v.trim()));
|
|
1404
|
+
if (!hasValue)
|
|
1405
|
+
throw new Error("Give --ids, or at least one filter value; a run over every contact is not a bulk action.");
|
|
1406
|
+
const expected = Number(opts.expected);
|
|
1407
|
+
if (!Number.isInteger(expected) || expected < 1)
|
|
1408
|
+
throw new Error("--expected must be the positive whole number the list showed for this filter.");
|
|
1409
|
+
body.filter = filter;
|
|
1410
|
+
body.expectedTotal = expected;
|
|
1411
|
+
}
|
|
1412
|
+
return client.post("/api/v1/batch-operations", body);
|
|
1413
|
+
}));
|
|
1414
|
+
batch
|
|
1415
|
+
.command("list")
|
|
1416
|
+
.description("List Run Batch runs (newest first) with their task, status and progress")
|
|
1417
|
+
.option("--status <status>", "queued | running | completed | cancelled | failed")
|
|
1418
|
+
.option("--page <n>", "page number", "1")
|
|
1419
|
+
.option("--limit <n>", "rows per page (max 50)", "20")
|
|
1420
|
+
.action(run(async (client, opts) => {
|
|
1421
|
+
const qp = new URLSearchParams({ page: opts.page, limit: opts.limit });
|
|
1422
|
+
if (opts.status)
|
|
1423
|
+
qp.set("status", opts.status);
|
|
1424
|
+
return client.get(`/api/v1/batch-operations?${qp.toString()}`);
|
|
1425
|
+
}));
|
|
1426
|
+
batch
|
|
1427
|
+
.command("get <id>")
|
|
1428
|
+
.description("One run with its per-contact items (success / error / skipped, with the reason)")
|
|
1429
|
+
.option("--item-status <status>", "all | pending | success | error | skipped", "all")
|
|
1430
|
+
.option("--page <n>", "item page", "1")
|
|
1431
|
+
.option("--limit <n>", "items per page (max 50)", "20")
|
|
1432
|
+
.action(run(async (client, id, opts) => {
|
|
1433
|
+
assertUuid(id);
|
|
1434
|
+
const qp = new URLSearchParams({ itemStatus: opts.itemStatus, page: opts.page, limit: opts.limit });
|
|
1435
|
+
return client.get(`/api/v1/batch-operations/${id}?${qp.toString()}`);
|
|
1436
|
+
}));
|
|
1437
|
+
batch
|
|
1438
|
+
.command("cancel <id>")
|
|
1439
|
+
.description("Stop a queued or running run; contacts already processed stay as they are (no undo)")
|
|
1440
|
+
.action(run(async (client, id) => {
|
|
1441
|
+
assertUuid(id);
|
|
1442
|
+
return client.post(`/api/v1/batch-operations/${id}/cancel`, {});
|
|
1443
|
+
}));
|
|
1191
1444
|
const companies = program.command("companies").description("Manage companies");
|
|
1192
1445
|
companies
|
|
1193
1446
|
.command("list")
|
package/package.json
CHANGED