@reventlessdev/reventless-core 3.0.0-alpha.196 → 3.0.0-alpha.198
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 +14 -0
- package/package.json +8 -8
- package/src/components/InboundTranslationSlice/InboundTranslationSlice_Builder.res +12 -8
- package/src/components/InboundTranslationSlice/InboundTranslationSlice_Builder.res.mjs +7 -8
- package/src/components/InboundTranslationSlice/InboundTranslationSlice_Callback.res +27 -0
- package/src/components/InboundTranslationSlice/InboundTranslationSlice_Callback.res.mjs +13 -0
- package/tests/api/SuryToJsonSchemaTest.res +52 -0
- package/tests/api/SuryToJsonSchemaTest.res.mjs +39 -0
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.198 (2026-08-01)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **core:** drain the inbound audit log per request, not wholesale ([e18d4ec](https://github.com/ReventlessDev/reventless-core/commit/e18d4ec4b7ffb38a9e2bd3594d39a07e2ee91851))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# 3.0.0-alpha.197 (2026-08-01)
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* **spec:** add DateRange semantic type ([d85b6cc](https://github.com/ReventlessDev/reventless-core/commit/d85b6cc18241644905241df2abd99949dd758059))
|
|
18
|
+
|
|
19
|
+
|
|
6
20
|
# 3.0.0-alpha.196 (2026-07-31)
|
|
7
21
|
|
|
8
22
|
* feat(rescript)!: one Node bindings package, not two ([1258d8c](https://github.com/ReventlessDev/reventless-core/commit/1258d8c2b2ff2636b36a849fc5bdf9005c6fb0eb))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-core",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.198",
|
|
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-hash-object": "1.2.0-alpha.14",
|
|
32
31
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
33
|
-
"@reventlessdev/rescript-
|
|
34
|
-
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
32
|
+
"@reventlessdev/rescript-hash-object": "1.2.0-alpha.14",
|
|
35
33
|
"@reventlessdev/rescript-fast-csv": "2.0.0-alpha.0",
|
|
36
|
-
"@reventlessdev/rescript-
|
|
34
|
+
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
35
|
+
"@reventlessdev/rescript-node": "2.0.0-alpha.0",
|
|
37
36
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
|
|
38
|
-
"@reventlessdev/
|
|
37
|
+
"@reventlessdev/rescript-ssh2": "2.0.0-alpha.0",
|
|
39
38
|
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
40
|
-
"@reventlessdev/reventless-
|
|
41
|
-
"@reventlessdev/reventless-
|
|
39
|
+
"@reventlessdev/reventless-interop": "3.0.0-alpha.30",
|
|
40
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.115",
|
|
41
|
+
"@reventlessdev/reventless-spec": "3.0.0-alpha.90"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"rescript": "12.3.0",
|
|
@@ -41,18 +41,19 @@ module Make = (
|
|
|
41
41
|
|
|
42
42
|
module SpecificQueryDb = QueryDb_Builder.Make(AuditQueryDbSpec, QueryDbStorage, QueryDbResolvers)
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
// Persist just this request's audit row, taking it out of the in-memory log
|
|
45
|
+
// as it goes — see `takeAuditRow` for why draining the whole dict is wrong.
|
|
46
|
+
let syncToQueryDb = async (queryDbOps: SpecificQueryDb.operations, requestId: string) =>
|
|
47
|
+
switch Callback.auditLog->InboundTranslationSlice_Callback.takeAuditRow(requestId) {
|
|
48
|
+
| Some(row) =>
|
|
48
49
|
let _ = await queryDbOps.save(
|
|
49
|
-
|
|
50
|
+
requestId->Reventless.Id.String.makeFromString,
|
|
50
51
|
row,
|
|
51
52
|
QueryDb.Overwrite,
|
|
52
53
|
None,
|
|
53
54
|
)
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
| None => ()
|
|
56
|
+
}
|
|
56
57
|
|
|
57
58
|
let construct = (~publishJsons, self, _name) => {
|
|
58
59
|
let opts = {Pulumi.ComponentResource.parent: self->Component.toPulumiResource}
|
|
@@ -77,7 +78,10 @@ module Make = (
|
|
|
77
78
|
let ops: InboundTranslationSlice.operations = {
|
|
78
79
|
receive: async inputJson => {
|
|
79
80
|
let result = await Callback.receive(publishJsonsFn, inputJson)
|
|
80
|
-
await syncToQueryDb(
|
|
81
|
+
await syncToQueryDb(
|
|
82
|
+
queryDbOps,
|
|
83
|
+
result->InboundTranslationSlice_Callback.requestIdOf,
|
|
84
|
+
)
|
|
81
85
|
result
|
|
82
86
|
},
|
|
83
87
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
-
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
4
3
|
import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
|
|
5
4
|
import * as Pulumi from "@pulumi/pulumi";
|
|
6
5
|
import * as ReadModel$Reventless from "@reventlessdev/reventless-spec/src/components/ReadModel.res.mjs";
|
|
@@ -27,12 +26,12 @@ function Make(QueryDbStorage) {
|
|
|
27
26
|
authorization: "AllowAuthenticated",
|
|
28
27
|
visibility: "Public"
|
|
29
28
|
})(QueryDbStorage)(QueryDbResolvers);
|
|
30
|
-
let syncToQueryDb = async queryDbOps => {
|
|
31
|
-
let
|
|
32
|
-
|
|
33
|
-
await
|
|
34
|
-
|
|
35
|
-
}
|
|
29
|
+
let syncToQueryDb = async (queryDbOps, requestId) => {
|
|
30
|
+
let row = InboundTranslationSlice_Callback$ReventlessCore.takeAuditRow(Callback.auditLog, requestId);
|
|
31
|
+
if (row !== undefined) {
|
|
32
|
+
await queryDbOps.save(Id$Reventless.$$String.makeFromString(requestId), row, "Overwrite", undefined);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
36
35
|
};
|
|
37
36
|
let make = (publishJsons, param, opts) => Component$ReventlessCore.make(ComponentType$ReventlessCore.toString(InboundTranslationSlice$ReventlessCore.componentType), Spec.name, (extra, extra$1) => {
|
|
38
37
|
let opts_parent = Component$ReventlessCore.toPulumiResource(extra);
|
|
@@ -58,7 +57,7 @@ function Make(QueryDbStorage) {
|
|
|
58
57
|
return {
|
|
59
58
|
receive: async inputJson => {
|
|
60
59
|
let result = await Callback.receive(publishJsonsFn, inputJson);
|
|
61
|
-
await syncToQueryDb(queryDbOps);
|
|
60
|
+
await syncToQueryDb(queryDbOps, InboundTranslationSlice_Callback$ReventlessCore.requestIdOf(result));
|
|
62
61
|
return result;
|
|
63
62
|
}
|
|
64
63
|
};
|
|
@@ -46,6 +46,33 @@ let receiveResultToOutcome = (
|
|
|
46
46
|
Rejected({msgId: requestId, errorCode: "TranslationFailed", errorDetail: Some(error)})
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
The request a `receive` outcome belongs to, from either arm. Both carry the id
|
|
51
|
+
that keys the outcome's audit row, so a caller draining the log does not have to
|
|
52
|
+
match on the result to find its own row.
|
|
53
|
+
*/
|
|
54
|
+
let requestIdOf = (result: ReventlessInfra.InboundTranslationSlice.receiveResult): string =>
|
|
55
|
+
switch result {
|
|
56
|
+
| Ok({requestId}) | Error({requestId}) => requestId
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
Remove one request's audit row from the in-memory log and hand it back.
|
|
61
|
+
|
|
62
|
+
`auditLog` is a hand-off buffer between `receive` and whoever persists the row,
|
|
63
|
+
not a log in its own right — the QueryDb it drains into is the durable one. It
|
|
64
|
+
has to be emptied as it drains: the dict lives as long as the process (a warm
|
|
65
|
+
Lambda container serves many requests), so a drain that walked the whole dict
|
|
66
|
+
would rewrite every row the container had ever seen on every request — quadratic
|
|
67
|
+
writes, unbounded retention, and, because the write is an overwrite by row id,
|
|
68
|
+
resurrection of rows deleted from the table since.
|
|
69
|
+
*/
|
|
70
|
+
let takeAuditRow = (auditLog: Dict.t<auditRow>, requestId: string): option<auditRow> => {
|
|
71
|
+
let row = auditLog->Dict.get(requestId)
|
|
72
|
+
auditLog->Dict.delete(requestId)
|
|
73
|
+
row
|
|
74
|
+
}
|
|
75
|
+
|
|
49
76
|
module type T = {
|
|
50
77
|
module Spec: Reventless.InboundTranslationSlice.Spec
|
|
51
78
|
module Translation: Reventless.InboundTranslationSlice.Translation with module Spec := Spec
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as S from "sury/src/S.res.mjs";
|
|
4
4
|
import * as Uuid from "uuid";
|
|
5
|
+
import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
|
|
5
6
|
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
6
7
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
7
8
|
import * as Effect from "effect/Effect";
|
|
@@ -55,6 +56,16 @@ function receiveResultToOutcome(result) {
|
|
|
55
56
|
};
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
function requestIdOf(result) {
|
|
60
|
+
return result._0.requestId;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function takeAuditRow(auditLog, requestId) {
|
|
64
|
+
let row = auditLog[requestId];
|
|
65
|
+
Stdlib_Dict.$$delete(auditLog, requestId);
|
|
66
|
+
return row;
|
|
67
|
+
}
|
|
68
|
+
|
|
58
69
|
function Make(Spec) {
|
|
59
70
|
return Translation => {
|
|
60
71
|
let auditLog = {};
|
|
@@ -221,6 +232,8 @@ export {
|
|
|
221
232
|
auditStatusSchema,
|
|
222
233
|
auditRowSchema,
|
|
223
234
|
receiveResultToOutcome,
|
|
235
|
+
requestIdOf,
|
|
236
|
+
takeAuditRow,
|
|
224
237
|
Make,
|
|
225
238
|
}
|
|
226
239
|
/* Not a pure module */
|
|
@@ -742,4 +742,56 @@ describe("SuryToJsonSchema:", () => {
|
|
|
742
742
|
))->toEqual((true, true, true, false))
|
|
743
743
|
})
|
|
744
744
|
})
|
|
745
|
+
|
|
746
|
+
// The second composite, and the one whose emission the reader's shape rung
|
|
747
|
+
// depends on. Like `Money` it turns the field into an object — same
|
|
748
|
+
// shape-changed evidence, same upcaster warning for a collapse. Unlike it, the
|
|
749
|
+
// two parts keep their own `dateTime` marker, so a walker that only understands
|
|
750
|
+
// date-times still sees into the composite. That last property is asserted, not
|
|
751
|
+
// described, because it is what lets the UI's primary rung key on the shape
|
|
752
|
+
// before the `dateRange` marker reaches it in a release.
|
|
753
|
+
describe("DateRange is an object whose parts stay date-times:", () => {
|
|
754
|
+
let json = SuryToJsonSchema.deriveObjectSchema(
|
|
755
|
+
S.schema(s =>
|
|
756
|
+
{
|
|
757
|
+
"deliveryWindow": s.matches(Reventless.DateRange.schema),
|
|
758
|
+
"orderedAt": s.matches(Reventless.DateTime.string),
|
|
759
|
+
}
|
|
760
|
+
)->S.castToUnknown,
|
|
761
|
+
)
|
|
762
|
+
let keyOf = (name, key) => getPropertyOf(json, name)->Option.flatMap(s => getProperty(s, key))
|
|
763
|
+
|
|
764
|
+
testSync("the field is an object, not a string", () =>
|
|
765
|
+
expect(keyOf("deliveryWindow", "type"))->toEqual(Some(JSON.Encode.string("object")))
|
|
766
|
+
)
|
|
767
|
+
|
|
768
|
+
testSync("it carries the dateRange id, sourced from the type", () =>
|
|
769
|
+
expect((
|
|
770
|
+
keyOf("deliveryWindow", "x-reventless-semantic"),
|
|
771
|
+
keyOf("deliveryWindow", "x-reventless-semantic-source"),
|
|
772
|
+
))->toEqual((Some(JSON.Encode.string("dateRange")), Some(JSON.Encode.string("type"))))
|
|
773
|
+
)
|
|
774
|
+
|
|
775
|
+
// The property that is not incidental: each part keeps its own date-time
|
|
776
|
+
// marker under the composite. `dateTime` predates the generic marker and
|
|
777
|
+
// surfaces as `format: "date-time"` (not `x-reventless-semantic`), so that is
|
|
778
|
+
// what a walker reads to see the two instants without knowing about ranges.
|
|
779
|
+
testSync("both instants keep their own date-time marker", () => {
|
|
780
|
+
let inner = getPropertyOf(json, "deliveryWindow")->Option.getOr(JSON.Encode.null)
|
|
781
|
+
expect((
|
|
782
|
+
getPropertyOf(inner, "start")->Option.flatMap(s => getProperty(s, "format")),
|
|
783
|
+
getPropertyOf(inner, "end")->Option.flatMap(s => getProperty(s, "format")),
|
|
784
|
+
))->toEqual((Some(JSON.Encode.string("date-time")), Some(JSON.Encode.string("date-time"))))
|
|
785
|
+
})
|
|
786
|
+
|
|
787
|
+
// The control, mirroring the `Money` block's plain-float case: a lone
|
|
788
|
+
// date-time field stays a string and never becomes an object, so the
|
|
789
|
+
// object-ness above is the composite's doing and not the walk's.
|
|
790
|
+
testSync("a lone date-time field stays a string", () =>
|
|
791
|
+
expect((keyOf("orderedAt", "type"), keyOf("orderedAt", "format")))->toEqual((
|
|
792
|
+
Some(JSON.Encode.string("string")),
|
|
793
|
+
Some(JSON.Encode.string("date-time")),
|
|
794
|
+
))
|
|
795
|
+
)
|
|
796
|
+
})
|
|
745
797
|
})
|
|
@@ -11,6 +11,7 @@ import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/component
|
|
|
11
11
|
import * as Percent$Reventless from "@reventlessdev/reventless-spec/src/semantic/Percent.res.mjs";
|
|
12
12
|
import * as Currency$Reventless from "@reventlessdev/reventless-spec/src/semantic/Currency.res.mjs";
|
|
13
13
|
import * as DateTime$Reventless from "@reventlessdev/reventless-spec/src/types/DateTime.res.mjs";
|
|
14
|
+
import * as DateRange$Reventless from "@reventlessdev/reventless-spec/src/semantic/DateRange.res.mjs";
|
|
14
15
|
import * as Reference$Reventless from "@reventlessdev/reventless-spec/src/components/Reference.res.mjs";
|
|
15
16
|
import * as StorageRef$Reventless from "@reventlessdev/reventless-spec/src/semantic/StorageRef.res.mjs";
|
|
16
17
|
import * as StateAnnotations$Reventless from "@reventlessdev/reventless-spec/src/components/StateAnnotations.res.mjs";
|
|
@@ -1035,6 +1036,44 @@ globalThis.describe("SuryToJsonSchema:", () => {
|
|
|
1035
1036
|
]);
|
|
1036
1037
|
});
|
|
1037
1038
|
});
|
|
1039
|
+
globalThis.describe("DateRange is an object whose parts stay date-times:", () => {
|
|
1040
|
+
let json = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(S.schema(s => ({
|
|
1041
|
+
deliveryWindow: s.m(DateRange$Reventless.schema),
|
|
1042
|
+
orderedAt: s.m(DateTime$Reventless.string)
|
|
1043
|
+
})));
|
|
1044
|
+
let keyOf = (name, key) => Stdlib_Option.flatMap(getPropertyOf(json, name), s => getProperty(s, key));
|
|
1045
|
+
globalThis.test("the field is an object, not a string", () => {
|
|
1046
|
+
globalThis.expect(keyOf("deliveryWindow", "type")).toEqual("object");
|
|
1047
|
+
});
|
|
1048
|
+
globalThis.test("it carries the dateRange id, sourced from the type", () => {
|
|
1049
|
+
globalThis.expect([
|
|
1050
|
+
keyOf("deliveryWindow", "x-reventless-semantic"),
|
|
1051
|
+
keyOf("deliveryWindow", "x-reventless-semantic-source")
|
|
1052
|
+
]).toEqual([
|
|
1053
|
+
"dateRange",
|
|
1054
|
+
"type"
|
|
1055
|
+
]);
|
|
1056
|
+
});
|
|
1057
|
+
globalThis.test("both instants keep their own date-time marker", () => {
|
|
1058
|
+
let inner = Stdlib_Option.getOr(getPropertyOf(json, "deliveryWindow"), null);
|
|
1059
|
+
globalThis.expect([
|
|
1060
|
+
Stdlib_Option.flatMap(getPropertyOf(inner, "start"), s => getProperty(s, "format")),
|
|
1061
|
+
Stdlib_Option.flatMap(getPropertyOf(inner, "end"), s => getProperty(s, "format"))
|
|
1062
|
+
]).toEqual([
|
|
1063
|
+
"date-time",
|
|
1064
|
+
"date-time"
|
|
1065
|
+
]);
|
|
1066
|
+
});
|
|
1067
|
+
globalThis.test("a lone date-time field stays a string", () => {
|
|
1068
|
+
globalThis.expect([
|
|
1069
|
+
keyOf("orderedAt", "type"),
|
|
1070
|
+
keyOf("orderedAt", "format")
|
|
1071
|
+
]).toEqual([
|
|
1072
|
+
"string",
|
|
1073
|
+
"date-time"
|
|
1074
|
+
]);
|
|
1075
|
+
});
|
|
1076
|
+
});
|
|
1038
1077
|
});
|
|
1039
1078
|
|
|
1040
1079
|
export {
|