@reventlessdev/reventless-seed 1.0.0-alpha.2 → 1.0.0-alpha.20
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/CHANGELOG.md +137 -0
- package/lib/bs/.compiler.log +2 -2
- package/lib/bs/.sourcedirs.json +1 -0
- package/lib/bs/ReventlessSeed.cmi +0 -0
- package/lib/bs/ReventlessSeed.cmt +0 -0
- package/lib/bs/ReventlessSeed.mlmap +5 -0
- package/lib/bs/compiler-info.json +2 -2
- package/lib/bs/src/Seed-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed.ast +0 -0
- package/lib/bs/src/Seed.res +6 -3
- package/lib/bs/src/Seed.res.mjs +3 -0
- package/lib/bs/src/Seed_Client-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Client-ReventlessSeed.cmj +0 -0
- package/lib/bs/src/Seed_Client-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Client.ast +0 -0
- package/lib/bs/src/Seed_Client.res +228 -15
- package/lib/bs/src/Seed_Client.res.mjs +196 -23
- package/lib/bs/src/Seed_Connect-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Connect-ReventlessSeed.cmj +0 -0
- package/lib/bs/src/Seed_Connect-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Connect.ast +0 -0
- package/lib/bs/src/Seed_Connect.res +27 -18
- package/lib/bs/src/Seed_Connect.res.mjs +10 -6
- package/lib/bs/src/Seed_Prompt-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Prompt-ReventlessSeed.cmj +0 -0
- package/lib/bs/src/Seed_Prompt-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Prompt.ast +0 -0
- package/lib/bs/src/Seed_Prompt.res +110 -35
- package/lib/bs/src/Seed_Prompt.res.mjs +73 -8
- package/lib/bs/src/Seed_Random-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Runner-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Runner-ReventlessSeed.cmj +0 -0
- package/lib/bs/src/Seed_Runner-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Runner.ast +0 -0
- package/lib/bs/src/Seed_Runner.res +38 -7
- package/lib/bs/src/Seed_Runner.res.mjs +19 -1
- package/lib/bs/src/Seed_Types-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Upload-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Upload-ReventlessSeed.cmj +0 -0
- package/lib/bs/src/Seed_Upload-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Upload.ast +0 -0
- package/lib/bs/src/Seed_Upload.res +40 -42
- package/lib/bs/src/Seed_Upload.res.mjs +65 -69
- package/lib/bs/src/Seed_Users-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Users-ReventlessSeed.cmj +0 -0
- package/lib/bs/src/Seed_Users-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Users.ast +0 -0
- package/lib/bs/src/Seed_Users.res +80 -0
- package/lib/bs/src/Seed_Users.res.mjs +90 -0
- package/lib/bs/tests/Seed_ClientTest-ReventlessSeed.cmi +0 -0
- package/lib/bs/tests/Seed_ClientTest-ReventlessSeed.cmj +0 -0
- package/lib/bs/tests/Seed_ClientTest-ReventlessSeed.cmt +0 -0
- package/lib/bs/tests/Seed_ClientTest.ast +0 -0
- package/lib/bs/tests/Seed_ClientTest.res +157 -0
- package/lib/bs/tests/Seed_ClientTest.res.mjs +167 -0
- package/lib/bs/tests/Seed_PromptTest-ReventlessSeed.cmi +0 -0
- package/lib/bs/tests/Seed_PromptTest-ReventlessSeed.cmj +0 -0
- package/lib/bs/tests/Seed_PromptTest-ReventlessSeed.cmt +0 -0
- package/lib/bs/tests/Seed_PromptTest.ast +0 -0
- package/lib/bs/tests/Seed_PromptTest.res +59 -0
- package/lib/bs/tests/Seed_PromptTest.res.mjs +63 -0
- package/lib/bs/tests/Seed_RandomTest-ReventlessSeed.cmi +0 -0
- package/lib/bs/tests/Seed_RandomTest-ReventlessSeed.cmj +0 -0
- package/lib/bs/tests/Seed_RandomTest-ReventlessSeed.cmt +0 -0
- package/lib/bs/tests/Seed_RandomTest.ast +0 -0
- package/lib/bs/tests/Seed_RandomTest.res +160 -0
- package/lib/bs/tests/Seed_RandomTest.res.mjs +208 -0
- package/lib/bs/tests/Seed_UsersTest-ReventlessSeed.cmi +0 -0
- package/lib/bs/tests/Seed_UsersTest-ReventlessSeed.cmj +0 -0
- package/lib/bs/tests/Seed_UsersTest-ReventlessSeed.cmt +0 -0
- package/lib/bs/tests/Seed_UsersTest.ast +0 -0
- package/lib/bs/tests/Seed_UsersTest.res +92 -0
- package/lib/bs/tests/Seed_UsersTest.res.mjs +95 -0
- package/lib/ocaml/.compiler.log +2 -2
- package/lib/ocaml/ReventlessSeed.cmi +0 -0
- package/lib/ocaml/ReventlessSeed.cmt +0 -0
- package/lib/ocaml/ReventlessSeed.mlmap +5 -0
- package/lib/ocaml/Seed-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed.ast +0 -0
- package/lib/ocaml/Seed.res +6 -3
- package/lib/ocaml/Seed_Client-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Client-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_Client-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Client.ast +0 -0
- package/lib/ocaml/Seed_Client.res +228 -15
- package/lib/ocaml/Seed_ClientTest-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_ClientTest-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_ClientTest-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_ClientTest.ast +0 -0
- package/lib/ocaml/Seed_ClientTest.res +157 -0
- package/lib/ocaml/Seed_Connect-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Connect-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_Connect-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Connect.ast +0 -0
- package/lib/ocaml/Seed_Connect.res +27 -18
- package/lib/ocaml/Seed_Prompt-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Prompt-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_Prompt-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Prompt.ast +0 -0
- package/lib/ocaml/Seed_Prompt.res +110 -35
- package/lib/ocaml/Seed_PromptTest-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_PromptTest-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_PromptTest-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_PromptTest.ast +0 -0
- package/lib/ocaml/Seed_PromptTest.res +59 -0
- package/lib/ocaml/Seed_Random-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_RandomTest-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_RandomTest-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_RandomTest-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_RandomTest.ast +0 -0
- package/lib/ocaml/Seed_RandomTest.res +160 -0
- package/lib/ocaml/Seed_Runner-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Runner-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_Runner-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Runner.ast +0 -0
- package/lib/ocaml/Seed_Runner.res +38 -7
- package/lib/ocaml/Seed_Types-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Upload-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Upload-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_Upload-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Upload.ast +0 -0
- package/lib/ocaml/Seed_Upload.res +40 -42
- package/lib/ocaml/Seed_Users-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Users-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_Users-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Users.ast +0 -0
- package/lib/ocaml/Seed_Users.res +80 -0
- package/lib/ocaml/Seed_UsersTest-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_UsersTest-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_UsersTest-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_UsersTest.ast +0 -0
- package/lib/ocaml/Seed_UsersTest.res +92 -0
- package/package.json +7 -2
- package/rescript.json +9 -1
- package/src/Seed.res +6 -3
- package/src/Seed.res.mjs +3 -0
- package/src/Seed_Client.res +228 -15
- package/src/Seed_Client.res.mjs +196 -23
- package/src/Seed_Connect.res +27 -18
- package/src/Seed_Connect.res.mjs +10 -6
- package/src/Seed_Prompt.res +110 -35
- package/src/Seed_Prompt.res.mjs +73 -8
- package/src/Seed_Runner.res +38 -7
- package/src/Seed_Runner.res.mjs +19 -1
- package/src/Seed_Upload.res +40 -42
- package/src/Seed_Upload.res.mjs +65 -69
- package/src/Seed_Users.res +80 -0
- package/src/Seed_Users.res.mjs +90 -0
- package/tests/Seed_ClientTest.res +157 -0
- package/tests/Seed_ClientTest.res.mjs +167 -0
- package/tests/Seed_PromptTest.res +59 -0
- package/tests/Seed_PromptTest.res.mjs +63 -0
- package/tests/Seed_RandomTest.res +160 -0
- package/tests/Seed_RandomTest.res.mjs +208 -0
- package/tests/Seed_UsersTest.res +92 -0
- package/tests/Seed_UsersTest.res.mjs +95 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
3
4
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
4
5
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
6
|
import * as Seed_Types$ReventlessSeed from "./Seed_Types.res.mjs";
|
|
@@ -21,10 +22,77 @@ function currentToken(t) {
|
|
|
21
22
|
return t.token;
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
function endpoint(t) {
|
|
26
|
+
return t.config.endpoint;
|
|
27
|
+
}
|
|
28
|
+
|
|
24
29
|
function useToken(t, token) {
|
|
25
30
|
t.token = token;
|
|
26
31
|
}
|
|
27
32
|
|
|
33
|
+
let availableRolesClaim = "availableRoles";
|
|
34
|
+
|
|
35
|
+
let groupClaimNames = [
|
|
36
|
+
"cognito:groups",
|
|
37
|
+
"groups"
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
function decodeSegment(segment) {
|
|
41
|
+
try {
|
|
42
|
+
return Stdlib_JSON.Decode.object(JSON.parse(Buffer.from(segment, "base64url").toString("utf8")));
|
|
43
|
+
} catch (exn) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function claims(t) {
|
|
49
|
+
return Stdlib_Option.flatMap(t.token, token => Stdlib_Array.reduce(token.split("."), undefined, (found, segment) => {
|
|
50
|
+
if (found !== undefined) {
|
|
51
|
+
return found;
|
|
52
|
+
} else {
|
|
53
|
+
return decodeSegment(segment);
|
|
54
|
+
}
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function asStrings(value) {
|
|
59
|
+
if (Array.isArray(value)) {
|
|
60
|
+
return Stdlib_Array.filterMap(value, Stdlib_JSON.Decode.string);
|
|
61
|
+
}
|
|
62
|
+
switch (typeof value) {
|
|
63
|
+
case "string" :
|
|
64
|
+
return value.split(",").map(prim => prim.trim()).filter(s => s !== "");
|
|
65
|
+
default:
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function claimStrings(t, name) {
|
|
71
|
+
return Stdlib_Option.flatMap(Stdlib_Option.flatMap(claims(t), c => c[name]), asStrings);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function effectiveGroups(t) {
|
|
75
|
+
return Stdlib_Array.reduce(groupClaimNames, undefined, (found, name) => {
|
|
76
|
+
if (found !== undefined) {
|
|
77
|
+
return found;
|
|
78
|
+
} else {
|
|
79
|
+
return claimStrings(t, name);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function identitySummary(t) {
|
|
85
|
+
return Stdlib_Option.map(effectiveGroups(t), groups => {
|
|
86
|
+
let held = groups.length === 0 ? "no groups" : groups.join(", ");
|
|
87
|
+
let available = claimStrings(t, availableRolesClaim);
|
|
88
|
+
if (available !== undefined && available.length !== 0) {
|
|
89
|
+
return held + ` — narrowed to this role from ` + available.join(", ");
|
|
90
|
+
} else {
|
|
91
|
+
return held;
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
28
96
|
function field(json, key) {
|
|
29
97
|
if (typeof json === "object" && json !== null && !Array.isArray(json)) {
|
|
30
98
|
return json[key];
|
|
@@ -94,6 +162,57 @@ async function login(t) {
|
|
|
94
162
|
t.token = Stdlib_Option.flatMap(field(json, "token"), asString);
|
|
95
163
|
}
|
|
96
164
|
|
|
165
|
+
let transientErrorTypes = [
|
|
166
|
+
"InternalFailure",
|
|
167
|
+
"ServiceUnavailable",
|
|
168
|
+
"ServiceUnavailableError",
|
|
169
|
+
"Throttling",
|
|
170
|
+
"ThrottlingException",
|
|
171
|
+
"TooManyRequestsException",
|
|
172
|
+
"RequestTimeout"
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
function isTransient(errors) {
|
|
176
|
+
let entries = Stdlib_JSON.Decode.array(errors);
|
|
177
|
+
if (entries !== undefined && entries.length !== 0) {
|
|
178
|
+
return entries.every(e => {
|
|
179
|
+
let errorType = nodeString(e, "errorType");
|
|
180
|
+
if (errorType !== undefined) {
|
|
181
|
+
return transientErrorTypes.includes(errorType);
|
|
182
|
+
} else {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
} else {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function isDenied(errors) {
|
|
192
|
+
let entries = Stdlib_JSON.Decode.array(errors);
|
|
193
|
+
if (entries !== undefined) {
|
|
194
|
+
return entries.some(e => {
|
|
195
|
+
let match = nodeString(e, "errorType");
|
|
196
|
+
let match$1 = nodeString(e, "message");
|
|
197
|
+
if (match !== undefined) {
|
|
198
|
+
if (match.includes("Unauthorized")) {
|
|
199
|
+
return true;
|
|
200
|
+
} else if (match$1 !== undefined) {
|
|
201
|
+
return match$1.includes("Not Authorized");
|
|
202
|
+
} else {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
} else if (match$1 !== undefined) {
|
|
206
|
+
return match$1.includes("Not Authorized");
|
|
207
|
+
} else {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
} else {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
97
216
|
async function gql(t, query, label) {
|
|
98
217
|
let headers = Object.fromEntries([[
|
|
99
218
|
"content-type",
|
|
@@ -107,30 +226,40 @@ async function gql(t, query, label) {
|
|
|
107
226
|
"query",
|
|
108
227
|
query
|
|
109
228
|
]]));
|
|
110
|
-
let
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
229
|
+
let attempt = async n => {
|
|
230
|
+
let res;
|
|
231
|
+
try {
|
|
232
|
+
res = await fetch(t.config.endpoint, {
|
|
233
|
+
method: "POST",
|
|
234
|
+
headers: headers,
|
|
235
|
+
body: body
|
|
236
|
+
});
|
|
237
|
+
} catch (exn) {
|
|
238
|
+
throw {
|
|
239
|
+
RE_EXN_ID: Seed_Types$ReventlessSeed.Failed,
|
|
240
|
+
_1: label + `: cannot reach ` + t.config.endpoint + ` — is the platform running?`,
|
|
241
|
+
Error: new Error()
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
let json = await res.json();
|
|
245
|
+
let errors = field(json, "errors");
|
|
246
|
+
if (errors === undefined) {
|
|
247
|
+
return Stdlib_Option.getOr(field(json, "data"), null);
|
|
248
|
+
}
|
|
249
|
+
if (isTransient(errors) && n < 4) {
|
|
250
|
+
await sleep((250 * (1 << (n - 1 | 0)) | 0) + (Math.random() * 250 | 0) | 0);
|
|
251
|
+
return await attempt(n + 1 | 0);
|
|
252
|
+
}
|
|
253
|
+
let tried = n > 1 ? `\n gave up after ` + n.toString() + ` attempts` : "";
|
|
254
|
+
let summary = isDenied(errors) ? identitySummary(t) : undefined;
|
|
255
|
+
let identity = summary !== undefined ? `\n identity: ` + summary : "";
|
|
127
256
|
throw {
|
|
128
257
|
RE_EXN_ID: Seed_Types$ReventlessSeed.Failed,
|
|
129
|
-
_1: label + ` failed\n query: ` + query + `\n response: ` + JSON.stringify(errors),
|
|
258
|
+
_1: label + ` failed\n query: ` + query + `\n response: ` + JSON.stringify(errors) + identity + tried,
|
|
130
259
|
Error: new Error()
|
|
131
260
|
};
|
|
132
|
-
}
|
|
133
|
-
return
|
|
261
|
+
};
|
|
262
|
+
return await attempt(1);
|
|
134
263
|
}
|
|
135
264
|
|
|
136
265
|
let commandResultSelection = `__typename
|
|
@@ -171,14 +300,16 @@ async function sendAll(t, mutations) {
|
|
|
171
300
|
}
|
|
172
301
|
}
|
|
173
302
|
|
|
174
|
-
async function queryAllNodes(t, fieldName, selection) {
|
|
303
|
+
async function queryAllNodes(t, fieldName, selection, argsOpt) {
|
|
304
|
+
let args = argsOpt !== undefined ? argsOpt : "";
|
|
175
305
|
let nodes = [];
|
|
176
306
|
let after;
|
|
177
307
|
let more = true;
|
|
308
|
+
let extra = args === "" ? "" : `, ` + args;
|
|
178
309
|
while (more) {
|
|
179
310
|
let c = after;
|
|
180
311
|
let cursor = c !== undefined ? `, after: ` + Seed_Types$ReventlessSeed.quote(c) : "";
|
|
181
|
-
let query = `{ ` + fieldName + `(first: 100` + cursor + `) { edges { node { ` + selection + ` } } pageInfo { hasNextPage endCursor } } }`;
|
|
312
|
+
let query = `{ ` + fieldName + `(first: 100` + extra + cursor + `) { edges { node { ` + selection + ` } } pageInfo { hasNextPage endCursor } } }`;
|
|
182
313
|
let data = await gql(t, query, fieldName);
|
|
183
314
|
let connection = Stdlib_Option.getOr(field(data, fieldName), null);
|
|
184
315
|
let match = field(connection, "edges");
|
|
@@ -205,7 +336,30 @@ async function queryAllNodes(t, fieldName, selection) {
|
|
|
205
336
|
}
|
|
206
337
|
|
|
207
338
|
async function countNodes(t, field) {
|
|
208
|
-
return (await queryAllNodes(t, field, "id")).length;
|
|
339
|
+
return (await queryAllNodes(t, field, "id", undefined)).length;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function queryAllNodesUntil(t, field, selection, argsOpt, satisfied, onTimeout, timeoutMsOpt) {
|
|
343
|
+
let args = argsOpt !== undefined ? argsOpt : "";
|
|
344
|
+
let timeoutMs = timeoutMsOpt !== undefined ? timeoutMsOpt : 60000;
|
|
345
|
+
let deadline = Date.now() + timeoutMs;
|
|
346
|
+
let result;
|
|
347
|
+
while (result === undefined) {
|
|
348
|
+
let nodes = await queryAllNodes(t, field, selection, args);
|
|
349
|
+
if (satisfied(nodes)) {
|
|
350
|
+
result = nodes;
|
|
351
|
+
} else {
|
|
352
|
+
if (Date.now() > deadline) {
|
|
353
|
+
throw {
|
|
354
|
+
RE_EXN_ID: Seed_Types$ReventlessSeed.Failed,
|
|
355
|
+
_1: onTimeout(nodes),
|
|
356
|
+
Error: new Error()
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
await sleep(500);
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
return Stdlib_Option.getOr(result, []);
|
|
209
363
|
}
|
|
210
364
|
|
|
211
365
|
async function waitForIds(t, fieldName, ids, timeoutMsOpt) {
|
|
@@ -240,21 +394,40 @@ async function waitForIds(t, fieldName, ids, timeoutMsOpt) {
|
|
|
240
394
|
return Stdlib_Option.getOr(result, 0);
|
|
241
395
|
}
|
|
242
396
|
|
|
397
|
+
let activeRoleClaim = "activeRole";
|
|
398
|
+
|
|
399
|
+
let attempts = 4;
|
|
400
|
+
|
|
243
401
|
export {
|
|
244
402
|
sleep,
|
|
245
403
|
make,
|
|
246
404
|
currentToken,
|
|
405
|
+
endpoint,
|
|
247
406
|
useToken,
|
|
407
|
+
activeRoleClaim,
|
|
408
|
+
availableRolesClaim,
|
|
409
|
+
groupClaimNames,
|
|
410
|
+
decodeSegment,
|
|
411
|
+
claims,
|
|
412
|
+
asStrings,
|
|
413
|
+
claimStrings,
|
|
414
|
+
effectiveGroups,
|
|
415
|
+
identitySummary,
|
|
248
416
|
field,
|
|
249
417
|
asString,
|
|
250
418
|
nodeString,
|
|
251
419
|
login,
|
|
420
|
+
transientErrorTypes,
|
|
421
|
+
isTransient,
|
|
422
|
+
isDenied,
|
|
423
|
+
attempts,
|
|
252
424
|
gql,
|
|
253
425
|
commandResultSelection,
|
|
254
426
|
send,
|
|
255
427
|
sendAll,
|
|
256
428
|
queryAllNodes,
|
|
257
429
|
countNodes,
|
|
430
|
+
queryAllNodesUntil,
|
|
258
431
|
waitForIds,
|
|
259
432
|
}
|
|
260
433
|
/* No side effect */
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
// Turns endpoints plus a login function into a ready `connection`.
|
|
2
2
|
//
|
|
3
3
|
// A connection is what a data set seeds against: an authenticated GraphQL
|
|
4
|
-
// client, the upload
|
|
5
|
-
// a human label for the target. `make` is provider-agnostic — it prompts for
|
|
4
|
+
// client, the deployment's upload endpoints (empty when it serves no uploads),
|
|
5
|
+
// and a human label for the target. `make` is provider-agnostic — it prompts for
|
|
6
6
|
// credentials, calls the supplied `login` to obtain a bearer, and wires it onto
|
|
7
7
|
// the client via `useToken`. Where the bearer comes from (a local `/login`
|
|
8
8
|
// round-trip, a Cognito id token) is the caller's concern.
|
|
9
9
|
|
|
10
10
|
open Seed_Types
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
// Under route B the client mints uploads through the domain API's `Upload_Presign`
|
|
13
|
+
// mutation (authenticated by the same bearer as commands), so a connection needs no
|
|
14
|
+
// upload endpoint — just the authenticated client and whether uploads are skipped this
|
|
15
|
+
// run (`SEED_SKIP_UPLOADS`). Which store an asset uploads into is the caller's `~store`.
|
|
16
|
+
type connection = {
|
|
17
|
+
client: Seed_Client.t,
|
|
18
|
+
uploadsSkipped: bool,
|
|
19
|
+
label: string,
|
|
20
|
+
}
|
|
13
21
|
|
|
14
22
|
/**
|
|
15
23
|
* Prompts for credentials, calls `login` to mint a bearer, and returns a
|
|
@@ -19,22 +27,25 @@ type connection = {client: Seed_Client.t, uploadEndpoint: string, label: string}
|
|
|
19
27
|
let make = async (
|
|
20
28
|
~label: string,
|
|
21
29
|
~endpoint: string,
|
|
22
|
-
~uploadEndpoint: string,
|
|
23
30
|
~login: (~username: string, ~password: string) => promise<string>,
|
|
24
31
|
~localDefaults: bool=false,
|
|
25
32
|
): connection => {
|
|
26
|
-
// `SEED_SKIP_UPLOADS` forces the
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
// `REVENTLESS_UPLOAD_ENDPOINT` env reads as "unset" (falls back to discovery),
|
|
31
|
-
// so this is the reliable way to disable uploads.
|
|
32
|
-
let uploadEndpoint = Seed_Prompt.envValue("SEED_SKIP_UPLOADS")->Option.isSome ? "" : uploadEndpoint
|
|
33
|
+
// `SEED_SKIP_UPLOADS` forces the upload phase to no-op — seed domain data fast, or
|
|
34
|
+
// skip a broken/absent upload path without editing the data set. The data set reads
|
|
35
|
+
// `connection.uploadsSkipped` and reports the skip.
|
|
36
|
+
let uploadsSkipped = Seed_Upload.uploadsSkipped()
|
|
33
37
|
let (username, password) = await Seed_Prompt.credentials(~localDefaults)
|
|
34
38
|
let token = await login(~username, ~password)
|
|
35
39
|
let client = Seed_Client.make(~config={endpoint: endpoint})
|
|
36
40
|
client->Seed_Client.useToken(token)
|
|
37
|
-
|
|
41
|
+
// What the bearer grants, which is not always what the account list showed: a
|
|
42
|
+
// token narrowed to one role presents that role alone. Said at login so the
|
|
43
|
+
// run starts from the identity it will actually be refused or served under.
|
|
44
|
+
switch Seed_Client.identitySummary(client) {
|
|
45
|
+
| Some(summary) => Console.log(`Acting as: ${summary}`)
|
|
46
|
+
| None => ()
|
|
47
|
+
}
|
|
48
|
+
{client, uploadsSkipped, label}
|
|
38
49
|
}
|
|
39
50
|
|
|
40
51
|
/**
|
|
@@ -63,21 +74,19 @@ let envOr = (key: string, fallback: string): string =>
|
|
|
63
74
|
* A ready `connect` thunk for the local dev platform, with the localhost
|
|
64
75
|
* defaults baked in and overridable per endpoint (arg, then env var, then
|
|
65
76
|
* default). Empty input at the credential prompt falls back to `admin`/`admin`.
|
|
77
|
+
*
|
|
78
|
+
* No upload endpoint: uploads mint through the domain API's `Upload_Presign` mutation
|
|
79
|
+
* on the same `graphql` endpoint, with the store passed per asset.
|
|
66
80
|
*/
|
|
67
|
-
let local = (~graphql=?, ~
|
|
81
|
+
let local = (~graphql=?, ~login=?, ()): (unit => promise<connection>) => {
|
|
68
82
|
let endpoint =
|
|
69
83
|
graphql->Option.getOr(envOr("REVENTLESS_GRAPHQL_ENDPOINT", "http://localhost:4000/graphql"))
|
|
70
|
-
let uploadEndpoint =
|
|
71
|
-
upload->Option.getOr(
|
|
72
|
-
envOr("REVENTLESS_UPLOAD_ENDPOINT", "http://localhost:4000/__inmemory/upload"),
|
|
73
|
-
)
|
|
74
84
|
let loginEndpoint =
|
|
75
85
|
login->Option.getOr(envOr("REVENTLESS_LOGIN_ENDPOINT", "http://localhost:4000/__inmemory/login"))
|
|
76
86
|
() =>
|
|
77
87
|
make(
|
|
78
88
|
~label="local",
|
|
79
89
|
~endpoint,
|
|
80
|
-
~uploadEndpoint,
|
|
81
90
|
~login=viaLoginEndpoint(~loginEndpoint),
|
|
82
91
|
~localDefaults=true,
|
|
83
92
|
)
|
|
@@ -4,19 +4,24 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
|
4
4
|
import * as Seed_Types$ReventlessSeed from "./Seed_Types.res.mjs";
|
|
5
5
|
import * as Seed_Client$ReventlessSeed from "./Seed_Client.res.mjs";
|
|
6
6
|
import * as Seed_Prompt$ReventlessSeed from "./Seed_Prompt.res.mjs";
|
|
7
|
+
import * as Seed_Upload$ReventlessSeed from "./Seed_Upload.res.mjs";
|
|
7
8
|
|
|
8
|
-
async function make(label, endpoint,
|
|
9
|
+
async function make(label, endpoint, login, localDefaultsOpt) {
|
|
9
10
|
let localDefaults = localDefaultsOpt !== undefined ? localDefaultsOpt : false;
|
|
10
|
-
let
|
|
11
|
+
let uploadsSkipped = Seed_Upload$ReventlessSeed.uploadsSkipped();
|
|
11
12
|
let match = await Seed_Prompt$ReventlessSeed.credentials(localDefaults);
|
|
12
13
|
let token = await login(match[0], match[1]);
|
|
13
14
|
let client = Seed_Client$ReventlessSeed.make({
|
|
14
15
|
endpoint: endpoint
|
|
15
16
|
});
|
|
16
17
|
Seed_Client$ReventlessSeed.useToken(client, token);
|
|
18
|
+
let summary = Seed_Client$ReventlessSeed.identitySummary(client);
|
|
19
|
+
if (summary !== undefined) {
|
|
20
|
+
console.log(`Acting as: ` + summary);
|
|
21
|
+
}
|
|
17
22
|
return {
|
|
18
23
|
client: client,
|
|
19
|
-
|
|
24
|
+
uploadsSkipped: uploadsSkipped,
|
|
20
25
|
label: label
|
|
21
26
|
};
|
|
22
27
|
}
|
|
@@ -46,11 +51,10 @@ function envOr(key, fallback) {
|
|
|
46
51
|
return Stdlib_Option.getOr(Seed_Prompt$ReventlessSeed.envValue(key), fallback);
|
|
47
52
|
}
|
|
48
53
|
|
|
49
|
-
function local(graphql,
|
|
54
|
+
function local(graphql, login, param) {
|
|
50
55
|
let endpoint = Stdlib_Option.getOr(graphql, Stdlib_Option.getOr(Seed_Prompt$ReventlessSeed.envValue("REVENTLESS_GRAPHQL_ENDPOINT"), "http://localhost:4000/graphql"));
|
|
51
|
-
let uploadEndpoint = Stdlib_Option.getOr(upload, Stdlib_Option.getOr(Seed_Prompt$ReventlessSeed.envValue("REVENTLESS_UPLOAD_ENDPOINT"), "http://localhost:4000/__inmemory/upload"));
|
|
52
56
|
let loginEndpoint = Stdlib_Option.getOr(login, Stdlib_Option.getOr(Seed_Prompt$ReventlessSeed.envValue("REVENTLESS_LOGIN_ENDPOINT"), "http://localhost:4000/__inmemory/login"));
|
|
53
|
-
return () => make("local", endpoint,
|
|
57
|
+
return () => make("local", endpoint, viaLoginEndpoint(loginEndpoint), true);
|
|
54
58
|
}
|
|
55
59
|
|
|
56
60
|
export {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// One shared readline interface for every interactive prompt in a seed run.
|
|
2
2
|
//
|
|
3
3
|
// The reason this module exists: creating and closing a fresh `readline`
|
|
4
|
-
// interface per prompt leaves stdin paused after the first `close`, so the
|
|
4
|
+
// interface per prompt leaves NodeProcess.stdin paused after the first `close`, so the
|
|
5
5
|
// second prompt (password after username) never fires and the run hangs. A
|
|
6
6
|
// single interface, created lazily and closed exactly once, avoids that — and a
|
|
7
7
|
// `muted` gate on its output lets password entry reuse the same interface
|
|
@@ -14,18 +14,8 @@ open Seed_Types
|
|
|
14
14
|
|
|
15
15
|
// ── Node bindings ─────────────────────────────────────────────────────────────
|
|
16
16
|
|
|
17
|
-
type stream
|
|
18
|
-
@val @scope("process") external stdin: stream = "stdin"
|
|
19
|
-
@val @scope("process") external stdout: stream = "stdout"
|
|
20
|
-
@send external writeStream: (stream, string) => unit = "write"
|
|
21
|
-
@get external isTTY: stream => bool = "isTTY"
|
|
22
|
-
@send external pauseStream: stream => unit = "pause"
|
|
23
|
-
@send external unrefStream: stream => unit = "unref"
|
|
24
|
-
|
|
25
|
-
@scope("process") @val external processEnv: dict<string> = "env"
|
|
26
|
-
|
|
27
17
|
type rl
|
|
28
|
-
type rlOptions = {input: stream, output: stream, terminal?: bool}
|
|
18
|
+
type rlOptions = {input: NodeProcess.stream, output: NodeProcess.stream, terminal?: bool}
|
|
29
19
|
@module("node:readline") external createInterface: rlOptions => rl = "createInterface"
|
|
30
20
|
@send external question: (rl, string, string => unit) => unit = "question"
|
|
31
21
|
@send external closeRl: rl => unit = "close"
|
|
@@ -42,10 +32,10 @@ let iface = (): rl =>
|
|
|
42
32
|
switch rlRef.contents {
|
|
43
33
|
| Some(rl) => rl
|
|
44
34
|
| None =>
|
|
45
|
-
let rl = createInterface({input: stdin, output: stdout, terminal: true})
|
|
35
|
+
let rl = createInterface({input: NodeProcess.stdin, output: NodeProcess.stdout, terminal: true})
|
|
46
36
|
rl->setWriteToOutput(s =>
|
|
47
37
|
if !muted.contents {
|
|
48
|
-
stdout->
|
|
38
|
+
NodeProcess.stdout->NodeProcess.write(s)
|
|
49
39
|
}
|
|
50
40
|
)
|
|
51
41
|
rlRef := Some(rl)
|
|
@@ -58,23 +48,23 @@ let close = (): unit =>
|
|
|
58
48
|
rl->closeRl
|
|
59
49
|
rlRef := None
|
|
60
50
|
// Closing the interface is not enough: a terminal-mode readline leaves
|
|
61
|
-
// stdin resumed and reffed, which keeps the event loop alive so the CLI
|
|
51
|
+
// NodeProcess.stdin resumed and reffed, which keeps the event loop alive so the CLI
|
|
62
52
|
// never exits after seeding. Release it explicitly.
|
|
63
|
-
stdin->
|
|
64
|
-
stdin->
|
|
53
|
+
NodeProcess.stdin->NodeProcess.pause
|
|
54
|
+
NodeProcess.stdin->NodeProcess.unref
|
|
65
55
|
| None => ()
|
|
66
56
|
}
|
|
67
57
|
|
|
68
58
|
// ── Env + TTY ─────────────────────────────────────────────────────────────────
|
|
69
59
|
|
|
70
60
|
let envValue = (key: string): option<string> =>
|
|
71
|
-
switch
|
|
61
|
+
switch NodeProcess.env->Dict.get(key) {
|
|
72
62
|
| Some(v) if v->String.trim != "" => Some(v->String.trim)
|
|
73
63
|
| _ => None
|
|
74
64
|
}
|
|
75
65
|
|
|
76
66
|
let requireTty = (): unit =>
|
|
77
|
-
if !(stdin->isTTY) {
|
|
67
|
+
if !(NodeProcess.stdin->NodeProcess.isTTY->Option.getOr(false)) {
|
|
78
68
|
throw(
|
|
79
69
|
Failed(
|
|
80
70
|
"no TTY for an interactive prompt — set the documented SEED_* / REVENTLESS_DEMO_* " ++
|
|
@@ -101,23 +91,45 @@ let askHidden = (query: string): promise<string> =>
|
|
|
101
91
|
let rl = iface()
|
|
102
92
|
rl->question(query, answer => {
|
|
103
93
|
muted := false
|
|
104
|
-
stdout->
|
|
94
|
+
NodeProcess.stdout->NodeProcess.write("\n")
|
|
105
95
|
resolve(answer)
|
|
106
96
|
})
|
|
107
97
|
muted := true
|
|
108
98
|
})
|
|
109
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Whether an answer means yes — the one vocabulary, for the typed reply and the
|
|
102
|
+
* env var that stands in for it alike.
|
|
103
|
+
*
|
|
104
|
+
* It exists because those two drifted: a `[y/N]` prompt took `y`, and the env
|
|
105
|
+
* var beside it took only `1` or `yes`, so the obvious `SEED_RESET_CONFIRM=y`
|
|
106
|
+
* silently fell through to the interactive branch — which, on the CI runs the
|
|
107
|
+
* variable exists for, has no TTY and throws. Two spellings of one question are
|
|
108
|
+
* one spelling too many; callers ask this instead of comparing strings.
|
|
109
|
+
*
|
|
110
|
+
* Trimmed and case-folded, so `Yes`/` y ` answer the same as `y`.
|
|
111
|
+
*/
|
|
112
|
+
let isAffirmative = (answer: string): bool =>
|
|
113
|
+
switch answer->String.trim->String.toLowerCase {
|
|
114
|
+
| "y" | "yes" | "1" => true
|
|
115
|
+
| _ => false
|
|
116
|
+
}
|
|
117
|
+
|
|
110
118
|
/**
|
|
111
119
|
* Chooses one of `options` (a label paired with the value it selects).
|
|
112
120
|
*
|
|
113
121
|
* Auto-returns when there is a single option, so a lone data set or stack needs
|
|
114
122
|
* no keypress. `env` (e.g. `SEED_SET`) preselects by matching label or 1-based
|
|
115
123
|
* index, so CI runs without a TTY. Otherwise it prints a numbered menu.
|
|
124
|
+
*
|
|
125
|
+
* `defaultIndex` makes Enter a valid answer, selecting that 0-based option;
|
|
126
|
+
* without one every reply must name a choice.
|
|
116
127
|
*/
|
|
117
128
|
let select = async (
|
|
118
129
|
~title: string,
|
|
119
130
|
~options: array<(string, 'a)>,
|
|
120
131
|
~env=?,
|
|
132
|
+
~defaultIndex=?,
|
|
121
133
|
): 'a =>
|
|
122
134
|
switch options {
|
|
123
135
|
| [(_, only)] => only
|
|
@@ -145,29 +157,38 @@ let select = async (
|
|
|
145
157
|
options->Array.forEachWithIndex(((label, _), i) =>
|
|
146
158
|
Console.log(` ${(i + 1)->Int.toString}) ${label}`)
|
|
147
159
|
)
|
|
160
|
+
let fallback = defaultIndex->Option.flatMap(i => options->Array.get(i))
|
|
161
|
+
let hint = switch (defaultIndex, fallback) {
|
|
162
|
+
| (Some(i), Some(_)) => ` (default ${(i + 1)->Int.toString})`
|
|
163
|
+
| _ => ""
|
|
164
|
+
}
|
|
148
165
|
let rec pick = async (): 'a => {
|
|
149
|
-
let answer = await ask(
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
166
|
+
let answer = await ask(
|
|
167
|
+
`\nSelect [1-${(options->Array.length)->Int.toString}]${hint}: `,
|
|
168
|
+
)
|
|
169
|
+
switch (answer, fallback) {
|
|
170
|
+
| ("", Some((_, value))) => value
|
|
154
171
|
| _ =>
|
|
155
|
-
|
|
156
|
-
|
|
172
|
+
switch Int.fromString(answer) {
|
|
173
|
+
| Some(n) if n >= 1 && n <= options->Array.length =>
|
|
174
|
+
let (_, value) = options->Array.getUnsafe(n - 1)
|
|
175
|
+
value
|
|
176
|
+
| _ =>
|
|
177
|
+
Console.log(" Not a valid choice.")
|
|
178
|
+
await pick()
|
|
179
|
+
}
|
|
157
180
|
}
|
|
158
181
|
}
|
|
159
182
|
await pick()
|
|
160
183
|
}
|
|
161
184
|
}
|
|
162
185
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
let credentials = async (~localDefaults: bool=false): (string, string) => {
|
|
170
|
-
let username = switch envValue("REVENTLESS_DEMO_USER") {
|
|
186
|
+
// Typing both halves is the fallback, not the first offer: it is what happens
|
|
187
|
+
// when the platform keeps no accounts file. `envUser` is threaded in so a
|
|
188
|
+
// `REVENTLESS_DEMO_USER` that named nobody in the file still skips the username
|
|
189
|
+
// prompt rather than asking for a name that was already given.
|
|
190
|
+
let askCredentials = async (~localDefaults: bool, ~envUser: option<string>): (string, string) => {
|
|
191
|
+
let username = switch envUser {
|
|
171
192
|
| Some(u) => u
|
|
172
193
|
| None =>
|
|
173
194
|
requireTty()
|
|
@@ -181,6 +202,60 @@ let credentials = async (~localDefaults: bool=false): (string, string) => {
|
|
|
181
202
|
let entered = await askHidden(localDefaults ? "Password [admin]: " : "Password: ")
|
|
182
203
|
entered == "" && localDefaults ? "admin" : entered
|
|
183
204
|
}
|
|
205
|
+
(username, password)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// The accounts file the platform already keeps (`.reventless/users.yaml`, or
|
|
209
|
+
// `SEED_USERS_FILE`). It records the password beside the username, so choosing
|
|
210
|
+
// an account is the whole credential step — nothing is typed.
|
|
211
|
+
//
|
|
212
|
+
// A named `REVENTLESS_DEMO_USER` picks its entry directly; otherwise the file's
|
|
213
|
+
// accounts are offered in the order it defines them, first as the default.
|
|
214
|
+
// `None` means the file has nothing to offer and the caller should ask.
|
|
215
|
+
let fromUsersFile = async (~envUser: option<string>): option<(string, string)> =>
|
|
216
|
+
switch Seed_Users.load(~path=?envValue("SEED_USERS_FILE")) {
|
|
217
|
+
| None => None
|
|
218
|
+
| Some((path, users)) =>
|
|
219
|
+
let chosen = switch envUser {
|
|
220
|
+
| Some(name) => users->Array.find(u => u.username == name)
|
|
221
|
+
| None =>
|
|
222
|
+
Some(
|
|
223
|
+
await select(
|
|
224
|
+
~title=`User (${path}):`,
|
|
225
|
+
~options=users->Array.map(u => (Seed_Users.label(u), u)),
|
|
226
|
+
~env="SEED_USER",
|
|
227
|
+
~defaultIndex=0,
|
|
228
|
+
),
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
// Logged for every arm, including the single-account file that selects
|
|
232
|
+
// itself without a keypress: which identity seeded the data is the thing an
|
|
233
|
+
// operator checks when owner-scoped rows turn up under the wrong account.
|
|
234
|
+
chosen->Option.map(u => {
|
|
235
|
+
Console.log(`Logging in as ${u.username} (from ${path})`)
|
|
236
|
+
(u.username, u.password)
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Resolves the credentials a seed run logs in with.
|
|
242
|
+
*
|
|
243
|
+
* `REVENTLESS_DEMO_USER` + `REVENTLESS_DEMO_PASSWORD` together are the
|
|
244
|
+
* non-interactive path and skip everything below. Otherwise the platform's
|
|
245
|
+
* `.reventless/users.yaml` supplies the accounts to choose from, and only a
|
|
246
|
+
* platform without one falls back to typing both halves — where, with
|
|
247
|
+
* `localDefaults`, empty input means `admin`/`admin`.
|
|
248
|
+
*/
|
|
249
|
+
let credentials = async (~localDefaults: bool=false): (string, string) => {
|
|
250
|
+
let envUser = envValue("REVENTLESS_DEMO_USER")
|
|
251
|
+
let (username, password) = switch (envUser, envValue("REVENTLESS_DEMO_PASSWORD")) {
|
|
252
|
+
| (Some(u), Some(p)) => (u, p)
|
|
253
|
+
| _ =>
|
|
254
|
+
switch await fromUsersFile(~envUser) {
|
|
255
|
+
| Some(pair) => pair
|
|
256
|
+
| None => await askCredentials(~localDefaults, ~envUser)
|
|
257
|
+
}
|
|
258
|
+
}
|
|
184
259
|
if username == "" || password == "" {
|
|
185
260
|
throw(Failed("username and password are required."))
|
|
186
261
|
}
|