@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.
Files changed (95) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/LICENSE +202 -0
  3. package/README.md +167 -0
  4. package/lib/bs/.compiler.log +2 -0
  5. package/lib/bs/ReventlessSeed.cmi +0 -0
  6. package/lib/bs/ReventlessSeed.cmj +0 -0
  7. package/lib/bs/ReventlessSeed.cmt +0 -0
  8. package/lib/bs/ReventlessSeed.mlmap +7 -0
  9. package/lib/bs/build.ninja +0 -0
  10. package/lib/bs/compiler-info.json +8 -0
  11. package/lib/bs/src/Seed-ReventlessSeed.cmi +0 -0
  12. package/lib/bs/src/Seed-ReventlessSeed.cmj +0 -0
  13. package/lib/bs/src/Seed-ReventlessSeed.cmt +0 -0
  14. package/lib/bs/src/Seed.ast +0 -0
  15. package/lib/bs/src/Seed.res +30 -0
  16. package/lib/bs/src/Seed.res.mjs +34 -0
  17. package/lib/bs/src/Seed_Client-ReventlessSeed.cmi +0 -0
  18. package/lib/bs/src/Seed_Client-ReventlessSeed.cmj +0 -0
  19. package/lib/bs/src/Seed_Client-ReventlessSeed.cmt +0 -0
  20. package/lib/bs/src/Seed_Client.ast +0 -0
  21. package/lib/bs/src/Seed_Client.res +251 -0
  22. package/lib/bs/src/Seed_Client.res.mjs +242 -0
  23. package/lib/bs/src/Seed_Random-ReventlessSeed.cmi +0 -0
  24. package/lib/bs/src/Seed_Random-ReventlessSeed.cmj +0 -0
  25. package/lib/bs/src/Seed_Random-ReventlessSeed.cmt +0 -0
  26. package/lib/bs/src/Seed_Random.ast +0 -0
  27. package/lib/bs/src/Seed_Random.res +80 -0
  28. package/lib/bs/src/Seed_Random.res.mjs +86 -0
  29. package/lib/bs/src/Seed_Runner-ReventlessSeed.cmi +0 -0
  30. package/lib/bs/src/Seed_Runner-ReventlessSeed.cmj +0 -0
  31. package/lib/bs/src/Seed_Runner-ReventlessSeed.cmt +0 -0
  32. package/lib/bs/src/Seed_Runner.ast +0 -0
  33. package/lib/bs/src/Seed_Runner.res +114 -0
  34. package/lib/bs/src/Seed_Runner.res.mjs +118 -0
  35. package/lib/bs/src/Seed_Types-ReventlessSeed.cmi +0 -0
  36. package/lib/bs/src/Seed_Types-ReventlessSeed.cmj +0 -0
  37. package/lib/bs/src/Seed_Types-ReventlessSeed.cmt +0 -0
  38. package/lib/bs/src/Seed_Types.ast +0 -0
  39. package/lib/bs/src/Seed_Types.res +49 -0
  40. package/lib/bs/src/Seed_Types.res.mjs +79 -0
  41. package/lib/bs/src/Seed_Upload-ReventlessSeed.cmi +0 -0
  42. package/lib/bs/src/Seed_Upload-ReventlessSeed.cmj +0 -0
  43. package/lib/bs/src/Seed_Upload-ReventlessSeed.cmt +0 -0
  44. package/lib/bs/src/Seed_Upload.ast +0 -0
  45. package/lib/bs/src/Seed_Upload.res +99 -0
  46. package/lib/bs/src/Seed_Upload.res.mjs +118 -0
  47. package/lib/ocaml/.compiler.log +2 -0
  48. package/lib/ocaml/ReventlessSeed.cmi +0 -0
  49. package/lib/ocaml/ReventlessSeed.cmj +0 -0
  50. package/lib/ocaml/ReventlessSeed.cmt +0 -0
  51. package/lib/ocaml/ReventlessSeed.mlmap +7 -0
  52. package/lib/ocaml/Seed-ReventlessSeed.cmi +0 -0
  53. package/lib/ocaml/Seed-ReventlessSeed.cmj +0 -0
  54. package/lib/ocaml/Seed-ReventlessSeed.cmt +0 -0
  55. package/lib/ocaml/Seed.ast +0 -0
  56. package/lib/ocaml/Seed.res +30 -0
  57. package/lib/ocaml/Seed_Client-ReventlessSeed.cmi +0 -0
  58. package/lib/ocaml/Seed_Client-ReventlessSeed.cmj +0 -0
  59. package/lib/ocaml/Seed_Client-ReventlessSeed.cmt +0 -0
  60. package/lib/ocaml/Seed_Client.ast +0 -0
  61. package/lib/ocaml/Seed_Client.res +251 -0
  62. package/lib/ocaml/Seed_Random-ReventlessSeed.cmi +0 -0
  63. package/lib/ocaml/Seed_Random-ReventlessSeed.cmj +0 -0
  64. package/lib/ocaml/Seed_Random-ReventlessSeed.cmt +0 -0
  65. package/lib/ocaml/Seed_Random.ast +0 -0
  66. package/lib/ocaml/Seed_Random.res +80 -0
  67. package/lib/ocaml/Seed_Runner-ReventlessSeed.cmi +0 -0
  68. package/lib/ocaml/Seed_Runner-ReventlessSeed.cmj +0 -0
  69. package/lib/ocaml/Seed_Runner-ReventlessSeed.cmt +0 -0
  70. package/lib/ocaml/Seed_Runner.ast +0 -0
  71. package/lib/ocaml/Seed_Runner.res +114 -0
  72. package/lib/ocaml/Seed_Types-ReventlessSeed.cmi +0 -0
  73. package/lib/ocaml/Seed_Types-ReventlessSeed.cmj +0 -0
  74. package/lib/ocaml/Seed_Types-ReventlessSeed.cmt +0 -0
  75. package/lib/ocaml/Seed_Types.ast +0 -0
  76. package/lib/ocaml/Seed_Types.res +49 -0
  77. package/lib/ocaml/Seed_Upload-ReventlessSeed.cmi +0 -0
  78. package/lib/ocaml/Seed_Upload-ReventlessSeed.cmj +0 -0
  79. package/lib/ocaml/Seed_Upload-ReventlessSeed.cmt +0 -0
  80. package/lib/ocaml/Seed_Upload.ast +0 -0
  81. package/lib/ocaml/Seed_Upload.res +99 -0
  82. package/package.json +26 -0
  83. package/rescript.json +20 -0
  84. package/src/Seed.res +30 -0
  85. package/src/Seed.res.mjs +34 -0
  86. package/src/Seed_Client.res +251 -0
  87. package/src/Seed_Client.res.mjs +242 -0
  88. package/src/Seed_Random.res +80 -0
  89. package/src/Seed_Random.res.mjs +86 -0
  90. package/src/Seed_Runner.res +114 -0
  91. package/src/Seed_Runner.res.mjs +118 -0
  92. package/src/Seed_Types.res +49 -0
  93. package/src/Seed_Types.res.mjs +79 -0
  94. package/src/Seed_Upload.res +99 -0
  95. 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 */