@reventlessdev/reventless-core 3.0.0-alpha.195 → 3.0.0-alpha.196

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,21 @@
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.196 (2026-07-31)
7
+
8
+ * feat(rescript)!: one Node bindings package, not two ([1258d8c](https://github.com/ReventlessDev/reventless-core/commit/1258d8c2b2ff2636b36a849fc5bdf9005c6fb0eb))
9
+ ### Features
10
+
11
+ * **spec:** add Money and a closed ISO 4217 Currency ([d4852ab](https://github.com/ReventlessDev/reventless-core/commit/d4852ab63e823e39fac793c4fa5ac31470db9655))
12
+
13
+ ### BREAKING CHANGES
14
+
15
+ * `@reventlessdev/rescript-node-streams` and
16
+ `@reventlessdev/rescript-node-zlib` are replaced by
17
+ `@reventlessdev/rescript-node`. Module names are unchanged.
18
+
19
+
20
+
6
21
  # 3.0.0-alpha.195 (2026-07-30)
7
22
 
8
23
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-core",
3
- "version": "3.0.0-alpha.195",
3
+ "version": "3.0.0-alpha.196",
4
4
  "description": "Core package for Reventless framework",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -28,17 +28,17 @@
28
28
  "dependencies": {
29
29
  "sury": "11.0.0-alpha.4",
30
30
  "uuid": "^13.0.0",
31
- "@reventlessdev/rescript-effect": "0.1.0-alpha.31",
32
- "@reventlessdev/rescript-hash-object": "1.2.0-alpha.13",
33
- "@reventlessdev/rescript-fast-csv": "1.2.0-alpha.14",
34
- "@reventlessdev/rescript-jest": "1.0.0-alpha.9",
35
- "@reventlessdev/rescript-node-streams": "1.1.0-alpha.13",
36
- "@reventlessdev/rescript-ssh2": "1.1.0-alpha.14",
37
- "@reventlessdev/rescript-uuid": "1.1.0-alpha.17",
38
- "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.17",
39
- "@reventlessdev/reventless-interop": "3.0.0-alpha.29",
40
- "@reventlessdev/reventless-spec": "3.0.0-alpha.88",
41
- "@reventlessdev/reventless-infra": "3.0.0-alpha.113"
31
+ "@reventlessdev/rescript-hash-object": "1.2.0-alpha.14",
32
+ "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
33
+ "@reventlessdev/rescript-node": "2.0.0-alpha.0",
34
+ "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
35
+ "@reventlessdev/rescript-fast-csv": "2.0.0-alpha.0",
36
+ "@reventlessdev/rescript-ssh2": "2.0.0-alpha.0",
37
+ "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
38
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.114",
39
+ "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
40
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.89",
41
+ "@reventlessdev/reventless-interop": "3.0.0-alpha.30"
42
42
  },
43
43
  "devDependencies": {
44
44
  "rescript": "12.3.0",
package/rescript.json CHANGED
@@ -29,7 +29,7 @@
29
29
  "@reventlessdev/rescript-effect",
30
30
  "@reventlessdev/rescript-fast-csv",
31
31
  "@reventlessdev/rescript-hash-object",
32
- "@reventlessdev/rescript-node-streams",
32
+ "@reventlessdev/rescript-node",
33
33
  "@reventlessdev/rescript-pulumi-pulumi",
34
34
  "@reventlessdev/rescript-ssh2",
35
35
  "@reventlessdev/rescript-uuid",
@@ -1165,16 +1165,14 @@ let getInteropMeta = (): Pulumi.Output.t<JSON.t> => {
1165
1165
  // Stack metadata export — environment, region, timestamp, actor, git SHA.
1166
1166
  // Called from both exportPluginOutputs and exportPlatformOutputs.
1167
1167
  // ---------------------------------------------------------------------------
1168
- @val external processEnv: dict<string> = "process.env"
1169
-
1170
1168
  let exportDeploymentMetadata = () => {
1171
1169
  let metadata =
1172
1170
  [
1173
1171
  ("environment", Pulumi.Pulumi.getStackName()),
1174
1172
  ("region", Pulumi.Config.make(Some("aws"))->Pulumi.Config.get("region")->Option.getOr("unknown")),
1175
1173
  ("timestamp", Date.make()->Date.toISOString),
1176
- ("gitSha", processEnv->Dict.get("GITHUB_SHA")->Option.getOr("unknown")),
1177
- ("actor", processEnv->Dict.get("GITHUB_ACTOR")->Option.getOr("unknown")),
1174
+ ("gitSha", NodeProcess.env->Dict.get("GITHUB_SHA")->Option.getOr("unknown")),
1175
+ ("actor", NodeProcess.env->Dict.get("GITHUB_ACTOR")->Option.getOr("unknown")),
1178
1176
  ]
1179
1177
  ->Dict.fromArray
1180
1178
  Pulumi.Pulumi.export(
@@ -1516,15 +1514,15 @@ let exportPluginOutputs = (pluginOutputs: Plugin.outputs) => {
1516
1514
  | Some(hook) =>
1517
1515
  // Read deployment provenance from env vars synchronously (before Output.apply).
1518
1516
  let actor =
1519
- processEnv
1517
+ NodeProcess.env
1520
1518
  ->Dict.get("GITHUB_ACTOR")
1521
- ->Option.orElse(processEnv->Dict.get("CI_COMMIT_AUTHOR"))
1522
- ->Option.orElse(processEnv->Dict.get("USER"))
1519
+ ->Option.orElse(NodeProcess.env->Dict.get("CI_COMMIT_AUTHOR"))
1520
+ ->Option.orElse(NodeProcess.env->Dict.get("USER"))
1523
1521
  ->Option.getOr("local")
1524
1522
  let deploymentId =
1525
- processEnv
1523
+ NodeProcess.env
1526
1524
  ->Dict.get("GITHUB_SHA")
1527
- ->Option.orElse(processEnv->Dict.get("CI_COMMIT_SHA"))
1525
+ ->Option.orElse(NodeProcess.env->Dict.get("CI_COMMIT_SHA"))
1528
1526
  ->Option.getOr(Date.make()->Date.toISOString)
1529
1527
  let schemaFor = name =>
1530
1528
  componentSchemaRegistry->Dict.get(name)->Option.getOr({})
@@ -2,7 +2,7 @@
2
2
 
3
3
  import * as SSH2 from "@reventlessdev/rescript-ssh2/src/SSH2.res.mjs";
4
4
  import * as Ssh2 from "ssh2";
5
- import * as Stream from "stream";
5
+ import * as Nodestream from "node:stream";
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
8
  import * as Effect from "effect/Effect";
@@ -122,7 +122,7 @@ function ftp(connectionParams, ftpAction) {
122
122
  let err$1 = new Error("FTPHandler: Error in Write Stream");
123
123
  sftp.emit("error", err$1);
124
124
  });
125
- return await Stream.promises.pipeline(ftpAction._0, ws);
125
+ return await Nodestream.promises.pipeline(ftpAction._0, ws);
126
126
  }
127
127
  }
128
128
  }, () => {});
@@ -677,4 +677,69 @@ describe("SuryToJsonSchema:", () => {
677
677
  ))->toEqual((Some(JSON.Encode.string("type")), Some(JSON.Encode.string("type"))))
678
678
  )
679
679
  })
680
+
681
+ // The other half of that claim, stated so it cannot be confused with it.
682
+ // `Money` is the first semantic type that is *not* a brand: it turns a number
683
+ // into an object, so the wire shape changes and stored events written against
684
+ // the old field do not decode. The assertions below are the evidence for that
685
+ // — a schema diff before and after a retype is exactly this, and it is what
686
+ // says a deployment needs an upcaster before it can take the change.
687
+ describe("Money changes the field's shape, and says so:", () => {
688
+ let json = SuryToJsonSchema.deriveObjectSchema(
689
+ S.schema(s =>
690
+ {
691
+ "price": s.matches(Reventless.Money.schema),
692
+ "quantity": s.matches(S.float),
693
+ }
694
+ )->S.castToUnknown,
695
+ )
696
+ let keyOf = (name, key) => getPropertyOf(json, name)->Option.flatMap(s => getProperty(s, key))
697
+
698
+ testSync("the field is an object, not a number", () =>
699
+ expect(keyOf("price", "type"))->toEqual(Some(JSON.Encode.string("object")))
700
+ )
701
+
702
+ testSync("it carries the money id, sourced from the type", () =>
703
+ expect((keyOf("price", "x-reventless-semantic"), keyOf("price", "x-reventless-semantic-source")))
704
+ ->toEqual((Some(JSON.Encode.string("money")), Some(JSON.Encode.string("type"))))
705
+ )
706
+
707
+ // The control: an ordinary `float` price is what the field looked like
708
+ // before, and it emits nothing at all. Without this the assertions above
709
+ // would pass just as well against a schema that had always said `money`.
710
+ testSync("a plain numeric field emits no semantic", () =>
711
+ expect((keyOf("quantity", "type"), keyOf("quantity", "x-reventless-semantic")))->toEqual((
712
+ Some(JSON.Encode.string("number")),
713
+ None,
714
+ ))
715
+ )
716
+
717
+ testSync("the amount and the currency both reach the wire", () => {
718
+ let inner = getPropertyOf(json, "price")->Option.getOr(JSON.Encode.null)
719
+ expect((
720
+ getPropertyOf(inner, "amount")->Option.flatMap(s => getProperty(s, "type")),
721
+ getPropertyOf(inner, "currency")->Option.flatMap(s => getProperty(s, "type")),
722
+ ))->toEqual((Some(JSON.Encode.string("number")), Some(JSON.Encode.string("string"))))
723
+ })
724
+
725
+ // The closed currency arrives as an enum rather than a free string, which is
726
+ // what lets a consumer build a currency picker instead of a text box — and
727
+ // what makes `"eur"` unrepresentable on the wire as well as in the source.
728
+ testSync("the currency is an enum of ISO codes", () => {
729
+ let codes =
730
+ getPropertyOf(json, "price")
731
+ ->Option.getOr(JSON.Encode.null)
732
+ ->getPropertyOf("currency")
733
+ ->Option.flatMap(s => getProperty(s, "enum"))
734
+ ->Option.flatMap(JSON.Decode.array)
735
+ ->Option.getOr([])
736
+ ->Array.filterMap(JSON.Decode.string)
737
+ expect((
738
+ codes->Array.length == Reventless.Currency.all->Array.length,
739
+ codes->Array.includes("EUR"),
740
+ codes->Array.includes("JPY"),
741
+ codes->Array.includes("eur"),
742
+ ))->toEqual((true, true, true, false))
743
+ })
744
+ })
680
745
  })
