@reventlessdev/reventless-seed 1.0.0-alpha.13 → 1.0.0-alpha.15

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 (68) hide show
  1. package/CHANGELOG.md +16 -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_Client-ReventlessSeed.cmi +0 -0
  10. package/lib/bs/src/Seed_Client-ReventlessSeed.cmj +0 -0
  11. package/lib/bs/src/Seed_Client-ReventlessSeed.cmt +0 -0
  12. package/lib/bs/src/Seed_Client.ast +0 -0
  13. package/lib/bs/src/Seed_Client.res +15 -5
  14. package/lib/bs/src/Seed_Client.res.mjs +8 -5
  15. package/lib/bs/src/Seed_Connect-ReventlessSeed.cmi +0 -0
  16. package/lib/bs/src/Seed_Connect-ReventlessSeed.cmt +0 -0
  17. package/lib/bs/src/Seed_Prompt-ReventlessSeed.cmi +0 -0
  18. package/lib/bs/src/Seed_Prompt-ReventlessSeed.cmj +0 -0
  19. package/lib/bs/src/Seed_Prompt-ReventlessSeed.cmt +0 -0
  20. package/lib/bs/src/Seed_Prompt.ast +0 -0
  21. package/lib/bs/src/Seed_Prompt.res +18 -0
  22. package/lib/bs/src/Seed_Prompt.res.mjs +13 -0
  23. package/lib/bs/src/Seed_Runner-ReventlessSeed.cmi +0 -0
  24. package/lib/bs/src/Seed_Runner-ReventlessSeed.cmt +0 -0
  25. package/lib/bs/src/Seed_Upload-ReventlessSeed.cmi +0 -0
  26. package/lib/bs/src/Seed_Upload-ReventlessSeed.cmt +0 -0
  27. package/lib/bs/tests/Seed_PromptTest-ReventlessSeed.cmi +0 -0
  28. package/lib/bs/tests/Seed_PromptTest-ReventlessSeed.cmj +0 -0
  29. package/lib/bs/tests/Seed_PromptTest-ReventlessSeed.cmt +0 -0
  30. package/lib/bs/tests/Seed_PromptTest.ast +0 -0
  31. package/lib/bs/tests/Seed_PromptTest.res +59 -0
  32. package/lib/bs/tests/Seed_PromptTest.res.mjs +63 -0
  33. package/lib/bs/tests/Seed_RandomTest-ReventlessSeed.cmt +0 -0
  34. package/lib/ocaml/.compiler.log +2 -2
  35. package/lib/ocaml/ReventlessSeed.cmi +0 -0
  36. package/lib/ocaml/ReventlessSeed.cmt +0 -0
  37. package/lib/ocaml/ReventlessSeed.mlmap +1 -0
  38. package/lib/ocaml/Seed-ReventlessSeed.cmi +0 -0
  39. package/lib/ocaml/Seed-ReventlessSeed.cmt +0 -0
  40. package/lib/ocaml/Seed_Client-ReventlessSeed.cmi +0 -0
  41. package/lib/ocaml/Seed_Client-ReventlessSeed.cmj +0 -0
  42. package/lib/ocaml/Seed_Client-ReventlessSeed.cmt +0 -0
  43. package/lib/ocaml/Seed_Client.ast +0 -0
  44. package/lib/ocaml/Seed_Client.res +15 -5
  45. package/lib/ocaml/Seed_Connect-ReventlessSeed.cmi +0 -0
  46. package/lib/ocaml/Seed_Connect-ReventlessSeed.cmt +0 -0
  47. package/lib/ocaml/Seed_Prompt-ReventlessSeed.cmi +0 -0
  48. package/lib/ocaml/Seed_Prompt-ReventlessSeed.cmj +0 -0
  49. package/lib/ocaml/Seed_Prompt-ReventlessSeed.cmt +0 -0
  50. package/lib/ocaml/Seed_Prompt.ast +0 -0
  51. package/lib/ocaml/Seed_Prompt.res +18 -0
  52. package/lib/ocaml/Seed_PromptTest-ReventlessSeed.cmi +0 -0
  53. package/lib/ocaml/Seed_PromptTest-ReventlessSeed.cmj +0 -0
  54. package/lib/ocaml/Seed_PromptTest-ReventlessSeed.cmt +0 -0
  55. package/lib/ocaml/Seed_PromptTest.ast +0 -0
  56. package/lib/ocaml/Seed_PromptTest.res +59 -0
  57. package/lib/ocaml/Seed_RandomTest-ReventlessSeed.cmt +0 -0
  58. package/lib/ocaml/Seed_Runner-ReventlessSeed.cmi +0 -0
  59. package/lib/ocaml/Seed_Runner-ReventlessSeed.cmt +0 -0
  60. package/lib/ocaml/Seed_Upload-ReventlessSeed.cmi +0 -0
  61. package/lib/ocaml/Seed_Upload-ReventlessSeed.cmt +0 -0
  62. package/package.json +2 -2
  63. package/src/Seed_Client.res +15 -5
  64. package/src/Seed_Client.res.mjs +8 -5
  65. package/src/Seed_Prompt.res +18 -0
  66. package/src/Seed_Prompt.res.mjs +13 -0
  67. package/tests/Seed_PromptTest.res +59 -0
  68. package/tests/Seed_PromptTest.res.mjs +63 -0
