@reventlessdev/reventless-seed 1.0.0-alpha.21 → 1.0.0-alpha.23

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 (84) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/lib/bs/.compiler.log +2 -2
  3. package/lib/bs/compiler-info.json +1 -1
  4. package/lib/bs/src/Seed-ReventlessSeed.cmi +0 -0
  5. package/lib/bs/src/Seed-ReventlessSeed.cmt +0 -0
  6. package/lib/bs/src/Seed.ast +0 -0
  7. package/lib/bs/src/Seed.res +1 -0
  8. package/lib/bs/src/Seed_Client-ReventlessSeed.cmi +0 -0
  9. package/lib/bs/src/Seed_Client-ReventlessSeed.cmt +0 -0
  10. package/lib/bs/src/Seed_Connect-ReventlessSeed.cmi +0 -0
  11. package/lib/bs/src/Seed_Connect-ReventlessSeed.cmt +0 -0
  12. package/lib/bs/src/Seed_Prompt-ReventlessSeed.cmi +0 -0
  13. package/lib/bs/src/Seed_Prompt-ReventlessSeed.cmt +0 -0
  14. package/lib/bs/src/Seed_Prompt.ast +0 -0
  15. package/lib/bs/src/Seed_Prompt.res +1 -0
  16. package/lib/bs/src/Seed_Prompt.res.mjs +2 -1
  17. package/lib/bs/src/Seed_Runner-ReventlessSeed.cmi +0 -0
  18. package/lib/bs/src/Seed_Runner-ReventlessSeed.cmt +0 -0
  19. package/lib/bs/src/Seed_Runner.ast +0 -0
  20. package/lib/bs/src/Seed_Runner.res +8 -0
  21. package/lib/bs/src/Seed_Runner.res.mjs +4 -0
  22. package/lib/bs/src/Seed_Upload-ReventlessSeed.cmi +0 -0
  23. package/lib/bs/src/Seed_Upload-ReventlessSeed.cmt +0 -0
  24. package/lib/bs/src/Seed_Users-ReventlessSeed.cmi +0 -0
  25. package/lib/bs/src/Seed_Users-ReventlessSeed.cmj +0 -0
  26. package/lib/bs/src/Seed_Users-ReventlessSeed.cmt +0 -0
  27. package/lib/bs/src/Seed_Users.ast +0 -0
  28. package/lib/bs/src/Seed_Users.res +16 -1
  29. package/lib/bs/src/Seed_Users.res.mjs +13 -1
  30. package/lib/bs/tests/Seed_ClientTest-ReventlessSeed.cmi +0 -0
  31. package/lib/bs/tests/Seed_ClientTest-ReventlessSeed.cmt +0 -0
  32. package/lib/bs/tests/Seed_ConnectTest-ReventlessSeed.cmi +0 -0
  33. package/lib/bs/tests/Seed_ConnectTest-ReventlessSeed.cmt +0 -0
  34. package/lib/bs/tests/Seed_PromptTest-ReventlessSeed.cmi +0 -0
  35. package/lib/bs/tests/Seed_PromptTest-ReventlessSeed.cmt +0 -0
  36. package/lib/bs/tests/Seed_UsersTest-ReventlessSeed.cmi +0 -0
  37. package/lib/bs/tests/Seed_UsersTest-ReventlessSeed.cmt +0 -0
  38. package/lib/bs/tests/Seed_UsersTest.ast +0 -0
  39. package/lib/bs/tests/Seed_UsersTest.res +39 -1
  40. package/lib/bs/tests/Seed_UsersTest.res.mjs +28 -3
  41. package/lib/ocaml/.compiler.log +2 -2
  42. package/lib/ocaml/Seed-ReventlessSeed.cmi +0 -0
  43. package/lib/ocaml/Seed-ReventlessSeed.cmt +0 -0
  44. package/lib/ocaml/Seed.ast +0 -0
  45. package/lib/ocaml/Seed.res +1 -0
  46. package/lib/ocaml/Seed_Client-ReventlessSeed.cmi +0 -0
  47. package/lib/ocaml/Seed_Client-ReventlessSeed.cmt +0 -0
  48. package/lib/ocaml/Seed_ClientTest-ReventlessSeed.cmi +0 -0
  49. package/lib/ocaml/Seed_ClientTest-ReventlessSeed.cmt +0 -0
  50. package/lib/ocaml/Seed_Connect-ReventlessSeed.cmi +0 -0
  51. package/lib/ocaml/Seed_Connect-ReventlessSeed.cmt +0 -0
  52. package/lib/ocaml/Seed_ConnectTest-ReventlessSeed.cmi +0 -0
  53. package/lib/ocaml/Seed_ConnectTest-ReventlessSeed.cmt +0 -0
  54. package/lib/ocaml/Seed_Prompt-ReventlessSeed.cmi +0 -0
  55. package/lib/ocaml/Seed_Prompt-ReventlessSeed.cmt +0 -0
  56. package/lib/ocaml/Seed_Prompt.ast +0 -0
  57. package/lib/ocaml/Seed_Prompt.res +1 -0
  58. package/lib/ocaml/Seed_PromptTest-ReventlessSeed.cmi +0 -0
  59. package/lib/ocaml/Seed_PromptTest-ReventlessSeed.cmt +0 -0
  60. package/lib/ocaml/Seed_Runner-ReventlessSeed.cmi +0 -0
  61. package/lib/ocaml/Seed_Runner-ReventlessSeed.cmt +0 -0
  62. package/lib/ocaml/Seed_Runner.ast +0 -0
  63. package/lib/ocaml/Seed_Runner.res +8 -0
  64. package/lib/ocaml/Seed_Upload-ReventlessSeed.cmi +0 -0
  65. package/lib/ocaml/Seed_Upload-ReventlessSeed.cmt +0 -0
  66. package/lib/ocaml/Seed_Users-ReventlessSeed.cmi +0 -0
  67. package/lib/ocaml/Seed_Users-ReventlessSeed.cmj +0 -0
  68. package/lib/ocaml/Seed_Users-ReventlessSeed.cmt +0 -0
  69. package/lib/ocaml/Seed_Users.ast +0 -0
  70. package/lib/ocaml/Seed_Users.res +16 -1
  71. package/lib/ocaml/Seed_UsersTest-ReventlessSeed.cmi +0 -0
  72. package/lib/ocaml/Seed_UsersTest-ReventlessSeed.cmt +0 -0
  73. package/lib/ocaml/Seed_UsersTest.ast +0 -0
  74. package/lib/ocaml/Seed_UsersTest.res +39 -1
  75. package/package.json +2 -2
  76. package/src/Seed.res +1 -0
  77. package/src/Seed_Prompt.res +1 -0
  78. package/src/Seed_Prompt.res.mjs +2 -1
  79. package/src/Seed_Runner.res +8 -0
  80. package/src/Seed_Runner.res.mjs +4 -0
  81. package/src/Seed_Users.res +16 -1
  82. package/src/Seed_Users.res.mjs +13 -1
  83. package/tests/Seed_UsersTest.res +39 -1
  84. package/tests/Seed_UsersTest.res.mjs +28 -3
