@reventlessdev/reventless-seed 1.0.0-alpha.14 → 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.
- package/CHANGELOG.md +8 -0
- package/lib/bs/.compiler.log +2 -2
- package/lib/bs/ReventlessSeed.cmi +0 -0
- package/lib/bs/ReventlessSeed.cmt +0 -0
- package/lib/bs/ReventlessSeed.mlmap +1 -0
- package/lib/bs/compiler-info.json +1 -1
- package/lib/bs/src/Seed-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Client-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Client-ReventlessSeed.cmj +0 -0
- package/lib/bs/src/Seed_Client-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Client.ast +0 -0
- package/lib/bs/src/Seed_Client.res +15 -5
- package/lib/bs/src/Seed_Client.res.mjs +8 -5
- package/lib/bs/src/Seed_Connect-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Connect-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Prompt-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Prompt-ReventlessSeed.cmj +0 -0
- package/lib/bs/src/Seed_Prompt-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Prompt.ast +0 -0
- package/lib/bs/src/Seed_Prompt.res +18 -0
- package/lib/bs/src/Seed_Prompt.res.mjs +13 -0
- package/lib/bs/src/Seed_Runner-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Runner-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Upload-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Upload-ReventlessSeed.cmt +0 -0
- package/lib/bs/tests/Seed_PromptTest-ReventlessSeed.cmi +0 -0
- package/lib/bs/tests/Seed_PromptTest-ReventlessSeed.cmj +0 -0
- package/lib/bs/tests/Seed_PromptTest-ReventlessSeed.cmt +0 -0
- package/lib/bs/tests/Seed_PromptTest.ast +0 -0
- package/lib/bs/tests/Seed_PromptTest.res +59 -0
- package/lib/bs/tests/Seed_PromptTest.res.mjs +63 -0
- package/lib/bs/tests/Seed_RandomTest-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/.compiler.log +2 -2
- package/lib/ocaml/ReventlessSeed.cmi +0 -0
- package/lib/ocaml/ReventlessSeed.cmt +0 -0
- package/lib/ocaml/ReventlessSeed.mlmap +1 -0
- package/lib/ocaml/Seed-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Client-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Client-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_Client-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Client.ast +0 -0
- package/lib/ocaml/Seed_Client.res +15 -5
- package/lib/ocaml/Seed_Connect-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Connect-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Prompt-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Prompt-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_Prompt-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Prompt.ast +0 -0
- package/lib/ocaml/Seed_Prompt.res +18 -0
- package/lib/ocaml/Seed_PromptTest-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_PromptTest-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_PromptTest-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_PromptTest.ast +0 -0
- package/lib/ocaml/Seed_PromptTest.res +59 -0
- package/lib/ocaml/Seed_RandomTest-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Runner-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Runner-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Upload-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Upload-ReventlessSeed.cmt +0 -0
- package/package.json +1 -1
- package/src/Seed_Client.res +15 -5
- package/src/Seed_Client.res.mjs +8 -5
- package/src/Seed_Prompt.res +18 -0
- package/src/Seed_Prompt.res.mjs +13 -0
- package/tests/Seed_PromptTest.res +59 -0
- package/tests/Seed_PromptTest.res.mjs +63 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
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
|
+
|
|
6
14
|
# 1.0.0-alpha.14 (2026-08-15)
|
|
7
15
|
|
|
8
16
|
**Note:** Version bump only for package @reventlessdev/reventless-seed
|
package/lib/bs/.compiler.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
#Start(
|
|
2
|
-
#Done(
|
|
1
|
+
#Start(1786876637985)
|
|
2
|
+
#Done(1786876638019)
|
|
Binary file
|
|
Binary file
|
|
@@ -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": "
|
|
7
|
+
"generated_at": "1786876638021"
|
|
8
8
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
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
|
-
|
|
183
|
-
|
|
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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
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 */
|
|
Binary file
|
package/lib/ocaml/.compiler.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
#Start(
|
|
2
|
-
#Done(
|
|
1
|
+
#Start(1786876637985)
|
|
2
|
+
#Done(1786876638019)
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
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
|
-
|
|
183
|
-
|
|
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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
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
|
|
Binary file
|
|
Binary file
|
|
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
|
+
})
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
package/src/Seed_Client.res
CHANGED
|
@@ -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
|
-
|
|
183
|
-
|
|
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 {
|
package/src/Seed_Client.res.mjs
CHANGED
|
@@ -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 {
|
package/src/Seed_Prompt.res
CHANGED
|
@@ -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
|
*
|
package/src/Seed_Prompt.res.mjs
CHANGED
|
@@ -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 */
|