@deployfoundation/foundation-deploy 0.2.53 → 0.2.55
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 +1 -1
- package/dist/bin/app.js +4 -4
- package/dist/bin/foundation-deploy.js +65 -22
- package/dist/{chunk-nvsj7bbw.js → chunk-345e0grd.js} +1 -1
- package/dist/{chunk-spq5kr1t.js → chunk-68jxyg2n.js} +13 -4
- package/dist/{chunk-mh1206ne.js → chunk-7k4e3d7n.js} +3 -3
- package/dist/{chunk-p7zkm4pp.js → chunk-caqstrxb.js} +1 -1
- package/dist/chunk-gpq7e1kq.js +101 -0
- package/dist/{chunk-m76jt5rc.js → chunk-hvntk8p5.js} +1 -1
- package/dist/chunk-p6yjbtqz.js +13 -0
- package/dist/{chunk-fjdgxw8h.js → chunk-pmjbzsq3.js} +1 -1
- package/dist/{chunk-96fjaysn.js → chunk-rvx0cncy.js} +33 -6
- package/dist/{chunk-jf5rw6h4.js → chunk-vcz9zbb3.js} +36 -11
- package/dist/{chunk-3614v9rr.js → chunk-xt9gdsa0.js} +1 -1
- package/dist/{chunk-1pww63wd.js → chunk-ztrhakgc.js} +90 -15
- package/dist/src/crm-conformance.d.ts +95 -0
- package/dist/src/crm-conformance.js +11 -0
- package/dist/src/index.js +4 -4
- package/package.json +11 -2
- package/src/crm-conformance.ts +27 -0
- package/src/deploy/cli.ts +51 -0
- package/src/deploy/crm.ts +175 -9
- package/src/stacks/api-stack.ts +4 -2
- package/src/stacks/network-stack.ts +15 -0
|
@@ -3,8 +3,8 @@ import {
|
|
|
3
3
|
putSecretString,
|
|
4
4
|
readSecretString,
|
|
5
5
|
secretExists
|
|
6
|
-
} from "./chunk-
|
|
7
|
-
import"./chunk-
|
|
6
|
+
} from "./chunk-345e0grd.js";
|
|
7
|
+
import"./chunk-pmjbzsq3.js";
|
|
8
8
|
import {
|
|
9
9
|
CrmIdempotencyConflictError,
|
|
10
10
|
CrmRecordNotFoundError
|
|
@@ -12,27 +12,30 @@ import {
|
|
|
12
12
|
import {
|
|
13
13
|
crmHttpBaseUrl,
|
|
14
14
|
isCrmHttpKey
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-hvntk8p5.js";
|
|
16
16
|
import {
|
|
17
17
|
CrmPolicyRefusedError
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-caqstrxb.js";
|
|
19
19
|
import {
|
|
20
20
|
CRM_MAX_ID_CHARS,
|
|
21
21
|
CrmInputError,
|
|
22
22
|
parseCrmLogActivityArgs,
|
|
23
23
|
parseCrmUpdateStatusArgs,
|
|
24
24
|
parseCrmUpsertRecordsArgs
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-xt9gdsa0.js";
|
|
26
26
|
import {
|
|
27
|
+
RawCrmStatusPolicy,
|
|
28
|
+
RawCrmStatusesByType,
|
|
27
29
|
crmOutreachConfigured,
|
|
28
30
|
parseInstanceConfig
|
|
29
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-rvx0cncy.js";
|
|
30
32
|
import {
|
|
31
33
|
__require
|
|
32
34
|
} from "./chunk-5wgwscd0.js";
|
|
33
35
|
|
|
34
36
|
// src/deploy/crm.ts
|
|
35
37
|
import { readFileSync } from "node:fs";
|
|
38
|
+
import { parse as parseYaml } from "yaml";
|
|
36
39
|
function httpProvider(ctx, command) {
|
|
37
40
|
const { instance } = ctx;
|
|
38
41
|
const provider = instance.crm.provider;
|
|
@@ -78,6 +81,11 @@ async function crmCheck(ctx, options, deps) {
|
|
|
78
81
|
});
|
|
79
82
|
return reportConformance(report, provider.baseUrl, deps.log);
|
|
80
83
|
}
|
|
84
|
+
var STDIN_KEYS = [
|
|
85
|
+
{ flag: "--key-stdin", name: "the crm/1 key" },
|
|
86
|
+
{ flag: "--readonly-key-stdin", name: "the read-only key" },
|
|
87
|
+
{ flag: "--harness-key-stdin", name: "the harness key" }
|
|
88
|
+
];
|
|
81
89
|
async function crmCheckTarget(options, deps) {
|
|
82
90
|
let baseUrl;
|
|
83
91
|
try {
|
|
@@ -87,20 +95,86 @@ async function crmCheckTarget(options, deps) {
|
|
|
87
95
|
}
|
|
88
96
|
if (!/^[\x21-\x7e]{1,256}$/.test(options.tenantId))
|
|
89
97
|
throw new Error("--tenant must be the tenant id the server's /meta names");
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
98
|
+
const writeInputs = [
|
|
99
|
+
["--harness-url", options.harnessUrl !== undefined],
|
|
100
|
+
["--readonly-key-stdin", options.readOnlyKeyStdin === true],
|
|
101
|
+
["--status-policy", options.statusPolicyFile !== undefined],
|
|
102
|
+
["--suppressed-statuses", options.suppressedStatusesFile !== undefined],
|
|
103
|
+
["--spend-weekly-allowance", options.spendWeeklyAllowance === true]
|
|
104
|
+
].filter(([, given]) => given).map(([flag]) => flag);
|
|
105
|
+
if (!options.write && writeInputs.length > 0)
|
|
106
|
+
throw new Error(`${writeInputs.join(", ")} only feed the write cases: add --write, against a sandbox tenant`);
|
|
107
|
+
const { createCrmHttpHarness, crmHarnessUrl } = await import("./chunk-gpq7e1kq.js");
|
|
108
|
+
let harnessUrl;
|
|
109
|
+
try {
|
|
110
|
+
harnessUrl = options.harnessUrl === undefined ? undefined : crmHarnessUrl(options.harnessUrl);
|
|
111
|
+
} catch {
|
|
112
|
+
throw new Error("--harness-url must be an HTTPS URL with no userinfo, query or fragment");
|
|
113
|
+
}
|
|
114
|
+
const statusPolicy = options.statusPolicyFile === undefined ? undefined : deploymentInput(deps, "--status-policy", options.statusPolicyFile, RawCrmStatusPolicy);
|
|
115
|
+
const suppressedStatuses = options.suppressedStatusesFile === undefined ? undefined : deploymentInput(deps, "--suppressed-statuses", options.suppressedStatusesFile, RawCrmStatusesByType);
|
|
116
|
+
const wanted = STDIN_KEYS.filter(({ flag }) => flag === "--key-stdin" || flag === "--readonly-key-stdin" && options.readOnlyKeyStdin === true || flag === "--harness-key-stdin" && harnessUrl !== undefined);
|
|
117
|
+
const keys = stdinKeys(await deps.readStdin(), wanted);
|
|
118
|
+
const key = (flag) => keys[wanted.findIndex((entry) => entry.flag === flag)];
|
|
119
|
+
const apiKey = key("--key-stdin");
|
|
120
|
+
const readOnlyApiKey = key("--readonly-key-stdin");
|
|
121
|
+
const harnessKey = key("--harness-key-stdin");
|
|
122
|
+
const harness = harnessUrl === undefined || harnessKey === undefined ? undefined : createCrmHttpHarness({
|
|
123
|
+
url: harnessUrl,
|
|
124
|
+
apiKey: harnessKey,
|
|
125
|
+
tenantId: options.tenantId,
|
|
126
|
+
fetchImpl: deps.fetch
|
|
127
|
+
});
|
|
93
128
|
const report = await deps.runConformance({
|
|
94
129
|
baseUrl,
|
|
95
130
|
apiKey,
|
|
96
131
|
tenantId: options.tenantId,
|
|
97
132
|
write: options.write,
|
|
98
|
-
strict: !options.allowSkips
|
|
133
|
+
strict: !options.allowSkips,
|
|
134
|
+
...readOnlyApiKey === undefined ? {} : { readOnlyApiKey },
|
|
135
|
+
...statusPolicy === undefined ? {} : { statusPolicy },
|
|
136
|
+
...suppressedStatuses === undefined ? {} : { suppressedStatuses },
|
|
137
|
+
...options.spendWeeklyAllowance === true ? { spendWeeklyAllowance: true } : {},
|
|
138
|
+
...harness === undefined ? {} : { harness }
|
|
99
139
|
});
|
|
100
|
-
return reportConformance(report, baseUrl, deps.log);
|
|
140
|
+
return reportConformance(report, baseUrl, deps.log, harnessUrl);
|
|
141
|
+
}
|
|
142
|
+
function stdinKeys(text, wanted) {
|
|
143
|
+
const lines = text.split(/\r?\n/).map((line) => line.trim()).filter((line) => line !== "");
|
|
144
|
+
if (wanted.length === 1) {
|
|
145
|
+
if (lines.length !== 1 || !isCrmHttpKey(lines[0]))
|
|
146
|
+
throw new Error("stdin did not hold a usable crm/1 key (--key-stdin)");
|
|
147
|
+
return lines;
|
|
148
|
+
}
|
|
149
|
+
if (lines.length !== wanted.length)
|
|
150
|
+
throw new Error(`stdin must hold ${wanted.length} keys, one per line: ${wanted.map(({ name, flag }) => `${name} (${flag})`).join(", then ")}; it held ${lines.length}`);
|
|
151
|
+
lines.forEach((line, index) => {
|
|
152
|
+
const entry = wanted[index];
|
|
153
|
+
if (!isCrmHttpKey(line))
|
|
154
|
+
throw new Error(`stdin line ${index + 1}, ${entry.name} (${entry.flag}), is not a usable key: 16 to 1024 visible ASCII characters, no spaces`);
|
|
155
|
+
});
|
|
156
|
+
return lines;
|
|
157
|
+
}
|
|
158
|
+
function deploymentInput(deps, flag, path, schema) {
|
|
159
|
+
let text;
|
|
160
|
+
try {
|
|
161
|
+
text = deps.readFile(path);
|
|
162
|
+
} catch {
|
|
163
|
+
throw new Error(`${flag} ${path} could not be read`);
|
|
164
|
+
}
|
|
165
|
+
let value;
|
|
166
|
+
try {
|
|
167
|
+
value = parseYaml(text);
|
|
168
|
+
} catch {
|
|
169
|
+
throw new Error(`${flag} ${path} is not JSON or YAML`);
|
|
170
|
+
}
|
|
171
|
+
const parsed = schema.safeParse(value);
|
|
172
|
+
if (!parsed.success)
|
|
173
|
+
throw new Error(`${flag} ${path}: ${parsed.error.issues.map((issue) => `${issue.path.join(".") || "(top level)"}: ${issue.message}`).join("; ")}`);
|
|
174
|
+
return parsed.data;
|
|
101
175
|
}
|
|
102
|
-
function reportConformance(report, baseUrl, log) {
|
|
103
|
-
log(`crm/1 conformance against ${baseUrl}`);
|
|
176
|
+
function reportConformance(report, baseUrl, log, harnessUrl) {
|
|
177
|
+
log(`crm/1 conformance against ${baseUrl}${harnessUrl === undefined ? "" : `, harness ${harnessUrl}`}`);
|
|
104
178
|
for (const entry of report.cases) {
|
|
105
179
|
const mark = entry.outcome === "fail" ? "FAIL" : entry.outcome;
|
|
106
180
|
log(` ${mark.padEnd(4)} ${entry.id}: ${entry.title}${entry.detail === undefined ? "" : ` (${entry.detail})`}`);
|
|
@@ -181,7 +255,7 @@ async function crmReplay(ctx, options, deps) {
|
|
|
181
255
|
const crm = crmCapability(config);
|
|
182
256
|
if (crm === undefined || crm.statuses.company.length === 0 || crm.statuses.contact.length === 0)
|
|
183
257
|
throw new Error(`${ctx.paths.configPath}: crm-replay needs capabilities.crm with its statuses`);
|
|
184
|
-
const { createDynamoCrmClient, deterministicRecordId } = await import("./chunk-
|
|
258
|
+
const { createDynamoCrmClient, deterministicRecordId } = await import("./chunk-7k4e3d7n.js");
|
|
185
259
|
const writes = parseCrmReplay(deps.readFile(options.from), crm, deterministicRecordId);
|
|
186
260
|
const counts = REPLAY_OPS.map((op) => `${writes.filter((write) => write.op === op).length} ${op}`).join(", ");
|
|
187
261
|
if (ctx.dryRun === true) {
|
|
@@ -282,12 +356,13 @@ function defaultCrmCommandDeps() {
|
|
|
282
356
|
putSecretString,
|
|
283
357
|
readSecretString,
|
|
284
358
|
runConformance: async (target) => {
|
|
285
|
-
const { runCrmConformance } = await import("./chunk-
|
|
359
|
+
const { runCrmConformance } = await import("./chunk-p6yjbtqz.js");
|
|
286
360
|
return runCrmConformance(target);
|
|
287
361
|
},
|
|
288
362
|
crmTableName: crmTableNameFromStack,
|
|
289
363
|
documentClient: operatorDocumentClient,
|
|
290
364
|
readFile: (path) => readFileSync(path, "utf8"),
|
|
365
|
+
fetch: (input, init) => fetch(input, init),
|
|
291
366
|
log: (line) => console.log(line)
|
|
292
367
|
};
|
|
293
368
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declarations for `@deployfoundation/foundation-deploy/crm-conformance`
|
|
3
|
+
* (src/crm-conformance.ts): the `crm/1` conformance suite. Self-contained on
|
|
4
|
+
* purpose: the source behind them is bundled from workspace packages that are
|
|
5
|
+
* never published. test/crm-conformance-export.test.ts fails the typecheck
|
|
6
|
+
* when these and the source drift apart.
|
|
7
|
+
*
|
|
8
|
+
* docs/crm-provider-contract.md#conformance describes each option.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** A fetch the suite sends every request through; default the global `fetch`. */
|
|
12
|
+
export type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
13
|
+
|
|
14
|
+
export type CrmRecordType = "company" | "contact";
|
|
15
|
+
|
|
16
|
+
/** The agent's allowed status changes on one record type. */
|
|
17
|
+
export interface CrmStatusRule {
|
|
18
|
+
/** From each status, the statuses the agent may move a record to. No entry, no exit. */
|
|
19
|
+
agentTransitions: Readonly<Record<string, readonly string[]>>;
|
|
20
|
+
/** Apply `personSet` to a status a person set; otherwise who set it does not matter. */
|
|
21
|
+
respectHumanChanges: boolean;
|
|
22
|
+
/** `advance`: only forward along `ladder`. `lock`: never. */
|
|
23
|
+
personSet: "lock" | "advance";
|
|
24
|
+
/** The evidence ladder, earliest status first. */
|
|
25
|
+
ladder: readonly string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Absent for a record type, any configured status of that type may be set. */
|
|
29
|
+
export type CrmStatusPolicy = Readonly<Partial<Record<CrmRecordType, CrmStatusRule>>>;
|
|
30
|
+
|
|
31
|
+
/** What only a person can do in a CRM, done directly by the server's own test code. */
|
|
32
|
+
export interface CrmConformanceHarness {
|
|
33
|
+
/** Set a record's status as a person would: stamps `person`, bumps statusVersion. */
|
|
34
|
+
setStatusAsPerson(recordType: CrmRecordType, recordId: string, status: string): Promise<void>;
|
|
35
|
+
/** Set the do-not-contact flag on a contact or a company record. */
|
|
36
|
+
setDoNotContact(recordType: CrmRecordType, recordId: string): Promise<void>;
|
|
37
|
+
/** Suppress an email address for the tenant. */
|
|
38
|
+
suppressEmail(email: string): Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* Log a note as a person (`attributes.by = person`); returns its activity
|
|
41
|
+
* id. The summary may exceed 4000 characters; the server must truncate it
|
|
42
|
+
* on read.
|
|
43
|
+
*/
|
|
44
|
+
addPersonNote(recordType: CrmRecordType, recordId: string, summary: string): Promise<string>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface CrmConformanceTarget {
|
|
48
|
+
baseUrl: string;
|
|
49
|
+
apiKey: string;
|
|
50
|
+
tenantId: string;
|
|
51
|
+
fetchImpl?: FetchLike;
|
|
52
|
+
/** Run cases that create records and spend outreach allowance. */
|
|
53
|
+
write?: boolean;
|
|
54
|
+
/** Also drive the weekly first-touch cap to its limit (CI against a throwaway database only). */
|
|
55
|
+
spendWeeklyAllowance?: boolean;
|
|
56
|
+
/** A key for the same tenant without write scopes: writes must be 403 forbidden. */
|
|
57
|
+
readOnlyApiKey?: string;
|
|
58
|
+
/**
|
|
59
|
+
* A status the agent may not move a fresh record of this type to (from the
|
|
60
|
+
* type's first status). Absent, it is read off `statusPolicy`.
|
|
61
|
+
*/
|
|
62
|
+
disallowedTransition?: { recordType: CrmRecordType; to: string };
|
|
63
|
+
/** The deployment's status policy, as the proxy bakes it. */
|
|
64
|
+
statusPolicy?: CrmStatusPolicy;
|
|
65
|
+
/** The deployment's suppressed statuses. */
|
|
66
|
+
suppressedStatuses?: { company: readonly string[]; contact: readonly string[] };
|
|
67
|
+
harness?: CrmConformanceHarness;
|
|
68
|
+
/** Count a case skipped for want of a harness as a failure. */
|
|
69
|
+
strict?: boolean;
|
|
70
|
+
/** Prefix for created externalIds and idempotency keys; default a random id. */
|
|
71
|
+
runId?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type CrmConformanceOutcome = "pass" | "fail" | "skip";
|
|
75
|
+
|
|
76
|
+
export interface CrmConformanceCase {
|
|
77
|
+
id: string;
|
|
78
|
+
title: string;
|
|
79
|
+
outcome: CrmConformanceOutcome;
|
|
80
|
+
/** Why it failed or was skipped: fixed text, codes and statuses, never a reply body. */
|
|
81
|
+
detail?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface CrmConformanceReport {
|
|
85
|
+
contract: "crm/1";
|
|
86
|
+
passed: number;
|
|
87
|
+
failed: number;
|
|
88
|
+
skipped: number;
|
|
89
|
+
cases: CrmConformanceCase[];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Run every case against one server and report each outcome. Never throws for a server fault. */
|
|
93
|
+
export declare function runCrmConformance(
|
|
94
|
+
target: CrmConformanceTarget,
|
|
95
|
+
): Promise<CrmConformanceReport>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import {
|
|
2
|
+
runCrmConformance
|
|
3
|
+
} from "../chunk-vcz9zbb3.js";
|
|
4
|
+
import"../chunk-hvntk8p5.js";
|
|
5
|
+
import"../chunk-caqstrxb.js";
|
|
6
|
+
import"../chunk-xt9gdsa0.js";
|
|
7
|
+
import"../chunk-rvx0cncy.js";
|
|
8
|
+
import"../chunk-5wgwscd0.js";
|
|
9
|
+
export {
|
|
10
|
+
runCrmConformance
|
|
11
|
+
};
|
package/dist/src/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
FoundationPipeline,
|
|
11
11
|
NewsletterStack,
|
|
12
12
|
deployStatements
|
|
13
|
-
} from "../chunk-
|
|
13
|
+
} from "../chunk-68jxyg2n.js";
|
|
14
14
|
import {
|
|
15
15
|
BUN_VERSION,
|
|
16
16
|
LAMBDA_ENTRY_POINTS,
|
|
@@ -34,7 +34,6 @@ import {
|
|
|
34
34
|
skipBundle,
|
|
35
35
|
verifyManifest
|
|
36
36
|
} from "../chunk-pxhftba1.js";
|
|
37
|
-
import"../chunk-dz91avrb.js";
|
|
38
37
|
import {
|
|
39
38
|
LIVE_ENDPOINT_NAME,
|
|
40
39
|
WEB_SEARCH_CONNECTOR_VERSION,
|
|
@@ -47,8 +46,9 @@ import {
|
|
|
47
46
|
loadInstanceFile,
|
|
48
47
|
provisionsIntegration,
|
|
49
48
|
requiresAuthenticatedQueue
|
|
50
|
-
} from "../chunk-
|
|
51
|
-
import"../chunk-
|
|
49
|
+
} from "../chunk-pmjbzsq3.js";
|
|
50
|
+
import"../chunk-dz91avrb.js";
|
|
51
|
+
import"../chunk-rvx0cncy.js";
|
|
52
52
|
import"../chunk-5wgwscd0.js";
|
|
53
53
|
export {
|
|
54
54
|
BUILD_TIMEOUT,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deployfoundation/foundation-deploy",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.55",
|
|
4
4
|
"description": "Deploys one Foundation instance from a published, signed release: the CDK app and the deploy tool.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"private": false,
|
|
@@ -9,7 +9,16 @@
|
|
|
9
9
|
"foundation-deploy": "./dist/bin/foundation-deploy.js"
|
|
10
10
|
},
|
|
11
11
|
"exports": {
|
|
12
|
-
".": "./dist/src/index.js"
|
|
12
|
+
".": "./dist/src/index.js",
|
|
13
|
+
"./crm-conformance": {
|
|
14
|
+
"types": "./dist/src/crm-conformance.d.ts",
|
|
15
|
+
"default": "./dist/src/crm-conformance.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"typesVersions": {
|
|
19
|
+
"*": {
|
|
20
|
+
"crm-conformance": ["./dist/src/crm-conformance.d.ts"]
|
|
21
|
+
}
|
|
13
22
|
},
|
|
14
23
|
"files": [
|
|
15
24
|
"dist",
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@deployfoundation/foundation-deploy/crm-conformance`: the `crm/1`
|
|
3
|
+
* conformance suite, for a CRM's own CI to run in-process with its own
|
|
4
|
+
* harness (docs/crm-provider-contract.md#conformance).
|
|
5
|
+
*
|
|
6
|
+
* import { runCrmConformance } from "@deployfoundation/foundation-deploy/crm-conformance";
|
|
7
|
+
*
|
|
8
|
+
* This is the published package's stable surface for the suite: the function,
|
|
9
|
+
* its target and report, and the harness interface. Its declarations are
|
|
10
|
+
* hand-written in `types/crm-conformance.d.ts`, because the source behind it
|
|
11
|
+
* lives in workspace packages that are bundled, never published;
|
|
12
|
+
* `test/crm-conformance-export.test.ts` holds the two to the same types.
|
|
13
|
+
*/
|
|
14
|
+
export type { CrmRecordType } from "@deployfoundation/foundation-connectors/crm";
|
|
15
|
+
export type { FetchLike } from "@deployfoundation/foundation-connectors/github";
|
|
16
|
+
export {
|
|
17
|
+
type CrmConformanceCase,
|
|
18
|
+
type CrmConformanceHarness,
|
|
19
|
+
type CrmConformanceOutcome,
|
|
20
|
+
type CrmConformanceReport,
|
|
21
|
+
type CrmConformanceTarget,
|
|
22
|
+
runCrmConformance,
|
|
23
|
+
} from "@deployfoundation/foundation-capability-crm/contract/conformance";
|
|
24
|
+
export type {
|
|
25
|
+
CrmStatusPolicy,
|
|
26
|
+
CrmStatusRule,
|
|
27
|
+
} from "@deployfoundation/foundation-capability-crm/proxy/policy";
|
package/src/deploy/cli.ts
CHANGED
|
@@ -113,6 +113,20 @@ crm-check options
|
|
|
113
113
|
--write with --base-url only: also run the write cases, which
|
|
114
114
|
create records and spend the week's outreach allowance;
|
|
115
115
|
point it at a sandbox tenant
|
|
116
|
+
with --base-url --write, what the remaining write cases need:
|
|
117
|
+
--harness-url <url> the CRM's harness endpoint (crm-harness/1), which does
|
|
118
|
+
--harness-key-stdin what only a person can; its key comes on stdin
|
|
119
|
+
--readonly-key-stdin a key for the same tenant without write scopes, on stdin
|
|
120
|
+
--status-policy <file>
|
|
121
|
+
the deployment's capabilities.crm.statusPolicy (JSON
|
|
122
|
+
or YAML)
|
|
123
|
+
--suppressed-statuses <file>
|
|
124
|
+
its capabilities.crm.outreach.suppressedStatuses
|
|
125
|
+
--spend-weekly-allowance
|
|
126
|
+
drive this week's outreach caps to their limits
|
|
127
|
+
(a throwaway database only)
|
|
128
|
+
Keys on stdin go one per line, in this order: the crm/1 key, the read-only
|
|
129
|
+
key, the harness key (each only when its flag is given).
|
|
116
130
|
|
|
117
131
|
crm-replay options
|
|
118
132
|
--from <file> REQUIRED: the writes to replay, one JSON object per line
|
|
@@ -128,6 +142,24 @@ function flag(args: readonly string[], name: string): string | undefined {
|
|
|
128
142
|
return index === -1 ? undefined : args[index + 1];
|
|
129
143
|
}
|
|
130
144
|
|
|
145
|
+
/** A flag that must carry a value when given: refuses `--x` alone or followed by another flag. */
|
|
146
|
+
function valueFlag(args: readonly string[], name: string, what: string): string | undefined {
|
|
147
|
+
if (!args.includes(name)) return undefined;
|
|
148
|
+
const value = flag(args, name);
|
|
149
|
+
if (value === undefined || value.startsWith("-")) throw new Error(`${name} needs ${what}`);
|
|
150
|
+
return value;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** `crm-check` inputs only the `--base-url` form takes: they feed the write cases. */
|
|
154
|
+
const CRM_CHECK_TARGET_FLAGS = [
|
|
155
|
+
"--harness-url",
|
|
156
|
+
"--harness-key-stdin",
|
|
157
|
+
"--readonly-key-stdin",
|
|
158
|
+
"--status-policy",
|
|
159
|
+
"--suppressed-statuses",
|
|
160
|
+
"--spend-weekly-allowance",
|
|
161
|
+
] as const;
|
|
162
|
+
|
|
131
163
|
/** The AWS context for a command, with the instance file already resolved. */
|
|
132
164
|
function contextFor(args: readonly string[]): AwsContext {
|
|
133
165
|
const paths = loadInstanceContext(resolveInstanceFilePath(args, process.env));
|
|
@@ -235,6 +267,15 @@ export async function main(argv: readonly string[] = process.argv.slice(2)): Pro
|
|
|
235
267
|
const tenantId = flag(args, "--tenant");
|
|
236
268
|
if (baseUrl === undefined || tenantId === undefined || !args.includes("--key-stdin"))
|
|
237
269
|
throw new Error("crm-check --base-url needs --tenant <id> and --key-stdin");
|
|
270
|
+
const harnessUrl = valueFlag(args, "--harness-url", "the CRM's harness endpoint URL");
|
|
271
|
+
// The harness acts as a person, so it is never called without its key,
|
|
272
|
+
// and the key, like every key here, comes on stdin.
|
|
273
|
+
if ((harnessUrl !== undefined) !== args.includes("--harness-key-stdin"))
|
|
274
|
+
throw new Error(
|
|
275
|
+
"crm-check --harness-url <url> and --harness-key-stdin go together: the harness key comes on stdin",
|
|
276
|
+
);
|
|
277
|
+
const statusPolicyFile = valueFlag(args, "--status-policy", "a file");
|
|
278
|
+
const suppressedStatusesFile = valueFlag(args, "--suppressed-statuses", "a file");
|
|
238
279
|
const { crmCheckTarget, defaultCrmCommandDeps } = await import("./crm.ts");
|
|
239
280
|
return crmCheckTarget(
|
|
240
281
|
{
|
|
@@ -242,6 +283,11 @@ export async function main(argv: readonly string[] = process.argv.slice(2)): Pro
|
|
|
242
283
|
tenantId,
|
|
243
284
|
write: args.includes("--write"),
|
|
244
285
|
allowSkips: args.includes("--allow-skips"),
|
|
286
|
+
readOnlyKeyStdin: args.includes("--readonly-key-stdin"),
|
|
287
|
+
spendWeeklyAllowance: args.includes("--spend-weekly-allowance"),
|
|
288
|
+
...(harnessUrl === undefined ? {} : { harnessUrl }),
|
|
289
|
+
...(statusPolicyFile === undefined ? {} : { statusPolicyFile }),
|
|
290
|
+
...(suppressedStatusesFile === undefined ? {} : { suppressedStatusesFile }),
|
|
245
291
|
},
|
|
246
292
|
defaultCrmCommandDeps(),
|
|
247
293
|
);
|
|
@@ -330,6 +376,11 @@ export async function main(argv: readonly string[] = process.argv.slice(2)): Pro
|
|
|
330
376
|
throw new Error(
|
|
331
377
|
"crm-check --write creates records and spends the week's outreach allowance, so it never runs against the instance's own tenant: use --base-url with a sandbox tenant",
|
|
332
378
|
);
|
|
379
|
+
const targetOnly = CRM_CHECK_TARGET_FLAGS.filter((name) => args.includes(name));
|
|
380
|
+
if (targetOnly.length > 0)
|
|
381
|
+
throw new Error(
|
|
382
|
+
`crm-check --instance runs the read cases with the deployment's own key and policy; ${targetOnly.join(", ")} feed the write cases: use --base-url --write with a sandbox tenant`,
|
|
383
|
+
);
|
|
333
384
|
const { crmCheck, defaultCrmCommandDeps } = await import("./crm.ts");
|
|
334
385
|
return crmCheck(ctx, { allowSkips: args.includes("--allow-skips") }, defaultCrmCommandDeps());
|
|
335
386
|
}
|