@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.
Files changed (154) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/lib/bs/.compiler.log +2 -2
  3. package/lib/bs/ReventlessSeed.cmi +0 -0
  4. package/lib/bs/ReventlessSeed.cmt +0 -0
  5. package/lib/bs/ReventlessSeed.mlmap +1 -0
  6. package/lib/bs/compiler-info.json +1 -1
  7. package/lib/bs/src/Seed-ReventlessSeed.cmi +0 -0
  8. package/lib/bs/src/Seed-ReventlessSeed.cmt +0 -0
  9. package/lib/bs/src/Seed.ast +0 -0
  10. package/lib/bs/src/Seed.res +8 -2
  11. package/lib/bs/src/Seed_Client-ReventlessSeed.cmi +0 -0
  12. package/lib/bs/src/Seed_Client-ReventlessSeed.cmj +0 -0
  13. package/lib/bs/src/Seed_Client-ReventlessSeed.cmt +0 -0
  14. package/lib/bs/src/Seed_Client.ast +0 -0
  15. package/lib/bs/src/Seed_Client.res +169 -21
  16. package/lib/bs/src/Seed_Client.res.mjs +142 -7
  17. package/lib/bs/src/Seed_Connect-ReventlessSeed.cmi +0 -0
  18. package/lib/bs/src/Seed_Connect-ReventlessSeed.cmj +0 -0
  19. package/lib/bs/src/Seed_Connect-ReventlessSeed.cmt +0 -0
  20. package/lib/bs/src/Seed_Connect.ast +0 -0
  21. package/lib/bs/src/Seed_Connect.res +258 -19
  22. package/lib/bs/src/Seed_Connect.res.mjs +158 -8
  23. package/lib/bs/src/Seed_Prompt-ReventlessSeed.cmi +0 -0
  24. package/lib/bs/src/Seed_Prompt-ReventlessSeed.cmj +0 -0
  25. package/lib/bs/src/Seed_Prompt-ReventlessSeed.cmt +0 -0
  26. package/lib/bs/src/Seed_Prompt.ast +0 -0
  27. package/lib/bs/src/Seed_Prompt.res +48 -23
  28. package/lib/bs/src/Seed_Prompt.res.mjs +33 -22
  29. package/lib/bs/src/Seed_Random-ReventlessSeed.cmi +0 -0
  30. package/lib/bs/src/Seed_Random-ReventlessSeed.cmt +0 -0
  31. package/lib/bs/src/Seed_Random.ast +0 -0
  32. package/lib/bs/src/Seed_Random.res +4 -2
  33. package/lib/bs/src/Seed_Runner-ReventlessSeed.cmi +0 -0
  34. package/lib/bs/src/Seed_Runner-ReventlessSeed.cmt +0 -0
  35. package/lib/bs/src/Seed_Runner.ast +0 -0
  36. package/lib/bs/src/Seed_Runner.res +7 -12
  37. package/lib/bs/src/Seed_Upload-ReventlessSeed.cmi +0 -0
  38. package/lib/bs/src/Seed_Upload-ReventlessSeed.cmt +0 -0
  39. package/lib/bs/src/Seed_Upload.ast +0 -0
  40. package/lib/bs/src/Seed_Upload.res +4 -2
  41. package/lib/bs/src/Seed_Users-ReventlessSeed.cmi +0 -0
  42. package/lib/bs/src/Seed_Users-ReventlessSeed.cmj +0 -0
  43. package/lib/bs/src/Seed_Users-ReventlessSeed.cmt +0 -0
  44. package/lib/bs/src/Seed_Users.ast +0 -0
  45. package/lib/bs/src/Seed_Users.res +30 -3
  46. package/lib/bs/src/Seed_Users.res.mjs +14 -1
  47. package/lib/bs/tests/Seed_ClientTest-ReventlessSeed.cmi +0 -0
  48. package/lib/bs/tests/Seed_ClientTest-ReventlessSeed.cmj +0 -0
  49. package/lib/bs/tests/Seed_ClientTest-ReventlessSeed.cmt +0 -0
  50. package/lib/bs/tests/Seed_ClientTest.ast +0 -0
  51. package/lib/bs/tests/Seed_ClientTest.res +146 -16
  52. package/lib/bs/tests/Seed_ClientTest.res.mjs +102 -1
  53. package/lib/bs/tests/Seed_ConnectTest-ReventlessSeed.cmi +0 -0
  54. package/lib/bs/tests/Seed_ConnectTest-ReventlessSeed.cmj +0 -0
  55. package/lib/bs/tests/Seed_ConnectTest-ReventlessSeed.cmt +0 -0
  56. package/lib/bs/tests/Seed_ConnectTest.ast +0 -0
  57. package/lib/bs/tests/Seed_ConnectTest.res +64 -0
  58. package/lib/bs/tests/Seed_ConnectTest.res.mjs +56 -0
  59. package/lib/bs/tests/Seed_PromptTest-ReventlessSeed.cmi +0 -0
  60. package/lib/bs/tests/Seed_PromptTest-ReventlessSeed.cmt +0 -0
  61. package/lib/bs/tests/Seed_PromptTest.ast +0 -0
  62. package/lib/bs/tests/Seed_PromptTest.res +8 -12
  63. package/lib/bs/tests/Seed_RandomTest-ReventlessSeed.cmi +0 -0
  64. package/lib/bs/tests/Seed_RandomTest-ReventlessSeed.cmt +0 -0
  65. package/lib/bs/tests/Seed_RandomTest.ast +0 -0
  66. package/lib/bs/tests/Seed_RandomTest.res +130 -90
  67. package/lib/bs/tests/Seed_UsersTest-ReventlessSeed.cmi +0 -0
  68. package/lib/bs/tests/Seed_UsersTest-ReventlessSeed.cmt +0 -0
  69. package/lib/bs/tests/Seed_UsersTest.ast +0 -0
  70. package/lib/bs/tests/Seed_UsersTest.res +77 -2
  71. package/lib/bs/tests/Seed_UsersTest.res.mjs +45 -2
  72. package/lib/ocaml/.compiler.log +2 -2
  73. package/lib/ocaml/ReventlessSeed.cmi +0 -0
  74. package/lib/ocaml/ReventlessSeed.cmt +0 -0
  75. package/lib/ocaml/ReventlessSeed.mlmap +1 -0
  76. package/lib/ocaml/Seed-ReventlessSeed.cmi +0 -0
  77. package/lib/ocaml/Seed-ReventlessSeed.cmt +0 -0
  78. package/lib/ocaml/Seed.ast +0 -0
  79. package/lib/ocaml/Seed.res +8 -2
  80. package/lib/ocaml/Seed_Client-ReventlessSeed.cmi +0 -0
  81. package/lib/ocaml/Seed_Client-ReventlessSeed.cmj +0 -0
  82. package/lib/ocaml/Seed_Client-ReventlessSeed.cmt +0 -0
  83. package/lib/ocaml/Seed_Client.ast +0 -0
  84. package/lib/ocaml/Seed_Client.res +169 -21
  85. package/lib/ocaml/Seed_ClientTest-ReventlessSeed.cmi +0 -0
  86. package/lib/ocaml/Seed_ClientTest-ReventlessSeed.cmj +0 -0
  87. package/lib/ocaml/Seed_ClientTest-ReventlessSeed.cmt +0 -0
  88. package/lib/ocaml/Seed_ClientTest.ast +0 -0
  89. package/lib/ocaml/Seed_ClientTest.res +146 -16
  90. package/lib/ocaml/Seed_Connect-ReventlessSeed.cmi +0 -0
  91. package/lib/ocaml/Seed_Connect-ReventlessSeed.cmj +0 -0
  92. package/lib/ocaml/Seed_Connect-ReventlessSeed.cmt +0 -0
  93. package/lib/ocaml/Seed_Connect.ast +0 -0
  94. package/lib/ocaml/Seed_Connect.res +258 -19
  95. package/lib/ocaml/Seed_ConnectTest-ReventlessSeed.cmi +0 -0
  96. package/lib/ocaml/Seed_ConnectTest-ReventlessSeed.cmj +0 -0
  97. package/lib/ocaml/Seed_ConnectTest-ReventlessSeed.cmt +0 -0
  98. package/lib/ocaml/Seed_ConnectTest.ast +0 -0
  99. package/lib/ocaml/Seed_ConnectTest.res +64 -0
  100. package/lib/ocaml/Seed_Prompt-ReventlessSeed.cmi +0 -0
  101. package/lib/ocaml/Seed_Prompt-ReventlessSeed.cmj +0 -0
  102. package/lib/ocaml/Seed_Prompt-ReventlessSeed.cmt +0 -0
  103. package/lib/ocaml/Seed_Prompt.ast +0 -0
  104. package/lib/ocaml/Seed_Prompt.res +48 -23
  105. package/lib/ocaml/Seed_PromptTest-ReventlessSeed.cmi +0 -0
  106. package/lib/ocaml/Seed_PromptTest-ReventlessSeed.cmt +0 -0
  107. package/lib/ocaml/Seed_PromptTest.ast +0 -0
  108. package/lib/ocaml/Seed_PromptTest.res +8 -12
  109. package/lib/ocaml/Seed_Random-ReventlessSeed.cmi +0 -0
  110. package/lib/ocaml/Seed_Random-ReventlessSeed.cmt +0 -0
  111. package/lib/ocaml/Seed_Random.ast +0 -0
  112. package/lib/ocaml/Seed_Random.res +4 -2
  113. package/lib/ocaml/Seed_RandomTest-ReventlessSeed.cmi +0 -0
  114. package/lib/ocaml/Seed_RandomTest-ReventlessSeed.cmt +0 -0
  115. package/lib/ocaml/Seed_RandomTest.ast +0 -0
  116. package/lib/ocaml/Seed_RandomTest.res +130 -90
  117. package/lib/ocaml/Seed_Runner-ReventlessSeed.cmi +0 -0
  118. package/lib/ocaml/Seed_Runner-ReventlessSeed.cmt +0 -0
  119. package/lib/ocaml/Seed_Runner.ast +0 -0
  120. package/lib/ocaml/Seed_Runner.res +7 -12
  121. package/lib/ocaml/Seed_Upload-ReventlessSeed.cmi +0 -0
  122. package/lib/ocaml/Seed_Upload-ReventlessSeed.cmt +0 -0
  123. package/lib/ocaml/Seed_Upload.ast +0 -0
  124. package/lib/ocaml/Seed_Upload.res +4 -2
  125. package/lib/ocaml/Seed_Users-ReventlessSeed.cmi +0 -0
  126. package/lib/ocaml/Seed_Users-ReventlessSeed.cmj +0 -0
  127. package/lib/ocaml/Seed_Users-ReventlessSeed.cmt +0 -0
  128. package/lib/ocaml/Seed_Users.ast +0 -0
  129. package/lib/ocaml/Seed_Users.res +30 -3
  130. package/lib/ocaml/Seed_UsersTest-ReventlessSeed.cmi +0 -0
  131. package/lib/ocaml/Seed_UsersTest-ReventlessSeed.cmt +0 -0
  132. package/lib/ocaml/Seed_UsersTest.ast +0 -0
  133. package/lib/ocaml/Seed_UsersTest.res +77 -2
  134. package/package.json +2 -2
  135. package/src/Seed.res +8 -2
  136. package/src/Seed_Client.res +169 -21
  137. package/src/Seed_Client.res.mjs +142 -7
  138. package/src/Seed_Connect.res +258 -19
  139. package/src/Seed_Connect.res.mjs +158 -8
  140. package/src/Seed_Prompt.res +48 -23
  141. package/src/Seed_Prompt.res.mjs +33 -22
  142. package/src/Seed_Random.res +4 -2
  143. package/src/Seed_Runner.res +7 -12
  144. package/src/Seed_Upload.res +4 -2
  145. package/src/Seed_Users.res +30 -3
  146. package/src/Seed_Users.res.mjs +14 -1
  147. package/tests/Seed_ClientTest.res +146 -16
  148. package/tests/Seed_ClientTest.res.mjs +102 -1
  149. package/tests/Seed_ConnectTest.res +64 -0
  150. package/tests/Seed_ConnectTest.res.mjs +56 -0
  151. package/tests/Seed_PromptTest.res +8 -12
  152. package/tests/Seed_RandomTest.res +130 -90
  153. package/tests/Seed_UsersTest.res +77 -2
  154. 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.username,
