@reventlessdev/reventless-aws 3.0.0-alpha.311 → 3.0.0-alpha.313

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 CHANGED
@@ -3,6 +3,20 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # 3.0.0-alpha.313 (2026-08-20)
7
+
8
+ ### Features
9
+
10
+ * **api:** emit a tagged-union state field as a GraphQL union ([3a380c0](https://github.com/ReventlessDev/reventless-core/commit/3a380c0ab055b87048d90a852ec1664f6aab6b00))
11
+
12
+
13
+ # 3.0.0-alpha.312 (2026-08-19)
14
+
15
+ ### Bug Fixes
16
+
17
+ * **aws:** build the platform's definition asset from the shared value ([c0e432d](https://github.com/ReventlessDev/reventless-core/commit/c0e432d98d96af3ff4c305b62f658ae102c5d142))
18
+
19
+
6
20
  # 3.0.0-alpha.311 (2026-08-19)
7
21
 
8
22
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-aws",
3
- "version": "3.0.0-alpha.311",
3
+ "version": "3.0.0-alpha.313",
4
4
  "description": "AWS adapters for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "dependencies": {
@@ -13,17 +13,17 @@
13
13
  "sury": "11.0.0-rc.1",
14
14
  "uuid": "^13.0.0",
15
15
  "@reventlessdev/rescript-aws-sdk": "3.0.0-alpha.12",
16
- "@reventlessdev/rescript-node": "2.0.0-alpha.8",
16
+ "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
17
17
  "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
18
+ "@reventlessdev/rescript-node": "2.0.0-alpha.8",
18
19
  "@reventlessdev/rescript-pulumi-aws": "3.0.0-alpha.1",
19
- "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
20
20
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
21
21
  "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
22
- "@reventlessdev/reventless-core": "3.0.0-alpha.240",
22
+ "@reventlessdev/reventless-core": "3.0.0-alpha.242",
23
+ "@reventlessdev/reventless-postgres": "3.0.0-alpha.106",
24
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.119",
23
25
  "@reventlessdev/reventless-interop": "3.0.0-alpha.32",
24
- "@reventlessdev/reventless-spec": "3.0.0-alpha.118",
25
- "@reventlessdev/reventless-postgres": "3.0.0-alpha.104",
26
- "@reventlessdev/reventless-infra": "3.0.0-alpha.146"
26
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.147"
27
27
  },
28
28
  "devDependencies": {
29
29
  "rescript": "12.3.0",
@@ -683,8 +683,12 @@ let loadPluginDefinition = (): Reventless.Plugin.pluginDefinition =>
683
683
  ->Reventless.Util_Sury.fromJsonString(Reventless.Plugin.pluginDefinitionSchema)
684
684
  } catch {
685
685
  | exn =>
686
+ // Util_Sury.exnMessage, not the local one: sury raises S.Exn, which is not a
687
+ // JsExn, so the local reader yields "unknown error" and loses the field path —
688
+ // the whole diagnosis for a decode failure here.
686
689
  JsError.throwWithMessage(
687
- "Failed to load pluginDefinition.json from asset zip: " ++ exnMessage(exn),
690
+ "Failed to load pluginDefinition.json from asset zip: " ++
691
+ Reventless.Util_Sury.exnMessage(exn),
688
692
  )
689
693
  }
690
694
 
@@ -524,7 +524,7 @@ function loadPluginDefinition() {
524
524
  return Util_Sury$Reventless.fromJsonString(Nodefs.readFileSync(assetPath("pluginDefinition.json"), "utf8"), Plugin$Reventless.pluginDefinitionSchema);
525
525
  } catch (raw_exn) {
526
526
  let exn = Primitive_exceptions.internalToException(raw_exn);
527
- return Stdlib_JsError.throwWithMessage("Failed to load pluginDefinition.json from asset zip: " + exnMessage(exn));
527
+ return Stdlib_JsError.throwWithMessage("Failed to load pluginDefinition.json from asset zip: " + Util_Sury$Reventless.exnMessage(exn, undefined));
528
528
  }
529
529
  }
530
530
 
