@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,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 */
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Run orchestration: progress output, view verification, and failure reporting.
|
|
2
|
+
|
|
3
|
+
open Seed_Types
|
|
4
|
+
|
|
5
|
+
@scope("process") @val external exit: int => unit = "exit"
|
|
6
|
+
@scope("process") @val external env: dict<string> = "env"
|
|
7
|
+
|
|
8
|
+
let envOr = (key: string, fallback: string): string =>
|
|
9
|
+
env->Dict.get(key)->Option.getOr(fallback)
|
|
10
|
+
|
|
11
|
+
/** One line of progress per phase, so a slow run never looks hung. */
|
|
12
|
+
let report = (line: string): unit => Console.log(` ${line}`)
|
|
13
|
+
|
|
14
|
+
let heading = (line: string): unit => {
|
|
15
|
+
Console.log("")
|
|
16
|
+
Console.log(line)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A view the seed is expected to fill, or one it demonstrably cannot.
|
|
21
|
+
*
|
|
22
|
+
* Splitting them matters: an unexpected empty view means the seed missed a
|
|
23
|
+
* component, which is exactly the "is it broken or is it empty?" ambiguity
|
|
24
|
+
* seeding exists to remove — so it fails the run. A view listed as unfillable
|
|
25
|
+
* reports its reason instead, so a zero is never mistaken for a gap.
|
|
26
|
+
*/
|
|
27
|
+
type view =
|
|
28
|
+
| Seeded(string)
|
|
29
|
+
| Unfillable(string, string)
|
|
30
|
+
|
|
31
|
+
let viewName = (v: view): string =>
|
|
32
|
+
switch v {
|
|
33
|
+
| Seeded(name) => name
|
|
34
|
+
| Unfillable(name, _) => name
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Counts every view and fails if one that should have rows is empty.
|
|
39
|
+
* Returns the counts so callers can fold them into their own summary.
|
|
40
|
+
*/
|
|
41
|
+
let verifyViews = async (client: Seed_Client.t, ~views: array<view>): dict<int> => {
|
|
42
|
+
let counts = Dict.make()
|
|
43
|
+
for i in 0 to views->Array.length - 1 {
|
|
44
|
+
switch views->Array.get(i) {
|
|
45
|
+
| Some(v) =>
|
|
46
|
+
let name = v->viewName
|
|
47
|
+
counts->Dict.set(name, await Seed_Client.countNodes(client, ~field=name))
|
|
48
|
+
| None => ()
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
heading("View row counts:")
|
|
53
|
+
views->Array.forEach(v => {
|
|
54
|
+
let name = v->viewName
|
|
55
|
+
let count = counts->Dict.get(name)->Option.getOr(0)
|
|
56
|
+
let note = switch v {
|
|
57
|
+
| Unfillable(_, _) => " (known empty — see warning below)"
|
|
58
|
+
| Seeded(_) => ""
|
|
59
|
+
}
|
|
60
|
+
Console.log(` ${count->Int.toString->String.padStart(4, " ")} ${name}${note}`)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
let empty =
|
|
64
|
+
views
|
|
65
|
+
->Array.filterMap(v =>
|
|
66
|
+
switch v {
|
|
67
|
+
| Seeded(name) if counts->Dict.get(name)->Option.getOr(0) == 0 => Some(name)
|
|
68
|
+
| _ => None
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
if empty->Array.length > 0 {
|
|
72
|
+
throw(Failed(`these views are still empty after seeding: ${empty->Array.join(", ")}`))
|
|
73
|
+
}
|
|
74
|
+
counts
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Collects the reasons for any view that legitimately could not be filled. */
|
|
78
|
+
let unfillableWarnings = (~views: array<view>, ~counts: dict<int>): array<string> =>
|
|
79
|
+
views->Array.filterMap(v =>
|
|
80
|
+
switch v {
|
|
81
|
+
| Unfillable(name, reason) if counts->Dict.get(name)->Option.getOr(0) == 0 =>
|
|
82
|
+
Some(`${name} is empty: ${reason}.`)
|
|
83
|
+
| _ => None
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
let warn = (warnings: array<string>): unit =>
|
|
88
|
+
if warnings->Array.length > 0 {
|
|
89
|
+
heading("WARNING — the dataset is complete, but not everything could be filled:")
|
|
90
|
+
warnings->Array.forEach(w => Console.log(` - ${w}`))
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Wraps a seed run. On failure it prints the cause and exits non-zero, saying
|
|
95
|
+
* plainly that the store is now half-seeded — recovery is a reset, not a re-run.
|
|
96
|
+
*/
|
|
97
|
+
let run = async (main: unit => promise<unit>): unit =>
|
|
98
|
+
try await main() catch {
|
|
99
|
+
| Failed(message) =>
|
|
100
|
+
Console.error("")
|
|
101
|
+
Console.error("Seeding aborted — the store is now half-seeded; re-run against a")
|
|
102
|
+
Console.error("fresh store once the cause below is fixed.")
|
|
103
|
+
Console.error("")
|
|
104
|
+
Console.error(message)
|
|
105
|
+
exit(1)
|
|
106
|
+
| exn =>
|
|
107
|
+
Console.error("")
|
|
108
|
+
Console.error("Seeding aborted with an unexpected error:")
|
|
109
|
+
Console.error("")
|
|
110
|
+
Console.error(
|
|
111
|
+
exn->JsExn.fromException->Option.flatMap(JsExn.message)->Option.getOr("unknown"),
|
|
112
|
+
)
|
|
113
|
+
exit(1)
|
|
114
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
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_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
5
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
|
+
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
7
|
+
import * as Seed_Types$ReventlessSeed from "./Seed_Types.res.mjs";
|
|
8
|
+
import * as Seed_Client$ReventlessSeed from "./Seed_Client.res.mjs";
|
|
9
|
+
|
|
10
|
+
function envOr(key, fallback) {
|
|
11
|
+
return Stdlib_Option.getOr(process.env[key], fallback);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function report(line) {
|
|
15
|
+
console.log(` ` + line);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function heading(line) {
|
|
19
|
+
console.log("");
|
|
20
|
+
console.log(line);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function viewName(v) {
|
|
24
|
+
return v._0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function verifyViews(client, views) {
|
|
28
|
+
let counts = {};
|
|
29
|
+
for (let i = 0, i_finish = views.length; i < i_finish; ++i) {
|
|
30
|
+
let v = views[i];
|
|
31
|
+
if (v !== undefined) {
|
|
32
|
+
let name = v._0;
|
|
33
|
+
counts[name] = await Seed_Client$ReventlessSeed.countNodes(client, name);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
heading("View row counts:");
|
|
37
|
+
views.forEach(v => {
|
|
38
|
+
let name = v._0;
|
|
39
|
+
let count = Stdlib_Option.getOr(counts[name], 0);
|
|
40
|
+
let note;
|
|
41
|
+
note = v.TAG === "Seeded" ? "" : " (known empty — see warning below)";
|
|
42
|
+
console.log(` ` + count.toString().padStart(4, " ") + ` ` + name + note);
|
|
43
|
+
});
|
|
44
|
+
let empty = Stdlib_Array.filterMap(views, v => {
|
|
45
|
+
if (v.TAG !== "Seeded") {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
let name = v._0;
|
|
49
|
+
if (Stdlib_Option.getOr(counts[name], 0) === 0) {
|
|
50
|
+
return name;
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
if (empty.length !== 0) {
|
|
54
|
+
throw {
|
|
55
|
+
RE_EXN_ID: Seed_Types$ReventlessSeed.Failed,
|
|
56
|
+
_1: `these views are still empty after seeding: ` + empty.join(", "),
|
|
57
|
+
Error: new Error()
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return counts;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function unfillableWarnings(views, counts) {
|
|
64
|
+
return Stdlib_Array.filterMap(views, v => {
|
|
65
|
+
if (v.TAG === "Seeded") {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
let name = v._0;
|
|
69
|
+
if (Stdlib_Option.getOr(counts[name], 0) === 0) {
|
|
70
|
+
return name + ` is empty: ` + v._1 + `.`;
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function warn(warnings) {
|
|
76
|
+
if (warnings.length !== 0) {
|
|
77
|
+
heading("WARNING — the dataset is complete, but not everything could be filled:");
|
|
78
|
+
warnings.forEach(w => {
|
|
79
|
+
console.log(` - ` + w);
|
|
80
|
+
});
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function run(main) {
|
|
86
|
+
try {
|
|
87
|
+
return await main();
|
|
88
|
+
} catch (raw_message) {
|
|
89
|
+
let message = Primitive_exceptions.internalToException(raw_message);
|
|
90
|
+
if (message.RE_EXN_ID === Seed_Types$ReventlessSeed.Failed) {
|
|
91
|
+
console.error("");
|
|
92
|
+
console.error("Seeding aborted — the store is now half-seeded; re-run against a");
|
|
93
|
+
console.error("fresh store once the cause below is fixed.");
|
|
94
|
+
console.error("");
|
|
95
|
+
console.error(message._1);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
} else {
|
|
98
|
+
console.error("");
|
|
99
|
+
console.error("Seeding aborted with an unexpected error:");
|
|
100
|
+
console.error("");
|
|
101
|
+
console.error(Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(message), Stdlib_JsExn.message), "unknown"));
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export {
|
|
109
|
+
envOr,
|
|
110
|
+
report,
|
|
111
|
+
heading,
|
|
112
|
+
viewName,
|
|
113
|
+
verifyViews,
|
|
114
|
+
unfillableWarnings,
|
|
115
|
+
warn,
|
|
116
|
+
run,
|
|
117
|
+
}
|
|
118
|
+
/* Stdlib_JsExn Not a pure module */
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Argument values for a seeded mutation, and their GraphQL literal rendering.
|
|
2
|
+
//
|
|
3
|
+
// Seeding sends inline literals rather than declared variables, so any mutation
|
|
4
|
+
// can be called without generating a per-command variable signature. That means
|
|
5
|
+
// values have to know how to render themselves — in particular enums, which are
|
|
6
|
+
// bare identifiers in GraphQL and would be rejected if quoted like strings.
|
|
7
|
+
|
|
8
|
+
exception Failed(string)
|
|
9
|
+
|
|
10
|
+
type rec value =
|
|
11
|
+
| String(string)
|
|
12
|
+
| Id(string)
|
|
13
|
+
| Int(int)
|
|
14
|
+
| Float(float)
|
|
15
|
+
| Bool(bool)
|
|
16
|
+
/** Rendered unquoted — GraphQL enum values are identifiers, not strings. */
|
|
17
|
+
| Enum(string)
|
|
18
|
+
| List(array<value>)
|
|
19
|
+
/** A nested GraphQL input object, e.g. a geo-point `{lat, lng}`. Keys are
|
|
20
|
+
rendered as bare identifiers, as GraphQL input-object fields require. */
|
|
21
|
+
| Object(array<(string, value)>)
|
|
22
|
+
|
|
23
|
+
let quote = (s: string): string => JSON.stringify(JSON.Encode.string(s))
|
|
24
|
+
|
|
25
|
+
let rec toLiteral = (v: value): string =>
|
|
26
|
+
switch v {
|
|
27
|
+
| String(s) | Id(s) => quote(s)
|
|
28
|
+
| Int(i) => i->Int.toString
|
|
29
|
+
| Float(f) => f->Float.toString
|
|
30
|
+
| Bool(b) => b ? "true" : "false"
|
|
31
|
+
| Enum(name) => name
|
|
32
|
+
| List(items) => `[${items->Array.map(toLiteral)->Array.join(", ")}]`
|
|
33
|
+
| Object(fields) =>
|
|
34
|
+
`{${fields->Array.map(((k, v)) => `${k}: ${toLiteral(v)}`)->Array.join(", ")}}`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let ids = (xs: array<string>): value => List(xs->Array.map(x => Id(x)))
|
|
38
|
+
let strings = (xs: array<string>): value => List(xs->Array.map(x => String(x)))
|
|
39
|
+
|
|
40
|
+
/** One mutation call: the field name and its inline arguments. */
|
|
41
|
+
type mutation = {field: string, args: array<(string, value)>}
|
|
42
|
+
|
|
43
|
+
let mutation = (field: string, args: array<(string, value)>): mutation => {field, args}
|
|
44
|
+
|
|
45
|
+
let renderArgs = (args: array<(string, value)>): string =>
|
|
46
|
+
args->Array.map(((k, v)) => `${k}: ${toLiteral(v)}`)->Array.join(", ")
|
|
47
|
+
|
|
48
|
+
/** Human-readable form used in progress output and failure messages. */
|
|
49
|
+
let describe = (m: mutation): string => `${m.field}(${renderArgs(m.args)})`
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
4
|
+
|
|
5
|
+
let Failed = /* @__PURE__ */Primitive_exceptions.create("Seed_Types-ReventlessSeed.Failed");
|
|
6
|
+
|
|
7
|
+
function quote(s) {
|
|
8
|
+
return JSON.stringify(s);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function toLiteral(v) {
|
|
12
|
+
switch (v.TAG) {
|
|
13
|
+
case "String" :
|
|
14
|
+
case "Id" :
|
|
15
|
+
return JSON.stringify(v._0);
|
|
16
|
+
case "Int" :
|
|
17
|
+
case "Float" :
|
|
18
|
+
return v._0.toString();
|
|
19
|
+
case "Bool" :
|
|
20
|
+
if (v._0) {
|
|
21
|
+
return "true";
|
|
22
|
+
} else {
|
|
23
|
+
return "false";
|
|
24
|
+
}
|
|
25
|
+
case "Enum" :
|
|
26
|
+
return v._0;
|
|
27
|
+
case "List" :
|
|
28
|
+
return `[` + v._0.map(toLiteral).join(", ") + `]`;
|
|
29
|
+
case "Object" :
|
|
30
|
+
return `{` + v._0.map(param => param[0] + `: ` + toLiteral(param[1])).join(", ") + `}`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function ids(xs) {
|
|
35
|
+
return {
|
|
36
|
+
TAG: "List",
|
|
37
|
+
_0: xs.map(x => ({
|
|
38
|
+
TAG: "Id",
|
|
39
|
+
_0: x
|
|
40
|
+
}))
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function strings(xs) {
|
|
45
|
+
return {
|
|
46
|
+
TAG: "List",
|
|
47
|
+
_0: xs.map(x => ({
|
|
48
|
+
TAG: "String",
|
|
49
|
+
_0: x
|
|
50
|
+
}))
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function mutation(field, args) {
|
|
55
|
+
return {
|
|
56
|
+
field: field,
|
|
57
|
+
args: args
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function renderArgs(args) {
|
|
62
|
+
return args.map(param => param[0] + `: ` + toLiteral(param[1])).join(", ");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function describe(m) {
|
|
66
|
+
return m.field + `(` + renderArgs(m.args) + `)`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export {
|
|
70
|
+
Failed,
|
|
71
|
+
quote,
|
|
72
|
+
toLiteral,
|
|
73
|
+
ids,
|
|
74
|
+
strings,
|
|
75
|
+
mutation,
|
|
76
|
+
renderArgs,
|
|
77
|
+
describe,
|
|
78
|
+
}
|
|
79
|
+
/* No side effect */
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Seed-time asset upload against the framework's presign-shaped upload contract.
|
|
2
|
+
//
|
|
3
|
+
// One code path for both providers: the AWS `Upload_Presign_S3` Function URL and
|
|
4
|
+
// the local dev upload route expose the SAME shape — POST `{fileName,contentType}`
|
|
5
|
+
// → `{uploadUrl, storageRef}`, then PUT the bytes to `uploadUrl`. `uploadAsset`
|
|
6
|
+
// resolves `Ok(storageRef)` — the same same-origin `/{prefix}/{key}` ref a command
|
|
7
|
+
// stores and the UI renders.
|
|
8
|
+
//
|
|
9
|
+
// The seed runs under Node, so the returned `uploadUrl` is resolved to an
|
|
10
|
+
// absolute URL against the endpoint's origin: AWS returns an absolute presigned
|
|
11
|
+
// S3 URL (unchanged), while the local route returns a same-origin relative
|
|
12
|
+
// `/{prefix}/{key}` that Node's `fetch` cannot PUT to until it is made absolute.
|
|
13
|
+
|
|
14
|
+
type fetchInit = {method: string, headers: dict<string>, body: string}
|
|
15
|
+
type response
|
|
16
|
+
|
|
17
|
+
@val external fetch: (string, fetchInit) => promise<response> = "fetch"
|
|
18
|
+
@send external responseJson: response => promise<JSON.t> = "json"
|
|
19
|
+
@send external responseText: response => promise<string> = "text"
|
|
20
|
+
@get external responseOk: response => bool = "ok"
|
|
21
|
+
@get external responseStatus: response => int = "status"
|
|
22
|
+
|
|
23
|
+
// Resolve a possibly-relative URL against a base (WHATWG URL semantics: an
|
|
24
|
+
// absolute `input` ignores `base`, a relative one is resolved against it).
|
|
25
|
+
type url
|
|
26
|
+
@new external makeUrl: (string, string) => url = "URL"
|
|
27
|
+
@get external urlHref: url => string = "href"
|
|
28
|
+
|
|
29
|
+
let field = (json: JSON.t, key: string): option<JSON.t> =>
|
|
30
|
+
switch json {
|
|
31
|
+
| Object(obj) => obj->Dict.get(key)
|
|
32
|
+
| _ => None
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let asString = (json: JSON.t): option<string> =>
|
|
36
|
+
switch json {
|
|
37
|
+
| String(s) => Some(s)
|
|
38
|
+
| _ => None
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// A `fetch` that reports an unreachable endpoint as `Error` instead of throwing,
|
|
42
|
+
// so the two-step upload can thread failures without exceptions.
|
|
43
|
+
let tryFetch = async (url: string, init: fetchInit): result<response, string> =>
|
|
44
|
+
try Ok(await fetch(url, init)) catch {
|
|
45
|
+
| _ => Error(`cannot reach ${url}`)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Uploads `bytes` under `fileName`/`contentType` through `uploadEndpoint` and
|
|
50
|
+
* resolves `Ok(storageRef)` (the stored `/{prefix}/{key}` ref), or `Error(msg)`
|
|
51
|
+
* describing the step that failed. `authToken`, when supplied, is sent as a
|
|
52
|
+
* Bearer on the presign request (the endpoint decides whether it is required;
|
|
53
|
+
* the PUT itself is unauthenticated — presigned on AWS, open locally).
|
|
54
|
+
*/
|
|
55
|
+
let uploadAsset = async (
|
|
56
|
+
~uploadEndpoint: string,
|
|
57
|
+
~bytes: string,
|
|
58
|
+
~fileName: string,
|
|
59
|
+
~contentType: string,
|
|
60
|
+
~authToken: option<string>=?,
|
|
61
|
+
): result<string, string> => {
|
|
62
|
+
let presignHeaders = Dict.fromArray([("content-type", "application/json")])
|
|
63
|
+
authToken->Option.forEach(t => presignHeaders->Dict.set("authorization", `Bearer ${t}`))
|
|
64
|
+
let presignBody = JSON.stringify(
|
|
65
|
+
JSON.Encode.object(
|
|
66
|
+
Dict.fromArray([
|
|
67
|
+
("fileName", JSON.Encode.string(fileName)),
|
|
68
|
+
("contentType", JSON.Encode.string(contentType)),
|
|
69
|
+
]),
|
|
70
|
+
),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
switch await tryFetch(uploadEndpoint, {method: "POST", headers: presignHeaders, body: presignBody}) {
|
|
74
|
+
| Error(m) => Error(`presign: ${m} — is the platform running?`)
|
|
75
|
+
| Ok(presignRes) if !(presignRes->responseOk) =>
|
|
76
|
+
let detail = await presignRes->responseText
|
|
77
|
+
Error(`presign failed with HTTP ${(presignRes->responseStatus)->Int.toString}: ${detail}`)
|
|
78
|
+
| Ok(presignRes) =>
|
|
79
|
+
let json = await presignRes->responseJson
|
|
80
|
+
switch (
|
|
81
|
+
json->field("uploadUrl")->Option.flatMap(asString),
|
|
82
|
+
json->field("storageRef")->Option.flatMap(asString),
|
|
83
|
+
) {
|
|
84
|
+
| (Some(uploadUrl), Some(storageRef)) =>
|
|
85
|
+
let putUrl = urlHref(makeUrl(uploadUrl, uploadEndpoint))
|
|
86
|
+
switch await tryFetch(
|
|
87
|
+
putUrl,
|
|
88
|
+
{method: "PUT", headers: Dict.fromArray([("content-type", contentType)]), body: bytes},
|
|
89
|
+
) {
|
|
90
|
+
| Error(m) => Error(`upload PUT: ${m}`)
|
|
91
|
+
| Ok(putRes) if putRes->responseOk => Ok(storageRef)
|
|
92
|
+
| Ok(putRes) =>
|
|
93
|
+
let detail = await putRes->responseText
|
|
94
|
+
Error(`upload PUT failed with HTTP ${(putRes->responseStatus)->Int.toString}: ${detail}`)
|
|
95
|
+
}
|
|
96
|
+
| _ => Error("presign response missing uploadUrl/storageRef")
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
4
|
+
|
|
5
|
+
function field(json, key) {
|
|
6
|
+
if (typeof json === "object" && json !== null && !Array.isArray(json)) {
|
|
7
|
+
return json[key];
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function asString(json) {
|
|
12
|
+
if (typeof json === "string") {
|
|
13
|
+
return json;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function tryFetch(url, init) {
|
|
18
|
+
try {
|
|
19
|
+
return {
|
|
20
|
+
TAG: "Ok",
|
|
21
|
+
_0: await fetch(url, init)
|
|
22
|
+
};
|
|
23
|
+
} catch (exn) {
|
|
24
|
+
return {
|
|
25
|
+
TAG: "Error",
|
|
26
|
+
_0: `cannot reach ` + url
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function uploadAsset(uploadEndpoint, bytes, fileName, contentType, authToken) {
|
|
32
|
+
let presignHeaders = Object.fromEntries([[
|
|
33
|
+
"content-type",
|
|
34
|
+
"application/json"
|
|
35
|
+
]]);
|
|
36
|
+
Stdlib_Option.forEach(authToken, t => {
|
|
37
|
+
presignHeaders["authorization"] = `Bearer ` + t;
|
|
38
|
+
});
|
|
39
|
+
let presignBody = JSON.stringify(Object.fromEntries([
|
|
40
|
+
[
|
|
41
|
+
"fileName",
|
|
42
|
+
fileName
|
|
43
|
+
],
|
|
44
|
+
[
|
|
45
|
+
"contentType",
|
|
46
|
+
contentType
|
|
47
|
+
]
|
|
48
|
+
]));
|
|
49
|
+
let m = await tryFetch(uploadEndpoint, {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers: presignHeaders,
|
|
52
|
+
body: presignBody
|
|
53
|
+
});
|
|
54
|
+
if (m.TAG !== "Ok") {
|
|
55
|
+
return {
|
|
56
|
+
TAG: "Error",
|
|
57
|
+
_0: `presign: ` + m._0 + ` — is the platform running?`
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
let presignRes = m._0;
|
|
61
|
+
if (presignRes.ok) {
|
|
62
|
+
let json = await presignRes.json();
|
|
63
|
+
let match = Stdlib_Option.flatMap(field(json, "uploadUrl"), asString);
|
|
64
|
+
let match$1 = Stdlib_Option.flatMap(field(json, "storageRef"), asString);
|
|
65
|
+
if (match === undefined) {
|
|
66
|
+
return {
|
|
67
|
+
TAG: "Error",
|
|
68
|
+
_0: "presign response missing uploadUrl/storageRef"
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (match$1 === undefined) {
|
|
72
|
+
return {
|
|
73
|
+
TAG: "Error",
|
|
74
|
+
_0: "presign response missing uploadUrl/storageRef"
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
let putUrl = new URL(match, uploadEndpoint).href;
|
|
78
|
+
let m$1 = await tryFetch(putUrl, {
|
|
79
|
+
method: "PUT",
|
|
80
|
+
headers: Object.fromEntries([[
|
|
81
|
+
"content-type",
|
|
82
|
+
contentType
|
|
83
|
+
]]),
|
|
84
|
+
body: bytes
|
|
85
|
+
});
|
|
86
|
+
if (m$1.TAG !== "Ok") {
|
|
87
|
+
return {
|
|
88
|
+
TAG: "Error",
|
|
89
|
+
_0: `upload PUT: ` + m$1._0
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
let putRes = m$1._0;
|
|
93
|
+
if (putRes.ok) {
|
|
94
|
+
return {
|
|
95
|
+
TAG: "Ok",
|
|
96
|
+
_0: match$1
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
let detail = await putRes.text();
|
|
100
|
+
return {
|
|
101
|
+
TAG: "Error",
|
|
102
|
+
_0: `upload PUT failed with HTTP ` + putRes.status.toString() + `: ` + detail
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
let detail$1 = await presignRes.text();
|
|
106
|
+
return {
|
|
107
|
+
TAG: "Error",
|
|
108
|
+
_0: `presign failed with HTTP ` + presignRes.status.toString() + `: ` + detail$1
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export {
|
|
113
|
+
field,
|
|
114
|
+
asString,
|
|
115
|
+
tryFetch,
|
|
116
|
+
uploadAsset,
|
|
117
|
+
}
|
|
118
|
+
/* No side effect */
|