184
- u.password
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 match$1;
204
+ let resolved;
194
205
  let exit = 0;
195
206
  if (envUser !== undefined && match !== undefined) {
196
- match$1 = [
197
- envUser,
198
- match
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 pair = await fromUsersFile(envUser);
205
- match$1 = pair !== undefined ? pair : await askCredentials(localDefaults, envUser);
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
- let password = match$1[1];
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
@@ -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 = b->Int.bitwiseXor(b + imul(b->Int.bitwiseXor(b->Int.shiftRightUnsigned(7)), b->Int.bitwiseOr(61)))
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
@@ -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
- views
63
- ->Array.filterMap(v =>
64
- switch v {
65
- | Seeded(name) if counts->Dict.get(name)->Option.getOr(0) == 0 => Some(name)
66
- | _ => None
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
@@ -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(fileName)}, contentType: ${q(contentType)}) { uploadUrl storageRef } }`
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 ${(putRes->responseStatus)->Int.toString}: ${detail}`)
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
@@ -11,8 +11,21 @@
11
11
 
12
12
  @module("yaml") external parseYaml: string => JSON.t = "parse"
13
13
 
14
- /** One account: what to log in as, and what to send with it. */
15
- type user = {username: string, password: string, groups: array<string>}
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({username, password, groups})
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
@@ -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
- ["InternalFailure", "ServiceUnavailable", "Throttling", "TooManyRequestsException"]
24
- ->Array.forEach(t =>
25
- expect(Seed_Client.isTransient(errors([errorOfType(t)])))->toBe(true)
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(t =>
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(Seed_Client.isTransient(errors([Dict.fromArray([("message", JSON.Encode.string("boom"))])])))
56
- ->toBe(false)
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
- `header.${b64url(JSON.stringify(JSON.Encode.object(Dict.fromArray(claims))))}.signature`
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(jwt([("cognito:groups", strings(["Admin", "Shopper"]))]))->Seed_Client.effectiveGroups,
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(~config={endpoint: "http://example.invalid/graphql"})
105
- ->Seed_Client.effectiveGroups,
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(jwt([("cognito:groups", strings(["Admin", "Shopper"]))]))
128
- ->Seed_Client.identitySummary,
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", () => {