@autifyhq/aximo-cli 0.13.0 → 0.14.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/README.md +15 -5
- package/dist/client.d.ts +5 -0
- package/dist/client.js +23 -22
- package/dist/client.test.js +28 -0
- package/dist/commands/case/archive.d.ts +20 -0
- package/dist/commands/case/archive.js +42 -0
- package/dist/commands/case/case.test.js +181 -0
- package/dist/commands/case/delete.js +4 -1
- package/dist/commands/case/index.js +2 -0
- package/dist/commands/case/restore.d.ts +20 -0
- package/dist/commands/case/restore.js +42 -0
- package/dist/commands/case/update.d.ts +22 -0
- package/dist/commands/case/update.js +34 -3
- package/dist/commands/device/status.d.ts +1 -10
- package/dist/commands/mcp/server.test.js +9 -1
- package/dist/commands/mcp/tools.js +140 -7
- package/dist/commands/mcp/tools.test.js +310 -0
- package/dist/commands/plan/archive.d.ts +20 -0
- package/dist/commands/plan/archive.js +42 -0
- package/dist/commands/plan/delete.d.ts +20 -0
- package/dist/commands/plan/delete.js +42 -0
- package/dist/commands/plan/index.js +3 -0
- package/dist/commands/plan/plan.test.js +115 -3
- package/dist/commands/plan/restore.d.ts +20 -0
- package/dist/commands/plan/restore.js +42 -0
- package/dist/commands/upload/upload.test.js +5 -2
- package/dist/errors.d.ts +5 -0
- package/dist/errors.js +110 -2
- package/dist/errors.test.js +106 -3
- package/dist/types.d.ts +41 -3
- package/dist/validate.d.ts +1 -0
- package/dist/validate.js +15 -3
- package/dist/validate.test.js +18 -1
- package/package.json +1 -1
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import { request } from "../../client.js";
|
|
3
|
+
import { handleApiError } from "../../errors.js";
|
|
4
|
+
import { dryRunEnvelope } from "../../mutate.js";
|
|
5
|
+
import { formatSuccess, printResult, validationError } from "../../output.js";
|
|
6
|
+
import { requireId } from "../../validate.js";
|
|
7
|
+
export async function runPlanRestore(options, deps) {
|
|
8
|
+
let planId;
|
|
9
|
+
try {
|
|
10
|
+
planId = requireId("planId", options.planId);
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
return validationError(err);
|
|
14
|
+
}
|
|
15
|
+
const path = `/api/plans/${encodeURIComponent(planId)}/restore`;
|
|
16
|
+
if (options.dryRun) {
|
|
17
|
+
return dryRunEnvelope("POST", path);
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const res = await request(path, { method: "POST" }, deps ?? {});
|
|
21
|
+
return formatSuccess(res);
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
return handleApiError(err);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export default defineCommand({
|
|
28
|
+
meta: {
|
|
29
|
+
name: "restore",
|
|
30
|
+
description: "Restore an archived test plan (returns it to lists; its schedule resumes as configured)",
|
|
31
|
+
},
|
|
32
|
+
args: {
|
|
33
|
+
planId: { type: "positional", description: "Plan ID", required: true },
|
|
34
|
+
"dry-run": { type: "boolean", description: "Validate and show request without calling API" },
|
|
35
|
+
},
|
|
36
|
+
async run({ args }) {
|
|
37
|
+
printResult(await runPlanRestore({
|
|
38
|
+
planId: args.planId,
|
|
39
|
+
dryRun: args["dry-run"],
|
|
40
|
+
}));
|
|
41
|
+
},
|
|
42
|
+
});
|
|
@@ -131,8 +131,11 @@ describe("runUploadPut", () => {
|
|
|
131
131
|
presignedUrl: "https://bucket.s3.test/uploads/u2",
|
|
132
132
|
});
|
|
133
133
|
expect(result.ok).toBe(false);
|
|
134
|
-
if (!result.ok)
|
|
135
|
-
expect(result.error.code).toBe("
|
|
134
|
+
if (!result.ok) {
|
|
135
|
+
expect(result.error.code).toBe("forbidden");
|
|
136
|
+
// A signature failure is not a role problem, so no admin hint.
|
|
137
|
+
expect(result.error.hint).toBeUndefined();
|
|
138
|
+
}
|
|
136
139
|
});
|
|
137
140
|
it("PUTs via the authenticated API when --url is relative", async () => {
|
|
138
141
|
let seenApiKey = null;
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,2 +1,7 @@
|
|
|
1
1
|
import type { ErrorEnvelope } from "./types.js";
|
|
2
|
+
export declare function describeDetails(details: unknown): string | undefined;
|
|
3
|
+
export declare function withDetails(message: string, details: unknown): string;
|
|
4
|
+
export declare function featureHint(message: string, details: unknown): string | undefined;
|
|
5
|
+
export declare const ARCHIVE_FIRST_HINT = "Archive the resource first, then retry: `aximo case archive <caseId>` / `aximo plan archive <planId>` (MCP: aximo_archive_case / aximo_archive_plan).";
|
|
6
|
+
export declare function isArchiveFirstConflict(message: string): boolean;
|
|
2
7
|
export declare function handleApiError(error: unknown): ErrorEnvelope;
|
package/dist/errors.js
CHANGED
|
@@ -5,8 +5,106 @@ function isHttpError(error) {
|
|
|
5
5
|
const e = error;
|
|
6
6
|
return typeof e["status"] === "number" && typeof e["message"] === "string";
|
|
7
7
|
}
|
|
8
|
+
function isRecord(value) {
|
|
9
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10
|
+
}
|
|
11
|
+
const MAX_ISSUE_LINES = 5;
|
|
12
|
+
// Walks zod treeifyError / flattenError output into "path: message" lines.
|
|
13
|
+
function collectIssues(node, path, out) {
|
|
14
|
+
if (out.length >= MAX_ISSUE_LINES)
|
|
15
|
+
return;
|
|
16
|
+
if (Array.isArray(node)) {
|
|
17
|
+
node.forEach((item, i) => collectIssues(item, `${path}[${i}]`, out));
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (!isRecord(node))
|
|
21
|
+
return;
|
|
22
|
+
for (const [key, value] of Object.entries(node)) {
|
|
23
|
+
if (out.length >= MAX_ISSUE_LINES)
|
|
24
|
+
return;
|
|
25
|
+
if (key === "errors" || key === "_errors" || key === "formErrors") {
|
|
26
|
+
if (!Array.isArray(value))
|
|
27
|
+
continue;
|
|
28
|
+
for (const msg of value) {
|
|
29
|
+
if (out.length >= MAX_ISSUE_LINES)
|
|
30
|
+
return;
|
|
31
|
+
if (typeof msg === "string")
|
|
32
|
+
out.push(path ? `${path}: ${msg}` : msg);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else if (key === "properties" || key === "fieldErrors" || key === "items") {
|
|
36
|
+
collectIssues(value, path, out);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
const childPath = path ? `${path}.${key}` : key;
|
|
40
|
+
if (Array.isArray(value) && value.every((v) => typeof v === "string")) {
|
|
41
|
+
for (const msg of value) {
|
|
42
|
+
if (out.length >= MAX_ISSUE_LINES)
|
|
43
|
+
return;
|
|
44
|
+
out.push(`${childPath}: ${msg}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
collectIssues(value, childPath, out);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export function describeDetails(details) {
|
|
54
|
+
if (!isRecord(details))
|
|
55
|
+
return undefined;
|
|
56
|
+
const parts = [];
|
|
57
|
+
if (typeof details["path"] === "string")
|
|
58
|
+
parts.push(`at ${details["path"]}`);
|
|
59
|
+
if (typeof details["feature"] === "string") {
|
|
60
|
+
parts.push(`requires feature ${details["feature"]}`);
|
|
61
|
+
}
|
|
62
|
+
if (Array.isArray(details["ids"]))
|
|
63
|
+
parts.push(`ids: ${details["ids"].map(String).join(", ")}`);
|
|
64
|
+
if (Array.isArray(details["unknownFields"])) {
|
|
65
|
+
parts.push(`unknown fields: ${details["unknownFields"].map(String).join(", ")}`);
|
|
66
|
+
}
|
|
67
|
+
if (typeof details["hint"] === "string")
|
|
68
|
+
parts.push(details["hint"]);
|
|
69
|
+
if (parts.length === 0)
|
|
70
|
+
collectIssues(details, "", parts);
|
|
71
|
+
return parts.length > 0 ? parts.join("; ") : undefined;
|
|
72
|
+
}
|
|
73
|
+
export function withDetails(message, details) {
|
|
74
|
+
const extra = describeDetails(details);
|
|
75
|
+
return extra ? `${message} (${extra})` : message;
|
|
76
|
+
}
|
|
77
|
+
const FEATURE_DISABLED_RE = /not enabled for this organization/i;
|
|
78
|
+
export function featureHint(message, details) {
|
|
79
|
+
const feature = isRecord(details) ? details["feature"] : undefined;
|
|
80
|
+
if (typeof feature === "string") {
|
|
81
|
+
return `Ask an Aximo organization admin to enable ${feature}.`;
|
|
82
|
+
}
|
|
83
|
+
if (FEATURE_DISABLED_RE.test(message)) {
|
|
84
|
+
return "Ask an Aximo organization admin to enable this feature.";
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
8
88
|
const NOT_FOUND_HINT = "Check the ID exists and you have access. List resources: aximo project list / aximo device list / aximo upload list";
|
|
9
89
|
const AUTH_HINT = "Run `aximo configure` to save credentials, or set AXIMO_API_KEY via `export AXIMO_API_KEY=your-key` or `node --env-file=.env`.";
|
|
90
|
+
// 403 is a permission outcome, not a credentials problem, so it no longer routes
|
|
91
|
+
// to AUTH_HINT: telling someone to `aximo configure` would have them replace a key
|
|
92
|
+
// that works. The admin wording is attached only when the server says the role is
|
|
93
|
+
// what is missing, following ARCHIVE_FIRST_HINT below; a presigned-upload 403
|
|
94
|
+
// (SignatureDoesNotMatch) is also a 403 and has nothing to do with roles.
|
|
95
|
+
const ADMIN_ROLE_HINT = "This action requires an organization admin or owner API key. Ask an admin to run it, or use a key belonging to an admin or owner.";
|
|
96
|
+
// The archived-first delete gate answers with 409 and a message naming the REST archive
|
|
97
|
+
// endpoint; this hint is what translates it into CLI and MCP terms. Only add it when the
|
|
98
|
+
// server says archiving is what is missing: other 409s (e.g. "Case already in plan") are
|
|
99
|
+
// not fixed by archiving anything.
|
|
100
|
+
// Exported so the MCP error mapper in commands/mcp/tools.ts uses the same string.
|
|
101
|
+
// The hint names both surfaces, so both surfaces have to be able to emit it.
|
|
102
|
+
export const ARCHIVE_FIRST_HINT = "Archive the resource first, then retry: `aximo case archive <caseId>` / `aximo plan archive <planId>` (MCP: aximo_archive_case / aximo_archive_plan).";
|
|
103
|
+
// The server wording is load-bearing for this test: it is the only signal either
|
|
104
|
+
// mapper has for telling an archive-first 409 apart from any other 409.
|
|
105
|
+
export function isArchiveFirstConflict(message) {
|
|
106
|
+
return /archiv/i.test(message);
|
|
107
|
+
}
|
|
10
108
|
// Node's net layer surfaces transient transport failures via these system error codes.
|
|
11
109
|
const RETRYABLE_NET_CODES = new Set([
|
|
12
110
|
"ECONNRESET",
|
|
@@ -34,12 +132,18 @@ function isTransientNetworkError(error) {
|
|
|
34
132
|
}
|
|
35
133
|
export function handleApiError(error) {
|
|
36
134
|
if (isHttpError(error)) {
|
|
37
|
-
const { status,
|
|
135
|
+
const { status, retryAfter, details } = error;
|
|
136
|
+
const message = withDetails(error.message, details);
|
|
38
137
|
if (status === 401) {
|
|
39
138
|
return formatError("unauthorized", message, false, AUTH_HINT);
|
|
40
139
|
}
|
|
41
140
|
if (status === 403) {
|
|
42
|
-
|
|
141
|
+
// A feature gate names its own remedy; otherwise fall back to the role hint
|
|
142
|
+
// when the server says which role is missing. Never the credentials hint: a
|
|
143
|
+
// 403 means the key is valid.
|
|
144
|
+
const hint = featureHint(error.message, details) ??
|
|
145
|
+
(/admin|owner/i.test(message) ? ADMIN_ROLE_HINT : undefined);
|
|
146
|
+
return formatError("forbidden", message, false, hint);
|
|
43
147
|
}
|
|
44
148
|
if (status === 404) {
|
|
45
149
|
return formatError("not_found", message, false, NOT_FOUND_HINT);
|
|
@@ -52,6 +156,10 @@ export function handleApiError(error) {
|
|
|
52
156
|
if (status >= 500) {
|
|
53
157
|
return formatError("api_error", message, true);
|
|
54
158
|
}
|
|
159
|
+
if (status === 409) {
|
|
160
|
+
const hint = isArchiveFirstConflict(message) ? ARCHIVE_FIRST_HINT : undefined;
|
|
161
|
+
return formatError("conflict", message, false, hint);
|
|
162
|
+
}
|
|
55
163
|
if (status >= 400) {
|
|
56
164
|
return formatError("validation_error", message, false);
|
|
57
165
|
}
|
package/dist/errors.test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { handleApiError } from "./errors.js";
|
|
2
|
+
import { describeDetails, featureHint, handleApiError, withDetails } from "./errors.js";
|
|
3
3
|
function httpErr(status, message = `status ${status}`, retryAfter) {
|
|
4
4
|
const err = Object.assign(new Error(message), { status });
|
|
5
5
|
if (retryAfter !== undefined)
|
|
@@ -15,8 +15,17 @@ describe("handleApiError", () => {
|
|
|
15
15
|
expect(r.error.hint).toContain("AXIMO_API_KEY");
|
|
16
16
|
expect(r.error.hint).toContain("--env-file");
|
|
17
17
|
});
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
// 403 is its own code as of GEN-2793: it means the key is valid but the role is
|
|
19
|
+
// not sufficient, which is a different remedy from a missing or bad key.
|
|
20
|
+
it("maps 403 to forbidden, with no credentials hint", () => {
|
|
21
|
+
const r = handleApiError(httpErr(403));
|
|
22
|
+
expect(r.error.code).toBe("forbidden");
|
|
23
|
+
expect(r.error.hint).toBeUndefined();
|
|
24
|
+
});
|
|
25
|
+
it("adds the admin-role hint only when the server names the role", () => {
|
|
26
|
+
const r = handleApiError(httpErr(403, "Only admins and owners can perform this action"));
|
|
27
|
+
expect(r.error.code).toBe("forbidden");
|
|
28
|
+
expect(r.error.hint).toContain("admin or owner");
|
|
20
29
|
});
|
|
21
30
|
it("maps 404 to not_found with list hint", () => {
|
|
22
31
|
const r = handleApiError(httpErr(404));
|
|
@@ -86,3 +95,97 @@ describe("handleApiError", () => {
|
|
|
86
95
|
expect(handleApiError("weird").error.message).toBe("weird");
|
|
87
96
|
});
|
|
88
97
|
});
|
|
98
|
+
function httpErrWithDetails(status, message, details) {
|
|
99
|
+
return Object.assign(new Error(message), { status, details });
|
|
100
|
+
}
|
|
101
|
+
describe("describeDetails", () => {
|
|
102
|
+
it("renders path, feature, ids, unknownFields and hint", () => {
|
|
103
|
+
expect(describeDetails({ path: "apiWebConfig.hostnameOverrideIds", ids: ["a", "b"] })).toBe("at apiWebConfig.hostnameOverrideIds; ids: a, b");
|
|
104
|
+
expect(describeDetails({ feature: "multiDeviceEnabled" })).toBe("requires feature multiDeviceEnabled");
|
|
105
|
+
expect(describeDetails({ unknownFields: ["userDeviceId"], hint: "Put it inside apiDesktopConfig." })).toBe("unknown fields: userDeviceId; Put it inside apiDesktopConfig.");
|
|
106
|
+
});
|
|
107
|
+
it("walks a zod treeifyError tree into path: message lines", () => {
|
|
108
|
+
const tree = {
|
|
109
|
+
errors: ["Unrecognized key"],
|
|
110
|
+
properties: {
|
|
111
|
+
apiMobileConfig: {
|
|
112
|
+
errors: [],
|
|
113
|
+
properties: {
|
|
114
|
+
deviceArn: { errors: ["Required"] },
|
|
115
|
+
apps: {
|
|
116
|
+
errors: [],
|
|
117
|
+
items: [null, { errors: [], properties: { uploadArn: { errors: ["Required"] } } }],
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
expect(describeDetails(tree)).toBe("Unrecognized key; apiMobileConfig.deviceArn: Required; apiMobileConfig.apps[1].uploadArn: Required");
|
|
124
|
+
});
|
|
125
|
+
it("walks a zod flattenError fieldErrors map", () => {
|
|
126
|
+
expect(describeDetails({
|
|
127
|
+
formErrors: [],
|
|
128
|
+
fieldErrors: { platform: ["Invalid enum value"], limit: ["Too big"] },
|
|
129
|
+
})).toBe("platform: Invalid enum value; limit: Too big");
|
|
130
|
+
});
|
|
131
|
+
it("caps zod issues at five lines", () => {
|
|
132
|
+
const fieldErrors = {};
|
|
133
|
+
for (let i = 0; i < 8; i++)
|
|
134
|
+
fieldErrors[`f${i}`] = ["bad"];
|
|
135
|
+
expect(describeDetails({ fieldErrors })?.split("; ")).toHaveLength(5);
|
|
136
|
+
});
|
|
137
|
+
it("returns undefined for non-object or empty details", () => {
|
|
138
|
+
expect(describeDetails(undefined)).toBeUndefined();
|
|
139
|
+
expect(describeDetails("x")).toBeUndefined();
|
|
140
|
+
expect(describeDetails(["x"])).toBeUndefined();
|
|
141
|
+
expect(describeDetails({})).toBeUndefined();
|
|
142
|
+
expect(describeDetails({ errors: [], properties: { a: { errors: 5 } } })).toBeUndefined();
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
describe("withDetails / featureHint", () => {
|
|
146
|
+
it("appends rendered details in parentheses", () => {
|
|
147
|
+
expect(withDetails("Upload not found", { path: "apiMobileConfig.apps[0].uploadArn" })).toBe("Upload not found (at apiMobileConfig.apps[0].uploadArn)");
|
|
148
|
+
expect(withDetails("Upload not found", undefined)).toBe("Upload not found");
|
|
149
|
+
});
|
|
150
|
+
it("names the feature from details, or recognises the not-enabled message", () => {
|
|
151
|
+
expect(featureHint("Forbidden", { feature: "byodEnabled" })).toBe("Ask an Aximo organization admin to enable byodEnabled.");
|
|
152
|
+
expect(featureHint("Multi-device cases are not enabled for this organization", undefined)).toBe("Ask an Aximo organization admin to enable this feature.");
|
|
153
|
+
expect(featureHint("Forbidden", { feature: 3 })).toBeUndefined();
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
describe("handleApiError with details", () => {
|
|
157
|
+
it("names the failing path on a 404", () => {
|
|
158
|
+
const r = handleApiError(httpErrWithDetails(404, "Upload not found", {
|
|
159
|
+
path: "apiMultiDeviceConfig[1].apps[0].uploadArn",
|
|
160
|
+
}));
|
|
161
|
+
expect(r.error.code).toBe("not_found");
|
|
162
|
+
expect(r.error.message).toBe("Upload not found (at apiMultiDeviceConfig[1].apps[0].uploadArn)");
|
|
163
|
+
});
|
|
164
|
+
it("swaps the credentials hint for an admin hint on a feature-gated 403", () => {
|
|
165
|
+
const r = handleApiError(httpErrWithDetails(403, "Desktop cases are not enabled for this organization", {
|
|
166
|
+
feature: "byodEnabled",
|
|
167
|
+
}));
|
|
168
|
+
expect(r.error.code).toBe("forbidden");
|
|
169
|
+
expect(r.error.message).toContain("requires feature byodEnabled");
|
|
170
|
+
expect(r.error.hint).toBe("Ask an Aximo organization admin to enable byodEnabled.");
|
|
171
|
+
});
|
|
172
|
+
it("renders zod issues on a 400", () => {
|
|
173
|
+
const r = handleApiError(httpErrWithDetails(400, "Invalid request body", {
|
|
174
|
+
errors: [],
|
|
175
|
+
properties: { apiWebConfig: { errors: ["Expected object, received string"] } },
|
|
176
|
+
}));
|
|
177
|
+
expect(r.error.code).toBe("validation_error");
|
|
178
|
+
expect(r.error.message).toBe("Invalid request body (apiWebConfig: Expected object, received string)");
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
describe("collectIssues cap", () => {
|
|
182
|
+
it("caps issue lines even within a single errors array", () => {
|
|
183
|
+
const details = {
|
|
184
|
+
properties: {
|
|
185
|
+
scenario: { errors: Array.from({ length: 12 }, (_, i) => `msg ${i}`) },
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
const text = describeDetails(details);
|
|
189
|
+
expect(text?.split("; ")).toHaveLength(5);
|
|
190
|
+
});
|
|
191
|
+
});
|
package/dist/types.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export interface SuccessEnvelope<T> {
|
|
|
6
6
|
data: T;
|
|
7
7
|
meta?: SuccessMeta | undefined;
|
|
8
8
|
}
|
|
9
|
-
export type ErrorCode = "unauthorized" | "rate_limited" | "not_found" | "validation_error" | "api_error";
|
|
9
|
+
export type ErrorCode = "unauthorized" | "forbidden" | "rate_limited" | "not_found" | "validation_error" | "conflict" | "api_error";
|
|
10
10
|
export interface ErrorEnvelope {
|
|
11
11
|
ok: false;
|
|
12
12
|
error: {
|
|
@@ -186,6 +186,17 @@ export interface Device {
|
|
|
186
186
|
arn?: string | undefined;
|
|
187
187
|
name?: string | undefined;
|
|
188
188
|
platform?: Platform | undefined;
|
|
189
|
+
os?: string | undefined;
|
|
190
|
+
[key: string]: unknown;
|
|
191
|
+
}
|
|
192
|
+
export interface UserDevice {
|
|
193
|
+
id: string;
|
|
194
|
+
name: string;
|
|
195
|
+
platform: string;
|
|
196
|
+
os?: string | null;
|
|
197
|
+
status: string;
|
|
198
|
+
lastSeenAt?: string | null;
|
|
199
|
+
createdAt: string;
|
|
189
200
|
[key: string]: unknown;
|
|
190
201
|
}
|
|
191
202
|
export interface CreateUploadInput {
|
|
@@ -202,6 +213,10 @@ export interface Upload {
|
|
|
202
213
|
id?: string | undefined;
|
|
203
214
|
fileName?: string | undefined;
|
|
204
215
|
fileType?: FileType | undefined;
|
|
216
|
+
uploadArn?: string | undefined;
|
|
217
|
+
status?: string | undefined;
|
|
218
|
+
appName?: string | null | undefined;
|
|
219
|
+
identifier?: string | null | undefined;
|
|
205
220
|
[key: string]: unknown;
|
|
206
221
|
}
|
|
207
222
|
export interface UploadPutResult {
|
|
@@ -232,11 +247,15 @@ export interface CreateCaseResult {
|
|
|
232
247
|
}
|
|
233
248
|
export interface UpdateCaseInput {
|
|
234
249
|
title?: string | undefined;
|
|
235
|
-
description?: string | undefined;
|
|
250
|
+
description?: string | null | undefined;
|
|
236
251
|
scenario?: string | undefined;
|
|
237
252
|
type?: CaseType | undefined;
|
|
238
253
|
format?: CaseFormat | undefined;
|
|
239
|
-
startUrl?: string | undefined;
|
|
254
|
+
startUrl?: string | null | undefined;
|
|
255
|
+
apiMobileConfig?: CaseMobileConfig | null | undefined;
|
|
256
|
+
apiDesktopConfig?: Pick<CaseDesktopConfig, "userDeviceId"> | null | undefined;
|
|
257
|
+
apiMultiDeviceConfig?: CaseMultiDeviceDevice[] | null | undefined;
|
|
258
|
+
apiWebConfig?: CaseWebConfig | null | undefined;
|
|
240
259
|
}
|
|
241
260
|
export interface UpdateCaseResult {
|
|
242
261
|
success: boolean;
|
|
@@ -250,11 +269,30 @@ export interface UpdateCaseResult {
|
|
|
250
269
|
format: CaseFormat;
|
|
251
270
|
startUrl: string | null;
|
|
252
271
|
updatedAt: string;
|
|
272
|
+
apiMobileConfig: CaseMobileConfig | null;
|
|
273
|
+
apiWebConfig: CaseWebConfig | null;
|
|
274
|
+
apiDesktopConfig: CaseDesktopConfig | null;
|
|
275
|
+
apiMultiDeviceConfig: CaseMultiDeviceDevice[] | null;
|
|
253
276
|
};
|
|
254
277
|
}
|
|
255
278
|
export interface DeleteCaseResult {
|
|
256
279
|
success: boolean;
|
|
257
280
|
}
|
|
281
|
+
export interface ArchiveCaseResult {
|
|
282
|
+
archived: boolean;
|
|
283
|
+
}
|
|
284
|
+
export interface ArchivePlanResult {
|
|
285
|
+
archived: boolean;
|
|
286
|
+
}
|
|
287
|
+
export interface RestoreCaseResult {
|
|
288
|
+
restored: boolean;
|
|
289
|
+
}
|
|
290
|
+
export interface RestorePlanResult {
|
|
291
|
+
restored: boolean;
|
|
292
|
+
}
|
|
293
|
+
export interface DeletePlanResult {
|
|
294
|
+
success: boolean;
|
|
295
|
+
}
|
|
258
296
|
export interface CreatePlanInput {
|
|
259
297
|
projectId: string;
|
|
260
298
|
title: string;
|
package/dist/validate.d.ts
CHANGED
|
@@ -17,3 +17,4 @@ export declare class JsonBodyError extends Error {
|
|
|
17
17
|
}
|
|
18
18
|
export declare function parseJsonBody(value: unknown): Record<string, unknown>;
|
|
19
19
|
export declare function parseJsonObject(name: string, value: unknown): Record<string, unknown> | undefined;
|
|
20
|
+
export declare function parseJsonArray(name: string, value: unknown): unknown[] | undefined;
|
package/dist/validate.js
CHANGED
|
@@ -130,20 +130,32 @@ export function parseJsonBody(value) {
|
|
|
130
130
|
}
|
|
131
131
|
return parsed;
|
|
132
132
|
}
|
|
133
|
-
|
|
133
|
+
function parseJsonFlag(name, value) {
|
|
134
134
|
if (value === undefined || value === "")
|
|
135
135
|
return undefined;
|
|
136
136
|
if (typeof value !== "string")
|
|
137
137
|
throw new Error(`${name} must be a JSON string`);
|
|
138
|
-
let parsed;
|
|
139
138
|
try {
|
|
140
|
-
|
|
139
|
+
return JSON.parse(value);
|
|
141
140
|
}
|
|
142
141
|
catch (err) {
|
|
143
142
|
throw new Error(`${name} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
144
143
|
}
|
|
144
|
+
}
|
|
145
|
+
export function parseJsonObject(name, value) {
|
|
146
|
+
const parsed = parseJsonFlag(name, value);
|
|
147
|
+
if (parsed === undefined)
|
|
148
|
+
return undefined;
|
|
145
149
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
146
150
|
throw new Error(`${name} must be a JSON object`);
|
|
147
151
|
}
|
|
148
152
|
return parsed;
|
|
149
153
|
}
|
|
154
|
+
export function parseJsonArray(name, value) {
|
|
155
|
+
const parsed = parseJsonFlag(name, value);
|
|
156
|
+
if (parsed === undefined)
|
|
157
|
+
return undefined;
|
|
158
|
+
if (!Array.isArray(parsed))
|
|
159
|
+
throw new Error(`${name} must be a JSON array`);
|
|
160
|
+
return parsed;
|
|
161
|
+
}
|
package/dist/validate.test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { requireString, requireId, parseCaseFormat, parseCaseType, parsePlatform, parseSaveAsCaseType, parseFileType, parseTags, parseJsonObject, parseJsonBody, JsonBodyError, } from "./validate.js";
|
|
2
|
+
import { requireString, requireId, parseCaseFormat, parseCaseType, parsePlatform, parseSaveAsCaseType, parseFileType, parseTags, parseJsonObject, parseJsonArray, parseJsonBody, JsonBodyError, } from "./validate.js";
|
|
3
3
|
describe("requireId", () => {
|
|
4
4
|
it("accepts plain IDs", () => {
|
|
5
5
|
expect(requireId("sessionId", "abc123_-")).toBe("abc123_-");
|
|
@@ -132,3 +132,20 @@ describe("parseCaseFormat", () => {
|
|
|
132
132
|
expect(() => parseCaseFormat("klingon")).toThrow(/must be gherkin, markdown, or plain_text/);
|
|
133
133
|
});
|
|
134
134
|
});
|
|
135
|
+
describe("parseJsonArray", () => {
|
|
136
|
+
it("parses a JSON array", () => {
|
|
137
|
+
expect(parseJsonArray("multiDeviceConfig", '[{"kind":"web"}]')).toEqual([{ kind: "web" }]);
|
|
138
|
+
});
|
|
139
|
+
it("rejects objects", () => {
|
|
140
|
+
expect(() => parseJsonArray("x", "{}")).toThrow("must be a JSON array");
|
|
141
|
+
});
|
|
142
|
+
it("rejects invalid JSON and non-strings", () => {
|
|
143
|
+
expect(() => parseJsonArray("x", "[bad")).toThrow("not valid JSON");
|
|
144
|
+
expect(() => parseJsonArray("x", 5)).toThrow("must be a JSON string");
|
|
145
|
+
expect(() => parseJsonObject("x", 5)).toThrow("must be a JSON string");
|
|
146
|
+
});
|
|
147
|
+
it("returns undefined for empty input", () => {
|
|
148
|
+
expect(parseJsonArray("x", "")).toBeUndefined();
|
|
149
|
+
expect(parseJsonArray("x", undefined)).toBeUndefined();
|
|
150
|
+
});
|
|
151
|
+
});
|