@reventlessdev/reventless-seed 1.0.0-alpha.1
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 +15 -0
- package/LICENSE +202 -0
- package/README.md +167 -0
- package/lib/bs/.compiler.log +2 -0
- package/lib/bs/ReventlessSeed.cmi +0 -0
- package/lib/bs/ReventlessSeed.cmj +0 -0
- package/lib/bs/ReventlessSeed.cmt +0 -0
- package/lib/bs/ReventlessSeed.mlmap +7 -0
- package/lib/bs/build.ninja +0 -0
- package/lib/bs/compiler-info.json +8 -0
- package/lib/bs/src/Seed-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed-ReventlessSeed.cmj +0 -0
- package/lib/bs/src/Seed-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed.ast +0 -0
- package/lib/bs/src/Seed.res +30 -0
- package/lib/bs/src/Seed.res.mjs +34 -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 +251 -0
- package/lib/bs/src/Seed_Client.res.mjs +242 -0
- package/lib/bs/src/Seed_Random-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Random-ReventlessSeed.cmj +0 -0
- package/lib/bs/src/Seed_Random-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Random.ast +0 -0
- package/lib/bs/src/Seed_Random.res +80 -0
- package/lib/bs/src/Seed_Random.res.mjs +86 -0
- package/lib/bs/src/Seed_Runner-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Runner-ReventlessSeed.cmj +0 -0
- package/lib/bs/src/Seed_Runner-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Runner.ast +0 -0
- package/lib/bs/src/Seed_Runner.res +114 -0
- package/lib/bs/src/Seed_Runner.res.mjs +118 -0
- package/lib/bs/src/Seed_Types-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Types-ReventlessSeed.cmj +0 -0
- package/lib/bs/src/Seed_Types-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Types.ast +0 -0
- package/lib/bs/src/Seed_Types.res +49 -0
- package/lib/bs/src/Seed_Types.res.mjs +79 -0
- package/lib/bs/src/Seed_Upload-ReventlessSeed.cmi +0 -0
- package/lib/bs/src/Seed_Upload-ReventlessSeed.cmj +0 -0
- package/lib/bs/src/Seed_Upload-ReventlessSeed.cmt +0 -0
- package/lib/bs/src/Seed_Upload.ast +0 -0
- package/lib/bs/src/Seed_Upload.res +99 -0
- package/lib/bs/src/Seed_Upload.res.mjs +118 -0
- package/lib/ocaml/.compiler.log +2 -0
- package/lib/ocaml/ReventlessSeed.cmi +0 -0
- package/lib/ocaml/ReventlessSeed.cmj +0 -0
- package/lib/ocaml/ReventlessSeed.cmt +0 -0
- package/lib/ocaml/ReventlessSeed.mlmap +7 -0
- package/lib/ocaml/Seed-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed.ast +0 -0
- package/lib/ocaml/Seed.res +30 -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 +251 -0
- package/lib/ocaml/Seed_Random-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Random-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_Random-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Random.ast +0 -0
- package/lib/ocaml/Seed_Random.res +80 -0
- package/lib/ocaml/Seed_Runner-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Runner-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_Runner-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Runner.ast +0 -0
- package/lib/ocaml/Seed_Runner.res +114 -0
- package/lib/ocaml/Seed_Types-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Types-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_Types-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Types.ast +0 -0
- package/lib/ocaml/Seed_Types.res +49 -0
- package/lib/ocaml/Seed_Upload-ReventlessSeed.cmi +0 -0
- package/lib/ocaml/Seed_Upload-ReventlessSeed.cmj +0 -0
- package/lib/ocaml/Seed_Upload-ReventlessSeed.cmt +0 -0
- package/lib/ocaml/Seed_Upload.ast +0 -0
- package/lib/ocaml/Seed_Upload.res +99 -0
- package/package.json +26 -0
- package/rescript.json +20 -0
- package/src/Seed.res +30 -0
- package/src/Seed.res.mjs +34 -0
- package/src/Seed_Client.res +251 -0
- package/src/Seed_Client.res.mjs +242 -0
- package/src/Seed_Random.res +80 -0
- package/src/Seed_Random.res.mjs +86 -0
- package/src/Seed_Runner.res +114 -0
- package/src/Seed_Runner.res.mjs +118 -0
- package/src/Seed_Types.res +49 -0
- package/src/Seed_Types.res.mjs +79 -0
- package/src/Seed_Upload.res +99 -0
- package/src/Seed_Upload.res.mjs +118 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
// GraphQL transport for seeding: authentication, mutation dispatch, and the
|
|
2
|
+
// query helpers a seed run needs to observe what it produced.
|
|
3
|
+
//
|
|
4
|
+
// Everything here is domain-agnostic — it knows about the shapes the framework
|
|
5
|
+
// generates (the CommandResult union, Relay-style connections) but nothing about
|
|
6
|
+
// any particular plugin.
|
|
7
|
+
|
|
8
|
+
open Seed_Types
|
|
9
|
+
|
|
10
|
+
// ── Node 18+ globals ────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
type fetchInit = {method: string, headers: dict<string>, body: string}
|
|
13
|
+
type response
|
|
14
|
+
|
|
15
|
+
@val external fetch: (string, fetchInit) => promise<response> = "fetch"
|
|
16
|
+
@send external responseJson: response => promise<JSON.t> = "json"
|
|
17
|
+
@send external responseText: response => promise<string> = "text"
|
|
18
|
+
@get external responseOk: response => bool = "ok"
|
|
19
|
+
@get external responseStatus: response => int = "status"
|
|
20
|
+
|
|
21
|
+
@val external setTimeout: (unit => unit, int) => unit = "setTimeout"
|
|
22
|
+
@scope("Date") @val external nowMs: unit => float = "now"
|
|
23
|
+
|
|
24
|
+
let sleep = (ms: int): promise<unit> => Promise.make((resolve, _) => setTimeout(() => resolve(), ms))
|
|
25
|
+
|
|
26
|
+
// ── Configuration ───────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
type config = {
|
|
29
|
+
endpoint: string,
|
|
30
|
+
loginEndpoint: string,
|
|
31
|
+
username: string,
|
|
32
|
+
password: string,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type t = {config: config, mutable token: option<string>}
|
|
36
|
+
|
|
37
|
+
let make = (~config: config): t => {config, token: None}
|
|
38
|
+
|
|
39
|
+
/** The bearer token from the last successful `login`, if any — so callers (e.g.
|
|
40
|
+
Seed.Upload) can authenticate side-channel requests with the same identity. */
|
|
41
|
+
let currentToken = (t: t): option<string> => t.token
|
|
42
|
+
|
|
43
|
+
// ── JSON helpers ────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
let field = (json: JSON.t, key: string): option<JSON.t> =>
|
|
46
|
+
switch json {
|
|
47
|
+
| Object(obj) => obj->Dict.get(key)
|
|
48
|
+
| _ => None
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let asString = (json: JSON.t): option<string> =>
|
|
52
|
+
switch json {
|
|
53
|
+
| String(s) => Some(s)
|
|
54
|
+
| _ => None
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Reads a string property off a node returned by `queryAllNodes`. */
|
|
58
|
+
let nodeString = (node: JSON.t, key: string): option<string> =>
|
|
59
|
+
node->field(key)->Option.flatMap(asString)
|
|
60
|
+
|
|
61
|
+
// ── Requests ────────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
let login = async (t: t): unit => {
|
|
64
|
+
let body = JSON.stringify(
|
|
65
|
+
JSON.Encode.object(
|
|
66
|
+
Dict.fromArray([
|
|
67
|
+
("username", JSON.Encode.string(t.config.username)),
|
|
68
|
+
("password", JSON.Encode.string(t.config.password)),
|
|
69
|
+
]),
|
|
70
|
+
),
|
|
71
|
+
)
|
|
72
|
+
let res = try await fetch(
|
|
73
|
+
t.config.loginEndpoint,
|
|
74
|
+
{method: "POST", headers: Dict.fromArray([("content-type", "application/json")]), body},
|
|
75
|
+
) catch {
|
|
76
|
+
| _ =>
|
|
77
|
+
throw(
|
|
78
|
+
Failed(`login: cannot reach ${t.config.loginEndpoint} — is the platform running?`),
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
if !(res->responseOk) {
|
|
82
|
+
let detail = await res->responseText
|
|
83
|
+
throw(
|
|
84
|
+
Failed(
|
|
85
|
+
`login as "${t.config.username}" failed with HTTP ${(res->responseStatus)
|
|
86
|
+
->Int.toString}: ${detail}`,
|
|
87
|
+
),
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
let json = await res->responseJson
|
|
91
|
+
t.token = json->field("token")->Option.flatMap(asString)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Sends one GraphQL document and returns its `data`. A GraphQL-level error
|
|
96
|
+
* aborts the run: a half-seeded store is worse than an empty one, because it
|
|
97
|
+
* looks like a working dataset.
|
|
98
|
+
*/
|
|
99
|
+
let gql = async (t: t, ~query: string, ~label: string): JSON.t => {
|
|
100
|
+
let headers = Dict.fromArray([("content-type", "application/json")])
|
|
101
|
+
switch t.token {
|
|
102
|
+
| Some(token) => headers->Dict.set("authorization", `Bearer ${token}`)
|
|
103
|
+
| None => ()
|
|
104
|
+
}
|
|
105
|
+
let body = JSON.stringify(
|
|
106
|
+
JSON.Encode.object(Dict.fromArray([("query", JSON.Encode.string(query))])),
|
|
107
|
+
)
|
|
108
|
+
let res = try await fetch(t.config.endpoint, {method: "POST", headers, body}) catch {
|
|
109
|
+
| _ =>
|
|
110
|
+
throw(Failed(`${label}: cannot reach ${t.config.endpoint} — is the platform running?`))
|
|
111
|
+
}
|
|
112
|
+
let json = await res->responseJson
|
|
113
|
+
switch json->field("errors") {
|
|
114
|
+
| Some(errors) =>
|
|
115
|
+
throw(
|
|
116
|
+
Failed(`${label} failed\n query: ${query}\n response: ${JSON.stringify(errors)}`),
|
|
117
|
+
)
|
|
118
|
+
| None => json->field("data")->Option.getOr(JSON.Encode.null)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── Commands ────────────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
let commandResultSelection = `__typename
|
|
125
|
+
... on CommandAccepted { eventCount }
|
|
126
|
+
... on CommandRejected { errorCode errorDetail }`
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Issues one command and insists it was accepted.
|
|
130
|
+
*
|
|
131
|
+
* `tolerate` names error codes that are an expected outcome of the domain
|
|
132
|
+
* rather than a broken seed; a tolerated rejection is returned to the caller so
|
|
133
|
+
* it can report on them instead of failing.
|
|
134
|
+
*/
|
|
135
|
+
let send = async (t: t, m: mutation, ~tolerate: array<string>=[]): option<string> => {
|
|
136
|
+
let label = m->describe
|
|
137
|
+
let query = `mutation { r: ${m.field}(${m.args->renderArgs}) { ${commandResultSelection} } }`
|
|
138
|
+
let data = await gql(t, ~query, ~label)
|
|
139
|
+
let result = data->field("r")->Option.getOr(JSON.Encode.null)
|
|
140
|
+
switch result->nodeString("__typename") {
|
|
141
|
+
| Some("CommandRejected") =>
|
|
142
|
+
let code = result->nodeString("errorCode")->Option.getOr("unknown")
|
|
143
|
+
let detail = result->nodeString("errorDetail")->Option.getOr("none")
|
|
144
|
+
if tolerate->Array.includes(code) {
|
|
145
|
+
Some(code)
|
|
146
|
+
} else {
|
|
147
|
+
throw(Failed(`${label} was rejected\n errorCode: ${code}\n errorDetail: ${detail}`))
|
|
148
|
+
}
|
|
149
|
+
| _ => None
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let sendAll = async (t: t, mutations: array<mutation>): unit =>
|
|
154
|
+
for i in 0 to mutations->Array.length - 1 {
|
|
155
|
+
switch mutations->Array.get(i) {
|
|
156
|
+
| Some(m) => (await send(t, m))->ignore
|
|
157
|
+
| None => ()
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── Queries ─────────────────────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Walks a Relay-style connection to the end. Connections page at 50 by default,
|
|
165
|
+
* so any count taken from a single request silently truncates.
|
|
166
|
+
*/
|
|
167
|
+
let queryAllNodes = async (t: t, ~field as fieldName: string, ~selection: string): array<
|
|
168
|
+
JSON.t,
|
|
169
|
+
> => {
|
|
170
|
+
let nodes = []
|
|
171
|
+
let after = ref(None)
|
|
172
|
+
let more = ref(true)
|
|
173
|
+
while more.contents {
|
|
174
|
+
let cursor = switch after.contents {
|
|
175
|
+
| Some(c) => `, after: ${quote(c)}`
|
|
176
|
+
| None => ""
|
|
177
|
+
}
|
|
178
|
+
let query = `{ ${fieldName}(first: 100${cursor}) { edges { node { ${selection} } } pageInfo { hasNextPage endCursor } } }`
|
|
179
|
+
let data = await gql(t, ~query, ~label=fieldName)
|
|
180
|
+
let connection = data->field(fieldName)->Option.getOr(JSON.Encode.null)
|
|
181
|
+
switch connection->field("edges") {
|
|
182
|
+
| Some(Array(edges)) =>
|
|
183
|
+
edges->Array.forEach(edge =>
|
|
184
|
+
switch edge->field("node") {
|
|
185
|
+
| Some(node) => nodes->Array.push(node)
|
|
186
|
+
| None => ()
|
|
187
|
+
}
|
|
188
|
+
)
|
|
189
|
+
| _ => ()
|
|
190
|
+
}
|
|
191
|
+
let pageInfo = connection->field("pageInfo")->Option.getOr(JSON.Encode.null)
|
|
192
|
+
let hasNext = switch pageInfo->field("hasNextPage") {
|
|
193
|
+
| Some(Boolean(b)) => b
|
|
194
|
+
| _ => false
|
|
195
|
+
}
|
|
196
|
+
let endCursor = pageInfo->nodeString("endCursor")
|
|
197
|
+
switch (hasNext, endCursor) {
|
|
198
|
+
| (true, Some(c)) => after := Some(c)
|
|
199
|
+
| _ => more := false
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
nodes
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let countNodes = async (t: t, ~field: string): int =>
|
|
206
|
+
(await queryAllNodes(t, ~field, ~selection="id"))->Array.length
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Polls a `<View>ByIds` field until every id is present.
|
|
210
|
+
*
|
|
211
|
+
* Cross-plugin propagation (extension point → extension → command) is
|
|
212
|
+
* asynchronous, so a seed that places orders immediately after creating
|
|
213
|
+
* products would race the shadow copy. Waiting on the observable result beats
|
|
214
|
+
* sleeping and hoping.
|
|
215
|
+
*/
|
|
216
|
+
let waitForIds = async (
|
|
217
|
+
t: t,
|
|
218
|
+
~field as fieldName: string,
|
|
219
|
+
~ids: array<string>,
|
|
220
|
+
~timeoutMs: int=60000,
|
|
221
|
+
): int => {
|
|
222
|
+
let deadline = nowMs() +. Int.toFloat(timeoutMs)
|
|
223
|
+
let idList = ids->Array.map(id => quote(id))->Array.join(", ")
|
|
224
|
+
let result = ref(None)
|
|
225
|
+
while result.contents == None {
|
|
226
|
+
let query = `{ ${fieldName}(ids: [${idList}]) { id } }`
|
|
227
|
+
let data = await gql(t, ~query, ~label=fieldName)
|
|
228
|
+
let found = switch data->field(fieldName) {
|
|
229
|
+
| Some(Array(rows)) => rows
|
|
230
|
+
| _ => []
|
|
231
|
+
}
|
|
232
|
+
if found->Array.length >= ids->Array.length {
|
|
233
|
+
result := Some(found->Array.length)
|
|
234
|
+
} else if nowMs() > deadline {
|
|
235
|
+
let present = found->Array.filterMap(row => row->nodeString("id"))
|
|
236
|
+
let missing = ids->Array.filter(id => !(present->Array.includes(id)))
|
|
237
|
+
let shown = missing->Array.slice(~start=0, ~end=10)->Array.join(", ")
|
|
238
|
+
let extra =
|
|
239
|
+
missing->Array.length > 10 ? ` (+${(missing->Array.length - 10)->Int.toString} more)` : ""
|
|
240
|
+
throw(
|
|
241
|
+
Failed(
|
|
242
|
+
`only ${(found->Array.length)->Int.toString}/${(ids->Array.length)
|
|
243
|
+
->Int.toString} ids reached ${fieldName} within ${timeoutMs->Int.toString}ms.\n missing: ${shown}${extra}`,
|
|
244
|
+
),
|
|
245
|
+
)
|
|
246
|
+
} else {
|
|
247
|
+
await sleep(250)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
result.contents->Option.getOr(0)
|
|
251
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
4
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
import * as Seed_Types$ReventlessSeed from "./Seed_Types.res.mjs";
|
|
6
|
+
|
|
7
|
+
function sleep(ms) {
|
|
8
|
+
return new Promise((resolve, param) => {
|
|
9
|
+
setTimeout(() => resolve(), ms);
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function make(config) {
|
|
14
|
+
return {
|
|
15
|
+
config: config,
|
|
16
|
+
token: undefined
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function currentToken(t) {
|
|
21
|
+
return t.token;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function field(json, key) {
|
|
25
|
+
if (typeof json === "object" && json !== null && !Array.isArray(json)) {
|
|
26
|
+
return json[key];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function asString(json) {
|
|
31
|
+
if (typeof json === "string") {
|
|
32
|
+
return json;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function nodeString(node, key) {
|
|
37
|
+
return Stdlib_Option.flatMap(field(node, key), asString);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function login(t) {
|
|
41
|
+
let body = JSON.stringify(Object.fromEntries([
|
|
42
|
+
[
|
|
43
|
+
"username",
|
|
44
|
+
t.config.username
|
|
45
|
+
],
|
|
46
|
+
[
|
|
47
|
+
"password",
|
|
48
|
+
t.config.password
|
|
49
|
+
]
|
|
50
|
+
]));
|
|
51
|
+
let res;
|
|
52
|
+
try {
|
|
53
|
+
res = await fetch(t.config.loginEndpoint, {
|
|
54
|
+
method: "POST",
|
|
55
|
+
headers: Object.fromEntries([[
|
|
56
|
+
"content-type",
|
|
57
|
+
"application/json"
|
|
58
|
+
]]),
|
|
59
|
+
body: body
|
|
60
|
+
});
|
|
61
|
+
} catch (exn) {
|
|
62
|
+
throw {
|
|
63
|
+
RE_EXN_ID: Seed_Types$ReventlessSeed.Failed,
|
|
64
|
+
_1: `login: cannot reach ` + t.config.loginEndpoint + ` — is the platform running?`,
|
|
65
|
+
Error: new Error()
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
if (!res.ok) {
|
|
69
|
+
let detail = await res.text();
|
|
70
|
+
throw {
|
|
71
|
+
RE_EXN_ID: Seed_Types$ReventlessSeed.Failed,
|
|
72
|
+
_1: `login as "` + t.config.username + `" failed with HTTP ` + res.status.toString() + `: ` + detail,
|
|
73
|
+
Error: new Error()
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
let json = await res.json();
|
|
77
|
+
t.token = Stdlib_Option.flatMap(field(json, "token"), asString);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function gql(t, query, label) {
|
|
81
|
+
let headers = Object.fromEntries([[
|
|
82
|
+
"content-type",
|
|
83
|
+
"application/json"
|
|
84
|
+
]]);
|
|
85
|
+
let token = t.token;
|
|
86
|
+
if (token !== undefined) {
|
|
87
|
+
headers["authorization"] = `Bearer ` + token;
|
|
88
|
+
}
|
|
89
|
+
let body = JSON.stringify(Object.fromEntries([[
|
|
90
|
+
"query",
|
|
91
|
+
query
|
|
92
|
+
]]));
|
|
93
|
+
let res;
|
|
94
|
+
try {
|
|
95
|
+
res = await fetch(t.config.endpoint, {
|
|
96
|
+
method: "POST",
|
|
97
|
+
headers: headers,
|
|
98
|
+
body: body
|
|
99
|
+
});
|
|
100
|
+
} catch (exn) {
|
|
101
|
+
throw {
|
|
102
|
+
RE_EXN_ID: Seed_Types$ReventlessSeed.Failed,
|
|
103
|
+
_1: label + `: cannot reach ` + t.config.endpoint + ` — is the platform running?`,
|
|
104
|
+
Error: new Error()
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
let json = await res.json();
|
|
108
|
+
let errors = field(json, "errors");
|
|
109
|
+
if (errors !== undefined) {
|
|
110
|
+
throw {
|
|
111
|
+
RE_EXN_ID: Seed_Types$ReventlessSeed.Failed,
|
|
112
|
+
_1: label + ` failed\n query: ` + query + `\n response: ` + JSON.stringify(errors),
|
|
113
|
+
Error: new Error()
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return Stdlib_Option.getOr(field(json, "data"), null);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let commandResultSelection = `__typename
|
|
120
|
+
... on CommandAccepted { eventCount }
|
|
121
|
+
... on CommandRejected { errorCode errorDetail }`;
|
|
122
|
+
|
|
123
|
+
async function send(t, m, tolerateOpt) {
|
|
124
|
+
let tolerate = tolerateOpt !== undefined ? tolerateOpt : [];
|
|
125
|
+
let label = Seed_Types$ReventlessSeed.describe(m);
|
|
126
|
+
let query = `mutation { r: ` + m.field + `(` + Seed_Types$ReventlessSeed.renderArgs(m.args) + `) { ` + commandResultSelection + ` } }`;
|
|
127
|
+
let data = await gql(t, query, label);
|
|
128
|
+
let result = Stdlib_Option.getOr(field(data, "r"), null);
|
|
129
|
+
let match = nodeString(result, "__typename");
|
|
130
|
+
if (match === undefined) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (match !== "CommandRejected") {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
let code = Stdlib_Option.getOr(nodeString(result, "errorCode"), "unknown");
|
|
137
|
+
let detail = Stdlib_Option.getOr(nodeString(result, "errorDetail"), "none");
|
|
138
|
+
if (tolerate.includes(code)) {
|
|
139
|
+
return code;
|
|
140
|
+
}
|
|
141
|
+
throw {
|
|
142
|
+
RE_EXN_ID: Seed_Types$ReventlessSeed.Failed,
|
|
143
|
+
_1: label + ` was rejected\n errorCode: ` + code + `\n errorDetail: ` + detail,
|
|
144
|
+
Error: new Error()
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function sendAll(t, mutations) {
|
|
149
|
+
for (let i = 0, i_finish = mutations.length; i < i_finish; ++i) {
|
|
150
|
+
let m = mutations[i];
|
|
151
|
+
if (m !== undefined) {
|
|
152
|
+
await send(t, m, undefined);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function queryAllNodes(t, fieldName, selection) {
|
|
158
|
+
let nodes = [];
|
|
159
|
+
let after;
|
|
160
|
+
let more = true;
|
|
161
|
+
while (more) {
|
|
162
|
+
let c = after;
|
|
163
|
+
let cursor = c !== undefined ? `, after: ` + Seed_Types$ReventlessSeed.quote(c) : "";
|
|
164
|
+
let query = `{ ` + fieldName + `(first: 100` + cursor + `) { edges { node { ` + selection + ` } } pageInfo { hasNextPage endCursor } } }`;
|
|
165
|
+
let data = await gql(t, query, fieldName);
|
|
166
|
+
let connection = Stdlib_Option.getOr(field(data, fieldName), null);
|
|
167
|
+
let match = field(connection, "edges");
|
|
168
|
+
if (Array.isArray(match)) {
|
|
169
|
+
match.forEach(edge => {
|
|
170
|
+
let node = field(edge, "node");
|
|
171
|
+
if (node !== undefined) {
|
|
172
|
+
nodes.push(node);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
let pageInfo = Stdlib_Option.getOr(field(connection, "pageInfo"), null);
|
|
178
|
+
let match$1 = field(pageInfo, "hasNextPage");
|
|
179
|
+
let hasNext = match$1 === true;
|
|
180
|
+
let endCursor = nodeString(pageInfo, "endCursor");
|
|
181
|
+
if (hasNext && endCursor !== undefined) {
|
|
182
|
+
after = endCursor;
|
|
183
|
+
} else {
|
|
184
|
+
more = false;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
return nodes;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function countNodes(t, field) {
|
|
191
|
+
return (await queryAllNodes(t, field, "id")).length;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function waitForIds(t, fieldName, ids, timeoutMsOpt) {
|
|
195
|
+
let timeoutMs = timeoutMsOpt !== undefined ? timeoutMsOpt : 60000;
|
|
196
|
+
let deadline = Date.now() + timeoutMs;
|
|
197
|
+
let idList = ids.map(Seed_Types$ReventlessSeed.quote).join(", ");
|
|
198
|
+
let result;
|
|
199
|
+
while (result === undefined) {
|
|
200
|
+
let query = `{ ` + fieldName + `(ids: [` + idList + `]) { id } }`;
|
|
201
|
+
let data = await gql(t, query, fieldName);
|
|
202
|
+
let match = field(data, fieldName);
|
|
203
|
+
let found = match !== undefined ? (
|
|
204
|
+
Array.isArray(match) ? match : []
|
|
205
|
+
) : [];
|
|
206
|
+
if (found.length >= ids.length) {
|
|
207
|
+
result = found.length;
|
|
208
|
+
} else {
|
|
209
|
+
if (Date.now() > deadline) {
|
|
210
|
+
let present = Stdlib_Array.filterMap(found, row => nodeString(row, "id"));
|
|
211
|
+
let missing = ids.filter(id => !present.includes(id));
|
|
212
|
+
let shown = missing.slice(0, 10).join(", ");
|
|
213
|
+
let extra = missing.length > 10 ? ` (+` + (missing.length - 10 | 0).toString() + ` more)` : "";
|
|
214
|
+
throw {
|
|
215
|
+
RE_EXN_ID: Seed_Types$ReventlessSeed.Failed,
|
|
216
|
+
_1: `only ` + found.length.toString() + `/` + ids.length.toString() + ` ids reached ` + fieldName + ` within ` + timeoutMs.toString() + `ms.\n missing: ` + shown + extra,
|
|
217
|
+
Error: new Error()
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
await sleep(250);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
return Stdlib_Option.getOr(result, 0);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export {
|
|
227
|
+
sleep,
|
|
228
|
+
make,
|
|
229
|
+
currentToken,
|
|
230
|
+
field,
|
|
231
|
+
asString,
|
|
232
|
+
nodeString,
|
|
233
|
+
login,
|
|
234
|
+
gql,
|
|
235
|
+
commandResultSelection,
|
|
236
|
+
send,
|
|
237
|
+
sendAll,
|
|
238
|
+
queryAllNodes,
|
|
239
|
+
countNodes,
|
|
240
|
+
waitForIds,
|
|
241
|
+
}
|
|
242
|
+
/* No side effect */
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Deterministic pseudo-randomness for reproducible datasets.
|
|
2
|
+
//
|
|
3
|
+
// A seeded generator rather than `Math.random`, so two runs against a fresh
|
|
4
|
+
// store produce identical data — which is what makes a seeded store usable as a
|
|
5
|
+
// baseline for screenshots or comparison.
|
|
6
|
+
|
|
7
|
+
@scope("Math") @val external imul: (int, int) => int = "imul"
|
|
8
|
+
|
|
9
|
+
type t = {mutable state: int}
|
|
10
|
+
|
|
11
|
+
let make = (~seed: int): t => {state: seed}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
/** mulberry32 — small, fast, and stable across JS engines and Node versions. */
|
|
15
|
+
let float = (r: t): float => {
|
|
16
|
+
r.state = (r.state + 0x6d2b79f5)->Int.bitwiseOr(0)
|
|
17
|
+
let a = r.state
|
|
18
|
+
let b = imul(a->Int.bitwiseXor(a->Int.shiftRightUnsigned(15)), a->Int.bitwiseOr(1))
|
|
19
|
+
let c = b->Int.bitwiseXor(b + imul(b->Int.bitwiseXor(b->Int.shiftRightUnsigned(7)), b->Int.bitwiseOr(61)))
|
|
20
|
+
let bits = c->Int.bitwiseXor(c->Int.shiftRightUnsigned(14))
|
|
21
|
+
// `bits` is a signed 32-bit result; fold the sign bit back in to get the
|
|
22
|
+
// unsigned value a `>>> 0` would produce.
|
|
23
|
+
let unsigned = bits < 0 ? Int.toFloat(bits) +. 4294967296.0 : Int.toFloat(bits)
|
|
24
|
+
unsigned /. 4294967296.0
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Inclusive on both ends. */
|
|
28
|
+
let int = (r: t, ~min: int, ~max: int): int =>
|
|
29
|
+
min + Int.fromFloat(float(r) *. Int.toFloat(max - min + 1))
|
|
30
|
+
|
|
31
|
+
let pick = (r: t, xs: array<'a>): option<'a> =>
|
|
32
|
+
xs->Array.get(Int.fromFloat(float(r) *. Int.toFloat(xs->Array.length)))
|
|
33
|
+
|
|
34
|
+
let pickOr = (r: t, ~fallback: 'a, xs: array<'a>): 'a => pick(r, xs)->Option.getOr(fallback)
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Weighted sample without replacement.
|
|
38
|
+
*
|
|
39
|
+
* Used both for skewing which items are chosen (a few popular ones, a long
|
|
40
|
+
* tail) and for shuffling — pass equal weights and a count equal to the input
|
|
41
|
+
* length to get a deterministic permutation.
|
|
42
|
+
*/
|
|
43
|
+
let sampleWeighted = (r: t, entries: array<('a, float)>, ~count: int): array<'a> => {
|
|
44
|
+
let pool = entries->Array.copy
|
|
45
|
+
let chosen = []
|
|
46
|
+
let remaining = count < pool->Array.length ? count : pool->Array.length
|
|
47
|
+
for _ in 1 to remaining {
|
|
48
|
+
let total = pool->Array.reduce(0.0, (sum, (_, w)) => sum +. w)
|
|
49
|
+
let cursor = ref(float(r) *. total)
|
|
50
|
+
let index = ref(pool->Array.length - 1)
|
|
51
|
+
let found = ref(false)
|
|
52
|
+
pool->Array.forEachWithIndex(((_, w), i) => {
|
|
53
|
+
if !found.contents {
|
|
54
|
+
cursor := cursor.contents -. w
|
|
55
|
+
if cursor.contents <= 0.0 {
|
|
56
|
+
index := i
|
|
57
|
+
found := true
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
switch pool->Array.get(index.contents) {
|
|
62
|
+
| Some((item, _)) =>
|
|
63
|
+
chosen->Array.push(item)
|
|
64
|
+
pool->Array.splice(~start=index.contents, ~remove=1, ~insert=[])
|
|
65
|
+
| None => ()
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
chosen
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Zipf-like weights over an already-ordered array: the first entry carries the
|
|
73
|
+
* most weight and the tail decays smoothly. `exponent` controls how sharp the
|
|
74
|
+
* head is — around 1.1 gives a clear leader with a long tail.
|
|
75
|
+
*/
|
|
76
|
+
let zipfWeights = (items: array<'a>, ~exponent: float): array<('a, float)> =>
|
|
77
|
+
items->Array.mapWithIndex((item, rank) => (
|
|
78
|
+
item,
|
|
79
|
+
1.0 /. Math.pow(Int.toFloat(rank + 1), ~exp=exponent),
|
|
80
|
+
))
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
4
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
|
|
6
|
+
function make(seed) {
|
|
7
|
+
return {
|
|
8
|
+
state: seed
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function float(r) {
|
|
13
|
+
r.state = r.state + 1831565813 | 0;
|
|
14
|
+
let a = r.state;
|
|
15
|
+
let b = Math.imul(a ^ (a >>> 15), a | 1);
|
|
16
|
+
let c = b ^ (b + Math.imul(b ^ (b >>> 7), b | 61) | 0);
|
|
17
|
+
let bits = c ^ (c >>> 14);
|
|
18
|
+
let unsigned = bits < 0 ? bits + 4294967296.0 : bits;
|
|
19
|
+
return unsigned / 4294967296.0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function int(r, min, max) {
|
|
23
|
+
return min + (float(r) * ((max - min | 0) + 1 | 0) | 0) | 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function pick(r, xs) {
|
|
27
|
+
return xs[float(r) * xs.length | 0];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function pickOr(r, fallback, xs) {
|
|
31
|
+
return Stdlib_Option.getOr(pick(r, xs), fallback);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function sampleWeighted(r, entries, count) {
|
|
35
|
+
let pool = entries.slice();
|
|
36
|
+
let chosen = [];
|
|
37
|
+
let remaining = count < pool.length ? count : pool.length;
|
|
38
|
+
for (let _for = 1; _for <= remaining; ++_for) {
|
|
39
|
+
let total = Stdlib_Array.reduce(pool, 0.0, (sum, param) => sum + param[1]);
|
|
40
|
+
let cursor = {
|
|
41
|
+
contents: float(r) * total
|
|
42
|
+
};
|
|
43
|
+
let index = {
|
|
44
|
+
contents: pool.length - 1 | 0
|
|
45
|
+
};
|
|
46
|
+
let found = {
|
|
47
|
+
contents: false
|
|
48
|
+
};
|
|
49
|
+
pool.forEach((param, i) => {
|
|
50
|
+
if (!found.contents) {
|
|
51
|
+
cursor.contents = cursor.contents - param[1];
|
|
52
|
+
if (cursor.contents <= 0.0) {
|
|
53
|
+
index.contents = i;
|
|
54
|
+
found.contents = true;
|
|
55
|
+
return;
|
|
56
|
+
} else {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
let match = pool[index.contents];
|
|
62
|
+
if (match !== undefined) {
|
|
63
|
+
chosen.push(match[0]);
|
|
64
|
+
pool.splice(index.contents, 1);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return chosen;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function zipfWeights(items, exponent) {
|
|
71
|
+
return items.map((item, rank) => [
|
|
72
|
+
item,
|
|
73
|
+
1.0 / Math.pow(rank + 1 | 0, exponent)
|
|
74
|
+
]);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export {
|
|
78
|
+
make,
|
|
79
|
+
float,
|
|
80
|
+
int,
|
|
81
|
+
pick,
|
|
82
|
+
pickOr,
|
|
83
|
+
sampleWeighted,
|
|
84
|
+
zipfWeights,
|
|
85
|
+
}
|
|
86
|
+
/* No side effect */
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|