package/CHANGELOG.md CHANGED
@@ -3,6 +3,22 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # 1.0.0-alpha.15 (2026-08-16)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **seed:** let seed:reset take the yes it asks for ([1f646f6](https://github.com/ReventlessDev/reventless-core/commit/1f646f6ce13a4238e92b7047ee31650ccc4de4f3))
11
+ * **seed:** report the retirements the projection has actually applied ([91bc877](https://github.com/ReventlessDev/reventless-core/commit/91bc877d90fc69dab76ff0fb9aae9ea989107aed))
12
+
13
+
14
+ # 1.0.0-alpha.14 (2026-08-15)
15
+
16
+ **Note:** Version bump only for package @reventlessdev/reventless-seed
17
+
18
+
19
+
20
+
21
+
6
22
  # 1.0.0-alpha.13 (2026-08-14)
7
23
 
8
24
  **Note:** Version bump only for package @reventlessdev/reventless-seed
@@ -1,2 +1,2 @@
1
- #Start(1786706473029)
2
- #Done(1786706473070)
1
+ #Start(1786876637985)
2
+ #Done(1786876638019)
Binary file
Binary file
@@ -3,6 +3,7 @@ Seed
3
3
  Seed_Client
4
4
  Seed_Connect
5
5
  Seed_Prompt
6
+ Seed_PromptTest
6
7
  Seed_Random
7
8
  Seed_RandomTest
8
9
  Seed_Runner
@@ -4,5 +4,5 @@
4
4
  "bsc_hash": "75e3a59c95cc953608fd3dd6ea6ed68141bba4414a20a52f114261b8a48385fa",
5
5
  "rescript_config_hash": "25cfff836d200084949fdfdfec2f6875901bfee1f04d84634eca252ef1ca1ef1",
6
6
  "runtime_path": "/home/runner/work/reventless-core/reventless-core/node_modules/@rescript/runtime",
7
- "generated_at": "1786706473073"
7
+ "generated_at": "1786876638021"
8
8
  }
Binary file
Binary file
Binary file
@@ -179,18 +179,27 @@ let sendAll = async (t: t, mutations: array<mutation>): unit =>
179
179
  * Walks a Relay-style connection to the end. Connections page at 50 by default,
180
180
  * so any count taken from a single request silently truncates.
181
181
  */
182
- let queryAllNodes = async (t: t, ~field as fieldName: string, ~selection: string): array<
183
- JSON.t,
184
- > => {
182
+ // `~args` is spliced into the connection's argument list — `"includeRetired: true"`
183
+ // is the case it exists for, and the only way to read rows the resolvers withhold
184
+ // by default. Literal GraphQL rather than a typed argument list because a seed
185
+ // writes the query it means; the alternative is a builder that has to grow a case
186
+ // per argument the platform adds.
187
+ let queryAllNodes = async (
188
+ t: t,
189
+ ~field as fieldName: string,
190
+ ~selection: string,
191
+ ~args: string="",
192
+ ): array<JSON.t> => {
185
193
  let nodes = []
186
194
  let after = ref(None)
187
195
  let more = ref(true)
196
+ let extra = args == "" ? "" : `, ${args}`
188
197
  while more.contents {
189
198
  let cursor = switch after.contents {
190
199
  | Some(c) => `, after: ${quote(c)}`
191
200
  | None => ""
192
201
  }
193
- let query = `{ ${fieldName}(first: 100${cursor}) { edges { node { ${selection} } } pageInfo { hasNextPage endCursor } } }`
202
+ let query = `{ ${fieldName}(first: 100${extra}${cursor}) { edges { node { ${selection} } } pageInfo { hasNextPage endCursor } } }`
194
203
  let data = await gql(t, ~query, ~label=fieldName)
195
204
  let connection = data->field(fieldName)->Option.getOr(JSON.Encode.null)
196
205
  switch connection->field("edges") {
@@ -237,6 +246,7 @@ let queryAllNodesUntil = async (
237
246
  t: t,
238
247
  ~field: string,
239
248
  ~selection: string,
249
+ ~args: string="",
240
250
  ~satisfied: array<JSON.t> => bool,
241
251
  ~onTimeout: array<JSON.t> => string,
242
252
  ~timeoutMs: int=60000,
@@ -244,7 +254,7 @@ let queryAllNodesUntil = async (
244
254
  let deadline = nowMs() +. Int.toFloat(timeoutMs)
245
255
  let result = ref(None)
246
256
  while result.contents == None {
247
- let nodes = await queryAllNodes(t, ~field, ~selection)
257
+ let nodes = await queryAllNodes(t, ~field, ~selection, ~args)
248
258
  if satisfied(nodes) {
249
259
  result := Some(nodes)
250
260
  } else if nowMs() > deadline {
@@ -175,14 +175,16 @@ async function sendAll(t, mutations) {
175
175
  }
176
176
  }
177
177
 
178
- async function queryAllNodes(t, fieldName, selection) {
178
+ async function queryAllNodes(t, fieldName, selection, argsOpt) {
179
+ let args = argsOpt !== undefined ? argsOpt : "";
179
180
  let nodes = [];
180
181
  let after;
181
182
  let more = true;
183
+ let extra = args === "" ? "" : `, ` + args;
182
184
  while (more) {
183
185
  let c = after;
184
186
  let cursor = c !== undefined ? `, after: ` + Seed_Types$ReventlessSeed.quote(c) : "";
185
- let query = `{ ` + fieldName + `(first: 100` + cursor + `) { edges { node { ` + selection + ` } } pageInfo { hasNextPage endCursor } } }`;
187
+ let query = `{ ` + fieldName + `(first: 100` + extra + cursor + `) { edges { node { ` + selection + ` } } pageInfo { hasNextPage endCursor } } }`;
186
188
  let data = await gql(t, query, fieldName);
187
189
  let connection = Stdlib_Option.getOr(field(data, fieldName), null);
188
190
  let match = field(connection, "edges");
@@ -209,15 +211,16 @@ async function queryAllNodes(t, fieldName, selection) {
209
211
  }
210
212
 
211
213
  async function countNodes(t, field) {
212
- return (await queryAllNodes(t, field, "id")).length;
214
+ return (await queryAllNodes(t, field, "id", undefined)).length;
213
215
  }
214
216
 
215
- async function queryAllNodesUntil(t, field, selection, satisfied, onTimeout, timeoutMsOpt) {
217
+ async function queryAllNodesUntil(t, field, selection, argsOpt, satisfied, onTimeout, timeoutMsOpt) {
218
+ let args = argsOpt !== undefined ? argsOpt : "";
216
219
  let timeoutMs = timeoutMsOpt !== undefined ? timeoutMsOpt : 60000;
217
220
  let deadline = Date.now() + timeoutMs;
218
221
  let result;
219
222
  while (result === undefined) {
220
- let nodes = await queryAllNodes(t, field, selection);
223
+ let nodes = await queryAllNodes(t, field, selection, args);
221
224
  if (satisfied(nodes)) {
222
225
  result = nodes;
223
226
  } else {
Binary file
@@ -97,6 +97,24 @@ let askHidden = (query: string): promise<string> =>
97
97
  muted := true
98
98
  })
99
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
+
100
118
  /**
101
119
  * Chooses one of `options` (a label paired with the value it selects).
102
120
  *
@@ -82,6 +82,18 @@ function askHidden(query) {
82
82
  });
83
83
  }
84
84
 
85
+ function isAffirmative(answer) {
86
+ let match = answer.trim().toLowerCase();
87
+ switch (match) {
88
+ case "1" :
89
+ case "y" :
90
+ case "yes" :
91
+ return true;
92
+ default:
93
+ return false;
94
+ }
95
+ }
96
+
85
97
  async function select(title, options, env) {
86
98
  if (options.length === 1) {
87
99
  return options[0][1];
@@ -166,6 +178,7 @@ export {
166
178
  requireTty,
167
179
  ask,
168
180
  askHidden,
181
+ isAffirmative,
169
182
  select,
170
183
  credentials,
171
184
  }
Binary file
@@ -0,0 +1,59 @@
1
+ open JestGlobals
2
+
3
+ // `isAffirmative` is the one vocabulary for "yes", shared by a typed `[y/N]`
4
+ // reply and by the env var that stands in for it on a run with no terminal.
5
+ //
6
+ // It exists because those two drifted: the prompt took `y`, the env var took
7
+ // only the literals `1` and `yes`, and every other value — `y` included, and an
8
+ // explicit `no` with it — fell through to the interactive branch, which throws
9
+ // when there is no TTY. So the variable that exists to make the tool
10
+ // non-interactive had two spellings that worked and a crash for the rest.
11
+ //
12
+ // The suite is written around the shape of that bug rather than around the
13
+ // function: what must confirm, what must decline, and — the case that actually
14
+ // broke CI — that declining is a normal answer and not a fall-through.
15
+
16
+ describe("Seed_Prompt.isAffirmative:", () => {
17
+ testSync("takes the three spellings of yes", () => {
18
+ ["y", "yes", "1"]->Array.forEach(v =>
19
+ expect(Seed_Prompt.isAffirmative(v))->Expect.toBe(true)
20
+ )
21
+ })
22
+
23
+ // A prompt that prints `[y/N]` invites a capital as readily as a lowercase
24
+ // one, and an env var set by hand is written however the hand felt.
25
+ testSync("ignores case, so Y and YES answer as y does", () => {
26
+ ["Y", "Yes", "YES"]->Array.forEach(v =>
27
+ expect(Seed_Prompt.isAffirmative(v))->Expect.toBe(true)
28
+ )
29
+ })
30
+
31
+ testSync("ignores surrounding space", () => {
32
+ [" y", "y ", " yes "]->Array.forEach(v =>
33
+ expect(Seed_Prompt.isAffirmative(v))->Expect.toBe(true)
34
+ )
35
+ })
36
+
37
+ // The half that matters most: every one of these is an ANSWER, and the caller
38
+ // reads it as "no". None may be mistaken for "nothing was said", which is what
39
+ // sent the old code to a prompt it could not open.
40
+ testSync("declines every spelling of no, capital N included", () => {
41
+ ["n", "N", "no", "No", "NO"]->Array.forEach(v =>
42
+ expect(Seed_Prompt.isAffirmative(v))->Expect.toBe(false)
43
+ )
44
+ })
45
+
46
+ testSync("declines anything it does not recognise", () => {
47
+ ["", " ", "0", "true", "yep", "y.", "yesplease", "affirmative"]->Array.forEach(v =>
48
+ expect(Seed_Prompt.isAffirmative(v))->Expect.toBe(false)
49
+ )
50
+ })
51
+
52
+ // `true` is deliberately absent from the vocabulary above. Named here so that
53
+ // adding it is a decision someone takes against a failing test rather than a
54
+ // kindness someone slips in: the prompt this stands in for asks `[y/N]`, and a
55
+ // reply nobody would type at that prompt should not work around it either.
56
+ testSync("does not quietly grow a fourth spelling", () => {
57
+ expect(Seed_Prompt.isAffirmative("true"))->Expect.toBe(false)
58
+ })
59
+ })
@@ -0,0 +1,63 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Seed_Prompt$ReventlessSeed from "../src/Seed_Prompt.res.mjs";
4
+
5
+ globalThis.describe("Seed_Prompt.isAffirmative:", () => {
6
+ globalThis.test("takes the three spellings of yes", () => {
7
+ [
8
+ "y",
9
+ "yes",
10
+ "1"
11
+ ].forEach(v => {
12
+ globalThis.expect(Seed_Prompt$ReventlessSeed.isAffirmative(v)).toBe(true);
13
+ });
14
+ });
15
+ globalThis.test("ignores case, so Y and YES answer as y does", () => {
16
+ [
17
+ "Y",
18
+ "Yes",
19
+ "YES"
20
+ ].forEach(v => {
21
+ globalThis.expect(Seed_Prompt$ReventlessSeed.isAffirmative(v)).toBe(true);
22
+ });
23
+ });
24
+ globalThis.test("ignores surrounding space", () => {
25
+ [
26
+ " y",
27
+ "y ",
28
+ " yes "
29
+ ].forEach(v => {
30
+ globalThis.expect(Seed_Prompt$ReventlessSeed.isAffirmative(v)).toBe(true);
31
+ });
32
+ });
33
+ globalThis.test("declines every spelling of no, capital N included", () => {
34
+ [
35
+ "n",
36
+ "N",
37
+ "no",
38
+ "No",
39
+ "NO"
40
+ ].forEach(v => {
41
+ globalThis.expect(Seed_Prompt$ReventlessSeed.isAffirmative(v)).toBe(false);
42
+ });
43
+ });
44
+ globalThis.test("declines anything it does not recognise", () => {
45
+ [
46
+ "",
47
+ " ",
48
+ "0",
49
+ "true",
50
+ "yep",
51
+ "y.",
52
+ "yesplease",
53
+ "affirmative"
54
+ ].forEach(v => {
55
+ globalThis.expect(Seed_Prompt$ReventlessSeed.isAffirmative(v)).toBe(false);
56
+ });
57
+ });
58
+ globalThis.test("does not quietly grow a fourth spelling", () => {
59
+ globalThis.expect(Seed_Prompt$ReventlessSeed.isAffirmative("true")).toBe(false);
60
+ });
61
+ });
62
+
63
+ /* Not a pure module */
@@ -1,2 +1,2 @@
1
- #Start(1786706473029)
2
- #Done(1786706473070)
1
+ #Start(1786876637985)
2
+ #Done(1786876638019)
Binary file
Binary file
@@ -3,6 +3,7 @@ Seed
3
3
  Seed_Client
4
4
  Seed_Connect
5
5
  Seed_Prompt
6
+ Seed_PromptTest
6
7
  Seed_Random
7
8
  Seed_RandomTest
8
9
  Seed_Runner
Binary file
Binary file
Binary file
@@ -179,18 +179,27 @@ let sendAll = async (t: t, mutations: array<mutation>): unit =>
179
179
  * Walks a Relay-style connection to the end. Connections page at 50 by default,
180
180
  * so any count taken from a single request silently truncates.
181
181
  */
182
- let queryAllNodes = async (t: t, ~field as fieldName: string, ~selection: string): array<
183
- JSON.t,
184
- > => {
182
+ // `~args` is spliced into the connection's argument list — `"includeRetired: true"`
183
+ // is the case it exists for, and the only way to read rows the resolvers withhold
184
+ // by default. Literal GraphQL rather than a typed argument list because a seed
185
+ // writes the query it means; the alternative is a builder that has to grow a case
186
+ // per argument the platform adds.
187
+ let queryAllNodes = async (
188
+ t: t,
189
+ ~field as fieldName: string,
190
+ ~selection: string,
191
+ ~args: string="",
192
+ ): array<JSON.t> => {
185
193
  let nodes = []
186
194
  let after = ref(None)
187
195
  let more = ref(true)
196
+ let extra = args == "" ? "" : `, ${args}`
188
197
  while more.contents {
189
198
  let cursor = switch after.contents {
190
199
  | Some(c) => `, after: ${quote(c)}`
191
200
  | None => ""
192
201
  }
193
- let query = `{ ${fieldName}(first: 100${cursor}) { edges { node { ${selection} } } pageInfo { hasNextPage endCursor } } }`
202
+ let query = `{ ${fieldName}(first: 100${extra}${cursor}) { edges { node { ${selection} } } pageInfo { hasNextPage endCursor } } }`
194
203
  let data = await gql(t, ~query, ~label=fieldName)
195
204
  let connection = data->field(fieldName)->Option.getOr(JSON.Encode.null)
196
205
  switch connection->field("edges") {
@@ -237,6 +246,7 @@ let queryAllNodesUntil = async (
237
246
  t: t,
238
247
  ~field: string,
239
248
  ~selection: string,
249
+ ~args: string="",
240
250
  ~satisfied: array<JSON.t> => bool,
241
251
  ~onTimeout: array<JSON.t> => string,
242
252
  ~timeoutMs: int=60000,
@@ -244,7 +254,7 @@ let queryAllNodesUntil = async (
244
254
  let deadline = nowMs() +. Int.toFloat(timeoutMs)
245
255
  let result = ref(None)
246
256
  while result.contents == None {
247
- let nodes = await queryAllNodes(t, ~field, ~selection)
257
+ let nodes = await queryAllNodes(t, ~field, ~selection, ~args)
248
258
  if satisfied(nodes) {
249
259
  result := Some(nodes)
250
260
  } else if nowMs() > deadline {
Binary file
@@ -97,6 +97,24 @@ let askHidden = (query: string): promise<string> =>
97
97
  muted := true
98
98
  })
99
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
+
100
118
  /**
101
119
  * Chooses one of `options` (a label paired with the value it selects).
102
120
  *
Binary file
@@ -0,0 +1,59 @@
1
+ open JestGlobals
2
+
3
+ // `isAffirmative` is the one vocabulary for "yes", shared by a typed `[y/N]`
4
+ // reply and by the env var that stands in for it on a run with no terminal.
5
+ //
6
+ // It exists because those two drifted: the prompt took `y`, the env var took
7
+ // only the literals `1` and `yes`, and every other value — `y` included, and an
8
+ // explicit `no` with it — fell through to the interactive branch, which throws
9
+ // when there is no TTY. So the variable that exists to make the tool
10
+ // non-interactive had two spellings that worked and a crash for the rest.
11
+ //
12
+ // The suite is written around the shape of that bug rather than around the
13
+ // function: what must confirm, what must decline, and — the case that actually
14
+ // broke CI — that declining is a normal answer and not a fall-through.
15
+
16
+ describe("Seed_Prompt.isAffirmative:", () => {
17
+ testSync("takes the three spellings of yes", () => {
18
+ ["y", "yes", "1"]->Array.forEach(v =>
19
+ expect(Seed_Prompt.isAffirmative(v))->Expect.toBe(true)
20
+ )
21
+ })
22
+
23
+ // A prompt that prints `[y/N]` invites a capital as readily as a lowercase
24
+ // one, and an env var set by hand is written however the hand felt.
25
+ testSync("ignores case, so Y and YES answer as y does", () => {
26
+ ["Y", "Yes", "YES"]->Array.forEach(v =>
27
+ expect(Seed_Prompt.isAffirmative(v))->Expect.toBe(true)
28
+ )
29
+ })
30
+
31
+ testSync("ignores surrounding space", () => {
32
+ [" y", "y ", " yes "]->Array.forEach(v =>
33
+ expect(Seed_Prompt.isAffirmative(v))->Expect.toBe(true)
34
+ )
35
+ })
36
+
37
+ // The half that matters most: every one of these is an ANSWER, and the caller
38
+ // reads it as "no". None may be mistaken for "nothing was said", which is what
39
+ // sent the old code to a prompt it could not open.
40
+ testSync("declines every spelling of no, capital N included", () => {
41
+ ["n", "N", "no", "No", "NO"]->Array.forEach(v =>
42
+ expect(Seed_Prompt.isAffirmative(v))->Expect.toBe(false)
43
+ )
44
+ })
45
+
46
+ testSync("declines anything it does not recognise", () => {
47
+ ["", " ", "0", "true", "yep", "y.", "yesplease", "affirmative"]->Array.forEach(v =>
48
+ expect(Seed_Prompt.isAffirmative(v))->Expect.toBe(false)
49
+ )
50
+ })
51
+
52
+ // `true` is deliberately absent from the vocabulary above. Named here so that
53
+ // adding it is a decision someone takes against a failing test rather than a
54
+ // kindness someone slips in: the prompt this stands in for asks `[y/N]`, and a
55
+ // reply nobody would type at that prompt should not work around it either.
56
+ testSync("does not quietly grow a fourth spelling", () => {
57
+ expect(Seed_Prompt.isAffirmative("true"))->Expect.toBe(false)
58
+ })
59
+ })
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-seed",
3
- "version": "1.0.0-alpha.13",
3
+ "version": "1.0.0-alpha.15",
4
4
  "description": "Reusable harness for seeding a Reventless platform through its public GraphQL command API",
5
5
  "license": "Apache-2.0",
6
6
  "dependencies": {
7
- "@reventlessdev/rescript-node": "2.0.0-alpha.6"
7
+ "@reventlessdev/rescript-node": "2.0.0-alpha.7"
8
8
  },
9
9
  "devDependencies": {
10
10
  "rescript": "12.3.0",
@@ -179,18 +179,27 @@ let sendAll = async (t: t, mutations: array<mutation>): unit =>
179
179
  * Walks a Relay-style connection to the end. Connections page at 50 by default,
180
180
  * so any count taken from a single request silently truncates.
181
181
  */
182
- let queryAllNodes = async (t: t, ~field as fieldName: string, ~selection: string): array<
183
- JSON.t,
184
- > => {
182
+ // `~args` is spliced into the connection's argument list — `"includeRetired: true"`
183
+ // is the case it exists for, and the only way to read rows the resolvers withhold
184
+ // by default. Literal GraphQL rather than a typed argument list because a seed
185
+ // writes the query it means; the alternative is a builder that has to grow a case
186
+ // per argument the platform adds.
187
+ let queryAllNodes = async (
188
+ t: t,
189
+ ~field as fieldName: string,
190
+ ~selection: string,
191
+ ~args: string="",
192
+ ): array<JSON.t> => {
185
193
  let nodes = []
186
194
  let after = ref(None)
187
195
  let more = ref(true)
196
+ let extra = args == "" ? "" : `, ${args}`
188
197
  while more.contents {
189
198
  let cursor = switch after.contents {
190
199
  | Some(c) => `, after: ${quote(c)}`
191
200
  | None => ""
192
201
  }
193
- let query = `{ ${fieldName}(first: 100${cursor}) { edges { node { ${selection} } } pageInfo { hasNextPage endCursor } } }`
202
+ let query = `{ ${fieldName}(first: 100${extra}${cursor}) { edges { node { ${selection} } } pageInfo { hasNextPage endCursor } } }`
194
203
  let data = await gql(t, ~query, ~label=fieldName)
195
204
  let connection = data->field(fieldName)->Option.getOr(JSON.Encode.null)
196
205
  switch connection->field("edges") {
@@ -237,6 +246,7 @@ let queryAllNodesUntil = async (
237
246
  t: t,
238
247
  ~field: string,
239
248
  ~selection: string,
249
+ ~args: string="",
240
250
  ~satisfied: array<JSON.t> => bool,
241
251
  ~onTimeout: array<JSON.t> => string,
242
252
  ~timeoutMs: int=60000,
@@ -244,7 +254,7 @@ let queryAllNodesUntil = async (
244
254
  let deadline = nowMs() +. Int.toFloat(timeoutMs)
245
255
  let result = ref(None)
246
256
  while result.contents == None {
247
- let nodes = await queryAllNodes(t, ~field, ~selection)
257
+ let nodes = await queryAllNodes(t, ~field, ~selection, ~args)
248
258
  if satisfied(nodes) {
249
259
  result := Some(nodes)
250
260
  } else if nowMs() > deadline {
@@ -175,14 +175,16 @@ async function sendAll(t, mutations) {
175
175
  }
176
176
  }
177
177
 
178
- async function queryAllNodes(t, fieldName, selection) {
178
+ async function queryAllNodes(t, fieldName, selection, argsOpt) {
179
+ let args = argsOpt !== undefined ? argsOpt : "";
179
180
  let nodes = [];
180
181
  let after;
181
182
  let more = true;
183
+ let extra = args === "" ? "" : `, ` + args;
182
184
  while (more) {
183
185
  let c = after;
184
186
  let cursor = c !== undefined ? `, after: ` + Seed_Types$ReventlessSeed.quote(c) : "";
185
- let query = `{ ` + fieldName + `(first: 100` + cursor + `) { edges { node { ` + selection + ` } } pageInfo { hasNextPage endCursor } } }`;
187
+ let query = `{ ` + fieldName + `(first: 100` + extra + cursor + `) { edges { node { ` + selection + ` } } pageInfo { hasNextPage endCursor } } }`;
186
188
  let data = await gql(t, query, fieldName);
187
189
  let connection = Stdlib_Option.getOr(field(data, fieldName), null);
188
190
  let match = field(connection, "edges");
@@ -209,15 +211,16 @@ async function queryAllNodes(t, fieldName, selection) {
209
211
  }
210
212
 
211
213
  async function countNodes(t, field) {
212
- return (await queryAllNodes(t, field, "id")).length;
214
+ return (await queryAllNodes(t, field, "id", undefined)).length;
213
215
  }
214
216
 
215
- async function queryAllNodesUntil(t, field, selection, satisfied, onTimeout, timeoutMsOpt) {
217
+ async function queryAllNodesUntil(t, field, selection, argsOpt, satisfied, onTimeout, timeoutMsOpt) {
218
+ let args = argsOpt !== undefined ? argsOpt : "";
216
219
  let timeoutMs = timeoutMsOpt !== undefined ? timeoutMsOpt : 60000;
217
220
  let deadline = Date.now() + timeoutMs;
218
221
  let result;
219
222
  while (result === undefined) {
220
- let nodes = await queryAllNodes(t, field, selection);
223
+ let nodes = await queryAllNodes(t, field, selection, args);
221
224
  if (satisfied(nodes)) {
222
225
  result = nodes;
223
226
  } else {
@@ -97,6 +97,24 @@ let askHidden = (query: string): promise<string> =>
97
97
  muted := true
98
98
  })
99
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
+
100
118
  /**
101
119
  * Chooses one of `options` (a label paired with the value it selects).
102
120
  *
@@ -82,6 +82,18 @@ function askHidden(query) {
82
82
  });
83
83
  }
84
84
 
85
+ function isAffirmative(answer) {
86
+ let match = answer.trim().toLowerCase();
87
+ switch (match) {
88
+ case "1" :
89
+ case "y" :
90
+ case "yes" :
91
+ return true;
92
+ default:
93
+ return false;
94
+ }
95
+ }
96
+
85
97
  async function select(title, options, env) {
86
98
  if (options.length === 1) {
87
99
  return options[0][1];
@@ -166,6 +178,7 @@ export {
166
178
  requireTty,
167
179
  ask,
168
180
  askHidden,
181
+ isAffirmative,
169
182
  select,
170
183
  credentials,
171
184
  }
@@ -0,0 +1,59 @@
1
+ open JestGlobals
2
+
3
+ // `isAffirmative` is the one vocabulary for "yes", shared by a typed `[y/N]`
4
+ // reply and by the env var that stands in for it on a run with no terminal.
5
+ //
6
+ // It exists because those two drifted: the prompt took `y`, the env var took
7
+ // only the literals `1` and `yes`, and every other value — `y` included, and an
8
+ // explicit `no` with it — fell through to the interactive branch, which throws
9
+ // when there is no TTY. So the variable that exists to make the tool
10
+ // non-interactive had two spellings that worked and a crash for the rest.
11
+ //
12
+ // The suite is written around the shape of that bug rather than around the
13
+ // function: what must confirm, what must decline, and — the case that actually
14
+ // broke CI — that declining is a normal answer and not a fall-through.
15
+
16
+ describe("Seed_Prompt.isAffirmative:", () => {
17
+ testSync("takes the three spellings of yes", () => {
18
+ ["y", "yes", "1"]->Array.forEach(v =>
19
+ expect(Seed_Prompt.isAffirmative(v))->Expect.toBe(true)
20
+ )
21
+ })
22
+
23
+ // A prompt that prints `[y/N]` invites a capital as readily as a lowercase
24
+ // one, and an env var set by hand is written however the hand felt.
25
+ testSync("ignores case, so Y and YES answer as y does", () => {
26
+ ["Y", "Yes", "YES"]->Array.forEach(v =>
27
+ expect(Seed_Prompt.isAffirmative(v))->Expect.toBe(true)
28
+ )
29
+ })
30
+
31
+ testSync("ignores surrounding space", () => {
32
+ [" y", "y ", " yes "]->Array.forEach(v =>
33
+ expect(Seed_Prompt.isAffirmative(v))->Expect.toBe(true)
34
+ )
35
+ })
36
+
37
+ // The half that matters most: every one of these is an ANSWER, and the caller
38
+ // reads it as "no". None may be mistaken for "nothing was said", which is what
39
+ // sent the old code to a prompt it could not open.
40
+ testSync("declines every spelling of no, capital N included", () => {
41
+ ["n", "N", "no", "No", "NO"]->Array.forEach(v =>
42
+ expect(Seed_Prompt.isAffirmative(v))->Expect.toBe(false)
43
+ )
44
+ })
45
+
46
+ testSync("declines anything it does not recognise", () => {
47
+ ["", " ", "0", "true", "yep", "y.", "yesplease", "affirmative"]->Array.forEach(v =>
48
+ expect(Seed_Prompt.isAffirmative(v))->Expect.toBe(false)
49
+ )
50
+ })
51
+
52
+ // `true` is deliberately absent from the vocabulary above. Named here so that
53
+ // adding it is a decision someone takes against a failing test rather than a
54
+ // kindness someone slips in: the prompt this stands in for asks `[y/N]`, and a
55
+ // reply nobody would type at that prompt should not work around it either.
56
+ testSync("does not quietly grow a fourth spelling", () => {
57
+ expect(Seed_Prompt.isAffirmative("true"))->Expect.toBe(false)
58
+ })
59
+ })
@@ -0,0 +1,63 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Seed_Prompt$ReventlessSeed from "../src/Seed_Prompt.res.mjs";
4
+
5
+ globalThis.describe("Seed_Prompt.isAffirmative:", () => {
6
+ globalThis.test("takes the three spellings of yes", () => {
7
+ [
8
+ "y",
9
+ "yes",
10
+ "1"
11
+ ].forEach(v => {
12
+ globalThis.expect(Seed_Prompt$ReventlessSeed.isAffirmative(v)).toBe(true);
13
+ });
14
+ });
15
+ globalThis.test("ignores case, so Y and YES answer as y does", () => {
16
+ [
17
+ "Y",
18
+ "Yes",
19
+ "YES"
20
+ ].forEach(v => {
21
+ globalThis.expect(Seed_Prompt$ReventlessSeed.isAffirmative(v)).toBe(true);
22
+ });
23
+ });
24
+ globalThis.test("ignores surrounding space", () => {
25
+ [
26
+ " y",
27
+ "y ",
28
+ " yes "
29
+ ].forEach(v => {
30
+ globalThis.expect(Seed_Prompt$ReventlessSeed.isAffirmative(v)).toBe(true);
31
+ });
32
+ });
33
+ globalThis.test("declines every spelling of no, capital N included", () => {
34
+ [
35
+ "n",
36
+ "N",
37
+ "no",
38
+ "No",
39
+ "NO"
40
+ ].forEach(v => {
41
+ globalThis.expect(Seed_Prompt$ReventlessSeed.isAffirmative(v)).toBe(false);
42
+ });
43
+ });
44
+ globalThis.test("declines anything it does not recognise", () => {
45
+ [
46
+ "",
47
+ " ",
48
+ "0",
49
+ "true",
50
+ "yep",
51
+ "y.",
52
+ "yesplease",
53
+ "affirmative"
54
+ ].forEach(v => {
55
+ globalThis.expect(Seed_Prompt$ReventlessSeed.isAffirmative(v)).toBe(false);
56
+ });
57
+ });
58
+ globalThis.test("does not quietly grow a fourth spelling", () => {
59
+ globalThis.expect(Seed_Prompt$ReventlessSeed.isAffirmative("true")).toBe(false);
60
+ });
61
+ });
62
+
63
+ /* Not a pure module */