package/CHANGELOG.md CHANGED
@@ -3,6 +3,20 @@
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.23 (2026-09-17)
7
+
8
+ ### Features
9
+
10
+ * **seed:** let a data set refuse to start before anything is sent ([6fc5135](https://github.com/ReventlessDev/reventless-core/commit/6fc5135107f9af3016a48423cfee625216babca1))
11
+
12
+
13
+ # 1.0.0-alpha.22 (2026-09-16)
14
+
15
+ ### Bug Fixes
16
+
17
+ * **aws:** make the demo accounts work on a pool that signs in by email ([97b036f](https://github.com/ReventlessDev/reventless-core/commit/97b036f86979ba55dbbf93d460fe8076c35abf1a))
18
+
19
+
6
20
  # 1.0.0-alpha.21 (2026-09-09)
7
21
 
8
22
  ### Bug Fixes
@@ -1,2 +1,2 @@
1
- #Start(1788992686773)
2
- #Done(1788992686834)
1
+ #Start(1789645504278)
2
+ #Done(1789645504316)
@@ -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": "1788992686837"
7
+ "generated_at": "1789645504318"
8
8
  }
Binary file
Binary file
Binary file
@@ -36,6 +36,7 @@ type dataSet = Seed_Runner.dataSet = {
36
36
  label: string,
37
37
  seed: connection => promise<unit>,
38
38
  probeViews?: array<string>,
39
+ preflight?: connection => promise<unit>,
39
40
  }
40
41
 
41
42
  type rec value = Seed_Types.value =
Binary file
@@ -197,6 +197,7 @@ let typedUser = (~username: string, ~password: string): Seed_Users.user => {
197
197
  password,
198
198
  groups: [],
199
199
  userId: None,
200
+ demoOwner: None,
200
201
  }
201
202
 
202
203
  // Typing both halves is the fallback, not the first offer: it is what happens
@@ -151,7 +151,8 @@ function typedUser(username, password) {
151
151
  username: username,
152
152
  password: password,
153
153
  groups: [],
154
- userId: undefined
154
+ userId: undefined,
155
+ demoOwner: undefined
155
156
  };
156
157
  }
157
158
 
Binary file
@@ -151,6 +151,10 @@ type dataSet = {
151
151
  // View names probed by the fresh-store guard before any command is sent; a
152
152
  // non-empty one aborts the run at startup. Omit to skip the guard.
153
153
  probeViews?: array<string>,
154
+ // Any other check that must pass before a command is sent — a set that adds to
155
+ // an existing store refuses an empty one here. Throw `Failed` to refuse; the
156
+ // run then reports that nothing was written.
157
+ preflight?: Seed_Connect.connection => promise<unit>,
154
158
  }
155
159
 
156
160
  /**
@@ -205,6 +209,10 @@ let seed = (~sets: array<dataSet>, ~connect: unit => promise<Seed_Connect.connec
205
209
  // Runs inside this pre-seed `try`, so refusing a non-empty store (or a
206
210
  // failed probe query) is reported as "did not start", not "half-seeded".
207
211
  await assertStoreEmpty(connection.client, ~probeViews=chosen.probeViews->Option.getOr([]))
212
+ switch chosen.preflight {
213
+ | Some(check) => await check(connection)
214
+ | None => ()
215
+ }
208
216
  Some((chosen, connection))
209
217
  } catch {
210
218
  | Failed(message) =>
@@ -163,6 +163,10 @@ function seed(sets, connect) {
163
163
  let connection = await connect();
164
164
  Seed_Prompt$ReventlessSeed.close();
165
165
  await assertStoreEmpty(connection.client, Stdlib_Option.getOr(chosen.probeViews, []));
166
+ let check = chosen.preflight;
167
+ if (check !== undefined) {
168
+ await check(connection);
169
+ }
166
170
  started = [
167
171
  chosen,
168
172
  connection
Binary file
@@ -24,6 +24,7 @@ type user = {
24
24
  password: string,
25
25
  groups: array<string>,
26
26
  userId: option<string>,
27
+ demoOwner: option<string>,
27
28
  }
28
29
 
29
30
  let asString = (json: JSON.t): option<string> =>
@@ -44,7 +45,13 @@ let userOf = (json: JSON.t): option<user> =>
44
45
  | Some(JSON.Array(items)) => items->Array.filterMap(asString)
45
46
  | _ => []
46
47
  }
47
- Some({username, password, groups, userId: obj->Dict.get("userId")->Option.flatMap(asString)})
48
+ Some({
49
+ username,
50
+ password,
51
+ groups,
52
+ userId: obj->Dict.get("userId")->Option.flatMap(asString),
53
+ demoOwner: obj->Dict.get("demoOwner")->Option.flatMap(asString),
54
+ })
48
55
  | _ => None
49
56
  }
50
57
  | _ => None
@@ -87,6 +94,14 @@ let load = (~path: option<string>=?): option<(string, array<user>)> => {
87
94
  users->Array.length == 0 ? None : Some((file, users))
88
95
  }
89
96
 
97
+ /** The account that plays `name` in a demo: the one whose `demoOwner` says so,
98
+ else the one named `name` — which is how a file without the field reads. */
99
+ let playing = (users: array<user>, name: string): option<user> =>
100
+ switch users->Array.find(u => u.demoOwner == Some(name)) {
101
+ | Some(_) as declared => declared
102
+ | None => users->Array.find(u => u.username == name)
103
+ }
104
+
90
105
  /** Menu label: the username, plus whatever groups the file records for it. */
91
106
  let label = (u: user): string =>
92
107
  u.groups->Array.length == 0 ? u.username : `${u.username} [${u.groups->Array.join(", ")}]`
@@ -5,6 +5,7 @@ import * as Nodefs from "node:fs";
5
5
  import * as Nodepath from "node:path";
6
6
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
7
7
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
8
+ import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
8
9
 
9
10
  function asString(json) {
10
11
  if (typeof json === "string") {
@@ -32,7 +33,8 @@ function userOf(json) {
32
33
  username: match,
33
34
  password: match$1,
34
35
  groups: groups,
35
- userId: Stdlib_Option.flatMap(json["userId"], asString)
36
+ userId: Stdlib_Option.flatMap(json["userId"], asString),
37
+ demoOwner: Stdlib_Option.flatMap(json["demoOwner"], asString)
36
38
  };
37
39
  }
38
40
 
@@ -72,6 +74,15 @@ function load(path) {
72
74
  }
73
75
  }
74
76
 
77
+ function playing(users, name) {
78
+ let declared = users.find(u => Primitive_object.equal(u.demoOwner, name));
79
+ if (declared !== undefined) {
80
+ return declared;
81
+ } else {
82
+ return users.find(u => u.username === name);
83
+ }
84
+ }
85
+
75
86
  function label(u) {
76
87
  if (u.groups.length === 0) {
77
88
  return u.username;
@@ -86,6 +97,7 @@ export {
86
97
  parseString,
87
98
  defaultPath,
88
99
  load,
100
+ playing,
89
101
  label,
90
102
  }
91
103
  /* yaml Not a pure module */
Binary file
@@ -95,6 +95,36 @@ describe("Seed_Users.load:", () => {
95
95
  })
96
96
  })
97
97
 
98
+ // A pool that signs in on an email address takes `shopper@example.com`, not
99
+ // `shopper`, so a demo cannot find its accounts by username alone there.
100
+ describe("Seed_Users.playing:", () => {
101
+ let users = Seed_Users.parseString(`
102
+ - username: shopper@example.com
103
+ password: pw
104
+ groups: [Shopper]
105
+ demoOwner: shopper
106
+ - username: admin
107
+ password: pw
108
+ groups: [Admin]
109
+ `)
110
+
111
+ testSync("finds the account whose demoOwner names the person", () => {
112
+ expect(
113
+ Seed_Users.playing(users, "shopper")->Option.map(u => u.Seed_Users.username),
114
+ )->Expect.toEqual(Some("shopper@example.com"))
115
+ })
116
+
117
+ testSync("falls back to the username when no entry declares one", () => {
118
+ expect(
119
+ Seed_Users.playing(users, "admin")->Option.map(u => u.Seed_Users.username),
120
+ )->Expect.toEqual(Some("admin"))
121
+ })
122
+
123
+ testSync("finds nobody for a person no entry plays", () => {
124
+ expect(Seed_Users.playing(users, "merch"))->Expect.toEqual(None)
125
+ })
126
+ })
127
+
98
128
  describe("Seed_Users.label:", () => {
99
129
  testSync("names the groups an account carries", () => {
100
130
  expect(
@@ -103,13 +133,20 @@ describe("Seed_Users.label:", () => {
103
133
  password: "x",
104
134
  groups: ["Admin", "Shopper"],
105
135
  userId: None,
136
+ demoOwner: None,
106
137
  }),
107
138
  )->Expect.toBe("admin [Admin, Shopper]")
108
139
  })
109
140
 
110
141
  testSync("is the bare username when the file records no groups", () => {
111
142
  expect(
112
- Seed_Users.label({username: "admin", password: "x", groups: [], userId: None}),
143
+ Seed_Users.label({
144
+ username: "admin",
145
+ password: "x",
146
+ groups: [],
147
+ userId: None,
148
+ demoOwner: None,
149
+ }),
113
150
  )->Expect.toBe("admin")
114
151
  })
115
152
 
@@ -123,6 +160,7 @@ describe("Seed_Users.label:", () => {
123
160
  password: "x",
124
161
  groups: ["Admin"],
125
162
  userId: Some("4275e4a4-00c1-70e1-1cb5-363b06b992b5"),
163
+ demoOwner: None,
126
164
  }),
127
165
  )->Expect.toBe("admin [Admin]")
128
166
  })
@@ -1,5 +1,6 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
3
4
  import * as Seed_Users$ReventlessSeed from "../src/Seed_Users.res.mjs";
4
5
 
5
6
  let document = `
@@ -77,6 +78,27 @@ globalThis.describe("Seed_Users.load:", () => {
77
78
  });
78
79
  });
79
80
 
81
+ globalThis.describe("Seed_Users.playing:", () => {
82
+ let users = Seed_Users$ReventlessSeed.parseString(`
83
+ - username: shopper@example.com
84
+ password: pw
85
+ groups: [Shopper]
86
+ demoOwner: shopper
87
+ - username: admin
88
+ password: pw
89
+ groups: [Admin]
90
+ `);
91
+ globalThis.test("finds the account whose demoOwner names the person", () => {
92
+ globalThis.expect(Stdlib_Option.map(Seed_Users$ReventlessSeed.playing(users, "shopper"), u => u.username)).toEqual("shopper@example.com");
93
+ });
94
+ globalThis.test("falls back to the username when no entry declares one", () => {
95
+ globalThis.expect(Stdlib_Option.map(Seed_Users$ReventlessSeed.playing(users, "admin"), u => u.username)).toEqual("admin");
96
+ });
97
+ globalThis.test("finds nobody for a person no entry plays", () => {
98
+ globalThis.expect(Seed_Users$ReventlessSeed.playing(users, "merch")).toEqual(undefined);
99
+ });
100
+ });
101
+
80
102
  globalThis.describe("Seed_Users.label:", () => {
81
103
  globalThis.test("names the groups an account carries", () => {
82
104
  globalThis.expect(Seed_Users$ReventlessSeed.label({
@@ -86,7 +108,8 @@ globalThis.describe("Seed_Users.label:", () => {
86
108
  "Admin",
87
109
  "Shopper"
88
110
  ],
89
- userId: undefined
111
+ userId: undefined,
112
+ demoOwner: undefined
90
113
  })).toBe("admin [Admin, Shopper]");
91
114
  });
92
115
  globalThis.test("is the bare username when the file records no groups", () => {
@@ -94,7 +117,8 @@ globalThis.describe("Seed_Users.label:", () => {
94
117
  username: "admin",
95
118
  password: "x",
96
119
  groups: [],
97
- userId: undefined
120
+ userId: undefined,
121
+ demoOwner: undefined
98
122
  })).toBe("admin");
99
123
  });
100
124
  globalThis.test("says nothing about the userId", () => {
@@ -102,7 +126,8 @@ globalThis.describe("Seed_Users.label:", () => {
102
126
  username: "admin",
103
127
  password: "x",
104
128
  groups: ["Admin"],
105
- userId: "4275e4a4-00c1-70e1-1cb5-363b06b992b5"
129
+ userId: "4275e4a4-00c1-70e1-1cb5-363b06b992b5",
130
+ demoOwner: undefined
106
131
  })).toBe("admin [Admin]");
107
132
  });
108
133
  });
@@ -1,2 +1,2 @@
1
- #Start(1788992686773)
2
- #Done(1788992686834)
1
+ #Start(1789645504278)
2
+ #Done(1789645504316)
Binary file
Binary file
Binary file
@@ -36,6 +36,7 @@ type dataSet = Seed_Runner.dataSet = {
36
36
  label: string,
37
37
  seed: connection => promise<unit>,
38
38
  probeViews?: array<string>,
39
+ preflight?: connection => promise<unit>,
39
40
  }
40
41
 
41
42
  type rec value = Seed_Types.value =
Binary file
@@ -197,6 +197,7 @@ let typedUser = (~username: string, ~password: string): Seed_Users.user => {
197
197
  password,
198
198
  groups: [],
199
199
  userId: None,
200
+ demoOwner: None,
200
201
  }
201
202
 
202
203
  // Typing both halves is the fallback, not the first offer: it is what happens
Binary file
@@ -151,6 +151,10 @@ type dataSet = {
151
151
  // View names probed by the fresh-store guard before any command is sent; a
152
152
  // non-empty one aborts the run at startup. Omit to skip the guard.
153
153
  probeViews?: array<string>,
154
+ // Any other check that must pass before a command is sent — a set that adds to
155
+ // an existing store refuses an empty one here. Throw `Failed` to refuse; the
156
+ // run then reports that nothing was written.
157
+ preflight?: Seed_Connect.connection => promise<unit>,
154
158
  }
155
159
 
156
160
  /**
@@ -205,6 +209,10 @@ let seed = (~sets: array<dataSet>, ~connect: unit => promise<Seed_Connect.connec
205
209
  // Runs inside this pre-seed `try`, so refusing a non-empty store (or a
206
210
  // failed probe query) is reported as "did not start", not "half-seeded".
207
211
  await assertStoreEmpty(connection.client, ~probeViews=chosen.probeViews->Option.getOr([]))
212
+ switch chosen.preflight {
213
+ | Some(check) => await check(connection)
214
+ | None => ()
215
+ }
208
216
  Some((chosen, connection))
209
217
  } catch {
210
218
  | Failed(message) =>
Binary file
@@ -24,6 +24,7 @@ type user = {
24
24
  password: string,
25
25
  groups: array<string>,
26
26
  userId: option<string>,
27
+ demoOwner: option<string>,
27
28
  }
28
29
 
29
30
  let asString = (json: JSON.t): option<string> =>
@@ -44,7 +45,13 @@ let userOf = (json: JSON.t): option<user> =>
44
45
  | Some(JSON.Array(items)) => items->Array.filterMap(asString)
45
46
  | _ => []
46
47
  }
47
- Some({username, password, groups, userId: obj->Dict.get("userId")->Option.flatMap(asString)})
48
+ Some({
49
+ username,
50
+ password,
51
+ groups,
52
+ userId: obj->Dict.get("userId")->Option.flatMap(asString),
53
+ demoOwner: obj->Dict.get("demoOwner")->Option.flatMap(asString),
54
+ })
48
55
  | _ => None
49
56
  }
50
57
  | _ => None
@@ -87,6 +94,14 @@ let load = (~path: option<string>=?): option<(string, array<user>)> => {
87
94
  users->Array.length == 0 ? None : Some((file, users))
88
95
  }
89
96
 
97
+ /** The account that plays `name` in a demo: the one whose `demoOwner` says so,
98
+ else the one named `name` — which is how a file without the field reads. */
99
+ let playing = (users: array<user>, name: string): option<user> =>
100
+ switch users->Array.find(u => u.demoOwner == Some(name)) {
101
+ | Some(_) as declared => declared
102
+ | None => users->Array.find(u => u.username == name)
103
+ }
104
+
90
105
  /** Menu label: the username, plus whatever groups the file records for it. */
91
106
  let label = (u: user): string =>
92
107
  u.groups->Array.length == 0 ? u.username : `${u.username} [${u.groups->Array.join(", ")}]`
Binary file
@@ -95,6 +95,36 @@ describe("Seed_Users.load:", () => {
95
95
  })
96
96
  })
97
97
 
98
+ // A pool that signs in on an email address takes `shopper@example.com`, not
99
+ // `shopper`, so a demo cannot find its accounts by username alone there.
100
+ describe("Seed_Users.playing:", () => {
101
+ let users = Seed_Users.parseString(`
102
+ - username: shopper@example.com
103
+ password: pw
104
+ groups: [Shopper]
105
+ demoOwner: shopper
106
+ - username: admin
107
+ password: pw
108
+ groups: [Admin]
109
+ `)
110
+
111
+ testSync("finds the account whose demoOwner names the person", () => {
112
+ expect(
113
+ Seed_Users.playing(users, "shopper")->Option.map(u => u.Seed_Users.username),
114
+ )->Expect.toEqual(Some("shopper@example.com"))
115
+ })
116
+
117
+ testSync("falls back to the username when no entry declares one", () => {
118
+ expect(
119
+ Seed_Users.playing(users, "admin")->Option.map(u => u.Seed_Users.username),
120
+ )->Expect.toEqual(Some("admin"))
121
+ })
122
+
123
+ testSync("finds nobody for a person no entry plays", () => {
124
+ expect(Seed_Users.playing(users, "merch"))->Expect.toEqual(None)
125
+ })
126
+ })
127
+
98
128
  describe("Seed_Users.label:", () => {
99
129
  testSync("names the groups an account carries", () => {
100
130
  expect(
@@ -103,13 +133,20 @@ describe("Seed_Users.label:", () => {
103
133
  password: "x",
104
134
  groups: ["Admin", "Shopper"],
105
135
  userId: None,
136
+ demoOwner: None,
106
137
  }),
107
138
  )->Expect.toBe("admin [Admin, Shopper]")
108
139
  })
109
140
 
110
141
  testSync("is the bare username when the file records no groups", () => {
111
142
  expect(
112
- Seed_Users.label({username: "admin", password: "x", groups: [], userId: None}),
143
+ Seed_Users.label({
144
+ username: "admin",
145
+ password: "x",
146
+ groups: [],
147
+ userId: None,
148
+ demoOwner: None,
149
+ }),
113
150
  )->Expect.toBe("admin")
114
151
  })
115
152
 
@@ -123,6 +160,7 @@ describe("Seed_Users.label:", () => {
123
160
  password: "x",
124
161
  groups: ["Admin"],
125
162
  userId: Some("4275e4a4-00c1-70e1-1cb5-363b06b992b5"),
163
+ demoOwner: None,
126
164
  }),
127
165
  )->Expect.toBe("admin [Admin]")
128
166
  })
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-seed",
3
- "version": "1.0.0-alpha.21",
3
+ "version": "1.0.0-alpha.23",
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
7
  "yaml": "^2.8.3",
8
- "@reventlessdev/rescript-node": "2.0.0-alpha.11"
8
+ "@reventlessdev/rescript-node": "2.0.0-alpha.12"
9
9
  },
10
10
  "devDependencies": {
11
11
  "rescript": "12.3.0",
package/src/Seed.res CHANGED
@@ -36,6 +36,7 @@ type dataSet = Seed_Runner.dataSet = {
36
36
  label: string,
37
37
  seed: connection => promise<unit>,
38
38
  probeViews?: array<string>,
39
+ preflight?: connection => promise<unit>,
39
40
  }
40
41
 
41
42
  type rec value = Seed_Types.value =
@@ -197,6 +197,7 @@ let typedUser = (~username: string, ~password: string): Seed_Users.user => {
197
197
  password,
198
198
  groups: [],
199
199
  userId: None,
200
+ demoOwner: None,
200
201
  }
201
202
 
202
203
  // Typing both halves is the fallback, not the first offer: it is what happens
@@ -151,7 +151,8 @@ function typedUser(username, password) {
151
151
  username: username,
152
152
  password: password,
153
153
  groups: [],
154
- userId: undefined
154
+ userId: undefined,
155
+ demoOwner: undefined
155
156
  };
156
157
  }
157
158
 
@@ -151,6 +151,10 @@ type dataSet = {
151
151
  // View names probed by the fresh-store guard before any command is sent; a
152
152
  // non-empty one aborts the run at startup. Omit to skip the guard.
153
153
  probeViews?: array<string>,
154
+ // Any other check that must pass before a command is sent — a set that adds to
155
+ // an existing store refuses an empty one here. Throw `Failed` to refuse; the
156
+ // run then reports that nothing was written.
157
+ preflight?: Seed_Connect.connection => promise<unit>,
154
158
  }
155
159
 
156
160
  /**
@@ -205,6 +209,10 @@ let seed = (~sets: array<dataSet>, ~connect: unit => promise<Seed_Connect.connec
205
209
  // Runs inside this pre-seed `try`, so refusing a non-empty store (or a
206
210
  // failed probe query) is reported as "did not start", not "half-seeded".
207
211
  await assertStoreEmpty(connection.client, ~probeViews=chosen.probeViews->Option.getOr([]))
212
+ switch chosen.preflight {
213
+ | Some(check) => await check(connection)
214
+ | None => ()
215
+ }
208
216
  Some((chosen, connection))
209
217
  } catch {
210
218
  | Failed(message) =>
@@ -163,6 +163,10 @@ function seed(sets, connect) {
163
163
  let connection = await connect();
164
164
  Seed_Prompt$ReventlessSeed.close();
165
165
  await assertStoreEmpty(connection.client, Stdlib_Option.getOr(chosen.probeViews, []));
166
+ let check = chosen.preflight;
167
+ if (check !== undefined) {
168
+ await check(connection);
169
+ }
166
170
  started = [
167
171
  chosen,
168
172
  connection
@@ -24,6 +24,7 @@ type user = {
24
24
  password: string,
25
25
  groups: array<string>,
26
26
  userId: option<string>,
27
+ demoOwner: option<string>,
27
28
  }
28
29
 
29
30
  let asString = (json: JSON.t): option<string> =>
@@ -44,7 +45,13 @@ let userOf = (json: JSON.t): option<user> =>
44
45
  | Some(JSON.Array(items)) => items->Array.filterMap(asString)
45
46
  | _ => []
46
47
  }
47
- Some({username, password, groups, userId: obj->Dict.get("userId")->Option.flatMap(asString)})
48
+ Some({
49
+ username,
50
+ password,
51
+ groups,
52
+ userId: obj->Dict.get("userId")->Option.flatMap(asString),
53
+ demoOwner: obj->Dict.get("demoOwner")->Option.flatMap(asString),
54
+ })
48
55
  | _ => None
49
56
  }
50
57
  | _ => None
@@ -87,6 +94,14 @@ let load = (~path: option<string>=?): option<(string, array<user>)> => {
87
94
  users->Array.length == 0 ? None : Some((file, users))
88
95
  }
89
96
 
97
+ /** The account that plays `name` in a demo: the one whose `demoOwner` says so,
98
+ else the one named `name` — which is how a file without the field reads. */
99
+ let playing = (users: array<user>, name: string): option<user> =>
100
+ switch users->Array.find(u => u.demoOwner == Some(name)) {
101
+ | Some(_) as declared => declared
102
+ | None => users->Array.find(u => u.username == name)
103
+ }
104
+
90
105
  /** Menu label: the username, plus whatever groups the file records for it. */
91
106
  let label = (u: user): string =>
92
107
  u.groups->Array.length == 0 ? u.username : `${u.username} [${u.groups->Array.join(", ")}]`
@@ -5,6 +5,7 @@ import * as Nodefs from "node:fs";
5
5
  import * as Nodepath from "node:path";
6
6
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
7
7
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
8
+ import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
8
9
 
9
10
  function asString(json) {
10
11
  if (typeof json === "string") {
@@ -32,7 +33,8 @@ function userOf(json) {
32
33
  username: match,
33
34
  password: match$1,
34
35
  groups: groups,
35
- userId: Stdlib_Option.flatMap(json["userId"], asString)
36
+ userId: Stdlib_Option.flatMap(json["userId"], asString),
37
+ demoOwner: Stdlib_Option.flatMap(json["demoOwner"], asString)
36
38
  };
37
39
  }
38
40
 
@@ -72,6 +74,15 @@ function load(path) {
72
74
  }
73
75
  }
74
76
 
77
+ function playing(users, name) {
78
+ let declared = users.find(u => Primitive_object.equal(u.demoOwner, name));
79
+ if (declared !== undefined) {
80
+ return declared;
81
+ } else {
82
+ return users.find(u => u.username === name);
83
+ }
84
+ }
85
+
75
86
  function label(u) {
76
87
  if (u.groups.length === 0) {
77
88
  return u.username;
@@ -86,6 +97,7 @@ export {
86
97
  parseString,
87
98
  defaultPath,
88
99
  load,
100
+ playing,
89
101
  label,
90
102
  }
91
103
  /* yaml Not a pure module */
@@ -95,6 +95,36 @@ describe("Seed_Users.load:", () => {
95
95
  })
96
96
  })
97
97
 
98
+ // A pool that signs in on an email address takes `shopper@example.com`, not
99
+ // `shopper`, so a demo cannot find its accounts by username alone there.
100
+ describe("Seed_Users.playing:", () => {
101
+ let users = Seed_Users.parseString(`
102
+ - username: shopper@example.com
103
+ password: pw
104
+ groups: [Shopper]
105
+ demoOwner: shopper
106
+ - username: admin
107
+ password: pw
108
+ groups: [Admin]
109
+ `)
110
+
111
+ testSync("finds the account whose demoOwner names the person", () => {
112
+ expect(
113
+ Seed_Users.playing(users, "shopper")->Option.map(u => u.Seed_Users.username),
114
+ )->Expect.toEqual(Some("shopper@example.com"))
115
+ })
116
+
117
+ testSync("falls back to the username when no entry declares one", () => {
118
+ expect(
119
+ Seed_Users.playing(users, "admin")->Option.map(u => u.Seed_Users.username),
120
+ )->Expect.toEqual(Some("admin"))
121
+ })
122
+
123
+ testSync("finds nobody for a person no entry plays", () => {
124
+ expect(Seed_Users.playing(users, "merch"))->Expect.toEqual(None)
125
+ })
126
+ })
127
+
98
128
  describe("Seed_Users.label:", () => {
99
129
  testSync("names the groups an account carries", () => {
100
130
  expect(
@@ -103,13 +133,20 @@ describe("Seed_Users.label:", () => {
103
133
  password: "x",
104
134
  groups: ["Admin", "Shopper"],
105
135
  userId: None,
136
+ demoOwner: None,
106
137
  }),
107
138
  )->Expect.toBe("admin [Admin, Shopper]")
108
139
  })
109
140
 
110
141
  testSync("is the bare username when the file records no groups", () => {
111
142
  expect(
112
- Seed_Users.label({username: "admin", password: "x", groups: [], userId: None}),
143
+ Seed_Users.label({
144
+ username: "admin",
145
+ password: "x",
146
+ groups: [],
147
+ userId: None,
148
+ demoOwner: None,
149
+ }),
113
150
  )->Expect.toBe("admin")
114
151
  })
115
152
 
@@ -123,6 +160,7 @@ describe("Seed_Users.label:", () => {
123
160
  password: "x",
124
161
  groups: ["Admin"],
125
162
  userId: Some("4275e4a4-00c1-70e1-1cb5-363b06b992b5"),
163
+ demoOwner: None,
126
164
  }),
127
165
  )->Expect.toBe("admin [Admin]")
128
166
  })
@@ -1,5 +1,6 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
3
4
  import * as Seed_Users$ReventlessSeed from "../src/Seed_Users.res.mjs";
4
5
 
5
6
  let document = `
@@ -77,6 +78,27 @@ globalThis.describe("Seed_Users.load:", () => {
77
78
  });
78
79
  });
