@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.
- package/CHANGELOG.md +7 -0
- package/lib/bs/.compiler.log +2 -2
- package/lib/bs/compiler-info.json +1 -1
- package/lib/bs/src/ReventlessSeedAws_Reset.ast +0 -0
- package/lib/bs/src/ReventlessSeedAws_Reset.cmi +0 -0
- package/lib/bs/src/ReventlessSeedAws_Reset.cmj +0 -0
- package/lib/bs/src/ReventlessSeedAws_Reset.cmt +0 -0
- package/lib/bs/src/ReventlessSeedAws_Reset.res +261 -6
- package/lib/bs/src/ReventlessSeedAws_Reset.res.mjs +211 -25
- package/lib/bs/tests/ObjectStoreResetTest.ast +0 -0
- package/lib/bs/tests/ObjectStoreResetTest.cmi +0 -0
- package/lib/bs/tests/ObjectStoreResetTest.cmj +0 -0
- package/lib/bs/tests/ObjectStoreResetTest.cmt +0 -0
- package/lib/bs/tests/ObjectStoreResetTest.res +170 -0
- package/lib/bs/tests/ObjectStoreResetTest.res.mjs +211 -0
- package/lib/ocaml/.compiler.log +2 -2
- package/lib/ocaml/ObjectStoreResetTest.ast +0 -0
- package/lib/ocaml/ObjectStoreResetTest.cmi +0 -0
- package/lib/ocaml/ObjectStoreResetTest.cmj +0 -0
- package/lib/ocaml/ObjectStoreResetTest.cmt +0 -0
- package/lib/ocaml/ObjectStoreResetTest.res +170 -0
- package/lib/ocaml/ReventlessSeedAws_Reset.ast +0 -0
- package/lib/ocaml/ReventlessSeedAws_Reset.cmi +0 -0
- package/lib/ocaml/ReventlessSeedAws_Reset.cmj +0 -0
- package/lib/ocaml/ReventlessSeedAws_Reset.cmt +0 -0
- package/lib/ocaml/ReventlessSeedAws_Reset.res +261 -6
- package/package.json +1 -1
- package/src/ReventlessSeedAws_Reset.res +261 -6
- package/src/ReventlessSeedAws_Reset.res.mjs +211 -25
- package/tests/ObjectStoreResetTest.res +170 -0
- package/tests/ObjectStoreResetTest.res.mjs +211 -0
|
@@ -5,6 +5,7 @@ import * as S3$AwsSdk from "@reventlessdev/rescript-aws-sdk/src/S3.res.mjs";
|
|
|
5
5
|
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
6
6
|
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
7
7
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
8
|
+
import * as Stdlib_String from "@rescript/runtime/lib/es6/Stdlib_String.js";
|
|
8
9
|
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
9
10
|
import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
|
|
10
11
|
import * as ReventlessSeedAws from "./ReventlessSeedAws.res.mjs";
|
|
@@ -198,6 +199,97 @@ async function discover(region, stack, platform) {
|
|
|
198
199
|
];
|
|
199
200
|
}
|
|
200
201
|
|
|
202
|
+
function splitQualified(key) {
|
|
203
|
+
return Stdlib_Option.map(Stdlib_String.indexOfOpt(key, "."), i => [
|
|
204
|
+
key.slice(0, i),
|
|
205
|
+
key.slice(i + 1 | 0, key.length)
|
|
206
|
+
]);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function parseObjectStores(json) {
|
|
210
|
+
if (json !== undefined) {
|
|
211
|
+
if (typeof json === "object" && json !== null && !Array.isArray(json)) {
|
|
212
|
+
return Stdlib_Array.reduce(Object.entries(json), {
|
|
213
|
+
TAG: "Ok",
|
|
214
|
+
_0: []
|
|
215
|
+
}, (acc, param) => {
|
|
216
|
+
let entry = param[1];
|
|
217
|
+
let qualified = param[0];
|
|
218
|
+
if (acc.TAG !== "Ok") {
|
|
219
|
+
return acc;
|
|
220
|
+
}
|
|
221
|
+
let match = splitQualified(qualified);
|
|
222
|
+
let match$1 = Stdlib_Option.flatMap(field(entry, "bucketName"), asString);
|
|
223
|
+
let match$2 = Stdlib_Option.flatMap(field(entry, "keyPrefix"), asString);
|
|
224
|
+
if (match !== undefined && match$1 !== undefined && match$2 !== undefined) {
|
|
225
|
+
return {
|
|
226
|
+
TAG: "Ok",
|
|
227
|
+
_0: acc._0.concat([{
|
|
228
|
+
qualified: qualified,
|
|
229
|
+
plugin: match[0],
|
|
230
|
+
store: match[1],
|
|
231
|
+
bucketName: match$1,
|
|
232
|
+
keyPrefix: match$2
|
|
233
|
+
}])
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
TAG: "Error",
|
|
238
|
+
_0: `the platform stack's \`objectStores\` output has a malformed entry for "` + qualified + `" — expected a {plugin}.{store} key carrying bucketName and keyPrefix.`
|
|
239
|
+
};
|
|
240
|
+
});
|
|
241
|
+
} else {
|
|
242
|
+
return {
|
|
243
|
+
TAG: "Error",
|
|
244
|
+
_0: "the platform stack's `objectStores` output is not an object."
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
} else {
|
|
248
|
+
return {
|
|
249
|
+
TAG: "Ok",
|
|
250
|
+
_0: []
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function validateStores(stores) {
|
|
256
|
+
let s = stores.find(s => {
|
|
257
|
+
if (s.keyPrefix === "") {
|
|
258
|
+
return true;
|
|
259
|
+
} else {
|
|
260
|
+
return s.store.includes("/");
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
if (s !== undefined) {
|
|
264
|
+
return {
|
|
265
|
+
TAG: "Error",
|
|
266
|
+
_0: `store "` + s.qualified + `" has an unusable key prefix ("` + s.keyPrefix + `") — a store name may not be empty or contain "/".`
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
let message = Stdlib_Array.findMap(stores, a => Stdlib_Array.findMap(stores, b => {
|
|
270
|
+
if (a.qualified === b.qualified || a.bucketName !== b.bucketName) {
|
|
271
|
+
return;
|
|
272
|
+
} else if (a.keyPrefix === b.keyPrefix) {
|
|
273
|
+
return `stores "` + a.qualified + `" and "` + b.qualified + `" both live at ` + (a.bucketName + `/` + a.keyPrefix + `/ — a prefix-scoped wipe cannot tell their objects `) + `apart. Rename one store, or qualify the \`@storageRef\` annotation if they were meant to be one shared store.`;
|
|
274
|
+
} else if (b.keyPrefix.startsWith(a.keyPrefix + "/")) {
|
|
275
|
+
return `store "` + a.qualified + `" (` + a.bucketName + `/` + a.keyPrefix + `/) encloses "` + b.qualified + `" ` + (`(` + b.keyPrefix + `/) — wiping the first would delete the second's objects. Rename one.`);
|
|
276
|
+
} else {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
}));
|
|
280
|
+
if (message !== undefined) {
|
|
281
|
+
return {
|
|
282
|
+
TAG: "Error",
|
|
283
|
+
_0: message
|
|
284
|
+
};
|
|
285
|
+
} else {
|
|
286
|
+
return {
|
|
287
|
+
TAG: "Ok",
|
|
288
|
+
_0: undefined
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
201
293
|
async function countTable(table) {
|
|
202
294
|
let loop = async (start, acc) => {
|
|
203
295
|
let out = await DynamoDb_DocumentClient$AwsSdk.ScanCommand.send(new LibDynamodb.ScanCommand({
|
|
@@ -216,10 +308,11 @@ async function countTable(table) {
|
|
|
216
308
|
return await loop(undefined, 0);
|
|
217
309
|
}
|
|
218
310
|
|
|
219
|
-
async function countBucket(bucket) {
|
|
311
|
+
async function countBucket(bucket, prefix) {
|
|
220
312
|
let loop = async (keyMarker, versionMarker, acc) => {
|
|
221
313
|
let out = await S3$AwsSdk.ListObjectVersionsCommand.send(new ClientS3.ListObjectVersionsCommand({
|
|
222
314
|
Bucket: bucket,
|
|
315
|
+
Prefix: prefix,
|
|
223
316
|
KeyMarker: keyMarker,
|
|
224
317
|
VersionIdMarker: versionMarker
|
|
225
318
|
}));
|
|
@@ -306,10 +399,11 @@ async function truncateTable(table) {
|
|
|
306
399
|
return await loop(undefined);
|
|
307
400
|
}
|
|
308
401
|
|
|
309
|
-
async function emptyBucket(bucket) {
|
|
402
|
+
async function emptyBucket(bucket, prefix) {
|
|
310
403
|
let loop = async (keyMarker, versionMarker) => {
|
|
311
404
|
let out = await S3$AwsSdk.ListObjectVersionsCommand.send(new ClientS3.ListObjectVersionsCommand({
|
|
312
405
|
Bucket: bucket,
|
|
406
|
+
Prefix: prefix,
|
|
313
407
|
KeyMarker: keyMarker,
|
|
314
408
|
VersionIdMarker: versionMarker
|
|
315
409
|
}));
|
|
@@ -341,6 +435,10 @@ async function emptyBucket(bucket) {
|
|
|
341
435
|
return await loop(undefined, undefined);
|
|
342
436
|
}
|
|
343
437
|
|
|
438
|
+
function pluginOf(t) {
|
|
439
|
+
return Stdlib_Option.getOr(t.plugin, t.label);
|
|
440
|
+
}
|
|
441
|
+
|
|
344
442
|
async function chooseScope(targets) {
|
|
345
443
|
let domain = targets.filter(t => t.group === "Domain");
|
|
346
444
|
let platform = targets.filter(t => t.group === "Platform");
|
|
@@ -479,6 +577,15 @@ function reportAll(resolvedList, stack, region) {
|
|
|
479
577
|
});
|
|
480
578
|
if (r.bucketCounts.length === 0) {
|
|
481
579
|
console.log(" (none)");
|
|
580
|
+
}
|
|
581
|
+
if (r.storeCounts.length !== 0) {
|
|
582
|
+
console.log(" Object stores:");
|
|
583
|
+
r.storeCounts.forEach(param => {
|
|
584
|
+
let c = param[1];
|
|
585
|
+
let s = param[0];
|
|
586
|
+
total.contents = total.contents + c | 0;
|
|
587
|
+
console.log(` ` + c.toString().padStart(8, " ") + ` ` + s.qualified + ` ` + (s.bucketName + `/` + s.keyPrefix + `/`));
|
|
588
|
+
});
|
|
482
589
|
return;
|
|
483
590
|
}
|
|
484
591
|
});
|
|
@@ -507,7 +614,38 @@ function run(stack, backend, targets, param) {
|
|
|
507
614
|
};
|
|
508
615
|
}
|
|
509
616
|
let selected = await chooseScope(targets);
|
|
510
|
-
let
|
|
617
|
+
let platformTarget = targets.find(t => t.group === "Platform");
|
|
618
|
+
let allStores;
|
|
619
|
+
if (platformTarget !== undefined) {
|
|
620
|
+
let output = ReventlessSeedAws.stackOutputs(platformTarget.projectDir, backend$1, stack$1);
|
|
621
|
+
let stores = parseObjectStores(field(output, "objectStores"));
|
|
622
|
+
if (stores.TAG === "Ok") {
|
|
623
|
+
allStores = stores._0;
|
|
624
|
+
} else {
|
|
625
|
+
throw {
|
|
626
|
+
RE_EXN_ID: Seed$ReventlessSeed.Failed,
|
|
627
|
+
_1: stores._0,
|
|
628
|
+
Error: new Error()
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
} else {
|
|
632
|
+
console.log("");
|
|
633
|
+
console.log("Note: no `platform` target is declared, so declared object stores could not be resolved — any uploaded objects will be left in place.");
|
|
634
|
+
allStores = [];
|
|
635
|
+
}
|
|
636
|
+
let message = validateStores(allStores);
|
|
637
|
+
if (message.TAG !== "Ok") {
|
|
638
|
+
throw {
|
|
639
|
+
RE_EXN_ID: Seed$ReventlessSeed.Failed,
|
|
640
|
+
_1: `refusing: ` + message._0,
|
|
641
|
+
Error: new Error()
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
let storeBucketNames = allStores.map(s => s.bucketName);
|
|
645
|
+
let selectedStores = allStores.filter(s => selected.some(t => pluginOf(t) === s.plugin));
|
|
646
|
+
let match = selectedStores.length !== 0;
|
|
647
|
+
let gated = match && platformTarget !== undefined && !selected.some(t => t.projectDir === platformTarget.projectDir) ? selected.concat([platformTarget]) : selected;
|
|
648
|
+
let regions = gated.map(t => gateTarget(t, backend$1, stack$1));
|
|
511
649
|
let region = regions[0];
|
|
512
650
|
if (regions.some(r => r !== region)) {
|
|
513
651
|
throw {
|
|
@@ -517,14 +655,29 @@ function run(stack, backend, targets, param) {
|
|
|
517
655
|
};
|
|
518
656
|
}
|
|
519
657
|
process.env["AWS_REGION"] = region;
|
|
658
|
+
if (selectedStores.length !== 0 && platformTarget !== undefined) {
|
|
659
|
+
let platformProject = projectName(platformTarget.projectDir);
|
|
660
|
+
let match$1 = await discover(region, stack$1, platformProject);
|
|
661
|
+
let platformBuckets = match$1[1];
|
|
662
|
+
selectedStores.forEach(s => {
|
|
663
|
+
if (platformBuckets.includes(s.bucketName)) {
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
throw {
|
|
667
|
+
RE_EXN_ID: Seed$ReventlessSeed.Failed,
|
|
668
|
+
_1: `refusing: store "` + s.qualified + `" names bucket ` + s.bucketName + `, which does not ` + (`carry reventless:platform=` + platformProject + ` + reventless:environment=` + stack$1 + `.`),
|
|
669
|
+
Error: new Error()
|
|
670
|
+
};
|
|
671
|
+
});
|
|
672
|
+
}
|
|
520
673
|
let resolvedList = [];
|
|
521
674
|
for (let i = 0, i_finish = selected.length; i < i_finish; ++i) {
|
|
522
675
|
let target = selected[i];
|
|
523
676
|
if (target !== undefined) {
|
|
524
677
|
let platform = projectName(target.projectDir);
|
|
525
|
-
let match = await discover(region, stack$1, platform);
|
|
526
|
-
let tables = match[0].toSorted(Primitive_string.compare);
|
|
527
|
-
let buckets = match[1].toSorted(Primitive_string.compare);
|
|
678
|
+
let match$2 = await discover(region, stack$1, platform);
|
|
679
|
+
let tables = match$2[0].toSorted(Primitive_string.compare);
|
|
680
|
+
let buckets = match$2[1].filter(b => !storeBucketNames.includes(b)).toSorted(Primitive_string.compare);
|
|
528
681
|
let tableCounts = [];
|
|
529
682
|
for (let j = 0, j_finish = tables.length; j < j_finish; ++j) {
|
|
530
683
|
let t = tables[j];
|
|
@@ -538,7 +691,18 @@ function run(stack, backend, targets, param) {
|
|
|
538
691
|
if (b !== undefined) {
|
|
539
692
|
bucketCounts.push([
|
|
540
693
|
b,
|
|
541
|
-
await countBucket(b)
|
|
694
|
+
await countBucket(b, undefined)
|
|
695
|
+
]);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
let stores$1 = selectedStores.filter(s => s.plugin === pluginOf(target));
|
|
699
|
+
let storeCounts = [];
|
|
700
|
+
for (let j$2 = 0, j_finish$2 = stores$1.length; j$2 < j_finish$2; ++j$2) {
|
|
701
|
+
let s = stores$1[j$2];
|
|
702
|
+
if (s !== undefined) {
|
|
703
|
+
storeCounts.push([
|
|
704
|
+
s,
|
|
705
|
+
await countBucket(s.bucketName, s.keyPrefix + `/`)
|
|
542
706
|
]);
|
|
543
707
|
}
|
|
544
708
|
}
|
|
@@ -547,7 +711,8 @@ function run(stack, backend, targets, param) {
|
|
|
547
711
|
platform: platform,
|
|
548
712
|
tables: tables,
|
|
549
713
|
tableCounts: tableCounts,
|
|
550
|
-
bucketCounts: bucketCounts
|
|
714
|
+
bucketCounts: bucketCounts,
|
|
715
|
+
storeCounts: storeCounts
|
|
551
716
|
});
|
|
552
717
|
}
|
|
553
718
|
}
|
|
@@ -576,37 +741,54 @@ function run(stack, backend, targets, param) {
|
|
|
576
741
|
let r = resolvedList[i$1];
|
|
577
742
|
if (r !== undefined) {
|
|
578
743
|
Seed_Runner$ReventlessSeed.heading(`Emptying ` + r.target.label + ` …`);
|
|
579
|
-
for (let j$
|
|
580
|
-
let t$1 = r.tables[j$
|
|
744
|
+
for (let j$3 = 0, j_finish$3 = r.tables.length; j$3 < j_finish$3; ++j$3) {
|
|
745
|
+
let t$1 = r.tables[j$3];
|
|
581
746
|
if (t$1 !== undefined) {
|
|
582
747
|
await truncateTable(t$1);
|
|
583
748
|
console.log(` truncated ` + t$1);
|
|
584
749
|
}
|
|
585
750
|
}
|
|
586
|
-
for (let j$
|
|
587
|
-
let match$
|
|
588
|
-
if (match$
|
|
589
|
-
let b$1 = match$
|
|
590
|
-
await emptyBucket(b$1);
|
|
751
|
+
for (let j$4 = 0, j_finish$4 = r.bucketCounts.length; j$4 < j_finish$4; ++j$4) {
|
|
752
|
+
let match$3 = r.bucketCounts[j$4];
|
|
753
|
+
if (match$3 !== undefined) {
|
|
754
|
+
let b$1 = match$3[0];
|
|
755
|
+
await emptyBucket(b$1, undefined);
|
|
591
756
|
console.log(` emptied ` + b$1);
|
|
592
757
|
}
|
|
593
758
|
}
|
|
759
|
+
for (let j$5 = 0, j_finish$5 = r.storeCounts.length; j$5 < j_finish$5; ++j$5) {
|
|
760
|
+
let match$4 = r.storeCounts[j$5];
|
|
761
|
+
if (match$4 !== undefined) {
|
|
762
|
+
let s$1 = match$4[0];
|
|
763
|
+
await emptyBucket(s$1.bucketName, s$1.keyPrefix + `/`);
|
|
764
|
+
console.log(` emptied ` + s$1.qualified + ` — ` + match$4[1].toString() + ` object(s) removed from ` + (s$1.bucketName + `/` + s$1.keyPrefix + `/`));
|
|
765
|
+
}
|
|
766
|
+
}
|
|
594
767
|
}
|
|
595
768
|
}
|
|
596
769
|
let remaining = 0;
|
|
597
770
|
for (let i$2 = 0, i_finish$2 = resolvedList.length; i$2 < i_finish$2; ++i$2) {
|
|
598
771
|
let r$1 = resolvedList[i$2];
|
|
599
772
|
if (r$1 !== undefined) {
|
|
600
|
-
for (let j$
|
|
601
|
-
let t$2 = r$1.tables[j$
|
|
773
|
+
for (let j$6 = 0, j_finish$6 = r$1.tables.length; j$6 < j_finish$6; ++j$6) {
|
|
774
|
+
let t$2 = r$1.tables[j$6];
|
|
602
775
|
if (t$2 !== undefined) {
|
|
603
776
|
remaining = remaining + await countTable(t$2) | 0;
|
|
604
777
|
}
|
|
605
778
|
}
|
|
606
|
-
for (let j$
|
|
607
|
-
let match$
|
|
608
|
-
if (match$
|
|
609
|
-
remaining = remaining + await countBucket(match$
|
|
779
|
+
for (let j$7 = 0, j_finish$7 = r$1.bucketCounts.length; j$7 < j_finish$7; ++j$7) {
|
|
780
|
+
let match$5 = r$1.bucketCounts[j$7];
|
|
781
|
+
if (match$5 !== undefined) {
|
|
782
|
+
remaining = remaining + await countBucket(match$5[0], undefined) | 0;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
for (let j$8 = 0, j_finish$8 = r$1.storeCounts.length; j$8 < j_finish$8; ++j$8) {
|
|
786
|
+
let match$6 = r$1.storeCounts[j$8];
|
|
787
|
+
if (match$6 !== undefined) {
|
|
788
|
+
let s$2 = match$6[0];
|
|
789
|
+
let left = await countBucket(s$2.bucketName, s$2.keyPrefix + `/`);
|
|
790
|
+
remaining = remaining + left | 0;
|
|
791
|
+
console.log(` verified empty: ` + s$2.qualified + ` — ` + left.toString() + ` object(s) remain under ` + (s$2.bucketName + `/` + s$2.keyPrefix + `/`));
|
|
610
792
|
}
|
|
611
793
|
}
|
|
612
794
|
}
|
|
@@ -623,17 +805,17 @@ function run(stack, backend, targets, param) {
|
|
|
623
805
|
process.exit(0);
|
|
624
806
|
return;
|
|
625
807
|
} catch (raw_message) {
|
|
626
|
-
let message = Primitive_exceptions.internalToException(raw_message);
|
|
627
|
-
if (message.RE_EXN_ID === Seed$ReventlessSeed.Failed) {
|
|
808
|
+
let message$1 = Primitive_exceptions.internalToException(raw_message);
|
|
809
|
+
if (message$1.RE_EXN_ID === Seed$ReventlessSeed.Failed) {
|
|
628
810
|
Seed_Prompt$ReventlessSeed.close();
|
|
629
811
|
console.error("");
|
|
630
|
-
console.error(`Reset aborted — ` + message._1);
|
|
812
|
+
console.error(`Reset aborted — ` + message$1._1);
|
|
631
813
|
process.exit(1);
|
|
632
814
|
} else {
|
|
633
815
|
Seed_Prompt$ReventlessSeed.close();
|
|
634
816
|
console.error("");
|
|
635
817
|
console.error("Reset aborted with an unexpected error:");
|
|
636
|
-
console.error(Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(message), Stdlib_JsExn.message), "unknown"));
|
|
818
|
+
console.error(Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(message$1), Stdlib_JsExn.message), "unknown"));
|
|
637
819
|
process.exit(1);
|
|
638
820
|
}
|
|
639
821
|
return;
|
|
@@ -662,11 +844,15 @@ export {
|
|
|
662
844
|
classify,
|
|
663
845
|
tagValue,
|
|
664
846
|
discover,
|
|
847
|
+
splitQualified,
|
|
848
|
+
parseObjectStores,
|
|
849
|
+
validateStores,
|
|
665
850
|
countTable,
|
|
666
851
|
countBucket,
|
|
667
852
|
sendBatch,
|
|
668
853
|
truncateTable,
|
|
669
854
|
emptyBucket,
|
|
855
|
+
pluginOf,
|
|
670
856
|
chooseScope,
|
|
671
857
|
gateTarget,
|
|
672
858
|
reportAll,
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
+
})
|