@@ -268,12 +268,13 @@ module Make = (
268
268
  // keeps the JSON shape consistent so the entry point can parse uniformly.
269
269
  let synthesizeAdminContext = (): ReventlessCore.Plugin_Helpers.eventCollectorContext => {
270
270
  let config = configRef.contents
271
- // Stable JSON literal matches Reventless.Plugin.pluginDefinitionSchema
272
- // (optional fields encoded as null via js_nullable). Must stay byte-aligned with
273
- // the ReScript-built twin in `Platform_Admin.construct`'s `fakePluginDefinition`.
274
- let platformId = ReventlessCore.Platform_Admin_Structure.pluginId
271
+ // Encoded from the one placeholder value, through the same schema the entry
272
+ // point decodes with. It was a hand-written JSON literal, which drifted: the
273
+ // schema gained `dcbEventLog` and `kind` and the literal did not follow.
275
274
  let fakePluginDefinitionJson =
276
- `{"id":"${platformId}@INTERNAL","name":"${platformId}","version":"INTERNAL","extensionPoints":[],"extensions":[],"eventCollector":"NOT-SET","extensionProtocols":[],"apiSchemaFragment":null,"apiTarget":null,"structure":null}`->Pulumi.Output.make
275
+ ReventlessCore.Platform_Admin_Structure.internalPluginDefinition
276
+ ->Reventless.Util_Sury.toJsonString(Reventless.Plugin.pluginDefinitionSchema)
277
+ ->Pulumi.Output.make
277
278
  let adminEpEventTopicArn = switch config.eventTopicArn {
278
279
  | Some(arn) => arn
279
280
  | None => Pulumi.Output.make("NOT_AVAILABLE")
@@ -5,6 +5,8 @@ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
5
5
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
6
  import * as Pulumi from "@pulumi/pulumi";
7
7
  import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
8
+ import * as Plugin$Reventless from "@reventlessdev/reventless-spec/src/components/Plugin.res.mjs";
9
+ import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
8
10
  import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
9
11
  import * as Component$ReventlessCore from "@reventlessdev/reventless-core/src/components/Component.res.mjs";
10
12
  import * as DcbBackend$ReventlessAws from "../../adapter/DcbEventLog/DcbBackend.res.mjs";
@@ -202,7 +204,7 @@ function Make(EventCollectorChannel) {
202
204
  };
203
205
  let synthesizeAdminContext = () => {
204
206
  let config = configRef.contents;
205
- let fakePluginDefinitionJson = Pulumi.output(`{"id":"` + Platform_Admin_Structure$ReventlessCore.pluginId + `@INTERNAL","name":"` + Platform_Admin_Structure$ReventlessCore.pluginId + `","version":"INTERNAL","extensionPoints":[],"extensions":[],"eventCollector":"NOT-SET","extensionProtocols":[],"apiSchemaFragment":null,"apiTarget":null,"structure":null}`);
207
+ let fakePluginDefinitionJson = Pulumi.output(Util_Sury$Reventless.toJsonString(Platform_Admin_Structure$ReventlessCore.internalPluginDefinition, Plugin$Reventless.pluginDefinitionSchema, undefined));
206
208
  let arn = config.eventTopicArn;
207
209
  let adminEpEventTopicArn = arn !== undefined ? arn : Pulumi.output("NOT_AVAILABLE");
208
210
  return {
@@ -101,4 +101,15 @@ describe("pluginDefinition.json write/read symmetry", () => {
101
101
  expect(def.structure->Option.isNone)->toBe(true)
102
102
  expect(def.dcbEventLog->Option.isNone)->toBe(true)
103
103
  })
104
+
105
+ // The platform's own EventCollector ships a placeholder rather than a real
106
+ // definition — the admin IS the Plugin extension point — but it is read back on
107
+ // this same path, so it has to satisfy the same schema. Its asset used to be a
108
+ // hand-written JSON literal in PluginRuntime_Builder and fell two fields behind
109
+ // (dcbEventLog, kind) with nothing to notice.
110
+ testSync("the platform's own placeholder asset survives the same round trip", () => {
111
+ let def = ReventlessCore.Platform_Admin_Structure.internalPluginDefinition->writeAsset->readAsset
112
+ expect(def.id)->toBe(ReventlessCore.Platform_Admin_Structure.pluginId ++ "@INTERNAL")
113
+ expect(def.kind)->toBe(Reventless.Plugin.Domain)
114
+ })
104
115
  })
@@ -8,6 +8,7 @@ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
8
8
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
9
9
  import * as Plugin$Reventless from "@reventlessdev/reventless-spec/src/components/Plugin.res.mjs";
10
10
  import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
11
+ import * as Platform_Admin_Structure$ReventlessCore from "@reventlessdev/reventless-core/src/admin/Platform_Admin_Structure.res.mjs";
11
12
  import * as PluginExtensionPointSpec$ReventlessInfra from "@reventlessdev/reventless-infra/src/types/PluginExtensionPointSpec.res.mjs";
12
13
  import * as EventCollectorEntryPoint_Ops$ReventlessAws from "../src/adapter/Runtime/EventCollectorEntryPoint_Ops.res.mjs";
13
14
 
@@ -118,6 +119,11 @@ globalThis.describe("pluginDefinition.json write/read symmetry", () => {
118
119
  globalThis.expect(Stdlib_Option.isNone(def.structure)).toBe(true);
119
120
  globalThis.expect(Stdlib_Option.isNone(def.dcbEventLog)).toBe(true);
120
121
  });
122
+ globalThis.test("the platform's own placeholder asset survives the same round trip", () => {
123
+ let def = readAsset(Util_Sury$Reventless.toJsonString(Platform_Admin_Structure$ReventlessCore.internalPluginDefinition, Plugin$Reventless.pluginDefinitionSchema, undefined));
124
+ globalThis.expect(def.id).toBe(Platform_Admin_Structure$ReventlessCore.pluginId + "@INTERNAL");
125
+ globalThis.expect(def.kind).toBe("Domain");
126
+ });
121
127
  });
122
128
 
123
129
  export {
@@ -0,0 +1,101 @@
1
+ // A tagged-union state field, saved and read back through the real Postgres
2
+ // QueryDb path.
3
+ //
4
+ // Run via `pnpm run test:integration:pg` (boots a Postgres sidecar, runs the PG
5
+ // suites serially, tears down). Self-skips unless PG_URL is set.
6
+ //
7
+ // The Postgres resolver is the one backend that stamps a `__typename` of its own
8
+ // — the top-level row's ([PgQueryResolver_Lambda]) — and it stamps only that one.
9
+ // A union field sits one level in, which is precisely why the member stamp is
10
+ // written by the *write* path instead: what this suite proves is that the key
11
+ // survives the jsonb round trip, so the resolver has something to return.
12
+
13
+ open JestGlobals
14
+
15
+ @schema
16
+ type geolocation =
17
+ | Pending({requestedFor: string})
18
+ | Located({lat: float, lng: float})
19
+ | Unresolvable({reason: string})
20
+
21
+ let geolocationSchema = Reventless.TaggedUnion.named(~name="Geolocation", geolocationSchema)
22
+
23
+ module CustomersSpec = {
24
+ module Id = Reventless.Id.StringPure
25
+ let name = "PgUnionCustomers"
26
+ let moduleUrl = ""
27
+
28
+ @schema
29
+ type state = {customerId: string, geolocation: geolocation}
30
+
31
+ let config = Reventless.ReadModel.config()
32
+ let subIdConfig = None
33
+ let authorization: Reventless.Authorization.permission = AllowAuthenticated
34
+ let visibility: Reventless.Visibility.t = Public
35
+ }
36
+
37
+ switch NodeProcess.env->Dict.get("PG_URL") {
38
+ | None =>
39
+ testSync("Postgres union round trip (skipped — set PG_URL to run)", () =>
40
+ expect(true)->toBe(true)
41
+ )
42
+ | Some(url) =>
43
+ let pool = ReventlessPostgres.PgDriver.makePool({connectionString: url})
44
+
45
+ let jsonOps = ReventlessPostgres.QueryDbStorage_Postgres.makeOperations(
46
+ ~pool,
47
+ ~name=CustomersSpec.name,
48
+ ~indexes=[],
49
+ ~subIdField=None,
50
+ )
51
+
52
+ module Ops = ReventlessCore.QueryDb_Operations.Make(
53
+ CustomersSpec,
54
+ {
55
+ let jsonOps = jsonOps
56
+ },
57
+ )
58
+
59
+ beforeAllAsync(async () => {
60
+ await ReventlessPostgres.PgSchema.ensureSchema(pool)
61
+ await ReventlessPostgres.PgSchema.truncateAll(pool)
62
+ let _ = await pool->ReventlessPostgres.PgDriver.query(
63
+ `DROP TABLE IF EXISTS qdb_${CustomersSpec.name}`,
64
+ [],
65
+ )
66
+ })
67
+ afterAll(() => {
68
+ let _ = pool->ReventlessPostgres.PgDriver.endPool
69
+ })
70
+
71
+ describe("QueryDb (Postgres) round-trips a union field", () => {
72
+ testPromise("a stored Located keeps its member type and its point", async () => {
73
+ let _ = await Ops.save(
74
+ "c-1"->CustomersSpec.Id.makeFromString,
75
+ {customerId: "c-1", geolocation: Located({lat: 48.2082, lng: 16.3738})},
76
+ ReventlessCore.QueryDb.Any,
77
+ None,
78
+ )
79
+
80
+ switch await jsonOps.load("c-1") {
81
+ | Ok(rows) =>
82
+ let typename =
83
+ rows
84
+ ->Array.get(0)
85
+ ->Option.flatMap(JSON.Decode.object)
86
+ ->Option.flatMap(o => o->Dict.get("geolocation"))
87
+ ->Option.flatMap(JSON.Decode.object)
88
+ ->Option.flatMap(o => o->Dict.get("__typename"))
89
+ ->Option.flatMap(JSON.Decode.string)
90
+ expect(typename)->toEqual(Some("GeolocationLocated"))
91
+ | Error(_) => expect("load failed")->toBe("Ok")
92
+ }
93
+
94
+ switch await Ops.load("c-1"->CustomersSpec.Id.makeFromString) {
95
+ | Ok([{geolocation: Located({lat, lng})}]) =>
96
+ expect((lat, lng))->toEqual((48.2082, 16.3738))
97
+ | _ => expect("round trip")->toBe("Located with its point intact")
98
+ }
99
+ })
100
+ })
101
+ }
@@ -0,0 +1,139 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Sury from "sury";
4
+ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
5
+ import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
6
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
7
+ import * as ReadModel$Reventless from "@reventlessdev/reventless-spec/src/components/ReadModel.res.mjs";
8
+ import * as TaggedUnion$Reventless from "@reventlessdev/reventless-spec/src/components/TaggedUnion.res.mjs";
9
+ import * as PgDriver$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/PgDriver.res.mjs";
10
+ import * as PgSchema$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/PgSchema.res.mjs";
11
+ import * as QueryDb_Operations$ReventlessCore from "@reventlessdev/reventless-core/src/components/QueryDb/QueryDb_Operations.res.mjs";
12
+ import * as QueryDbStorage_Postgres$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/QueryDbStorage_Postgres.res.mjs";
13
+
14
+ let geolocationSchema = Sury.union([
15
+ Sury.$schema(s => ({
16
+ TAG: "Pending",
17
+ requestedFor: s.m(Sury.string)
18
+ })),
19
+ Sury.$schema(s => ({
20
+ TAG: "Located",
21
+ lat: s.m(Sury.float),
22
+ lng: s.m(Sury.float)
23
+ })),
24
+ Sury.$schema(s => ({
25
+ TAG: "Unresolvable",
26
+ reason: s.m(Sury.string)
27
+ }))
28
+ ]);
29
+
30
+ let geolocationSchema$1 = TaggedUnion$Reventless.named("Geolocation", geolocationSchema);
31
+
32
+ let name = "PgUnionCustomers";
33
+
34
+ let moduleUrl = "";
35
+
36
+ let stateSchema = Sury.$schema(s => ({
37
+ customerId: s.m(Sury.string),
38
+ geolocation: s.m(geolocationSchema$1)
39
+ }));
40
+
41
+ let config = ReadModel$Reventless.config(undefined, undefined, undefined);
42
+
43
+ let CustomersSpec = {
44
+ Id: undefined,
45
+ name: name,
46
+ moduleUrl: moduleUrl,
47
+ stateSchema: stateSchema,
48
+ config: config,
49
+ subIdConfig: undefined,
50
+ authorization: "AllowAuthenticated",
51
+ visibility: "Public"
52
+ };
53
+
54
+ let url = process.env["PG_URL"];
55
+
56
+ if (url !== undefined) {
57
+ let pool = PgDriver$ReventlessPostgres.makePool({
58
+ connectionString: url
59
+ });
60
+ let jsonOps = QueryDbStorage_Postgres$ReventlessPostgres.makeOperations(pool, name, [], undefined);
61
+ let Ops = QueryDb_Operations$ReventlessCore.Make({
62
+ Id: {
63
+ schema: Id$Reventless.StringPure.schema,
64
+ make: prim => prim,
65
+ makeFromString: prim => prim,
66
+ toString: prim => prim,
67
+ cmp: Id$Reventless.StringPure.cmp
68
+ },
69
+ name: name,
70
+ moduleUrl: moduleUrl,
71
+ stateSchema: stateSchema,
72
+ config: config,
73
+ subIdConfig: undefined,
74
+ authorization: "AllowAuthenticated",
75
+ visibility: "Public"
76
+ })({
77
+ jsonOps: jsonOps
78
+ });
79
+ globalThis.beforeAll(async () => {
80
+ await PgSchema$ReventlessPostgres.ensureSchema(pool);
81
+ await PgSchema$ReventlessPostgres.truncateAll(pool);
82
+ await PgDriver$ReventlessPostgres.query(pool, `DROP TABLE IF EXISTS qdb_` + name, []);
83
+ });
84
+ globalThis.afterAll(() => {
85
+ PgDriver$ReventlessPostgres.endPool(pool);
86
+ });
87
+ globalThis.describe("QueryDb (Postgres) round-trips a union field", () => {
88
+ globalThis.test("a stored Located keeps its member type and its point", async () => {
89
+ await Ops.save("c-1", {
90
+ customerId: "c-1",
91
+ geolocation: {
92
+ TAG: "Located",
93
+ lat: 48.2082,
94
+ lng: 16.3738
95
+ }
96
+ }, "Any", undefined);
97
+ let rows = await jsonOps.load("c-1");
98
+ if (rows.TAG === "Ok") {
99
+ let typename = Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(rows._0[0], Stdlib_JSON.Decode.object), o => o["geolocation"]), Stdlib_JSON.Decode.object), o => o["__typename"]), Stdlib_JSON.Decode.string);
100
+ globalThis.expect(typename).toEqual("GeolocationLocated");
101
+ } else {
102
+ globalThis.expect("load failed").toBe("Ok");
103
+ }
104
+ let match = await Ops.load("c-1");
105
+ if (match.TAG === "Ok") {
106
+ let match$1 = match._0;
107
+ if (match$1.length === 1) {
108
+ let match$2 = match$1[0];
109
+ let match$3 = match$2.geolocation;
110
+ switch (match$3.TAG) {
111
+ case "Located" :
112
+ globalThis.expect([
113
+ match$3.lat,
114
+ match$3.lng
115
+ ]).toEqual([
116
+ 48.2082,
117
+ 16.3738
118
+ ]);
119
+ return;
120
+ case "Pending" :
121
+ case "Unresolvable" :
122
+ break;
123
+ }
124
+ }
125
+ }
126
+ globalThis.expect("round trip").toBe("Located with its point intact");
127
+ });
128
+ });
129
+ } else {
130
+ globalThis.test("Postgres union round trip (skipped — set PG_URL to run)", () => {
131
+ globalThis.expect(true).toBe(true);
132
+ });
133
+ }
134
+
135
+ export {
136
+ geolocationSchema$1 as geolocationSchema,
137
+ CustomersSpec,
138
+ }
139
+ /* geolocationSchema Not a pure module */
@@ -47,6 +47,26 @@ let createEventLogTable = async (tableName): Util_DynamoDb_Runtime.resolvedTable
47
47
  }