79
80
 
81
+ globalThis.describe("Seed_Users.playing:", () => {
82
+ let users = Seed_Users$ReventlessSeed.parseString(`
83
+ - username: shopper@example.com
84
+ password: pw
85
+ groups: [Shopper]
86
+ demoOwner: shopper
87
+ - username: admin
88
+ password: pw
89
+ groups: [Admin]
90
+ `);
91
+ globalThis.test("finds the account whose demoOwner names the person", () => {
92
+ globalThis.expect(Stdlib_Option.map(Seed_Users$ReventlessSeed.playing(users, "shopper"), u => u.username)).toEqual("shopper@example.com");
93
+ });
94
+ globalThis.test("falls back to the username when no entry declares one", () => {
95
+ globalThis.expect(Stdlib_Option.map(Seed_Users$ReventlessSeed.playing(users, "admin"), u => u.username)).toEqual("admin");
96
+ });
97
+ globalThis.test("finds nobody for a person no entry plays", () => {
98
+ globalThis.expect(Seed_Users$ReventlessSeed.playing(users, "merch")).toEqual(undefined);
99
+ });
100
+ });
101
+
80
102
  globalThis.describe("Seed_Users.label:", () => {
81
103
  globalThis.test("names the groups an account carries", () => {
82
104
  globalThis.expect(Seed_Users$ReventlessSeed.label({
@@ -86,7 +108,8 @@ globalThis.describe("Seed_Users.label:", () => {
86
108
  "Admin",
87
109
  "Shopper"
88
110
  ],
89
- userId: undefined
111
+ userId: undefined,
112
+ demoOwner: undefined
90
113
  })).toBe("admin [Admin, Shopper]");
91
114
  });
92
115
  globalThis.test("is the bare username when the file records no groups", () => {
@@ -94,7 +117,8 @@ globalThis.describe("Seed_Users.label:", () => {
94
117
  username: "admin",
95
118
  password: "x",
96
119
  groups: [],
97
- userId: undefined
120
+ userId: undefined,
121
+ demoOwner: undefined
98
122
  })).toBe("admin");
99
123
  });
100
124
  globalThis.test("says nothing about the userId", () => {
@@ -102,7 +126,8 @@ globalThis.describe("Seed_Users.label:", () => {
102
126
  username: "admin",
103
127
  password: "x",
104
128
  groups: ["Admin"],
105
- userId: "4275e4a4-00c1-70e1-1cb5-363b06b992b5"
129
+ userId: "4275e4a4-00c1-70e1-1cb5-363b06b992b5",
130
+ demoOwner: undefined
106
131
  })).toBe("admin [Admin]");
107
132
  });
108
133
  });