@reventlessdev/reventless-seed-aws 1.0.0-alpha.7 → 1.0.0-alpha.8

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 (31) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/lib/bs/.compiler.log +2 -2
  3. package/lib/bs/compiler-info.json +1 -1
  4. package/lib/bs/src/ReventlessSeedAws_Reset.ast +0 -0
  5. package/lib/bs/src/ReventlessSeedAws_Reset.cmi +0 -0
  6. package/lib/bs/src/ReventlessSeedAws_Reset.cmj +0 -0
  7. package/lib/bs/src/ReventlessSeedAws_Reset.cmt +0 -0
  8. package/lib/bs/src/ReventlessSeedAws_Reset.res +261 -6
  9. package/lib/bs/src/ReventlessSeedAws_Reset.res.mjs +211 -25
  10. package/lib/bs/tests/ObjectStoreResetTest.ast +0 -0
  11. package/lib/bs/tests/ObjectStoreResetTest.cmi +0 -0
  12. package/lib/bs/tests/ObjectStoreResetTest.cmj +0 -0
  13. package/lib/bs/tests/ObjectStoreResetTest.cmt +0 -0
  14. package/lib/bs/tests/ObjectStoreResetTest.res +170 -0
  15. package/lib/bs/tests/ObjectStoreResetTest.res.mjs +211 -0
  16. package/lib/ocaml/.compiler.log +2 -2
  17. package/lib/ocaml/ObjectStoreResetTest.ast +0 -0
  18. package/lib/ocaml/ObjectStoreResetTest.cmi +0 -0
  19. package/lib/ocaml/ObjectStoreResetTest.cmj +0 -0
  20. package/lib/ocaml/ObjectStoreResetTest.cmt +0 -0
  21. package/lib/ocaml/ObjectStoreResetTest.res +170 -0
  22. package/lib/ocaml/ReventlessSeedAws_Reset.ast +0 -0
  23. package/lib/ocaml/ReventlessSeedAws_Reset.cmi +0 -0
  24. package/lib/ocaml/ReventlessSeedAws_Reset.cmj +0 -0
  25. package/lib/ocaml/ReventlessSeedAws_Reset.cmt +0 -0
  26. package/lib/ocaml/ReventlessSeedAws_Reset.res +261 -6
  27. package/package.json +1 -1
  28. package/src/ReventlessSeedAws_Reset.res +261 -6
  29. package/src/ReventlessSeedAws_Reset.res.mjs +211 -25
  30. package/tests/ObjectStoreResetTest.res +170 -0
  31. package/tests/ObjectStoreResetTest.res.mjs +211 -0