@@ -5,9 +5,11 @@ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
5
5
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
6
6
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
7
7
  import * as Email$Reventless from "@reventlessdev/reventless-spec/src/semantic/Email.res.mjs";
8
+ import * as Money$Reventless from "@reventlessdev/reventless-spec/src/semantic/Money.res.mjs";
8
9
  import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
9
10
  import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
10
11
  import * as Percent$Reventless from "@reventlessdev/reventless-spec/src/semantic/Percent.res.mjs";
12
+ import * as Currency$Reventless from "@reventlessdev/reventless-spec/src/semantic/Currency.res.mjs";
11
13
  import * as DateTime$Reventless from "@reventlessdev/reventless-spec/src/types/DateTime.res.mjs";
12
14
  import * as Reference$Reventless from "@reventlessdev/reventless-spec/src/components/Reference.res.mjs";
13
15
  import * as StorageRef$Reventless from "@reventlessdev/reventless-spec/src/semantic/StorageRef.res.mjs";
@@ -981,6 +983,58 @@ globalThis.describe("SuryToJsonSchema:", () => {
981
983
  ]);
982
984
  });
983
985
  });
986
+ globalThis.describe("Money changes the field's shape, and says so:", () => {
987
+ let json = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(S.schema(s => ({
988
+ price: s.m(Money$Reventless.schema),
989
+ quantity: s.m(S.float)
990
+ })));
991
+ let keyOf = (name, key) => Stdlib_Option.flatMap(getPropertyOf(json, name), s => getProperty(s, key));
992
+ globalThis.test("the field is an object, not a number", () => {
993
+ globalThis.expect(keyOf("price", "type")).toEqual("object");
994
+ });
995
+ globalThis.test("it carries the money id, sourced from the type", () => {
996
+ globalThis.expect([
997
+ keyOf("price", "x-reventless-semantic"),
998
+ keyOf("price", "x-reventless-semantic-source")
999
+ ]).toEqual([
1000
+ "money",
1001
+ "type"
1002
+ ]);
1003
+ });
1004
+ globalThis.test("a plain numeric field emits no semantic", () => {
1005
+ globalThis.expect([
1006
+ keyOf("quantity", "type"),
1007
+ keyOf("quantity", "x-reventless-semantic")
1008
+ ]).toEqual([
1009
+ "number",
1010
+ undefined
1011
+ ]);
1012
+ });
1013
+ globalThis.test("the amount and the currency both reach the wire", () => {
1014
+ let inner = Stdlib_Option.getOr(getPropertyOf(json, "price"), null);
1015
+ globalThis.expect([
1016
+ Stdlib_Option.flatMap(getPropertyOf(inner, "amount"), s => getProperty(s, "type")),
1017
+ Stdlib_Option.flatMap(getPropertyOf(inner, "currency"), s => getProperty(s, "type"))
1018
+ ]).toEqual([
1019
+ "number",
1020
+ "string"
1021
+ ]);
1022
+ });
1023
+ globalThis.test("the currency is an enum of ISO codes", () => {
1024
+ let codes = Stdlib_Array.filterMap(Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(getPropertyOf(Stdlib_Option.getOr(getPropertyOf(json, "price"), null), "currency"), s => getProperty(s, "enum")), Stdlib_JSON.Decode.array), []), Stdlib_JSON.Decode.string);
1025
+ globalThis.expect([
1026
+ codes.length === Currency$Reventless.all.length,
1027
+ codes.includes("EUR"),
1028
+ codes.includes("JPY"),
1029
+ codes.includes("eur")
1030
+ ]).toEqual([
1031
+ true,
1032
+ true,
1033
+ true,
1034
+ false
1035
+ ]);
1036
+ });
1037
+ });
984
1038
  });