48
48
  }
49
49
 
50
+ // A read-model table: `id` (HASH) alone. No range key — a projection with no
51
+ // `@subId` writes one row per id, which is the shape `QueryDbStorage_DynamoDb`
52
+ // provisions when `subIdField` is absent.
53
+ let createQueryDbTable = async (tableName): Util_DynamoDb_Runtime.resolvedTable => {
54
+ let input =
55
+ Dict.fromArray([
56
+ ("TableName", s(tableName)),
57
+ ("AttributeDefinitions", [attrDef("id")]->JSON.Encode.array),
58
+ ("KeySchema", [keyEl("id", "HASH")]->JSON.Encode.array),
59
+ ("BillingMode", s("PAY_PER_REQUEST")),
60
+ ])->JSON.Encode.object
61
+ let _ = await send(createTableCommand(input))
62
+ {
63
+ Util_DynamoDb_Runtime.id: tableName,
64
+ name: tableName,
65
+ arn: `arn:aws:dynamodb:local:000000000000:table/${tableName}`,
66
+ hashKey: "id",
67
+ }
68
+ }
69
+
50
70
  let deleteTable = async (table: Util_DynamoDb_Runtime.resolvedTable) => {
51
71
  let input = Dict.fromArray([("TableName", s(table.name))])->JSON.Encode.object
52
72
  let _ = await send(deleteTableCommand(input))
@@ -72,6 +72,35 @@ async function createEventLogTable(tableName) {
72
72
  };
73
73
  }
