@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.
Files changed (158) hide show
  1. package/CHANGELOG.md +137 -0
  2. package/lib/bs/.compiler.log +2 -2
  3. package/lib/bs/.sourcedirs.json +1 -0
  4. package/lib/bs/ReventlessSeed.cmi +0 -0
  5. package/lib/bs/ReventlessSeed.cmt +0 -0
  6. package/lib/bs/ReventlessSeed.mlmap +5 -0
  7. package/lib/bs/compiler-info.json +2 -2
  8. package/lib/bs/src/Seed-ReventlessSeed.cmi +0 -0
  9. package/lib/bs/src/Seed-ReventlessSeed.cmt +0 -0
  10. package/lib/bs/src/Seed.ast +0 -0
  11. package/lib/bs/src/Seed.res +6 -3
  12. package/lib/bs/src/Seed.res.mjs +3 -0
  13. package/lib/bs/src/Seed_Client-ReventlessSeed.cmi +0 -0
  14. package/lib/bs/src/Seed_Client-ReventlessSeed.cmj +0 -0
  15. package/lib/bs/src/Seed_Client-ReventlessSeed.cmt +0 -0
  16. package/lib/bs/src/Seed_Client.ast +0 -0
  17. package/lib/bs/src/Seed_Client.res +228 -15
  18. package/lib/bs/src/Seed_Client.res.mjs +196 -23
  19. package/lib/bs/src/Seed_Connect-ReventlessSeed.cmi +0 -0
  20. package/lib/bs/src/Seed_Connect-ReventlessSeed.cmj +0 -0
  21. package/lib/bs/src/Seed_Connect-ReventlessSeed.cmt +0 -0
  22. package/lib/bs/src/Seed_Connect.ast +0 -0
  23. package/lib/bs/src/Seed_Connect.res +27 -18
  24. package/lib/bs/src/Seed_Connect.res.mjs +10 -6
  25. package/lib/bs/src/Seed_Prompt-ReventlessSeed.cmi +0 -0
  26. package/lib/bs/src/Seed_Prompt-ReventlessSeed.cmj +0 -0
  27. package/lib/bs/src/Seed_Prompt-ReventlessSeed.cmt +0 -0
  28. package/lib/bs/src/Seed_Prompt.ast +0 -0
  29. package/lib/bs/src/Seed_Prompt.res +110 -35
  30. package/lib/bs/src/Seed_Prompt.res.mjs +73 -8
  31. package/lib/bs/src/Seed_Random-ReventlessSeed.cmt +0 -0
  32. package/lib/bs/src/Seed_Runner-ReventlessSeed.cmi +0 -0
  33. package/lib/bs/src/Seed_Runner-ReventlessSeed.cmj +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 +38 -7
  37. package/lib/bs/src/Seed_Runner.res.mjs +19 -1
  38. package/lib/bs/src/Seed_Types-ReventlessSeed.cmt +0 -0
  39. package/lib/bs/src/Seed_Upload-ReventlessSeed.cmi +0 -0
  40. package/lib/bs/src/Seed_Upload-ReventlessSeed.cmj +0 -0
  41. package/lib/bs/src/Seed_Upload-ReventlessSeed.cmt +0 -0
  42. package/lib/bs/src/Seed_Upload.ast +0 -0
  43. package/lib/bs/src/Seed_Upload.res +40 -42
  44. package/lib/bs/src/Seed_Upload.res.mjs +65 -69
  45. package/lib/bs/src/Seed_Users-ReventlessSeed.cmi +0 -0
  46. package/lib/bs/src/Seed_Users-ReventlessSeed.cmj +0 -0
  47. package/lib/bs/src/Seed_Users-ReventlessSeed.cmt +0 -0
  48. package/lib/bs/src/Seed_Users.ast +0 -0
  49. package/lib/bs/src/Seed_Users.res +80 -0
  50. package/lib/bs/src/Seed_Users.res.mjs +90 -0
  51. package/lib/bs/tests/Seed_ClientTest-ReventlessSeed.cmi +0 -0
  52. package/lib/bs/tests/Seed_ClientTest-ReventlessSeed.cmj +0 -0
  53. package/lib/bs/tests/Seed_ClientTest-ReventlessSeed.cmt +0 -0
  54. package/lib/bs/tests/Seed_ClientTest.ast +0 -0
  55. package/lib/bs/tests/Seed_ClientTest.res +157 -0
  56. package/lib/bs/tests/Seed_ClientTest.res.mjs +167 -0
  57. package/lib/bs/tests/Seed_PromptTest-ReventlessSeed.cmi +0 -0
  58. package/lib/bs/tests/Seed_PromptTest-ReventlessSeed.cmj +0 -0
  59. package/lib/bs/tests/Seed_PromptTest-ReventlessSeed.cmt +0 -0
  60. package/lib/bs/tests/Seed_PromptTest.ast +0 -0
  61. package/lib/bs/tests/Seed_PromptTest.res +59 -0
  62. package/lib/bs/tests/Seed_PromptTest.res.mjs +63 -0
  63. package/lib/bs/tests/Seed_RandomTest-ReventlessSeed.cmi +0 -0
  64. package/lib/bs/tests/Seed_RandomTest-ReventlessSeed.cmj +0 -0
  65. package/lib/bs/tests/Seed_RandomTest-ReventlessSeed.cmt +0 -0
  66. package/lib/bs/tests/Seed_RandomTest.ast +0 -0
  67. package/lib/bs/tests/Seed_RandomTest.res +160 -0
  68. package/lib/bs/tests/Seed_RandomTest.res.mjs +208 -0
  69. package/lib/bs/tests/Seed_UsersTest-ReventlessSeed.cmi +0 -0
  70. package/lib/bs/tests/Seed_UsersTest-ReventlessSeed.cmj +0 -0
  71. package/lib/bs/tests/Seed_UsersTest-ReventlessSeed.cmt +0 -0
  72. package/lib/bs/tests/Seed_UsersTest.ast +0 -0
  73. package/lib/bs/tests/Seed_UsersTest.res +92 -0
  74. package/lib/bs/tests/Seed_UsersTest.res.mjs +95 -0
  75. package/lib/ocaml/.compiler.log +2 -2
  76. package/lib/ocaml/ReventlessSeed.cmi +0 -0
  77. package/lib/ocaml/ReventlessSeed.cmt +0 -0
  78. package/lib/ocaml/ReventlessSeed.mlmap +5 -0
  79. package/lib/ocaml/Seed-ReventlessSeed.cmi +0 -0
  80. package/lib/ocaml/Seed-ReventlessSeed.cmt +0 -0
  81. package/lib/ocaml/Seed.ast +0 -0
  82. package/lib/ocaml/Seed.res +6 -3
  83. package/lib/ocaml/Seed_Client-ReventlessSeed.cmi +0 -0
  84. package/lib/ocaml/Seed_Client-ReventlessSeed.cmj +0 -0
  85. package/lib/ocaml/Seed_Client-ReventlessSeed.cmt +0 -0
  86. package/lib/ocaml/Seed_Client.ast +0 -0
  87. package/lib/ocaml/Seed_Client.res +228 -15
  88. package/lib/ocaml/Seed_ClientTest-ReventlessSeed.cmi +0 -0
  89. package/lib/ocaml/Seed_ClientTest-ReventlessSeed.cmj +0 -0
  90. package/lib/ocaml/Seed_ClientTest-ReventlessSeed.cmt +0 -0
  91. package/lib/ocaml/Seed_ClientTest.ast +0 -0
  92. package/lib/ocaml/Seed_ClientTest.res +157 -0
  93. package/lib/ocaml/Seed_Connect-ReventlessSeed.cmi +0 -0
  94. package/lib/ocaml/Seed_Connect-ReventlessSeed.cmj +0 -0
  95. package/lib/ocaml/Seed_Connect-ReventlessSeed.cmt +0 -0
  96. package/lib/ocaml/Seed_Connect.ast +0 -0
  97. package/lib/ocaml/Seed_Connect.res +27 -18
  98. package/lib/ocaml/Seed_Prompt-ReventlessSeed.cmi +0 -0
  99. package/lib/ocaml/Seed_Prompt-ReventlessSeed.cmj +0 -0
  100. package/lib/ocaml/Seed_Prompt-ReventlessSeed.cmt +0 -0
  101. package/lib/ocaml/Seed_Prompt.ast +0 -0
  102. package/lib/ocaml/Seed_Prompt.res +110 -35
  103. package/lib/ocaml/Seed_PromptTest-ReventlessSeed.cmi +0 -0
  104. package/lib/ocaml/Seed_PromptTest-ReventlessSeed.cmj +0 -0
  105. package/lib/ocaml/Seed_PromptTest-ReventlessSeed.cmt +0 -0
  106. package/lib/ocaml/Seed_PromptTest.ast +0 -0
  107. package/lib/ocaml/Seed_PromptTest.res +59 -0
  108. package/lib/ocaml/Seed_Random-ReventlessSeed.cmt +0 -0
  109. package/lib/ocaml/Seed_RandomTest-ReventlessSeed.cmi +0 -0
  110. package/lib/ocaml/Seed_RandomTest-ReventlessSeed.cmj +0 -0
  111. package/lib/ocaml/Seed_RandomTest-ReventlessSeed.cmt +0 -0
  112. package/lib/ocaml/Seed_RandomTest.ast +0 -0
  113. package/lib/ocaml/Seed_RandomTest.res +160 -0
  114. package/lib/ocaml/Seed_Runner-ReventlessSeed.cmi +0 -0
  115. package/lib/ocaml/Seed_Runner-ReventlessSeed.cmj +0 -0
  116. package/lib/ocaml/Seed_Runner-ReventlessSeed.cmt +0 -0
  117. package/lib/ocaml/Seed_Runner.ast +0 -0
  118. package/lib/ocaml/Seed_Runner.res +38 -7
  119. package/lib/ocaml/Seed_Types-ReventlessSeed.cmt +0 -0
  120. package/lib/ocaml/Seed_Upload-ReventlessSeed.cmi +0 -0
  121. package/lib/ocaml/Seed_Upload-ReventlessSeed.cmj +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 +40 -42
  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 +80 -0
  130. package/lib/ocaml/Seed_UsersTest-ReventlessSeed.cmi +0 -0
  131. package/lib/ocaml/Seed_UsersTest-ReventlessSeed.cmj +0 -0
  132. package/lib/ocaml/Seed_UsersTest-ReventlessSeed.cmt +0 -0
  133. package/lib/ocaml/Seed_UsersTest.ast +0 -0
  134. package/lib/ocaml/Seed_UsersTest.res +92 -0
  135. package/package.json +7 -2
  136. package/rescript.json +9 -1
  137. package/src/Seed.res +6 -3
  138. package/src/Seed.res.mjs +3 -0
  139. package/src/Seed_Client.res +228 -15
  140. package/src/Seed_Client.res.mjs +196 -23
  141. package/src/Seed_Connect.res +27 -18
  142. package/src/Seed_Connect.res.mjs +10 -6
  143. package/src/Seed_Prompt.res +110 -35
  144. package/src/Seed_Prompt.res.mjs +73 -8
  145. package/src/Seed_Runner.res +38 -7
  146. package/src/Seed_Runner.res.mjs +19 -1
  147. package/src/Seed_Upload.res +40 -42
  148. package/src/Seed_Upload.res.mjs +65 -69
  149. package/src/Seed_Users.res +80 -0
  150. package/src/Seed_Users.res.mjs +90 -0
  151. package/tests/Seed_ClientTest.res +157 -0
  152. package/tests/Seed_ClientTest.res.mjs +167 -0
  153. package/tests/Seed_PromptTest.res +59 -0
  154. package/tests/Seed_PromptTest.res.mjs +63 -0
  155. package/tests/Seed_RandomTest.res +160 -0
  156. package/tests/Seed_RandomTest.res.mjs +208 -0
  157. package/tests/Seed_UsersTest.res +92 -0
  158. 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 res;
