@reventlessdev/reventless-seed 1.0.0-alpha.20 → 1.0.0-alpha.22
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 +19 -0
- package/lib/bs/.compiler.log +2 -2
- package/lib/bs/ReventlessSeed.cmi +0 -0
- package/lib/bs/ReventlessSeed.cmt +0 -0
- package/lib/bs/ReventlessSeed.mlmap +1 -0
- package/lib/bs/compiler-info.json +1 -1
- 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 +8 -2
- 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 +169 -21
- package/lib/bs/src/Seed_Client.res.mjs +142 -7
- 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 +258 -19
- package/lib/bs/src/Seed_Connect.res.mjs +158 -8
- 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 +48 -23
- package/lib/bs/src/Seed_Prompt.res.mjs +33 -22
- package/lib/bs/src/Seed_Random-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Random-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Random.ast +0 -0
- package/lib/bs/src/Seed_Random.res +4 -2
- package/lib/bs/src/Seed_Runner-ReventlessSeed.cmi +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 +7 -12
- package/lib/bs/src/Seed_Upload-ReventlessSeed.cmi +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 +4 -2
- 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 +30 -3
- package/lib/bs/src/Seed_Users.res.mjs +14 -1
- 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 +146 -16
- package/lib/bs/tests/Seed_ClientTest.res.mjs +102 -1
- package/lib/bs/tests/Seed_ConnectTest-ReventlessSeed.cmi +0 -0
- package/lib/bs/tests/Seed_ConnectTest-ReventlessSeed.cmj +0 -0
- package/lib/bs/tests/Seed_ConnectTest-ReventlessSeed.cmt +0 -0
- package/lib/bs/tests/Seed_ConnectTest.ast +0 -0
- package/lib/bs/tests/Seed_ConnectTest.res +64 -0
- package/lib/bs/tests/Seed_ConnectTest.res.mjs +56 -0
- package/lib/bs/tests/Seed_PromptTest-ReventlessSeed.cmi +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 +8 -12
- package/lib/bs/tests/Seed_RandomTest-ReventlessSeed.cmi +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 +130 -90
- package/lib/bs/tests/Seed_UsersTest-ReventlessSeed.cmi +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 +77 -2
- package/lib/bs/tests/Seed_UsersTest.res.mjs +45 -2
- 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 +1 -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 +8 -2
- 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 +169 -21
- 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 +146 -16
- 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 +258 -19
- package/lib/ocaml/Seed_ConnectTest-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_ConnectTest-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_ConnectTest-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_ConnectTest.ast +0 -0
- package/lib/ocaml/Seed_ConnectTest.res +64 -0
- 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 +48 -23
- package/lib/ocaml/Seed_PromptTest-ReventlessSeed.cmi +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 +8 -12
- package/lib/ocaml/Seed_Random-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Random-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Random.ast +0 -0
- package/lib/ocaml/Seed_Random.res +4 -2
- package/lib/ocaml/Seed_RandomTest-ReventlessSeed.cmi +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 +130 -90
- package/lib/ocaml/Seed_Runner-ReventlessSeed.cmi +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 +7 -12
- package/lib/ocaml/Seed_Upload-ReventlessSeed.cmi +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 +4 -2
- 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 +30 -3
- package/lib/ocaml/Seed_UsersTest-ReventlessSeed.cmi +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 +77 -2
- package/package.json +2 -2
- package/src/Seed.res +8 -2
- package/src/Seed_Client.res +169 -21
- package/src/Seed_Client.res.mjs +142 -7
- package/src/Seed_Connect.res +258 -19
- package/src/Seed_Connect.res.mjs +158 -8
- package/src/Seed_Prompt.res +48 -23
- package/src/Seed_Prompt.res.mjs +33 -22
- package/src/Seed_Random.res +4 -2
- package/src/Seed_Runner.res +7 -12
- package/src/Seed_Upload.res +4 -2
- package/src/Seed_Users.res +30 -3
- package/src/Seed_Users.res.mjs +14 -1
- package/tests/Seed_ClientTest.res +146 -16
- package/tests/Seed_ClientTest.res.mjs +102 -1
- package/tests/Seed_ConnectTest.res +64 -0
- package/tests/Seed_ConnectTest.res.mjs +56 -0
- package/tests/Seed_PromptTest.res +8 -12
- package/tests/Seed_RandomTest.res +130 -90
- package/tests/Seed_UsersTest.res +77 -2
- package/tests/Seed_UsersTest.res.mjs +45 -2
|
@@ -53,6 +53,10 @@ function envValue(key) {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
function hasTty() {
|
|
57
|
+
return Stdlib_Option.getOr(process.stdin.isTTY, false);
|
|
58
|
+
}
|
|
59
|
+
|
|
56
60
|
function requireTty() {
|
|
57
61
|
if (Stdlib_Option.getOr(process.stdin.isTTY, false)) {
|
|
58
62
|
return;
|
|
@@ -142,6 +146,16 @@ async function select(title, options, env, defaultIndex) {
|
|
|
142
146
|
return await pick();
|
|
143
147
|
}
|
|
144
148
|
|
|
149
|
+
function typedUser(username, password) {
|
|
150
|
+
return {
|
|
151
|
+
username: username,
|
|
152
|
+
password: password,
|
|
153
|
+
groups: [],
|
|
154
|
+
userId: undefined,
|
|
155
|
+
demoOwner: undefined
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
145
159
|
async function askCredentials(localDefaults, envUser) {
|
|
146
160
|
let username;
|
|
147
161
|
if (envUser !== undefined) {
|
|
@@ -160,10 +174,7 @@ async function askCredentials(localDefaults, envUser) {
|
|
|
160
174
|
let entered$1 = await askHidden(localDefaults ? "Password [admin]: " : "Password: ");
|
|
161
175
|
password = entered$1 === "" && localDefaults ? "admin" : entered$1;
|
|
162
176
|
}
|
|
163
|
-
return
|
|
164
|
-
username,
|
|
165
|
-
password
|
|
166
|
-
];
|
|
177
|
+
return typedUser(username, password);
|
|
167
178
|
}
|
|
168
179
|
|
|
169
180
|
async function fromUsersFile(envUser) {
|
|
@@ -179,10 +190,10 @@ async function fromUsersFile(envUser) {
|
|
|
179
190
|
]), "SEED_USER", 0);
|
|
180
191
|
return Stdlib_Option.map(chosen, u => {
|
|
181
192
|
console.log(`Logging in as ` + u.username + ` (from ` + path + `)`);
|
|
182
|
-
return
|
|
183
|
-
u
|
|
184
|
-
|
|
185
|
-
|
|
193
|
+
return {
|
|
194
|
+
caller: u,
|
|
195
|
+
accounts: users
|
|
196
|
+
};
|
|
186
197
|
});
|
|
187
198
|
}
|
|
188
199
|
|
|
@@ -190,33 +201,31 @@ async function credentials(localDefaultsOpt) {
|
|
|
190
201
|
let localDefaults = localDefaultsOpt !== undefined ? localDefaultsOpt : false;
|
|
191
202
|
let envUser = envValue("REVENTLESS_DEMO_USER");
|
|
192
203
|
let match = envValue("REVENTLESS_DEMO_PASSWORD");
|
|
193
|
-
let
|
|
204
|
+
let resolved;
|
|
194
205
|
let exit = 0;
|
|
195
206
|
if (envUser !== undefined && match !== undefined) {
|
|
196
|
-
|
|
197
|
-
envUser,
|
|
198
|
-
|
|
199
|
-
|
|
207
|
+
resolved = {
|
|
208
|
+
caller: typedUser(envUser, match),
|
|
209
|
+
accounts: []
|
|
210
|
+
};
|
|
200
211
|
} else {
|
|
201
212
|
exit = 1;
|
|
202
213
|
}
|
|
203
214
|
if (exit === 1) {
|
|
204
|
-
let
|
|
205
|
-
|
|
215
|
+
let resolved$1 = await fromUsersFile(envUser);
|
|
216
|
+
resolved = resolved$1 !== undefined ? resolved$1 : ({
|
|
217
|
+
caller: await askCredentials(localDefaults, envUser),
|
|
218
|
+
accounts: []
|
|
219
|
+
});
|
|
206
220
|
}
|
|
207
|
-
|
|
208
|
-
let username = match$1[0];
|
|
209
|
-
if (username === "" || password === "") {
|
|
221
|
+
if (resolved.caller.username === "" || resolved.caller.password === "") {
|
|
210
222
|
throw {
|
|
211
223
|
RE_EXN_ID: Seed_Types$ReventlessSeed.Failed,
|
|
212
224
|
_1: "username and password are required.",
|
|
213
225
|
Error: new Error()
|
|
214
226
|
};
|
|
215
227
|
}
|
|
216
|
-
return
|
|
217
|
-
username,
|
|
218
|
-
password
|
|
219
|
-
];
|
|
228
|
+
return resolved;
|
|
220
229
|
}
|
|
221
230
|
|
|
222
231
|
export {
|
|
@@ -225,11 +234,13 @@ export {
|
|
|
225
234
|
iface,
|
|
226
235
|
close,
|
|
227
236
|
envValue,
|
|
237
|
+
hasTty,
|
|
228
238
|
requireTty,
|
|
229
239
|
ask,
|
|
230
240
|
askHidden,
|
|
231
241
|
isAffirmative,
|
|
232
242
|
select,
|
|
243
|
+
typedUser,
|
|
233
244
|
askCredentials,
|
|
234
245
|
fromUsersFile,
|
|
235
246
|
credentials,
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -10,13 +10,15 @@ type t = {mutable state: int}
|
|
|
10
10
|
|
|
11
11
|
let make = (~seed: int): t => {state: seed}
|
|
12
12
|
|
|
13
|
-
|
|
14
13
|
/** mulberry32 — small, fast, and stable across JS engines and Node versions. */
|
|
15
14
|
let float = (r: t): float => {
|
|
16
15
|
r.state = (r.state + 0x6d2b79f5)->Int.bitwiseOr(0)
|
|
17
16
|
let a = r.state
|
|
18
17
|
let b = imul(a->Int.bitwiseXor(a->Int.shiftRightUnsigned(15)), a->Int.bitwiseOr(1))
|
|
19
|
-
let c =
|
|
18
|
+
let c =
|
|
19
|
+
b->Int.bitwiseXor(
|
|
20
|
+
b + imul(b->Int.bitwiseXor(b->Int.shiftRightUnsigned(7)), b->Int.bitwiseOr(61)),
|
|
21
|
+
)
|
|
20
22
|
let bits = c->Int.bitwiseXor(c->Int.shiftRightUnsigned(14))
|
|
21
23
|
// `bits` is a signed 32-bit result; fold the sign bit back in to get the
|
|
22
24
|
// unsigned value a `>>> 0` would produce.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
open Seed_Types
|
|
4
4
|
|
|
5
|
-
|
|
6
5
|
let envOr = (key: string, fallback: string): string =>
|
|
7
6
|
NodeProcess.env->Dict.get(key)->Option.getOr(fallback)
|
|
8
7
|
|
|
@@ -58,14 +57,12 @@ let verifyViews = async (client: Seed_Client.t, ~views: array<view>): dict<int>
|
|
|
58
57
|
Console.log(` ${count->Int.toString->String.padStart(4, " ")} ${name}${note}`)
|
|
59
58
|
})
|
|
60
59
|
|
|
61
|
-
let empty =
|
|
62
|
-
|
|
63
|
-
->
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
)
|
|
60
|
+
let empty = views->Array.filterMap(v =>
|
|
61
|
+
switch v {
|
|
62
|
+
| Seeded(name) if counts->Dict.get(name)->Option.getOr(0) == 0 => Some(name)
|
|
63
|
+
| _ => None
|
|
64
|
+
}
|
|
65
|
+
)
|
|
69
66
|
if empty->Array.length > 0 {
|
|
70
67
|
throw(Failed(`these views are still empty after seeding: ${empty->Array.join(", ")}`))
|
|
71
68
|
}
|
|
@@ -138,9 +135,7 @@ let run = async (main: unit => promise<unit>): unit =>
|
|
|
138
135
|
Console.error("")
|
|
139
136
|
Console.error("Seeding aborted with an unexpected error:")
|
|
140
137
|
Console.error("")
|
|
141
|
-
Console.error(
|
|
142
|
-
exn->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr("unknown"),
|
|
143
|
-
)
|
|
138
|
+
Console.error(exn->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr("unknown"))
|
|
144
139
|
NodeProcess.exit(1)
|
|
145
140
|
}
|
|
146
141
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -64,7 +64,9 @@ let uploadAsset = async (
|
|
|
64
64
|
// GraphQL string literals: JSON-quote the argument values (JSON string escaping is a
|
|
65
65
|
// superset-safe subset of GraphQL's for these ASCII-ish inputs).
|
|
66
66
|
let q = s => JSON.stringify(JSON.Encode.string(s))
|
|
67
|
-
let query = `mutation { r: Upload_Presign(store: ${q(store)}, fileName: ${q(
|
|
67
|
+
let query = `mutation { r: Upload_Presign(store: ${q(store)}, fileName: ${q(
|
|
68
|
+
fileName,
|
|
69
|
+
)}, contentType: ${q(contentType)}) { uploadUrl storageRef } }`
|
|
68
70
|
|
|
69
71
|
let presign = try Ok(await Seed_Client.gql(client, ~query, ~label="Upload_Presign")) catch {
|
|
70
72
|
| Seed_Types.Failed(m) => Error(m)
|
|
@@ -89,7 +91,7 @@ let uploadAsset = async (
|
|
|
89
91
|
| Ok(putRes) if putRes->responseOk => Ok(storageRef)
|
|
90
92
|
| Ok(putRes) =>
|
|
91
93
|
let detail = await putRes->responseText
|
|
92
|
-
Error(`upload PUT failed with HTTP ${
|
|
94
|
+
Error(`upload PUT failed with HTTP ${putRes->responseStatus->Int.toString}: ${detail}`)
|
|
93
95
|
}
|
|
94
96
|
| _ => Error("Upload_Presign response missing uploadUrl/storageRef")
|
|
95
97
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -11,8 +11,21 @@
|
|
|
11
11
|
|
|
12
12
|
@module("yaml") external parseYaml: string => JSON.t = "parse"
|
|
13
13
|
|
|
14
|
-
/**
|
|
15
|
-
|
|
14
|
+
/**
|
|
15
|
+
* One account: what to log in as, what to send with it, and — when the file
|
|
16
|
+
* records one — the id the platform stamps on rows this account writes.
|
|
17
|
+
*
|
|
18
|
+
* `userId` is optional because absence is normal on both platforms: a local file
|
|
19
|
+
* may omit it and let the auth adapter default it to the username, and a
|
|
20
|
+
* hand-maintained AWS file records subs the pool minted, so it can lag.
|
|
21
|
+
*/
|
|
22
|
+
type user = {
|
|
23
|
+
username: string,
|
|
24
|
+
password: string,
|
|
25
|
+
groups: array<string>,
|
|
26
|
+
userId: option<string>,
|
|
27
|
+
demoOwner: option<string>,
|
|
28
|
+
}
|
|
16
29
|
|
|
17
30
|
let asString = (json: JSON.t): option<string> =>
|
|
18
31
|
switch json {
|
|
@@ -32,7 +45,13 @@ let userOf = (json: JSON.t): option<user> =>
|
|
|
32
45
|
| Some(JSON.Array(items)) => items->Array.filterMap(asString)
|
|
33
46
|
| _ => []
|
|
34
47
|
}
|
|
35
|
-
Some({
|
|
48
|
+
Some({
|
|
49
|
+
username,
|
|
50
|
+
password,
|
|
51
|
+
groups,
|
|
52
|
+
userId: obj->Dict.get("userId")->Option.flatMap(asString),
|
|
53
|
+
demoOwner: obj->Dict.get("demoOwner")->Option.flatMap(asString),
|
|
54
|
+
})
|
|
36
55
|
| _ => None
|
|
37
56
|
}
|
|
38
57
|
| _ => None
|
|
@@ -75,6 +94,14 @@ let load = (~path: option<string>=?): option<(string, array<user>)> => {
|
|
|
75
94
|
users->Array.length == 0 ? None : Some((file, users))
|
|
76
95
|
}
|
|
77
96
|
|
|
97
|
+
/** The account that plays `name` in a demo: the one whose `demoOwner` says so,
|
|
98
|
+
else the one named `name` — which is how a file without the field reads. */
|
|
99
|
+
let playing = (users: array<user>, name: string): option<user> =>
|
|
100
|
+
switch users->Array.find(u => u.demoOwner == Some(name)) {
|
|
101
|
+
| Some(_) as declared => declared
|
|
102
|
+
| None => users->Array.find(u => u.username == name)
|
|
103
|
+
}
|
|
104
|
+
|
|
78
105
|
/** Menu label: the username, plus whatever groups the file records for it. */
|
|
79
106
|
let label = (u: user): string =>
|
|
80
107
|
u.groups->Array.length == 0 ? u.username : `${u.username} [${u.groups->Array.join(", ")}]`
|
|
@@ -5,6 +5,7 @@ import * as Nodefs from "node:fs";
|
|
|
5
5
|
import * as Nodepath from "node:path";
|
|
6
6
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
7
7
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
8
|
+
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
8
9
|
|
|
9
10
|
function asString(json) {
|
|
10
11
|
if (typeof json === "string") {
|
|
@@ -31,7 +32,9 @@ function userOf(json) {
|
|
|
31
32
|
return {
|
|
32
33
|
username: match,
|
|
33
34
|
password: match$1,
|
|
34
|
-
groups: groups
|
|
35
|
+
groups: groups,
|
|
36
|
+
userId: Stdlib_Option.flatMap(json["userId"], asString),
|
|
37
|
+
demoOwner: Stdlib_Option.flatMap(json["demoOwner"], asString)
|
|
35
38
|
};
|
|
36
39
|
}
|
|
37
40
|
|
|
@@ -71,6 +74,15 @@ function load(path) {
|
|
|
71
74
|
}
|
|
72
75
|
}
|
|
73
76
|
|
|
77
|
+
function playing(users, name) {
|
|
78
|
+
let declared = users.find(u => Primitive_object.equal(u.demoOwner, name));
|
|
79
|
+
if (declared !== undefined) {
|
|
80
|
+
return declared;
|
|
81
|
+
} else {
|
|
82
|
+
return users.find(u => u.username === name);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
74
86
|
function label(u) {
|
|
75
87
|
if (u.groups.length === 0) {
|
|
76
88
|
return u.username;
|
|
@@ -85,6 +97,7 @@ export {
|
|
|
85
97
|
parseString,
|
|
86
98
|
defaultPath,
|
|
87
99
|
load,
|
|
100
|
+
playing,
|
|
88
101
|
label,
|
|
89
102
|
}
|
|
90
103
|
/* yaml Not a pure module */
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -15,15 +15,16 @@ open JestGlobals
|
|
|
15
15
|
|
|
16
16
|
let errors = entries => JSON.Encode.array(entries->Array.map(JSON.Encode.object))
|
|
17
17
|
|
|
18
|
-
let errorOfType = errorType =>
|
|
19
|
-
Dict.fromArray([("errorType", JSON.Encode.string(errorType))])
|
|
18
|
+
let errorOfType = errorType => Dict.fromArray([("errorType", JSON.Encode.string(errorType))])
|
|
20
19
|
|
|
21
20
|
describe("Seed_Client.isTransient:", () => {
|
|
22
21
|
testSync("resends a fault on the endpoint's own side", () =>
|
|
23
|
-
[
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
[
|
|
23
|
+
"InternalFailure",
|
|
24
|
+
"ServiceUnavailable",
|
|
25
|
+
"Throttling",
|
|
26
|
+
"TooManyRequestsException",
|
|
27
|
+
]->Array.forEach(t => expect(Seed_Client.isTransient(errors([errorOfType(t)])))->toBe(true))
|
|
27
28
|
)
|
|
28
29
|
|
|
29
30
|
// The exact payload the seed aborted on.
|
|
@@ -33,8 +34,8 @@ describe("Seed_Client.isTransient:", () => {
|
|
|
33
34
|
})
|
|
34
35
|
|
|
35
36
|
testSync("fails fast on an error that is an answer", () =>
|
|
36
|
-
["ValidationException", "Unauthorized", "FieldUndefined"]->Array.forEach(
|
|
37
|
-
expect(Seed_Client.isTransient(errors([errorOfType(t)])))->toBe(false)
|
|
37
|
+
["ValidationException", "Unauthorized", "FieldUndefined"]->Array.forEach(
|
|
38
|
+
t => expect(Seed_Client.isTransient(errors([errorOfType(t)])))->toBe(false),
|
|
38
39
|
)
|
|
39
40
|
)
|
|
40
41
|
|
|
@@ -52,8 +53,9 @@ describe("Seed_Client.isTransient:", () => {
|
|
|
52
53
|
// endpoint said errors but named none" into an endless resend.
|
|
53
54
|
testSync("treats an empty or untyped error list as final", () => {
|
|
54
55
|
expect(Seed_Client.isTransient(errors([])))->toBe(false)
|
|
55
|
-
expect(
|
|
56
|
-
|
|
56
|
+
expect(
|
|
57
|
+
Seed_Client.isTransient(errors([Dict.fromArray([("message", JSON.Encode.string("boom"))])])),
|
|
58
|
+
)->toBe(false)
|
|
57
59
|
expect(Seed_Client.isTransient(JSON.Encode.null))->toBe(false)
|
|
58
60
|
})
|
|
59
61
|
})
|
|
@@ -70,8 +72,15 @@ let b64url = payload =>
|
|
|
70
72
|
->NodeBuffer.fromStringUtf8
|
|
71
73
|
->NodeBuffer.toStringBase64Url
|
|
72
74
|
|
|
75
|
+
// A REAL JOSE header, not the literal string "header". The difference is the
|
|
76
|
+
// whole bug: `{"kid":…,"alg":"RS256"}` is base64url JSON and decodes exactly as
|
|
77
|
+
// the payload does, so `claims` took it as the claims set and found no groups
|
|
78
|
+
// and no id in any Cognito token ever issued. A fake header that happened not to
|
|
79
|
+
// decode is what let every assertion below pass against broken code.
|
|
80
|
+
let joseHeader = b64url(`{"kid":"abc123","alg":"RS256"}`)
|
|
81
|
+
|
|
73
82
|
let jwt = claims =>
|
|
74
|
-
|
|
83
|
+
`${joseHeader}.${b64url(JSON.stringify(JSON.Encode.object(Dict.fromArray(claims))))}.signature`
|
|
75
84
|
|
|
76
85
|
let localToken = claims =>
|
|
77
86
|
`${b64url(JSON.stringify(JSON.Encode.object(Dict.fromArray(claims))))}.signature`
|
|
@@ -84,12 +93,41 @@ let clientWith = token => {
|
|
|
84
93
|
c
|
|
85
94
|
}
|
|
86
95
|
|
|
96
|
+
describe("Seed_Client.claims:", () => {
|
|
97
|
+
// The regression this file could not previously express. A JWT header is a
|
|
98
|
+
// decodable JSON object sitting in front of the payload, so "the first segment
|
|
99
|
+
// that parses" is the header on every real token — and the seed then reported
|
|
100
|
+
// no groups, printed no `Acting as:` line, and attached no identity to a
|
|
101
|
+
// refusal, which is exactly the diagnosis it exists to give.
|
|
102
|
+
testSync("skips the JOSE header and reads the payload behind it", () =>
|
|
103
|
+
expect(
|
|
104
|
+
clientWith(jwt([("sub", JSON.Encode.string("real-sub"))]))
|
|
105
|
+
->Seed_Client.claims
|
|
106
|
+
->Option.flatMap(c => c->Dict.get("sub"))
|
|
107
|
+
->Option.flatMap(JSON.Decode.string),
|
|
108
|
+
)->toEqual(Some("real-sub"))
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
// The header is identified by `alg`, not by position — a two-segment local dev
|
|
112
|
+
// token has no header at all, and its payload must not be mistaken for one.
|
|
113
|
+
testSync("takes the first segment when there is no header", () =>
|
|
114
|
+
expect(
|
|
115
|
+
clientWith(localToken([("userId", JSON.Encode.string("local-shopper"))]))
|
|
116
|
+
->Seed_Client.claims
|
|
117
|
+
->Option.flatMap(c => c->Dict.get("userId"))
|
|
118
|
+
->Option.flatMap(JSON.Decode.string),
|
|
119
|
+
)->toEqual(Some("local-shopper"))
|
|
120
|
+
)
|
|
121
|
+
})
|
|
122
|
+
|
|
87
123
|
describe("Seed_Client.effectiveGroups:", () => {
|
|
88
124
|
// A Cognito id token carries its payload in the second segment, a local dev
|
|
89
125
|
// token in the first. Neither platform should have to announce which it is.
|
|
90
126
|
testSync("reads the payload wherever the provider put it", () => {
|
|
91
127
|
expect(
|
|
92
|
-
clientWith(
|
|
128
|
+
clientWith(
|
|
129
|
+
jwt([("cognito:groups", strings(["Admin", "Shopper"]))]),
|
|
130
|
+
)->Seed_Client.effectiveGroups,
|
|
93
131
|
)->toEqual(Some(["Admin", "Shopper"]))
|
|
94
132
|
expect(
|
|
95
133
|
clientWith(localToken([("groups", strings(["Merchandiser"]))]))->Seed_Client.effectiveGroups,
|
|
@@ -101,12 +139,64 @@ describe("Seed_Client.effectiveGroups:", () => {
|
|
|
101
139
|
testSync("says nothing about a token it cannot read", () => {
|
|
102
140
|
expect(clientWith("not-a-token")->Seed_Client.effectiveGroups)->toEqual(None)
|
|
103
141
|
expect(
|
|
104
|
-
Seed_Client.make(
|
|
105
|
-
|
|
142
|
+
Seed_Client.make(
|
|
143
|
+
~config={endpoint: "http://example.invalid/graphql"},
|
|
144
|
+
)->Seed_Client.effectiveGroups,
|
|
106
145
|
)->toEqual(None)
|
|
107
146
|
})
|
|
108
147
|
})
|
|
109
148
|
|
|
149
|
+
// The id an `@owner` field is stamped with. A seed that cannot read it can only
|
|
150
|
+
// key owner-scoped rows to a literal, and a literal matches one platform's
|
|
151
|
+
// accounts at most — which is how a demo shopper came to be registered on a
|
|
152
|
+
// deployment where nobody can ever be that shopper.
|
|
153
|
+
|
|
154
|
+
describe("Seed_Client.callerId:", () => {
|
|
155
|
+
// A Cognito id token says `sub`; the local dev token is a base64url
|
|
156
|
+
// `Identity.t`, whose field is `userId`. Neither platform announces which.
|
|
157
|
+
testSync("reads the id under whichever name the provider gave it", () => {
|
|
158
|
+
expect(
|
|
159
|
+
clientWith(jwt([("sub", JSON.Encode.string("4275e4a4-00c1-70e1"))]))->Seed_Client.callerId,
|
|
160
|
+
)->toEqual(Some("4275e4a4-00c1-70e1"))
|
|
161
|
+
expect(
|
|
162
|
+
clientWith(
|
|
163
|
+
localToken([("userId", JSON.Encode.string("local-shopper"))]),
|
|
164
|
+
)->Seed_Client.callerId,
|
|
165
|
+
)->toEqual(Some("local-shopper"))
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
// `sub` first, so a token carrying both is read the way the pool that minted
|
|
169
|
+
// it means it.
|
|
170
|
+
testSync("prefers sub when a token carries both names", () =>
|
|
171
|
+
expect(
|
|
172
|
+
clientWith(
|
|
173
|
+
jwt([
|
|
174
|
+
("sub", JSON.Encode.string("f215e4b4-30f1-700c")),
|
|
175
|
+
("userId", JSON.Encode.string("local-shopper")),
|
|
176
|
+
]),
|
|
177
|
+
)->Seed_Client.callerId,
|
|
178
|
+
)->toEqual(Some("f215e4b4-30f1-700c"))
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
// `None` is not an error — it means this cannot add anything, and the caller
|
|
182
|
+
// falls back to whatever the accounts file declared.
|
|
183
|
+
testSync("says nothing about a token it cannot read, or one naming no id", () => {
|
|
184
|
+
expect(clientWith("not-a-token")->Seed_Client.callerId)->toEqual(None)
|
|
185
|
+
expect(
|
|
186
|
+
clientWith(jwt([("cognito:groups", strings(["Admin"]))]))->Seed_Client.callerId,
|
|
187
|
+
)->toEqual(None)
|
|
188
|
+
expect(
|
|
189
|
+
Seed_Client.make(~config={endpoint: "http://example.invalid/graphql"})->Seed_Client.callerId,
|
|
190
|
+
)->toEqual(None)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
// A claim of the wrong shape is not an id. Taking the array's first element
|
|
194
|
+
// would be a guess, and a guessed owner id seeds rows nobody can read.
|
|
195
|
+
testSync("refuses a claim that is not a single string", () =>
|
|
196
|
+
expect(clientWith(jwt([("sub", strings(["a", "b"]))]))->Seed_Client.callerId)->toEqual(None)
|
|
197
|
+
)
|
|
198
|
+
})
|
|
199
|
+
|
|
110
200
|
describe("Seed_Client.identitySummary:", () => {
|
|
111
201
|
// The case that cost the debugging: eligible in the user list, refused by the
|
|
112
202
|
// token. Naming what it was narrowed FROM is what points at the role switch.
|
|
@@ -124,8 +214,9 @@ describe("Seed_Client.identitySummary:", () => {
|
|
|
124
214
|
// An ordinary login is not narrowed and has nothing to explain.
|
|
125
215
|
testSync("stays quiet about an unnarrowed token", () =>
|
|
126
216
|
expect(
|
|
127
|
-
clientWith(
|
|
128
|
-
|
|
217
|
+
clientWith(
|
|
218
|
+
jwt([("cognito:groups", strings(["Admin", "Shopper"]))]),
|
|
219
|
+
)->Seed_Client.identitySummary,
|
|
129
220
|
)->toEqual(Some("Admin, Shopper"))
|
|
130
221
|
)
|
|
131
222
|
|
|
@@ -137,6 +228,45 @@ describe("Seed_Client.identitySummary:", () => {
|
|
|
137
228
|
)
|
|
138
229
|
})
|
|
139
230
|
|
|
231
|
+
// Whether a token is narrowed cannot be read off the group claim: a bearer
|
|
232
|
+
// carrying one group looks identical whether the account holds one role or five.
|
|
233
|
+
// `availableRoles` is written only when a role was chosen, so its presence IS
|
|
234
|
+
// the narrowing — and that is what decides whether the seed has anything to ask.
|
|
235
|
+
|
|
236
|
+
describe("Seed_Client.narrowedFrom:", () => {
|
|
237
|
+
testSync("names the membership a narrowed token was reduced from", () =>
|
|
238
|
+
expect(
|
|
239
|
+
clientWith(
|
|
240
|
+
jwt([
|
|
241
|
+
("cognito:groups", strings(["Shopper"])),
|
|
242
|
+
("availableRoles", JSON.Encode.string("Admin,Shopper")),
|
|
243
|
+
]),
|
|
244
|
+
)->Seed_Client.narrowedFrom,
|
|
245
|
+
)->toEqual(Some(["Admin", "Shopper"]))
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
// The case that must NOT prompt: a single-role account is not narrowed, it is
|
|
249
|
+
// simply small. Asking it to choose would put a question in every run.
|
|
250
|
+
testSync("says nothing about an unnarrowed token", () => {
|
|
251
|
+
expect(
|
|
252
|
+
clientWith(jwt([("cognito:groups", strings(["Shopper"]))]))->Seed_Client.narrowedFrom,
|
|
253
|
+
)->toEqual(None)
|
|
254
|
+
expect(
|
|
255
|
+
clientWith(
|
|
256
|
+
jwt([("cognito:groups", strings(["Admin", "Shopper"]))]),
|
|
257
|
+
)->Seed_Client.narrowedFrom,
|
|
258
|
+
)->toEqual(None)
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
// An empty claim is not a narrowing to nothing — it is a claim carrying no
|
|
262
|
+
// information, and offering a menu of no roles would be a dead end.
|
|
263
|
+
testSync("treats an empty availableRoles as not narrowed", () =>
|
|
264
|
+
expect(
|
|
265
|
+
clientWith(jwt([("availableRoles", JSON.Encode.string(""))]))->Seed_Client.narrowedFrom,
|
|
266
|
+
)->toEqual(None)
|
|
267
|
+
)
|
|
268
|
+
})
|
|
269
|
+
|
|
140
270
|
describe("Seed_Client.isDenied:", () => {
|
|
141
271
|
// AppSync types it; the local server carries it in the message.
|
|
142
272
|
testSync("recognises a refusal in either platform's vocabulary", () => {
|