@dench.com/cli 2.5.1 → 2.6.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 +308 -2
- package/email.ts +20 -6
- package/lib/api-schemas.ts +210 -0
- package/lib/command-registry.ts +81 -0
- 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]
|
|
@@ -3024,7 +3330,7 @@ Fields (columns):
|
|
|
3024
3330
|
Appends <value> to the field's enumValues. --color is a hex like
|
|
3025
3331
|
"#22c55e"; --position is a 0-based insertion index (defaults to append).
|
|
3026
3332
|
Idempotent (case-insensitive): re-adding "won" when "Won" exists does
|
|
3027
|
-
NOT create a twin option — the response's
|
|
3333
|
+
NOT create a twin option — the response's "value" echoes the canonical
|
|
3028
3334
|
casing; a given --color still updates the existing option's color.
|
|
3029
3335
|
For a board-backed Status/Stage field this also creates the matching
|
|
3030
3336
|
kanban column (positioned to match), so you do NOT need a separate
|
package/email.ts
CHANGED
|
@@ -196,6 +196,7 @@ 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]
|
|
199
200
|
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
201
|
dench email message status --message <messageId> [--json]
|
|
201
202
|
dench email message list [--limit 25] [--json]
|
|
@@ -505,13 +506,15 @@ async function runIdentity(ctx: EmailCliContext) {
|
|
|
505
506
|
out(ctx, { ok: true, identities }, text);
|
|
506
507
|
return;
|
|
507
508
|
}
|
|
508
|
-
if (sub === "disable" || sub === "restore") {
|
|
509
|
+
if (sub === "disable" || sub === "restore" || sub === "delete") {
|
|
509
510
|
const identity = requiredFlag(ctx.args, "--identity");
|
|
510
511
|
const payload = (await callMutation(
|
|
511
512
|
ctx,
|
|
512
513
|
sub === "disable"
|
|
513
514
|
? "functions/emailCampaigns:disableIdentity"
|
|
514
|
-
: "
|
|
515
|
+
: sub === "restore"
|
|
516
|
+
? "functions/emailCampaigns:restoreIdentity"
|
|
517
|
+
: "functions/emailCampaigns:deleteIdentity",
|
|
515
518
|
{ identity },
|
|
516
519
|
)) as JsonRecord;
|
|
517
520
|
const rows = (
|
|
@@ -519,15 +522,26 @@ async function runIdentity(ctx: EmailCliContext) {
|
|
|
519
522
|
? payload.disabled
|
|
520
523
|
: Array.isArray(payload.restored)
|
|
521
524
|
? payload.restored
|
|
522
|
-
:
|
|
525
|
+
: Array.isArray(payload.deleted)
|
|
526
|
+
? payload.deleted
|
|
527
|
+
: []
|
|
523
528
|
) as JsonRecord[];
|
|
524
|
-
const verb =
|
|
529
|
+
const verb =
|
|
530
|
+
sub === "disable"
|
|
531
|
+
? "Disabled"
|
|
532
|
+
: sub === "restore"
|
|
533
|
+
? "Restored"
|
|
534
|
+
: "Deleted";
|
|
535
|
+
const suffix =
|
|
536
|
+
sub === "delete" && rows.length > 0
|
|
537
|
+
? "\nDeleted senders must be re-verified before they can send again."
|
|
538
|
+
: "";
|
|
525
539
|
const text =
|
|
526
540
|
rows.length === 0
|
|
527
541
|
? `${verb} 0 identities.`
|
|
528
542
|
: `${verb} ${rows.length} sender(s):\n${rows
|
|
529
543
|
.map((row) => ` ${row.fromEmail} (${row.identityId})`)
|
|
530
|
-
.join("\n")}`;
|
|
544
|
+
.join("\n")}${suffix}`;
|
|
531
545
|
out(ctx, payload, text);
|
|
532
546
|
return;
|
|
533
547
|
}
|
|
@@ -627,7 +641,7 @@ async function runIdentity(ctx: EmailCliContext) {
|
|
|
627
641
|
return;
|
|
628
642
|
}
|
|
629
643
|
throw new EmailCliError(
|
|
630
|
-
"Usage: dench email identity verify|list|dns|status|disable|restore",
|
|
644
|
+
"Usage: dench email identity verify|list|dns|status|disable|restore|delete",
|
|
631
645
|
);
|
|
632
646
|
}
|
|
633
647
|
|
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
|
@@ -583,6 +583,75 @@ const rawApiOperations = [
|
|
|
583
583
|
backend: convex("mutation", "functions/crm/objects:remove"),
|
|
584
584
|
}),
|
|
585
585
|
|
|
586
|
+
op({
|
|
587
|
+
id: "crm.views.list",
|
|
588
|
+
group: "crm",
|
|
589
|
+
summary: "List the saved views on a CRM object (plus the active view).",
|
|
590
|
+
cli: "dench crm views list",
|
|
591
|
+
method: "GET",
|
|
592
|
+
path: "/crm/objects/{objectName}/views",
|
|
593
|
+
requestSchema: noBody,
|
|
594
|
+
responseSchema: anyResponse,
|
|
595
|
+
backend: convex("query", "functions/crm/objects:listViews"),
|
|
596
|
+
}),
|
|
597
|
+
op({
|
|
598
|
+
id: "crm.views.get",
|
|
599
|
+
group: "crm",
|
|
600
|
+
summary: "Get one saved view's definition by name.",
|
|
601
|
+
cli: "dench crm views get",
|
|
602
|
+
method: "GET",
|
|
603
|
+
path: "/crm/objects/{objectName}/views/{viewName}",
|
|
604
|
+
requestSchema: noBody,
|
|
605
|
+
responseSchema: anyResponse,
|
|
606
|
+
backend: convex("query", "functions/crm/objects:getView"),
|
|
607
|
+
}),
|
|
608
|
+
op({
|
|
609
|
+
id: "crm.views.entries",
|
|
610
|
+
group: "crm",
|
|
611
|
+
summary:
|
|
612
|
+
"Materialize a saved view's rows (applies its filters/sort/pins), paginated.",
|
|
613
|
+
cli: "dench crm views entries",
|
|
614
|
+
method: "GET",
|
|
615
|
+
path: "/crm/objects/{objectName}/views/{viewName}/entries",
|
|
616
|
+
requestSchema: noBody,
|
|
617
|
+
responseSchema: anyResponse,
|
|
618
|
+
backend: convex("query", "functions/crm/objects:materializeView"),
|
|
619
|
+
}),
|
|
620
|
+
op({
|
|
621
|
+
id: "crm.views.create",
|
|
622
|
+
group: "crm",
|
|
623
|
+
summary:
|
|
624
|
+
"Create a saved view (named filters/sort/columns combo) on a CRM object.",
|
|
625
|
+
cli: "dench crm views create",
|
|
626
|
+
method: "POST",
|
|
627
|
+
path: "/crm/objects/{objectName}/views",
|
|
628
|
+
requestSchema: anyObject,
|
|
629
|
+
responseSchema: anyResponse,
|
|
630
|
+
backend: convex("mutation", "functions/crm/objects:saveView"),
|
|
631
|
+
}),
|
|
632
|
+
op({
|
|
633
|
+
id: "crm.views.update",
|
|
634
|
+
group: "crm",
|
|
635
|
+
summary: "Replace a saved view's definition (upsert by name).",
|
|
636
|
+
cli: "dench crm views update",
|
|
637
|
+
method: "PATCH",
|
|
638
|
+
path: "/crm/objects/{objectName}/views/{viewName}",
|
|
639
|
+
requestSchema: anyObject,
|
|
640
|
+
responseSchema: anyResponse,
|
|
641
|
+
backend: convex("mutation", "functions/crm/objects:saveView"),
|
|
642
|
+
}),
|
|
643
|
+
op({
|
|
644
|
+
id: "crm.views.delete",
|
|
645
|
+
group: "crm",
|
|
646
|
+
summary: "Delete a saved view by name (idempotent).",
|
|
647
|
+
cli: "dench crm views delete",
|
|
648
|
+
method: "DELETE",
|
|
649
|
+
path: "/crm/objects/{objectName}/views/{viewName}",
|
|
650
|
+
requestSchema: anyObject,
|
|
651
|
+
responseSchema: anyResponse,
|
|
652
|
+
backend: convex("mutation", "functions/crm/objects:deleteView"),
|
|
653
|
+
}),
|
|
654
|
+
|
|
586
655
|
op({
|
|
587
656
|
id: "crm.fields.list",
|
|
588
657
|
group: "crm",
|
|
@@ -1366,6 +1435,18 @@ const rawApiOperations = [
|
|
|
1366
1435
|
responseSchema: anyResponse,
|
|
1367
1436
|
backend: convex("mutation", "functions/emailCampaigns:restoreIdentity"),
|
|
1368
1437
|
}),
|
|
1438
|
+
op({
|
|
1439
|
+
id: "email.identity.delete",
|
|
1440
|
+
group: "email",
|
|
1441
|
+
summary:
|
|
1442
|
+
"Permanently delete a sender (or every sender on a domain) from the workspace; re-adding it requires verification from scratch.",
|
|
1443
|
+
cli: "dench email identity delete",
|
|
1444
|
+
method: "DELETE",
|
|
1445
|
+
path: "/email/sending-identities/{identity}",
|
|
1446
|
+
requestSchema: anyObject,
|
|
1447
|
+
responseSchema: anyResponse,
|
|
1448
|
+
backend: convex("mutation", "functions/emailCampaigns:deleteIdentity"),
|
|
1449
|
+
}),
|
|
1369
1450
|
op({
|
|
1370
1451
|
id: "email.send",
|
|
1371
1452
|
group: "email",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dench.com/cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.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": {
|