@@ -0,0 +1,211 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as JestGlobals from "@reventlessdev/rescript-jest/src/JestGlobals.res.mjs";
4
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
+ import * as ReventlessSeedAws_Reset from "../src/ReventlessSeedAws_Reset.res.mjs";
6
+
7
+ function obj(entries) {
8
+ return Object.fromEntries(entries);
9
+ }
10
+
11
+ function str(prim) {
12
+ return prim;
13
+ }
14
+
15
+ function storeEntry(bucket, prefix) {
16
+ return Object.fromEntries([
17
+ [
18
+ "bucketName",
19
+ bucket
20
+ ],
21
+ [
22
+ "keyPrefix",
23
+ prefix
24
+ ]
25
+ ]);
26
+ }
27
+
28
+ function output(entries) {
29
+ return Object.fromEntries(entries);
30
+ }
31
+
32
+ function store(qualified, bucket, prefix) {
33
+ let match = Stdlib_Option.getOr(ReventlessSeedAws_Reset.splitQualified(qualified), [
34
+ "",
35
+ qualified
36
+ ]);
37
+ return {
38
+ qualified: qualified,
39
+ plugin: match[0],
40
+ store: match[1],
41
+ bucketName: bucket,
42
+ keyPrefix: prefix
43
+ };
44
+ }
45
+
46
+ globalThis.describe("parseObjectStores", () => {
47
+ globalThis.test("splits the qualified key into plugin and store", () => {
48
+ let entries = [[
49
+ "Catalog.productImages",
50
+ storeEntry("alpha-stores", "productImages")
51
+ ]];
52
+ let other = ReventlessSeedAws_Reset.parseObjectStores(Object.fromEntries(entries));
53
+ if (other.TAG === "Ok") {
54
+ let match = other._0;
55
+ if (match.length === 1) {
56
+ let s = match[0];
57
+ globalThis.expect([
58
+ s.plugin,
59
+ s.store,
60
+ s.bucketName,
61
+ s.keyPrefix
62
+ ]).toEqual([
63
+ "Catalog",
64
+ "productImages",
65
+ "alpha-stores",
66
+ "productImages"
67
+ ]);
68
+ return;
69
+ }
70
+ }
71
+ JestGlobals.fail(`expected one parsed store, got ` + Stdlib_Option.getOr(JSON.stringify(other), "?"));
72
+ });
73
+ globalThis.test("splits at the first dot only", () => {
74
+ let match = ReventlessSeedAws_Reset.splitQualified("Catalog.product.images");
75
+ if (match !== undefined) {
76
+ globalThis.expect([
77
+ match[0],
78
+ match[1]
79
+ ]).toEqual([
80
+ "Catalog",
81
+ "product.images"
82
+ ]);
83
+ return;
84
+ } else {
85
+ return JestGlobals.fail("expected a split");
86
+ }
87
+ });
88
+ globalThis.test("absent output yields no stores", () => {
89
+ let stores = ReventlessSeedAws_Reset.parseObjectStores(undefined);
90
+ if (stores.TAG !== "Ok") {
91
+ return JestGlobals.fail(stores._0);
92
+ }
93
+ globalThis.expect(stores._0.length).toBe(0);
94
+ });
95
+ globalThis.test("a malformed entry is an error, not a skip", () => {
96
+ let entries = [[
97
+ "Catalog.productImages",
98
+ Object.fromEntries([[
99
+ "keyPrefix",
100
+ "x"
101
+ ]])
102
+ ]];
103
+ let message = ReventlessSeedAws_Reset.parseObjectStores(Object.fromEntries(entries));
104
+ if (message.TAG === "Ok") {
105
+ return JestGlobals.fail("expected a malformed entry to be refused");
106
+ }
107
+ globalThis.expect(message._0.includes("Catalog.productImages")).toBe(true);
108
+ });
109
+ globalThis.test("an unqualified key is an error", () => {
110
+ let entries = [[
111
+ "productImages",
112
+ storeEntry("alpha-stores", "productImages")
113
+ ]];
114
+ let message = ReventlessSeedAws_Reset.parseObjectStores(Object.fromEntries(entries));
115
+ if (message.TAG === "Ok") {
116
+ return JestGlobals.fail("expected an unqualified key to be refused");
117
+ }
118
+ globalThis.expect(message._0.includes("productImages")).toBe(true);
119
+ });
120
+ });
121
+
122
+ globalThis.describe("validateStores", () => {
123
+ globalThis.test("distinct prefixes in one bucket are fine", () => {
124
+ globalThis.expect(ReventlessSeedAws_Reset.validateStores([
125
+ store("Catalog.productImages", "alpha-stores", "productImages"),
126
+ store("Ordering.labels", "alpha-stores", "labels")
127
+ ])).toEqual({
128
+ TAG: "Ok",
129
+ _0: undefined
130
+ });
131
+ });
132
+ globalThis.test("two plugins on one prefix are refused", () => {
133
+ let message = ReventlessSeedAws_Reset.validateStores([
134
+ store("Catalog.productImages", "alpha-stores", "productImages"),
135
+ store("Ordering.productImages", "alpha-stores", "productImages")
136
+ ]);
137
+ if (message.TAG === "Ok") {
138
+ return JestGlobals.fail("expected a collision to be refused");
139
+ }
140
+ let message$1 = message._0;
141
+ globalThis.expect(message$1.includes("Catalog.productImages") && message$1.includes("Ordering.productImages")).toBe(true);
142
+ });
143
+ globalThis.test("an enclosing prefix is refused", () => {
144
+ let message = ReventlessSeedAws_Reset.validateStores([
145
+ store("Legacy.catalog", "alpha-stores", "Catalog"),
146
+ store("Catalog.productImages", "alpha-stores", "Catalog/productImages")
147
+ ]);
148
+ if (message.TAG === "Ok") {
149
+ return JestGlobals.fail("expected an enclosing prefix to be refused");
150
+ }
151
+ globalThis.expect(message._0.includes("encloses")).toBe(true);
152
+ });
153
+ globalThis.test("one prefix in two different buckets is fine", () => {
154
+ globalThis.expect(ReventlessSeedAws_Reset.validateStores([
155
+ store("Catalog.productImages", "catalog-productImages", "productImages"),
156
+ store("Ordering.productImages", "ordering-productImages", "productImages")
157
+ ])).toEqual({
158
+ TAG: "Ok",
159
+ _0: undefined
160
+ });
161
+ });
162
+ globalThis.test("a sibling prefix sharing a character run is fine", () => {
163
+ globalThis.expect(ReventlessSeedAws_Reset.validateStores([
164
+ store("Catalog.images", "alpha-stores", "images"),
165
+ store("Catalog.imagesArchive", "alpha-stores", "images-archive")
166
+ ])).toEqual({
167
+ TAG: "Ok",
168
+ _0: undefined
169
+ });
170
+ });
171
+ globalThis.test("an empty or slash-bearing store name is refused", () => {
172
+ let match = ReventlessSeedAws_Reset.validateStores([store("Catalog.productImages", "alpha-stores", "")]);
173
+ if (match.TAG === "Ok") {
174
+ JestGlobals.fail("expected an empty prefix to be refused");
175
+ }
176
+ let match$1 = ReventlessSeedAws_Reset.validateStores([store("Catalog.a/b", "alpha-stores", "a/b")]);
177
+ if (match$1.TAG === "Ok") {
178
+ return JestGlobals.fail("expected a store name containing a slash to be refused");
179
+ }
180
+ });
181
+ });
182
+
183
+ globalThis.describe("pluginOf", () => {
184
+ globalThis.test("prefers the declared plugin name over the label", () => {
185
+ globalThis.expect(ReventlessSeedAws_Reset.pluginOf({
186
+ projectDir: "../catalog-aws",
187
+ label: "catalog",
188
+ group: "Domain",
189
+ plugin: "Catalog"
190
+ })).toBe("Catalog");
191
+ });
192
+ globalThis.test("falls back to the label when no plugin name is declared", () => {
193
+ globalThis.expect(ReventlessSeedAws_Reset.pluginOf({
194
+ projectDir: ".",
195
+ label: "platform",
196
+ group: "Platform"
197
+ })).toBe("platform");
198
+ });
199
+ });
200
+
201
+ let Reset;
202
+
203
+ export {
204
+ Reset,
205
+ obj,
206
+ str,
207
+ storeEntry,
208
+ output,
209
+ store,
210
+ }
211
+ /* Not a pure module */
@@ -1,2 +1,2 @@
1
- #Start(1785603404469)
2
- #Done(1785603404495)
1
+ #Start(1785624003215)
2
+ #Done(1785624003246)
@@ -0,0 +1,170 @@
1
+ // How the reset learns which uploaded objects belong to which plugin, and what
2
+ // it refuses.
3
+ //
4
+ // A declared store's bucket is built by the PLATFORM deploy, so its tags say
5
+ // which project built it, not whose data is in it. Ownership therefore comes
6
+ // from the platform's `objectStores` stack output, and these pin the pure half
7
+ // of that: parsing the output, and refusing any store set a prefix-scoped wipe
8
+ // could not separate. The impure half is a `pulumi stack output` subprocess and
9
+ // the S3 calls, and the decisions under test are in neither.
10
+
11
+ open JestGlobals
12
+
13
+ module Reset = ReventlessSeedAws_Reset
14
+
15
+ let obj = entries => JSON.Encode.object(entries->Dict.fromArray)
16
+ let str = JSON.Encode.string
17
+
18
+ let storeEntry = (~bucket, ~prefix) =>
19
+ obj([("bucketName", str(bucket)), ("keyPrefix", str(prefix))])
20
+
21
+ let output = entries => Some(obj(entries))
22
+
23
+ let store = (~qualified, ~bucket, ~prefix): Reset.objectStore => {
24
+ let (plugin, name) = Reset.splitQualified(qualified)->Option.getOr(("", qualified))
25
+ {qualified, plugin, store: name, bucketName: bucket, keyPrefix: prefix}
26
+ }
27
+
28
+ describe("parseObjectStores", () => {
29
+ testSync("splits the qualified key into plugin and store", () =>
30
+ switch Reset.parseObjectStores(
31
+ output([("Catalog.productImages", storeEntry(~bucket="alpha-stores", ~prefix="productImages"))]),
32
+ ) {
33
+ | Ok([s]) =>
34
+ expect((s.plugin, s.store, s.bucketName, s.keyPrefix))->toEqual((
35
+ "Catalog",
36
+ "productImages",
37
+ "alpha-stores",
38
+ "productImages",
39
+ ))
40
+ | other => fail(`expected one parsed store, got ${other->JSON.stringifyAny->Option.getOr("?")}`)
41
+ }
42
+ )
43
+
44
+ // A store name may contain a dot; a registered plugin name may not — so the
45
+ // split is at the first one, matching how the key is composed.
46
+ testSync("splits at the first dot only", () =>
47
+ switch Reset.splitQualified("Catalog.product.images") {
48
+ | Some((plugin, name)) => expect((plugin, name))->toEqual(("Catalog", "product.images"))
49
+ | None => fail("expected a split")
50
+ }
51
+ )
52
+
53
+ // A platform that declares no stores is ordinary, not an error.
54
+ testSync("absent output yields no stores", () =>
55
+ switch Reset.parseObjectStores(None) {
56
+ | Ok(stores) => expect(stores->Array.length)->toBe(0)
57
+ | Error(message) => fail(message)
58
+ }
59
+ )
60
+
61
+ // A store the reset cannot read is a store it would silently leave behind, so
62
+ // a malformed entry fails the run rather than being skipped.
63
+ testSync("a malformed entry is an error, not a skip", () =>
64
+ switch Reset.parseObjectStores(output([("Catalog.productImages", obj([("keyPrefix", str("x"))]))])) {
65
+ | Ok(_) => fail("expected a malformed entry to be refused")
66
+ | Error(message) => expect(message->String.includes("Catalog.productImages"))->toBe(true)
67
+ }
68
+ )
69
+
70
+ testSync("an unqualified key is an error", () =>
71
+ switch Reset.parseObjectStores(
72
+ output([("productImages", storeEntry(~bucket="alpha-stores", ~prefix="productImages"))]),
73
+ ) {
74
+ | Ok(_) => fail("expected an unqualified key to be refused")
75
+ | Error(message) => expect(message->String.includes("productImages"))->toBe(true)
76
+ }
77
+ )
78
+ })
79
+
80
+ describe("validateStores", () => {
81
+ testSync("distinct prefixes in one bucket are fine", () =>
82
+ expect(
83
+ Reset.validateStores([
84
+ store(~qualified="Catalog.productImages", ~bucket="alpha-stores", ~prefix="productImages"),
85
+ store(~qualified="Ordering.labels", ~bucket="alpha-stores", ~prefix="labels"),
86
+ ]),
87
+ )->toEqual(Ok())
88
+ )
89
+
90
+ // The cross-plugin collision: one prefix, two owners, nothing to tell their
91
+ // objects apart.
92
+ testSync("two plugins on one prefix are refused", () =>
93
+ switch Reset.validateStores([
94
+ store(~qualified="Catalog.productImages", ~bucket="alpha-stores", ~prefix="productImages"),
95
+ store(~qualified="Ordering.productImages", ~bucket="alpha-stores", ~prefix="productImages"),
96
+ ]) {
97
+ | Ok() => fail("expected a collision to be refused")
98
+ | Error(message) =>
99
+ expect(
100
+ message->String.includes("Catalog.productImages") &&
101
+ message->String.includes("Ordering.productImages"),
102
+ )->toBe(true)
103
+ }
104
+ )
105
+
106
+ // The same shape one level up: `Catalog` encloses `Catalog/productImages`, so
107
+ // wiping the first would delete the second's objects.
108
+ testSync("an enclosing prefix is refused", () =>
109
+ switch Reset.validateStores([
110
+ store(~qualified="Legacy.catalog", ~bucket="alpha-stores", ~prefix="Catalog"),
111
+ store(~qualified="Catalog.productImages", ~bucket="alpha-stores", ~prefix="Catalog/productImages"),
112
+ ]) {
113
+ | Ok() => fail("expected an enclosing prefix to be refused")
114
+ | Error(message) => expect(message->String.includes("encloses"))->toBe(true)
115
+ }
116
+ )
117
+
118
+ // Different buckets cannot reach each other, so an equal prefix there is not a
119
+ // collision — which is what the per-store layout produces in production.
120
+ testSync("one prefix in two different buckets is fine", () =>
121
+ expect(
122
+ Reset.validateStores([
123
+ store(~qualified="Catalog.productImages", ~bucket="catalog-productImages", ~prefix="productImages"),
124
+ store(~qualified="Ordering.productImages", ~bucket="ordering-productImages", ~prefix="productImages"),
125
+ ]),
126
+ )->toEqual(Ok())
127
+ )
128
+
129
+ // A shared prefix that merely starts with another's characters is not nesting:
130
+ // the delete prefix carries a trailing slash, so `images` cannot reach
131
+ // `images-archive`.
132
+ testSync("a sibling prefix sharing a character run is fine", () =>
133
+ expect(
134
+ Reset.validateStores([
135
+ store(~qualified="Catalog.images", ~bucket="alpha-stores", ~prefix="images"),
136
+ store(~qualified="Catalog.imagesArchive", ~bucket="alpha-stores", ~prefix="images-archive"),
137
+ ]),
138
+ )->toEqual(Ok())
139
+ )
140
+
141
+ testSync("an empty or slash-bearing store name is refused", () => {
142
+ switch Reset.validateStores([
143
+ store(~qualified="Catalog.productImages", ~bucket="alpha-stores", ~prefix=""),
144
+ ]) {
145
+ | Ok() => fail("expected an empty prefix to be refused")
146
+ | Error(_) => ()
147
+ }
148
+ switch Reset.validateStores([
149
+ store(~qualified="Catalog.a/b", ~bucket="alpha-stores", ~prefix="a/b"),
150
+ ]) {
151
+ | Ok() => fail("expected a store name containing a slash to be refused")
152
+ | Error(_) => ()
153
+ }
154
+ })
155
+ })
156
+
157
+ describe("pluginOf", () => {
158
+ // The `objectStores` keys carry the REGISTERED plugin name; the menu carries
159
+ // the operator-facing label. Attribution uses the declared name so the reset
160
+ // never has to guess the relation between the two.
161
+ testSync("prefers the declared plugin name over the label", () =>
162
+ expect(
163
+ Reset.pluginOf({projectDir: "../catalog-aws", label: "catalog", group: Domain, plugin: "Catalog"}),
164
+ )->toBe("Catalog")
165
+ )
166
+
167
+ testSync("falls back to the label when no plugin name is declared", () =>
168
+ expect(Reset.pluginOf({projectDir: ".", label: "platform", group: Platform}))->toBe("platform")
169
+ )
170
+ })