985
1039
 
986
1040
  export {
@@ -7,8 +7,7 @@ S.enableJson()
7
7
  // included — to JSON, which no-ops `bold`/`fmtComp`. The human-format
8
8
  // assertions below expect ANSI, so force text mode for this file. The
9
9
  // "JSON sink" block flips to json and restores text in its own setup.
10
- @val external _processEnv: Dict.t<string> = "process.env"
11
- _processEnv->Dict.set("REVENTLESS_LOG_FORMAT", "text")
10
+ NodeProcess.env->Dict.set("REVENTLESS_LOG_FORMAT", "text")
12
11
  Reventless.AnsiStyle.reload()
13
12
 
14
13
  describe("LogFormat", () => {
@@ -193,11 +192,11 @@ describe("JSON sink", () => {
193
192
  // Flip to json for the whole block; restore text afterwards so a re-run of the
194
193
  // file (or shared module state) keeps the human-format assertions valid.
195
194
  beforeAll(() => {
196
- _processEnv->Dict.set("REVENTLESS_LOG_FORMAT", "json")
195
+ NodeProcess.env->Dict.set("REVENTLESS_LOG_FORMAT", "json")
197
196
  Reventless.AnsiStyle.reload()
198
197
  })
199
198
  afterAll(() => {
200
- _processEnv->Dict.set("REVENTLESS_LOG_FORMAT", "text")
199
+ NodeProcess.env->Dict.set("REVENTLESS_LOG_FORMAT", "text")
201
200
  Reventless.AnsiStyle.reload()
202
201
  })
203
202
 
@@ -344,10 +343,10 @@ describe("JSON sink", () => {
344
343
  })
345
344
 
346
345
  testSync("service field is emitted when REVENTLESS_SERVICE is set", () => {
347
- _processEnv->Dict.set("REVENTLESS_SERVICE", "TestService")
346
+ NodeProcess.env->Dict.set("REVENTLESS_SERVICE", "TestService")
348
347
  let lines = captureLogs(() => log.info(~comp="Platform", "tick"))
349
348
  let got = lines->Array.get(0)->Option.flatMap(l => l->fieldOf("service"))
350
- _processEnv->Dict.delete("REVENTLESS_SERVICE")
349
+ NodeProcess.env->Dict.delete("REVENTLESS_SERVICE")
351
350
  expect(got)->toEqual(Some("TestService"))
352
351
  })
353
352
 
@@ -4,16 +4,13 @@
4
4
 
5
5
  open JestGlobals
6
6
 
7
- // Write a UTF-8 string to a file synchronously (no return value).
8
- @module("fs") external writeFileSync: (string, string) => unit = "writeFileSync"
9
-
10
7
  // Counter for unique temp file names — avoids cross-test collisions.
11
8
  let fileCounter = ref(0)
12
9
 
13
10
  let writeTempCsv = (content: string): string => {
14
11
  fileCounter := fileCounter.contents + 1
15
12
  let path = `/tmp/reventless-csvstream-test-${Int.toString(fileCounter.contents)}.csv`
16
- writeFileSync(path, content)
13
+ NodeFs.writeFileSync(path, content)
17
14
  path
18
15
  }
19
16
 
@@ -1,7 +1,7 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
- import * as Fs from "fs";
4
3
  import * as Stream from "@reventlessdev/rescript-effect/src/Stream.res.mjs";
4
+ import * as Nodefs from "node:fs";
5
5
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
6
6
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
7
7
  import * as Effect from "effect/Effect";
@@ -15,7 +15,7 @@ let fileCounter = {
15
15
  function writeTempCsv(content) {
16
16
  fileCounter.contents = fileCounter.contents + 1 | 0;
17
17
  let path = `/tmp/reventless-csvstream-test-` + fileCounter.contents.toString() + `.csv`;
18
- Fs.writeFileSync(path, content);
18
+ Nodefs.writeFileSync(path, content, "utf8");
19
19
  return path;
20
20
  }
21
21