111
- try {
112
- res = await fetch(t.config.endpoint, {
113
- method: "POST",
114
- headers: headers,
115
- body: body
116
- });
117
- } catch (exn) {
118
- throw {
119
- RE_EXN_ID: Seed_Types$ReventlessSeed.Failed,
120
- _1: label + `: cannot reach ` + t.config.endpoint + ` — is the platform running?`,
121
- Error: new Error()
122
- };
123
- }
124
- let json = await res.json();
125
- let errors = field(json, "errors");
126
- if (errors !== undefined) {
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 Stdlib_Option.getOr(field(json, "data"), null);
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
@@ -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 endpoint (empty when the deployment serves no uploads), and
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
- type connection = {client: Seed_Client.t, uploadEndpoint: string, label: string}
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 connection to carry no upload endpoint, so a
27
- // data set's upload phase no-ops the same way it does when a deployment serves
28
- // none. Use it to seed domain data fast, or to skip a broken/absent upload path
29
- // without editing the data set. It is the single knob: an empty
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
- {client, uploadEndpoint, label}
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=?, ~upload=?, ~login=?, ()): (unit => promise<connection>) => {
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, uploadEndpoint, login, localDefaultsOpt) {
9
+ async function make(label, endpoint, login, localDefaultsOpt) {
9
10
  let localDefaults = localDefaultsOpt !== undefined ? localDefaultsOpt : false;
10
- let uploadEndpoint$1 = Stdlib_Option.isSome(Seed_Prompt$ReventlessSeed.envValue("SEED_SKIP_UPLOADS")) ? "" : uploadEndpoint;
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
- uploadEndpoint: uploadEndpoint$1,
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, upload, login, param) {
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, uploadEndpoint, viaLoginEndpoint(loginEndpoint), true);
57
+ return () => make("local", endpoint, viaLoginEndpoint(loginEndpoint), true);
54
58
  }
55
59
 
56
60
  export {
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->writeStream(s)
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->pauseStream
64
- stdin->unrefStream
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 processEnv->Dict.get(key) {
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->writeStream("\n")
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(`\nSelect [1-${(options->Array.length)->Int.toString}]: `)
150
- switch Int.fromString(answer) {
151
- | Some(n) if n >= 1 && n <= options->Array.length =>
152
- let (_, value) = options->Array.getUnsafe(n - 1)
153
- value
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
- Console.log(" Not a valid choice.")
156
- await pick()
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
- * Prompts for a username (visible) and password (echo muted).
165
- *
166
- * `REVENTLESS_DEMO_USER` / `REVENTLESS_DEMO_PASSWORD` skip the prompts. With
167
- * `localDefaults`, empty input falls back to `admin`/`admin` — the dev login.
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
  }