@dench.com/cli 2.5.2 → 2.7.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/crm.ts +307 -1
- package/cron.ts +29 -9
- package/email.ts +51 -6
- package/lib/api-schemas.ts +210 -0
- package/lib/command-registry.ts +83 -1
- package/package.json +1 -1
package/crm.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*
|
|
17
17
|
* The full surface mirrors the plan's "dench-cli CRM surface" section:
|
|
18
18
|
* crm objects list / get / create / update / delete / rename
|
|
19
|
-
* crm views list / get / entries
|
|
19
|
+
* crm views list / get / entries / create / update / delete
|
|
20
20
|
* crm fields list / create / update / delete / reorder
|
|
21
21
|
* crm entries list / get / create / create-many / update / update-many / delete / bulk-delete
|
|
22
22
|
* crm cells get / set / set-many / append
|
|
@@ -562,6 +562,12 @@ const api = {
|
|
|
562
562
|
update: makeFunctionReference<"mutation">("functions/crm/objects:update"),
|
|
563
563
|
rename: makeFunctionReference<"mutation">("functions/crm/objects:rename"),
|
|
564
564
|
remove: makeFunctionReference<"mutation">("functions/crm/objects:remove"),
|
|
565
|
+
saveView: makeFunctionReference<"mutation">(
|
|
566
|
+
"functions/crm/objects:saveView",
|
|
567
|
+
),
|
|
568
|
+
deleteView: makeFunctionReference<"mutation">(
|
|
569
|
+
"functions/crm/objects:deleteView",
|
|
570
|
+
),
|
|
565
571
|
},
|
|
566
572
|
fields: {
|
|
567
573
|
list: makeFunctionReference<"query">("functions/crm/fields:list"),
|
|
@@ -968,6 +974,216 @@ function summarizeSavedView(view: SavedViewForCli): Record<string, unknown> {
|
|
|
968
974
|
};
|
|
969
975
|
}
|
|
970
976
|
|
|
977
|
+
const VIEW_TYPES = [
|
|
978
|
+
"table",
|
|
979
|
+
"kanban",
|
|
980
|
+
"calendar",
|
|
981
|
+
"timeline",
|
|
982
|
+
"gallery",
|
|
983
|
+
"list",
|
|
984
|
+
] as const;
|
|
985
|
+
|
|
986
|
+
type SortRuleForCli = { field: string; direction: "asc" | "desc" };
|
|
987
|
+
|
|
988
|
+
/**
|
|
989
|
+
* Parse `--sort` for views. Accepts either the canonical JSON array
|
|
990
|
+
* (`[{"field":"Score","direction":"desc"}]`) or the compact syntax the
|
|
991
|
+
* rest of the CRM CLI uses (`-Score,Name` — leading `-` means desc).
|
|
992
|
+
*/
|
|
993
|
+
function parseViewSortFlag(
|
|
994
|
+
raw: string | undefined,
|
|
995
|
+
): SortRuleForCli[] | undefined {
|
|
996
|
+
if (raw === undefined) return undefined;
|
|
997
|
+
const trimmed = raw.trim();
|
|
998
|
+
if (!trimmed) return undefined;
|
|
999
|
+
if (trimmed.startsWith("[")) {
|
|
1000
|
+
const parsed = parseJson(trimmed);
|
|
1001
|
+
if (!Array.isArray(parsed)) {
|
|
1002
|
+
throw new CrmCliError("--sort must be a JSON array of sort rules");
|
|
1003
|
+
}
|
|
1004
|
+
return parsed.map((rule, index) => {
|
|
1005
|
+
if (
|
|
1006
|
+
!isJsonRecord(rule) ||
|
|
1007
|
+
typeof rule.field !== "string" ||
|
|
1008
|
+
!rule.field ||
|
|
1009
|
+
(rule.direction !== "asc" && rule.direction !== "desc")
|
|
1010
|
+
) {
|
|
1011
|
+
throw new CrmCliError(
|
|
1012
|
+
`--sort rule ${index + 1} must look like {"field":"Score","direction":"desc"}`,
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
return { field: rule.field, direction: rule.direction };
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
return trimmed
|
|
1019
|
+
.split(",")
|
|
1020
|
+
.map((token) => token.trim())
|
|
1021
|
+
.filter(Boolean)
|
|
1022
|
+
.map((token) =>
|
|
1023
|
+
token.startsWith("-")
|
|
1024
|
+
? { field: token.slice(1), direction: "desc" as const }
|
|
1025
|
+
: { field: token, direction: "asc" as const },
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
/**
|
|
1030
|
+
* Turn the `--where '{"Status":"Hot"}'` equality-map sugar into a real
|
|
1031
|
+
* FilterGroup. The operator per rule is chosen from the object's field
|
|
1032
|
+
* schema (enum → is / is_any_of, boolean → is_true / is_false, number
|
|
1033
|
+
* → eq, date → on, relation → has_any, everything else → equals) so
|
|
1034
|
+
* the saved view round-trips through the savedViewValidator.
|
|
1035
|
+
*/
|
|
1036
|
+
async function buildFiltersFromWhere(
|
|
1037
|
+
ctx: CrmCliContext,
|
|
1038
|
+
objectName: string,
|
|
1039
|
+
where: JsonRecord,
|
|
1040
|
+
): Promise<JsonRecord> {
|
|
1041
|
+
const fields = (await callQuery(ctx, api.fields.list, { objectName })) as
|
|
1042
|
+
| Array<{ name?: unknown; type?: unknown }>
|
|
1043
|
+
| null
|
|
1044
|
+
| undefined;
|
|
1045
|
+
const knownFields = Array.isArray(fields)
|
|
1046
|
+
? fields.filter(
|
|
1047
|
+
(field): field is { name: string; type?: string } =>
|
|
1048
|
+
typeof field?.name === "string",
|
|
1049
|
+
)
|
|
1050
|
+
: [];
|
|
1051
|
+
const rules = Object.entries(where).map(([rawName, value], index) => {
|
|
1052
|
+
const field =
|
|
1053
|
+
knownFields.find((candidate) => candidate.name === rawName) ??
|
|
1054
|
+
knownFields.find(
|
|
1055
|
+
(candidate) => candidate.name.toLowerCase() === rawName.toLowerCase(),
|
|
1056
|
+
);
|
|
1057
|
+
if (!field && knownFields.length > 0) {
|
|
1058
|
+
const suggestion = suggestKnownField(
|
|
1059
|
+
rawName,
|
|
1060
|
+
knownFields.map((candidate) => candidate.name),
|
|
1061
|
+
);
|
|
1062
|
+
throw new CrmCliError(
|
|
1063
|
+
`Unknown field "${rawName}" on ${objectName}` +
|
|
1064
|
+
`${suggestion ? ` (did you mean "${suggestion}"?)` : ""}. ` +
|
|
1065
|
+
`Valid fields: ${knownFields.map((candidate) => candidate.name).join(", ")}`,
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
const fieldName = field?.name ?? rawName;
|
|
1069
|
+
const type = field?.type;
|
|
1070
|
+
const id = `w${index + 1}`;
|
|
1071
|
+
if (type === "boolean") {
|
|
1072
|
+
const isFalsy =
|
|
1073
|
+
!value ||
|
|
1074
|
+
value === "false" ||
|
|
1075
|
+
value === "0" ||
|
|
1076
|
+
value === "no" ||
|
|
1077
|
+
value === "off";
|
|
1078
|
+
return {
|
|
1079
|
+
id,
|
|
1080
|
+
field: fieldName,
|
|
1081
|
+
operator: isFalsy ? "is_false" : "is_true",
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
if (type === "enum") {
|
|
1085
|
+
return Array.isArray(value)
|
|
1086
|
+
? {
|
|
1087
|
+
id,
|
|
1088
|
+
field: fieldName,
|
|
1089
|
+
operator: "is_any_of",
|
|
1090
|
+
value: value.map(String),
|
|
1091
|
+
}
|
|
1092
|
+
: { id, field: fieldName, operator: "is", value: String(value) };
|
|
1093
|
+
}
|
|
1094
|
+
if (type === "number") {
|
|
1095
|
+
return { id, field: fieldName, operator: "eq", value: Number(value) };
|
|
1096
|
+
}
|
|
1097
|
+
if (type === "date") {
|
|
1098
|
+
return { id, field: fieldName, operator: "on", value: String(value) };
|
|
1099
|
+
}
|
|
1100
|
+
if (type === "relation") {
|
|
1101
|
+
return {
|
|
1102
|
+
id,
|
|
1103
|
+
field: fieldName,
|
|
1104
|
+
operator: "has_any",
|
|
1105
|
+
value: Array.isArray(value) ? value.map(String) : [String(value)],
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
return { id, field: fieldName, operator: "equals", value: String(value) };
|
|
1109
|
+
});
|
|
1110
|
+
return { id: "where", conjunction: "and", rules };
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
/**
|
|
1114
|
+
* Shared flag parsing for `crm views create` / `crm views update`.
|
|
1115
|
+
* Returns only the keys the caller actually passed so `update` can
|
|
1116
|
+
* merge over the existing view without clobbering untouched keys.
|
|
1117
|
+
*/
|
|
1118
|
+
async function collectViewFlags(
|
|
1119
|
+
ctx: CrmCliContext,
|
|
1120
|
+
objectName: string,
|
|
1121
|
+
command: string,
|
|
1122
|
+
): Promise<JsonRecord> {
|
|
1123
|
+
const filtersRaw = getFlag(ctx.args, "--filters");
|
|
1124
|
+
const whereRaw = getFlag(ctx.args, "--where");
|
|
1125
|
+
const sortRaw = getFlag(ctx.args, "--sort");
|
|
1126
|
+
const columnsRaw = getFlag(ctx.args, "--columns");
|
|
1127
|
+
const typeRaw = getFlag(ctx.args, "--type");
|
|
1128
|
+
const settingsRaw = getFlag(ctx.args, "--settings");
|
|
1129
|
+
const pinnedRaw = getFlag(ctx.args, "--pinned-entry-ids");
|
|
1130
|
+
|
|
1131
|
+
if (filtersRaw !== undefined && whereRaw !== undefined) {
|
|
1132
|
+
throw new CrmCliError(
|
|
1133
|
+
`${command} accepts either --filters (full FilterGroup JSON) or --where (equality map), not both`,
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
const patch: JsonRecord = {};
|
|
1138
|
+
if (filtersRaw !== undefined) {
|
|
1139
|
+
const filters = parseJson(filtersRaw);
|
|
1140
|
+
if (
|
|
1141
|
+
!isJsonRecord(filters) ||
|
|
1142
|
+
typeof filters.conjunction !== "string" ||
|
|
1143
|
+
!Array.isArray(filters.rules)
|
|
1144
|
+
) {
|
|
1145
|
+
throw new CrmCliError(
|
|
1146
|
+
'--filters must be a FilterGroup JSON object: {"id":"g1","conjunction":"and","rules":[{"id":"r1","field":"Status","operator":"is","value":"Hot"}]}',
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
if (typeof filters.id !== "string" || !filters.id) filters.id = "g1";
|
|
1150
|
+
patch.filters = filters;
|
|
1151
|
+
}
|
|
1152
|
+
if (whereRaw !== undefined) {
|
|
1153
|
+
const where = requireNonEmptyJsonRecord(parseJson(whereRaw), "--where");
|
|
1154
|
+
patch.filters = await buildFiltersFromWhere(ctx, objectName, where);
|
|
1155
|
+
}
|
|
1156
|
+
const sort = parseViewSortFlag(sortRaw);
|
|
1157
|
+
if (sort !== undefined) patch.sort = sort;
|
|
1158
|
+
if (columnsRaw !== undefined) {
|
|
1159
|
+
patch.columns = columnsRaw
|
|
1160
|
+
.split(",")
|
|
1161
|
+
.map((column) => column.trim())
|
|
1162
|
+
.filter(Boolean);
|
|
1163
|
+
}
|
|
1164
|
+
if (typeRaw !== undefined) {
|
|
1165
|
+
if (!(VIEW_TYPES as readonly string[]).includes(typeRaw)) {
|
|
1166
|
+
throw new CrmCliError(
|
|
1167
|
+
`--type must be one of: ${VIEW_TYPES.join(", ")} (got "${typeRaw}")`,
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1170
|
+
patch.view_type = typeRaw;
|
|
1171
|
+
}
|
|
1172
|
+
if (settingsRaw !== undefined) {
|
|
1173
|
+
patch.settings = requireNonEmptyJsonRecord(
|
|
1174
|
+
parseJson(settingsRaw),
|
|
1175
|
+
"--settings",
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
if (pinnedRaw !== undefined) {
|
|
1179
|
+
patch.pinnedEntryIds = pinnedRaw
|
|
1180
|
+
.split(",")
|
|
1181
|
+
.map((id) => id.trim())
|
|
1182
|
+
.filter(Boolean);
|
|
1183
|
+
}
|
|
1184
|
+
return patch;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
971
1187
|
async function runViewsCommand(ctx: CrmCliContext): Promise<void> {
|
|
972
1188
|
const verb = ctx.args.shift();
|
|
973
1189
|
switch (verb) {
|
|
@@ -1071,6 +1287,82 @@ async function runViewsCommand(ctx: CrmCliContext): Promise<void> {
|
|
|
1071
1287
|
});
|
|
1072
1288
|
return;
|
|
1073
1289
|
}
|
|
1290
|
+
case "create": {
|
|
1291
|
+
const objectName = shift(ctx.args, "object name");
|
|
1292
|
+
// View name is positional, but `--name` is accepted too because
|
|
1293
|
+
// that's the flag agents reach for first.
|
|
1294
|
+
const nameFlag = getFlag(ctx.args, "--name");
|
|
1295
|
+
const viewName =
|
|
1296
|
+
nameFlag ??
|
|
1297
|
+
(ctx.args[0] && !ctx.args[0].startsWith("--")
|
|
1298
|
+
? shift(ctx.args, "view name")
|
|
1299
|
+
: undefined);
|
|
1300
|
+
if (!viewName?.trim()) {
|
|
1301
|
+
throw new CrmCliError(
|
|
1302
|
+
'crm views create requires a view name: dench crm views create <object> "<view name>" [--where …]',
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
1305
|
+
const replace = hasFlag(ctx.args, "--replace");
|
|
1306
|
+
const setActive = hasFlag(ctx.args, "--set-active");
|
|
1307
|
+
const patch = await collectViewFlags(ctx, objectName, "crm views create");
|
|
1308
|
+
assertNoUnknownFlags(ctx.args, "crm views create");
|
|
1309
|
+
out(
|
|
1310
|
+
ctx,
|
|
1311
|
+
await callMutation(ctx, api.objects.saveView, {
|
|
1312
|
+
objectName,
|
|
1313
|
+
view: { name: viewName.trim(), ...patch },
|
|
1314
|
+
replaceExisting: replace,
|
|
1315
|
+
setActive,
|
|
1316
|
+
}),
|
|
1317
|
+
);
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
case "update": {
|
|
1321
|
+
const objectName = shift(ctx.args, "object name");
|
|
1322
|
+
const viewName = shift(ctx.args, "view name");
|
|
1323
|
+
const setActive = hasFlag(ctx.args, "--set-active");
|
|
1324
|
+
const patch = await collectViewFlags(ctx, objectName, "crm views update");
|
|
1325
|
+
assertNoUnknownFlags(ctx.args, "crm views update");
|
|
1326
|
+
if (Object.keys(patch).length === 0 && !setActive) {
|
|
1327
|
+
throw new CrmCliError(
|
|
1328
|
+
"crm views update needs at least one change: --filters/--where, --sort, --columns, --type, --settings, --pinned-entry-ids, or --set-active",
|
|
1329
|
+
);
|
|
1330
|
+
}
|
|
1331
|
+
// Merge over the existing view so untouched keys survive. The
|
|
1332
|
+
// existing (exact-cased) name is reused, which also makes a
|
|
1333
|
+
// case-insensitive <view name> argument update the right view.
|
|
1334
|
+
const object = await callQuery(ctx, api.objects.get, {
|
|
1335
|
+
name: objectName,
|
|
1336
|
+
});
|
|
1337
|
+
const existing = resolveSavedView(
|
|
1338
|
+
objectName,
|
|
1339
|
+
savedViewsFromObject(objectName, object),
|
|
1340
|
+
viewName,
|
|
1341
|
+
);
|
|
1342
|
+
out(
|
|
1343
|
+
ctx,
|
|
1344
|
+
await callMutation(ctx, api.objects.saveView, {
|
|
1345
|
+
objectName,
|
|
1346
|
+
view: { ...existing, ...patch, name: readViewName(existing) },
|
|
1347
|
+
replaceExisting: true,
|
|
1348
|
+
setActive,
|
|
1349
|
+
}),
|
|
1350
|
+
);
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
case "delete": {
|
|
1354
|
+
const objectName = shift(ctx.args, "object name");
|
|
1355
|
+
const viewName = shift(ctx.args, "view name");
|
|
1356
|
+
assertNoUnknownFlags(ctx.args, "crm views delete");
|
|
1357
|
+
out(
|
|
1358
|
+
ctx,
|
|
1359
|
+
await callMutation(ctx, api.objects.deleteView, {
|
|
1360
|
+
objectName,
|
|
1361
|
+
viewName,
|
|
1362
|
+
}),
|
|
1363
|
+
);
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1074
1366
|
default:
|
|
1075
1367
|
throw new CrmCliError(`Unknown crm views verb: ${verb ?? "<none>"}`);
|
|
1076
1368
|
}
|
|
@@ -3000,6 +3292,20 @@ Views (saved CRM subsets):
|
|
|
3000
3292
|
dench crm views list <object> [--json]
|
|
3001
3293
|
dench crm views get <object> <view-name> [--json]
|
|
3002
3294
|
dench crm views entries <object> <view-name> [--limit N] [--page N] [--with-meta] [--json]
|
|
3295
|
+
dench crm views create <object> <view-name> [--where '{"Status":"Hot"}']
|
|
3296
|
+
[--filters '<FilterGroup JSON>'] [--sort '-Score' | --sort '[{"field":"Score","direction":"desc"}]']
|
|
3297
|
+
[--columns Name,Email] [--type table|kanban|calendar|timeline|gallery|list]
|
|
3298
|
+
[--settings '{"kanbanField":"Status"}'] [--pinned-entry-ids id1,id2]
|
|
3299
|
+
[--replace] [--set-active] [--json]
|
|
3300
|
+
--where is equality-map sugar (operators picked from the field
|
|
3301
|
+
schema); --filters takes a full FilterGroup for AND/OR logic.
|
|
3302
|
+
Fails on a name collision unless --replace is passed.
|
|
3303
|
+
dench crm views update <object> <view-name> [same flags as create] [--set-active]
|
|
3304
|
+
Merges the provided flags over the existing view definition;
|
|
3305
|
+
untouched keys survive.
|
|
3306
|
+
dench crm views delete <object> <view-name>
|
|
3307
|
+
Idempotent; also clears the active-view pointer if it referenced
|
|
3308
|
+
the deleted view.
|
|
3003
3309
|
|
|
3004
3310
|
Fields (columns):
|
|
3005
3311
|
dench crm fields list <object> [--json]
|
package/cron.ts
CHANGED
|
@@ -18,11 +18,9 @@
|
|
|
18
18
|
* both formats; we just pass whichever bearer the parent runtime
|
|
19
19
|
* resolved through `sessionToken`.
|
|
20
20
|
*
|
|
21
|
-
* Convex calls go direct via `ConvexHttpClient` (same pattern as
|
|
22
|
-
* `cli/crm.ts`)
|
|
23
|
-
*
|
|
24
|
-
* needs to start a Vercel Workflow which can only be done from a
|
|
25
|
-
* Next.js route.
|
|
21
|
+
* Convex calls generally go direct via `ConvexHttpClient` (same pattern as
|
|
22
|
+
* `cli/crm.ts`). Run-now and delete go through REST: run-now starts a Vercel
|
|
23
|
+
* Workflow, while delete coordinates upstream app-event cleanup.
|
|
26
24
|
*
|
|
27
25
|
* Subcommands:
|
|
28
26
|
* list List all cron jobs in the org.
|
|
@@ -166,6 +164,28 @@ async function getJson(
|
|
|
166
164
|
return json;
|
|
167
165
|
}
|
|
168
166
|
|
|
167
|
+
async function deleteJson(
|
|
168
|
+
url: string,
|
|
169
|
+
headers: Record<string, string>,
|
|
170
|
+
): Promise<unknown> {
|
|
171
|
+
const response = await fetch(url, { method: "DELETE", headers });
|
|
172
|
+
const text = await response.text();
|
|
173
|
+
let json: unknown;
|
|
174
|
+
try {
|
|
175
|
+
json = text ? JSON.parse(text) : null;
|
|
176
|
+
} catch {
|
|
177
|
+
json = { raw: text };
|
|
178
|
+
}
|
|
179
|
+
if (!response.ok) {
|
|
180
|
+
const detail =
|
|
181
|
+
json && typeof json === "object" && "error" in json
|
|
182
|
+
? String((json as Record<string, unknown>).error)
|
|
183
|
+
: JSON.stringify(json);
|
|
184
|
+
throw new CronCliError(`${url} failed (${response.status}): ${detail}`);
|
|
185
|
+
}
|
|
186
|
+
return json;
|
|
187
|
+
}
|
|
188
|
+
|
|
169
189
|
async function callQuery(
|
|
170
190
|
ctx: CronCliContext,
|
|
171
191
|
fn: Parameters<ConvexHttpClient["query"]>[0],
|
|
@@ -339,9 +359,6 @@ const createCronTriggerRef = makeFunctionReference<"mutation">(
|
|
|
339
359
|
const updateCronTriggerRef = makeFunctionReference<"mutation">(
|
|
340
360
|
"functions/triggers:updateCronTrigger",
|
|
341
361
|
);
|
|
342
|
-
const deleteCronTriggerRef = makeFunctionReference<"mutation">(
|
|
343
|
-
"functions/triggers:deleteCronTrigger",
|
|
344
|
-
);
|
|
345
362
|
const setCronEnabledRef = makeFunctionReference<"mutation">(
|
|
346
363
|
"functions/triggers:setCronEnabled",
|
|
347
364
|
);
|
|
@@ -709,7 +726,10 @@ async function runUpdate(ctx: CronCliContext): Promise<void> {
|
|
|
709
726
|
|
|
710
727
|
async function runDelete(ctx: CronCliContext): Promise<void> {
|
|
711
728
|
const jobId = shift(ctx.args, "job id");
|
|
712
|
-
const result = await
|
|
729
|
+
const result = await deleteJson(
|
|
730
|
+
`${getApiBase()}/api/cron/jobs/${encodeURIComponent(jobId)}`,
|
|
731
|
+
apiKeyHeaders(ctx.sessionToken ?? ""),
|
|
732
|
+
);
|
|
713
733
|
out(ctx, result);
|
|
714
734
|
}
|
|
715
735
|
|
package/email.ts
CHANGED
|
@@ -196,6 +196,9 @@ Usage:
|
|
|
196
196
|
dench email identity status --identity-id <identityId> [--json]
|
|
197
197
|
dench email identity disable --identity <identityId|email|domain> [--json]
|
|
198
198
|
dench email identity restore --identity <identityId|email|domain> [--json]
|
|
199
|
+
dench email identity delete --identity <identityId|email|domain> [--json]
|
|
200
|
+
dench email identity forward --identity <identityId|email> --to inbox@you.com [--json] # forward this sender's sequence replies
|
|
201
|
+
dench email identity forward --identity <identityId|email> --off [--json] # turn forwarding off
|
|
199
202
|
dench email send --from founder@example.com --to "Ada <ada@x.com>,bob@y.com" --subject "Hi" (--html-file body.html | --text "...") [--cc ...] [--bcc ...] [--from-name "Ada"] [--reply-to support@x.com] [--reply-message-id <messageId|rfc-id>] [--json]
|
|
200
203
|
dench email message status --message <messageId> [--json]
|
|
201
204
|
dench email message list [--limit 25] [--json]
|
|
@@ -499,19 +502,22 @@ async function runIdentity(ctx: EmailCliContext) {
|
|
|
499
502
|
`${identity.identityId} ${identity.fromEmail} type=${identity.type}` +
|
|
500
503
|
`${identity.domain ? ` domain=${identity.domain}` : ""}` +
|
|
501
504
|
` delivery=${identity.deliveryVerificationStatus} workspace=${identity.denchVerificationStatus}` +
|
|
505
|
+
`${identity.forwardRepliesTo ? ` forward=${identity.forwardRepliesTo}` : ""}` +
|
|
502
506
|
`${identity.disabled ? " DISABLED" : ""}`,
|
|
503
507
|
)
|
|
504
508
|
.join("\n");
|
|
505
509
|
out(ctx, { ok: true, identities }, text);
|
|
506
510
|
return;
|
|
507
511
|
}
|
|
508
|
-
if (sub === "disable" || sub === "restore") {
|
|
512
|
+
if (sub === "disable" || sub === "restore" || sub === "delete") {
|
|
509
513
|
const identity = requiredFlag(ctx.args, "--identity");
|
|
510
514
|
const payload = (await callMutation(
|
|
511
515
|
ctx,
|
|
512
516
|
sub === "disable"
|
|
513
517
|
? "functions/emailCampaigns:disableIdentity"
|
|
514
|
-
: "
|
|
518
|
+
: sub === "restore"
|
|
519
|
+
? "functions/emailCampaigns:restoreIdentity"
|
|
520
|
+
: "functions/emailCampaigns:deleteIdentity",
|
|
515
521
|
{ identity },
|
|
516
522
|
)) as JsonRecord;
|
|
517
523
|
const rows = (
|
|
@@ -519,18 +525,57 @@ async function runIdentity(ctx: EmailCliContext) {
|
|
|
519
525
|
? payload.disabled
|
|
520
526
|
: Array.isArray(payload.restored)
|
|
521
527
|
? payload.restored
|
|
522
|
-
:
|
|
528
|
+
: Array.isArray(payload.deleted)
|
|
529
|
+
? payload.deleted
|
|
530
|
+
: []
|
|
523
531
|
) as JsonRecord[];
|
|
524
|
-
const verb =
|
|
532
|
+
const verb =
|
|
533
|
+
sub === "disable"
|
|
534
|
+
? "Disabled"
|
|
535
|
+
: sub === "restore"
|
|
536
|
+
? "Restored"
|
|
537
|
+
: "Deleted";
|
|
538
|
+
const suffix =
|
|
539
|
+
sub === "delete" && rows.length > 0
|
|
540
|
+
? "\nDeleted senders must be re-verified before they can send again."
|
|
541
|
+
: "";
|
|
525
542
|
const text =
|
|
526
543
|
rows.length === 0
|
|
527
544
|
? `${verb} 0 identities.`
|
|
528
545
|
: `${verb} ${rows.length} sender(s):\n${rows
|
|
529
546
|
.map((row) => ` ${row.fromEmail} (${row.identityId})`)
|
|
530
|
-
.join("\n")}`;
|
|
547
|
+
.join("\n")}${suffix}`;
|
|
531
548
|
out(ctx, payload, text);
|
|
532
549
|
return;
|
|
533
550
|
}
|
|
551
|
+
if (sub === "forward") {
|
|
552
|
+
// Configure where a sender's managed sequence replies are forwarded.
|
|
553
|
+
// `--to <email>` turns it on (composed from the verified sender, Reply-To =
|
|
554
|
+
// the prospect); `--off` clears it. Replies are always still captured
|
|
555
|
+
// in-app regardless — forwarding is an extra copy to a chosen inbox.
|
|
556
|
+
const identity = requiredFlag(ctx.args, "--identity");
|
|
557
|
+
const off = hasFlag(ctx.args, "--off");
|
|
558
|
+
const to = off ? "" : (getFlag(ctx.args, "--to")?.trim() ?? "");
|
|
559
|
+
if (!off && !to) {
|
|
560
|
+
throw new Error(
|
|
561
|
+
"Provide --to <email> to forward this sender's replies, or --off to turn forwarding off.",
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
const payload = (await callMutation(
|
|
565
|
+
ctx,
|
|
566
|
+
"functions/emailCampaigns:setSenderReplyForward",
|
|
567
|
+
{ identity, forwardRepliesTo: to },
|
|
568
|
+
)) as JsonRecord;
|
|
569
|
+
const forwardTo =
|
|
570
|
+
typeof payload.forwardRepliesTo === "string"
|
|
571
|
+
? payload.forwardRepliesTo
|
|
572
|
+
: null;
|
|
573
|
+
const text = forwardTo
|
|
574
|
+
? `Replies to ${identity}'s sequences will be forwarded to ${forwardTo}.`
|
|
575
|
+
: `Reply forwarding turned off for ${identity}.`;
|
|
576
|
+
out(ctx, { ok: true, ...payload }, text);
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
534
579
|
if (sub === "dns") {
|
|
535
580
|
const identity = requiredFlag(ctx.args, "--identity");
|
|
536
581
|
if (!identity.includes("@") && !identity.includes(".")) {
|
|
@@ -627,7 +672,7 @@ async function runIdentity(ctx: EmailCliContext) {
|
|
|
627
672
|
return;
|
|
628
673
|
}
|
|
629
674
|
throw new EmailCliError(
|
|
630
|
-
"Usage: dench email identity verify|list|dns|status|disable|restore",
|
|
675
|
+
"Usage: dench email identity verify|list|dns|status|disable|restore|delete",
|
|
631
676
|
);
|
|
632
677
|
}
|
|
633
678
|
|
package/lib/api-schemas.ts
CHANGED
|
@@ -440,6 +440,51 @@ const chatSpawnResponse = z.object({
|
|
|
440
440
|
|
|
441
441
|
// -- crm ---------------------------------------------------------------------
|
|
442
442
|
|
|
443
|
+
const savedViewSortRule = z.object({
|
|
444
|
+
field: z.string().describe("Field name to sort by."),
|
|
445
|
+
direction: z.enum(["asc", "desc"]),
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* SavedView definition as stored in `crmObjects.savedViews` and
|
|
450
|
+
* validated by `convex/lib/savedView.ts`. `filters` is a FilterGroup:
|
|
451
|
+
* `{id, conjunction: "and"|"or", rules: [{id, field, operator,
|
|
452
|
+
* value?}]}` with one level of nested groups allowed.
|
|
453
|
+
*/
|
|
454
|
+
const savedViewDefinition = z
|
|
455
|
+
.object({
|
|
456
|
+
name: z
|
|
457
|
+
.string()
|
|
458
|
+
.min(1)
|
|
459
|
+
.max(100)
|
|
460
|
+
.describe("View name (upsert key; shows in the view dropdown)."),
|
|
461
|
+
view_type: viewKind.optional(),
|
|
462
|
+
filters: z
|
|
463
|
+
.record(z.string(), z.unknown())
|
|
464
|
+
.optional()
|
|
465
|
+
.describe(
|
|
466
|
+
'FilterGroup, e.g. {"id":"g1","conjunction":"and","rules":[{"id":"r1","field":"Status","operator":"is","value":"Hot"}]}.',
|
|
467
|
+
),
|
|
468
|
+
sort: z.array(savedViewSortRule).optional(),
|
|
469
|
+
columns: z
|
|
470
|
+
.array(z.string())
|
|
471
|
+
.optional()
|
|
472
|
+
.describe("Visible-column ordering. Omit to keep the default set."),
|
|
473
|
+
settings: z
|
|
474
|
+
.record(z.string(), z.unknown())
|
|
475
|
+
.optional()
|
|
476
|
+
.describe(
|
|
477
|
+
"View-type settings (kanbanField, calendarDateField, timelineStartField, …).",
|
|
478
|
+
),
|
|
479
|
+
pinnedEntryIds: z
|
|
480
|
+
.array(z.string())
|
|
481
|
+
.optional()
|
|
482
|
+
.describe(
|
|
483
|
+
"Pinned-list mode: show EXACTLY these entry ids (mutually exclusive with filters).",
|
|
484
|
+
),
|
|
485
|
+
})
|
|
486
|
+
.passthrough();
|
|
487
|
+
|
|
443
488
|
const crmObjectDoc = z
|
|
444
489
|
.object({
|
|
445
490
|
_id: z.string(),
|
|
@@ -1338,6 +1383,46 @@ export const requestSchemas: Record<string, ZodTypeAny> = {
|
|
|
1338
1383
|
}),
|
|
1339
1384
|
"crm.objects.delete": z.object({ name: z.string() }),
|
|
1340
1385
|
|
|
1386
|
+
// CRM saved views
|
|
1387
|
+
"crm.views.list": z.object({ objectName: z.string() }),
|
|
1388
|
+
"crm.views.get": z.object({
|
|
1389
|
+
objectName: z.string(),
|
|
1390
|
+
viewName: z.string(),
|
|
1391
|
+
}),
|
|
1392
|
+
"crm.views.entries": z.object({
|
|
1393
|
+
objectName: z.string(),
|
|
1394
|
+
viewName: z.string(),
|
|
1395
|
+
page: z.number().int().min(1).optional(),
|
|
1396
|
+
pageSize: z.number().int().min(1).optional(),
|
|
1397
|
+
}),
|
|
1398
|
+
"crm.views.create": z.object({
|
|
1399
|
+
objectName: z.string(),
|
|
1400
|
+
view: savedViewDefinition,
|
|
1401
|
+
replaceExisting: z
|
|
1402
|
+
.boolean()
|
|
1403
|
+
.optional()
|
|
1404
|
+
.describe("Overwrite an existing view with the same name."),
|
|
1405
|
+
setActive: z
|
|
1406
|
+
.boolean()
|
|
1407
|
+
.optional()
|
|
1408
|
+
.describe("Also switch the object's active view to this one."),
|
|
1409
|
+
}),
|
|
1410
|
+
"crm.views.update": z.object({
|
|
1411
|
+
objectName: z.string(),
|
|
1412
|
+
viewName: z.string().describe("Name of the view to replace (path)."),
|
|
1413
|
+
view: savedViewDefinition
|
|
1414
|
+
.partial()
|
|
1415
|
+
.optional()
|
|
1416
|
+
.describe(
|
|
1417
|
+
"Full replacement definition; the path viewName is used as the name.",
|
|
1418
|
+
),
|
|
1419
|
+
setActive: z.boolean().optional(),
|
|
1420
|
+
}),
|
|
1421
|
+
"crm.views.delete": z.object({
|
|
1422
|
+
objectName: z.string(),
|
|
1423
|
+
viewName: z.string(),
|
|
1424
|
+
}),
|
|
1425
|
+
|
|
1341
1426
|
// CRM fields
|
|
1342
1427
|
"crm.fields.list": z.object({ objectName: z.string() }),
|
|
1343
1428
|
"crm.fields.create": z.object({
|
|
@@ -1701,6 +1786,13 @@ export const requestSchemas: Record<string, ZodTypeAny> = {
|
|
|
1701
1786
|
"email.identity.restore": z.object({
|
|
1702
1787
|
identity: z.string().describe("Identity id, sender email, or domain."),
|
|
1703
1788
|
}),
|
|
1789
|
+
"email.identity.delete": z.object({
|
|
1790
|
+
identity: z
|
|
1791
|
+
.string()
|
|
1792
|
+
.describe(
|
|
1793
|
+
"Identity id, sender email, or domain (deletes every sender on the domain). Permanent: re-adding requires re-verification.",
|
|
1794
|
+
),
|
|
1795
|
+
}),
|
|
1704
1796
|
"email.send": z.object({
|
|
1705
1797
|
fromEmail: z.string().describe("Verified sender mailbox."),
|
|
1706
1798
|
fromName: z.string().optional().describe("Display name override."),
|
|
@@ -2078,6 +2170,34 @@ export const responseSchemas: Record<string, ZodTypeAny> = {
|
|
|
2078
2170
|
"crm.objects.rename": okResponse,
|
|
2079
2171
|
"crm.objects.delete": okResponse,
|
|
2080
2172
|
|
|
2173
|
+
// CRM saved views
|
|
2174
|
+
"crm.views.list": z.object({
|
|
2175
|
+
objectName: z.string(),
|
|
2176
|
+
activeView: z.string().nullable(),
|
|
2177
|
+
views: z.array(z.record(z.string(), z.unknown())),
|
|
2178
|
+
}),
|
|
2179
|
+
"crm.views.get": z.record(z.string(), z.unknown()),
|
|
2180
|
+
"crm.views.create": z.object({
|
|
2181
|
+
ok: z.boolean(),
|
|
2182
|
+
objectName: z.string(),
|
|
2183
|
+
viewName: z.string(),
|
|
2184
|
+
replaced: z.boolean(),
|
|
2185
|
+
activeView: z.string().nullable(),
|
|
2186
|
+
}),
|
|
2187
|
+
"crm.views.update": z.object({
|
|
2188
|
+
ok: z.boolean(),
|
|
2189
|
+
objectName: z.string(),
|
|
2190
|
+
viewName: z.string(),
|
|
2191
|
+
replaced: z.boolean(),
|
|
2192
|
+
activeView: z.string().nullable(),
|
|
2193
|
+
}),
|
|
2194
|
+
"crm.views.delete": z.object({
|
|
2195
|
+
ok: z.boolean(),
|
|
2196
|
+
objectName: z.string(),
|
|
2197
|
+
viewName: z.string(),
|
|
2198
|
+
removed: z.boolean(),
|
|
2199
|
+
}),
|
|
2200
|
+
|
|
2081
2201
|
// CRM fields
|
|
2082
2202
|
"crm.fields.list": z.array(crmFieldDoc),
|
|
2083
2203
|
"crm.fields.create": z.object({ id: z.string() }),
|
|
@@ -2411,6 +2531,10 @@ export const responseSchemas: Record<string, ZodTypeAny> = {
|
|
|
2411
2531
|
ok: z.boolean(),
|
|
2412
2532
|
restored: z.array(identitySummaryShape),
|
|
2413
2533
|
}),
|
|
2534
|
+
"email.identity.delete": z.object({
|
|
2535
|
+
ok: z.boolean(),
|
|
2536
|
+
deleted: z.array(identitySummaryShape),
|
|
2537
|
+
}),
|
|
2414
2538
|
"email.send": z.object({
|
|
2415
2539
|
ok: z.boolean(),
|
|
2416
2540
|
messageId: z
|
|
@@ -2583,6 +2707,27 @@ export type ConvexExtras = {
|
|
|
2583
2707
|
export const convexExtras: Record<string, ConvexExtras> = {
|
|
2584
2708
|
"cron.enable": { fixedArgs: { enabled: true } },
|
|
2585
2709
|
"cron.disable": { fixedArgs: { enabled: false } },
|
|
2710
|
+
// PATCH /crm/objects/{objectName}/views/{viewName} — the path's
|
|
2711
|
+
// viewName is the upsert key: fold it into `view.name` and force
|
|
2712
|
+
// replaceExisting so the mutation overwrites instead of erroring on
|
|
2713
|
+
// the (guaranteed) name collision.
|
|
2714
|
+
"crm.views.update": {
|
|
2715
|
+
transformArgs: (input) => {
|
|
2716
|
+
const { viewName, view, ...rest } = input as {
|
|
2717
|
+
viewName?: unknown;
|
|
2718
|
+
view?: unknown;
|
|
2719
|
+
} & Record<string, unknown>;
|
|
2720
|
+
const baseView =
|
|
2721
|
+
view && typeof view === "object" && !Array.isArray(view)
|
|
2722
|
+
? (view as Record<string, unknown>)
|
|
2723
|
+
: {};
|
|
2724
|
+
return {
|
|
2725
|
+
...rest,
|
|
2726
|
+
view: { ...baseView, name: String(viewName ?? baseView.name ?? "") },
|
|
2727
|
+
replaceExisting: true,
|
|
2728
|
+
};
|
|
2729
|
+
},
|
|
2730
|
+
},
|
|
2586
2731
|
"crm.entries.updateMany": {
|
|
2587
2732
|
transformArgs: (input) => {
|
|
2588
2733
|
const updates = Array.isArray(input.updates) ? input.updates : [];
|
|
@@ -2629,6 +2774,25 @@ export const requestExamples: Record<string, unknown> = {
|
|
|
2629
2774
|
enumColors: ["#94a3b8", "#3b82f6", "#22c55e", "#ef4444"],
|
|
2630
2775
|
indexed: true,
|
|
2631
2776
|
},
|
|
2777
|
+
"crm.views.create": {
|
|
2778
|
+
view: {
|
|
2779
|
+
name: "Hot leads",
|
|
2780
|
+
view_type: "table",
|
|
2781
|
+
filters: {
|
|
2782
|
+
id: "g1",
|
|
2783
|
+
conjunction: "and",
|
|
2784
|
+
rules: [{ id: "r1", field: "Status", operator: "is", value: "Hot" }],
|
|
2785
|
+
},
|
|
2786
|
+
sort: [{ field: "created_at", direction: "desc" }],
|
|
2787
|
+
},
|
|
2788
|
+
setActive: true,
|
|
2789
|
+
},
|
|
2790
|
+
"crm.views.update": {
|
|
2791
|
+
view: {
|
|
2792
|
+
sort: [{ field: "Score", direction: "desc" }],
|
|
2793
|
+
columns: ["Name", "Email", "Status", "Score"],
|
|
2794
|
+
},
|
|
2795
|
+
},
|
|
2632
2796
|
"crm.fields.enum.color": { color: "#22c55e" },
|
|
2633
2797
|
"crm.fields.enum.add": { value: "Nurturing", color: "#eab308", position: 2 },
|
|
2634
2798
|
"crm.entries.create": {
|
|
@@ -3048,6 +3212,42 @@ export const responseExamples: Record<string, unknown> = {
|
|
|
3048
3212
|
"crm.objects.update": { ok: true },
|
|
3049
3213
|
"crm.objects.rename": { ok: true },
|
|
3050
3214
|
"crm.objects.delete": { ok: true },
|
|
3215
|
+
"crm.views.list": {
|
|
3216
|
+
objectName: "lead",
|
|
3217
|
+
activeView: "Hot leads",
|
|
3218
|
+
views: [
|
|
3219
|
+
{
|
|
3220
|
+
name: "Hot leads",
|
|
3221
|
+
view_type: "table",
|
|
3222
|
+
filters: {
|
|
3223
|
+
id: "g1",
|
|
3224
|
+
conjunction: "and",
|
|
3225
|
+
rules: [{ id: "r1", field: "Status", operator: "is", value: "Hot" }],
|
|
3226
|
+
},
|
|
3227
|
+
sort: [{ field: "created_at", direction: "desc" }],
|
|
3228
|
+
},
|
|
3229
|
+
],
|
|
3230
|
+
},
|
|
3231
|
+
"crm.views.create": {
|
|
3232
|
+
ok: true,
|
|
3233
|
+
objectName: "lead",
|
|
3234
|
+
viewName: "Hot leads",
|
|
3235
|
+
replaced: false,
|
|
3236
|
+
activeView: "Hot leads",
|
|
3237
|
+
},
|
|
3238
|
+
"crm.views.update": {
|
|
3239
|
+
ok: true,
|
|
3240
|
+
objectName: "lead",
|
|
3241
|
+
viewName: "Hot leads",
|
|
3242
|
+
replaced: true,
|
|
3243
|
+
activeView: null,
|
|
3244
|
+
},
|
|
3245
|
+
"crm.views.delete": {
|
|
3246
|
+
ok: true,
|
|
3247
|
+
objectName: "lead",
|
|
3248
|
+
viewName: "Hot leads",
|
|
3249
|
+
removed: true,
|
|
3250
|
+
},
|
|
3051
3251
|
"crm.fields.list": [
|
|
3052
3252
|
{
|
|
3053
3253
|
_id: "fld_status01",
|
|
@@ -3431,6 +3631,16 @@ export const responseExamples: Record<string, unknown> = {
|
|
|
3431
3631
|
},
|
|
3432
3632
|
],
|
|
3433
3633
|
},
|
|
3634
|
+
"email.identity.delete": {
|
|
3635
|
+
ok: true,
|
|
3636
|
+
deleted: [
|
|
3637
|
+
{
|
|
3638
|
+
identityId: "esi_abc123",
|
|
3639
|
+
fromEmail: "founder@example.com",
|
|
3640
|
+
disabled: false,
|
|
3641
|
+
},
|
|
3642
|
+
],
|
|
3643
|
+
},
|
|
3434
3644
|
"email.send": {
|
|
3435
3645
|
ok: true,
|
|
3436
3646
|
messageId: "emsg_abc123",
|
package/lib/command-registry.ts
CHANGED
|
@@ -29,6 +29,7 @@ export type CustomOperation = {
|
|
|
29
29
|
| "commandsList"
|
|
30
30
|
| "chatSpawn"
|
|
31
31
|
| "chatFollow"
|
|
32
|
+
| "cronDelete"
|
|
32
33
|
| "cronRunNow"
|
|
33
34
|
| "billingTopup"
|
|
34
35
|
| "upgrade"
|
|
@@ -583,6 +584,75 @@ const rawApiOperations = [
|
|
|
583
584
|
backend: convex("mutation", "functions/crm/objects:remove"),
|
|
584
585
|
}),
|
|
585
586
|
|
|
587
|
+
op({
|
|
588
|
+
id: "crm.views.list",
|
|
589
|
+
group: "crm",
|
|
590
|
+
summary: "List the saved views on a CRM object (plus the active view).",
|
|
591
|
+
cli: "dench crm views list",
|
|
592
|
+
method: "GET",
|
|
593
|
+
path: "/crm/objects/{objectName}/views",
|
|
594
|
+
requestSchema: noBody,
|
|
595
|
+
responseSchema: anyResponse,
|
|
596
|
+
backend: convex("query", "functions/crm/objects:listViews"),
|
|
597
|
+
}),
|
|
598
|
+
op({
|
|
599
|
+
id: "crm.views.get",
|
|
600
|
+
group: "crm",
|
|
601
|
+
summary: "Get one saved view's definition by name.",
|
|
602
|
+
cli: "dench crm views get",
|
|
603
|
+
method: "GET",
|
|
604
|
+
path: "/crm/objects/{objectName}/views/{viewName}",
|
|
605
|
+
requestSchema: noBody,
|
|
606
|
+
responseSchema: anyResponse,
|
|
607
|
+
backend: convex("query", "functions/crm/objects:getView"),
|
|
608
|
+
}),
|
|
609
|
+
op({
|
|
610
|
+
id: "crm.views.entries",
|
|
611
|
+
group: "crm",
|
|
612
|
+
summary:
|
|
613
|
+
"Materialize a saved view's rows (applies its filters/sort/pins), paginated.",
|
|
614
|
+
cli: "dench crm views entries",
|
|
615
|
+
method: "GET",
|
|
616
|
+
path: "/crm/objects/{objectName}/views/{viewName}/entries",
|
|
617
|
+
requestSchema: noBody,
|
|
618
|
+
responseSchema: anyResponse,
|
|
619
|
+
backend: convex("query", "functions/crm/objects:materializeView"),
|
|
620
|
+
}),
|
|
621
|
+
op({
|
|
622
|
+
id: "crm.views.create",
|
|
623
|
+
group: "crm",
|
|
624
|
+
summary:
|
|
625
|
+
"Create a saved view (named filters/sort/columns combo) on a CRM object.",
|
|
626
|
+
cli: "dench crm views create",
|
|
627
|
+
method: "POST",
|
|
628
|
+
path: "/crm/objects/{objectName}/views",
|
|
629
|
+
requestSchema: anyObject,
|
|
630
|
+
responseSchema: anyResponse,
|
|
631
|
+
backend: convex("mutation", "functions/crm/objects:saveView"),
|
|
632
|
+
}),
|
|
633
|
+
op({
|
|
634
|
+
id: "crm.views.update",
|
|
635
|
+
group: "crm",
|
|
636
|
+
summary: "Replace a saved view's definition (upsert by name).",
|
|
637
|
+
cli: "dench crm views update",
|
|
638
|
+
method: "PATCH",
|
|
639
|
+
path: "/crm/objects/{objectName}/views/{viewName}",
|
|
640
|
+
requestSchema: anyObject,
|
|
641
|
+
responseSchema: anyResponse,
|
|
642
|
+
backend: convex("mutation", "functions/crm/objects:saveView"),
|
|
643
|
+
}),
|
|
644
|
+
op({
|
|
645
|
+
id: "crm.views.delete",
|
|
646
|
+
group: "crm",
|
|
647
|
+
summary: "Delete a saved view by name (idempotent).",
|
|
648
|
+
cli: "dench crm views delete",
|
|
649
|
+
method: "DELETE",
|
|
650
|
+
path: "/crm/objects/{objectName}/views/{viewName}",
|
|
651
|
+
requestSchema: anyObject,
|
|
652
|
+
responseSchema: anyResponse,
|
|
653
|
+
backend: convex("mutation", "functions/crm/objects:deleteView"),
|
|
654
|
+
}),
|
|
655
|
+
|
|
586
656
|
op({
|
|
587
657
|
id: "crm.fields.list",
|
|
588
658
|
group: "crm",
|
|
@@ -1366,6 +1436,18 @@ const rawApiOperations = [
|
|
|
1366
1436
|
responseSchema: anyResponse,
|
|
1367
1437
|
backend: convex("mutation", "functions/emailCampaigns:restoreIdentity"),
|
|
1368
1438
|
}),
|
|
1439
|
+
op({
|
|
1440
|
+
id: "email.identity.delete",
|
|
1441
|
+
group: "email",
|
|
1442
|
+
summary:
|
|
1443
|
+
"Permanently delete a sender (or every sender on a domain) from the workspace; re-adding it requires verification from scratch.",
|
|
1444
|
+
cli: "dench email identity delete",
|
|
1445
|
+
method: "DELETE",
|
|
1446
|
+
path: "/email/sending-identities/{identity}",
|
|
1447
|
+
requestSchema: anyObject,
|
|
1448
|
+
responseSchema: anyResponse,
|
|
1449
|
+
backend: convex("mutation", "functions/emailCampaigns:deleteIdentity"),
|
|
1450
|
+
}),
|
|
1369
1451
|
op({
|
|
1370
1452
|
id: "email.send",
|
|
1371
1453
|
group: "email",
|
|
@@ -1814,7 +1896,7 @@ const rawApiOperations = [
|
|
|
1814
1896
|
path: "/routines/{jobId}",
|
|
1815
1897
|
requestSchema: anyObject,
|
|
1816
1898
|
responseSchema: anyResponse,
|
|
1817
|
-
backend:
|
|
1899
|
+
backend: custom("cronDelete", "bearer"),
|
|
1818
1900
|
}),
|
|
1819
1901
|
op({
|
|
1820
1902
|
id: "cron.enable",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dench.com/cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.0",
|
|
4
4
|
"description": "Dench agent workspace CLI. v2 unifies auth behind `dench signin`; the legacy `dench login` / `dench onboard` / `dench setup` / `dench what-can-i-do` / `dench register` commands now error with a redirect.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|