74
74
 
75
+ async function createQueryDbTable(tableName) {
76
+ let input = Object.fromEntries([
77
+ [
78
+ "TableName",
79
+ tableName
80
+ ],
81
+ [
82
+ "AttributeDefinitions",
83
+ [attrDef("id")]
84
+ ],
85
+ [
86
+ "KeySchema",
87
+ [keyEl("id", "HASH")]
88
+ ],
89
+ [
90
+ "BillingMode",
91
+ "PAY_PER_REQUEST"
92
+ ]
93
+ ]);
94
+ let cmd = new ClientDynamodb.CreateTableCommand(input);
95
+ await DynamoDb_DynamoDb$AwsSdk.client().send(cmd);
96
+ return {
97
+ id: tableName,
98
+ name: tableName,
99
+ arn: `arn:aws:dynamodb:local:000000000000:table/` + tableName,
100
+ hashKey: "id"
101
+ };
102
+ }
103
+
75
104
  async function deleteTable(table) {
76
105
  let input = Object.fromEntries([[
77
106
  "TableName",
@@ -87,6 +116,7 @@ export {
87
116
  attrDef,
88
117
  keyEl,
89
118
  createEventLogTable,
119
+ createQueryDbTable,
90
120
  deleteTable,
91
121
  }
92
122
  /* @aws-sdk/client-dynamodb Not a pure module */
@@ -0,0 +1,87 @@
1
+ // A tagged-union state field, saved and read back through the real DynamoDB
2
+ // QueryDb path (DynamoDB Local — NOT run by the default unit suite; see
3
+ // `jest.integration.config.js` + `scripts/run-integration-tests.sh`).
4
+ //
5
+ // What a backend has to do here is carry a nested map with one extra key and
6
+ // hand it back unchanged. That is exactly what the AppSync resolvers rely on:
7
+ // they return the stored item as it is, and the `__typename` the write path
8
+ // stamped is the only thing that tells GraphQL which union member the value is.
9
+ // A backend that dropped or renamed the key would not produce a decode error —
10
+ // it would produce a member that resolves to null, and a null in a non-nullable
11
+ // field takes its parent row with it.
12
+
13
+ open JestGlobals
14
+
15
+ module H = AggIntegrationHarness
16
+
17
+ @schema
18
+ type geolocation =
19
+ | Pending({requestedFor: string})
20
+ | Located({lat: float, lng: float})
21
+ | Unresolvable({reason: string})
22
+
23
+ let geolocationSchema = Reventless.TaggedUnion.named(~name="Geolocation", geolocationSchema)
24
+
25
+ module CustomersSpec = {
26
+ module Id = Reventless.Id.StringPure
27
+ let name = "UnionRoundTripCustomers"
28
+ let moduleUrl = ""
29
+
30
+ @schema
31
+ type state = {customerId: string, geolocation: geolocation}
32
+
33
+ let config = Reventless.ReadModel.config()
34
+ let subIdConfig = None
35
+ let authorization: Reventless.Authorization.permission = AllowAuthenticated
36
+ let visibility: Reventless.Visibility.t = Public
37
+ }
38
+
39
+ let tableName = "qdb-union-round-trip"
40
+
41
+ describe("QueryDb (DynamoDB) round-trips a union field", () => {
42
+ testPromise("a stored Located keeps its member type and its point", async () => {
43
+ let table = await H.createQueryDbTable(tableName)
44
+ let jsonOps = QueryDbEntryPoint_Ops.makeDynamoQueryDbOps(~tableName=table.name)
45
+ module Ops = ReventlessCore.QueryDb_Operations.Make(
46
+ CustomersSpec,
47
+ {
48
+ let jsonOps = jsonOps
49
+ },
50
+ )
51
+
52
+ let _ = await Ops.save(
53
+ "c-1",
54
+ {customerId: "c-1", geolocation: Located({lat: 48.2082, lng: 16.3738})},
55
+ ReventlessCore.QueryDb.Any,
56
+ None,
57
+ )
58
+
59
+ // The item as a read door hands it back.
60
+ let stored =
61
+ await jsonOps.loadStream("c-1")
62
+ ->Stream.runCollect
63
+ ->Effect.catchAll(_ => Effect.succeed([]))
64
+ ->Effect.runPromise
65
+ let typename =
66
+ stored
67
+ ->Array.get(0)
68
+ ->Option.flatMap(JSON.Decode.object)
69
+ ->Option.flatMap(o => o->Dict.get("geolocation"))
70
+ ->Option.flatMap(JSON.Decode.object)
71
+ ->Option.flatMap(o => o->Dict.get("__typename"))
72
+ ->Option.flatMap(JSON.Decode.string)
73
+ expect(typename)->toEqual(Some("GeolocationLocated"))
74
+
75
+ let items =
76
+ await Ops.loadStream("c-1"->CustomersSpec.Id.makeFromString)
77
+ ->Stream.runCollect
78
+ ->Effect.catchAll(_ => Effect.succeed([]))
79
+ ->Effect.runPromise
80
+ switch items {
81
+ | [{geolocation: Located({lat, lng})}] => expect((lat, lng))->toEqual((48.2082, 16.3738))
82
+ | _ => expect("round trip")->toBe("Located with its point intact")
83
+ }
84
+
85
+ await H.deleteTable(table)
86
+ })
87
+ })
@@ -0,0 +1,128 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Sury from "sury";
4
+ import * as Stream from "@reventlessdev/rescript-effect/src/Stream.res.mjs";
5
+ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
6
+ import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
7
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
8
+ import * as Effect from "effect/Effect";
9
+ import * as ReadModel$Reventless from "@reventlessdev/reventless-spec/src/components/ReadModel.res.mjs";
10
+ import * as TaggedUnion$Reventless from "@reventlessdev/reventless-spec/src/components/TaggedUnion.res.mjs";
11
+ import * as QueryDb_Operations$ReventlessCore from "@reventlessdev/reventless-core/src/components/QueryDb/QueryDb_Operations.res.mjs";
12
+ import * as AggIntegrationHarness$ReventlessAws from "./AggIntegrationHarness.res.mjs";
13
+ import * as QueryDbEntryPoint_Ops$ReventlessAws from "../../src/adapter/Runtime/QueryDbEntryPoint_Ops.res.mjs";
14
+
15
+ let geolocationSchema = Sury.union([
16
+ Sury.$schema(s => ({
17
+ TAG: "Pending",
18
+ requestedFor: s.m(Sury.string)
19
+ })),
20
+ Sury.$schema(s => ({
21
+ TAG: "Located",
22
+ lat: s.m(Sury.float),
23
+ lng: s.m(Sury.float)
24
+ })),
25
+ Sury.$schema(s => ({
26
+ TAG: "Unresolvable",
27
+ reason: s.m(Sury.string)
28
+ }))
29
+ ]);
30
+
31
+ let geolocationSchema$1 = TaggedUnion$Reventless.named("Geolocation", geolocationSchema);
32
+
33
+ let name = "UnionRoundTripCustomers";
34
+
35
+ let moduleUrl = "";
36
+
37
+ let stateSchema = Sury.$schema(s => ({
38
+ customerId: s.m(Sury.string),
39
+ geolocation: s.m(geolocationSchema$1)
40
+ }));
41
+
42
+ let config = ReadModel$Reventless.config(undefined, undefined, undefined);
43
+
44
+ let CustomersSpec = {
45
+ Id: undefined,
46
+ name: name,
47
+ moduleUrl: moduleUrl,
48
+ stateSchema: stateSchema,
49
+ config: config,
50
+ subIdConfig: undefined,
51
+ authorization: "AllowAuthenticated",
52
+ visibility: "Public"
53
+ };
54
+
55
+ let tableName = "qdb-union-round-trip";
56
+
57
+ globalThis.describe("QueryDb (DynamoDB) round-trips a union field", () => {
58
+ globalThis.test("a stored Located keeps its member type and its point", async () => {
59
+ let table = await AggIntegrationHarness$ReventlessAws.createQueryDbTable(tableName);
60
+ let jsonOps = QueryDbEntryPoint_Ops$ReventlessAws.makeDynamoQueryDbOps(table.name);
61
+ let Ops = QueryDb_Operations$ReventlessCore.Make({
62
+ Id: {
63
+ schema: Id$Reventless.StringPure.schema,
64
+ make: prim => prim,
65
+ makeFromString: prim => prim,
66
+ toString: prim => prim,
67
+ cmp: Id$Reventless.StringPure.cmp
68
+ },
69
+ name: name,
70
+ moduleUrl: moduleUrl,
71
+ stateSchema: stateSchema,
72
+ config: config,
73
+ subIdConfig: undefined,
74
+ authorization: "AllowAuthenticated",
75
+ visibility: "Public"
76
+ })({
77
+ jsonOps: jsonOps
78
+ });
79
+ await Ops.save("c-1", {
80
+ customerId: "c-1",
81
+ geolocation: {
82
+ TAG: "Located",
83
+ lat: 48.2082,
84
+ lng: 16.3738
85
+ }
86
+ }, "Any", undefined);
87
+ let stored = await Effect.runPromise(Effect.catchAll(Stream.runCollect(jsonOps.loadStream("c-1")), param => Effect.succeed([])));
88
+ let typename = Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(stored[0], Stdlib_JSON.Decode.object), o => o["geolocation"]), Stdlib_JSON.Decode.object), o => o["__typename"]), Stdlib_JSON.Decode.string);
89
+ globalThis.expect(typename).toEqual("GeolocationLocated");
90
+ let items = await Effect.runPromise(Effect.catchAll(Stream.runCollect(Ops.loadStream("c-1")), param => Effect.succeed([])));
91
+ let exit = 0;
92
+ if (items.length !== 1) {
93
+ exit = 1;
94
+ } else {
95
+ let match = items[0];
96
+ let match$1 = match.geolocation;
97
+ switch (match$1.TAG) {
98
+ case "Located" :
99
+ globalThis.expect([
100
+ match$1.lat,
101
+ match$1.lng
102
+ ]).toEqual([
103
+ 48.2082,
104
+ 16.3738
105
+ ]);
106
+ break;
107
+ case "Pending" :
108
+ case "Unresolvable" :
109
+ exit = 1;
110
+ break;
111
+ }
112
+ }
113
+ if (exit === 1) {
114
+ globalThis.expect("round trip").toBe("Located with its point intact");
115
+ }
116
+ return await AggIntegrationHarness$ReventlessAws.deleteTable(table);
117
+ });
118
+ });
119
+
120
+ let H;
121
+
122
+ export {
123
+ H,
124
+ geolocationSchema$1 as geolocationSchema,
125
+ CustomersSpec,
126
+ tableName,
127
+ }
128
+ /* geolocationSchema Not a pure module */