@reventlessdev/reventless-core 3.0.0-alpha.244 → 3.0.0-alpha.245

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,13 @@
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.245 (2026-08-20)
7
+
8
+ ### Features
9
+
10
+ * **core:** report a union value stored without its member type ([3a834b5](https://github.com/ReventlessDev/reventless-core/commit/3a834b51d1d9441f5dfcaebec5fac365063c905e))
11
+
12
+
6
13
  # 3.0.0-alpha.244 (2026-08-20)
7
14
 
8
15
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-core",
3
- "version": "3.0.0-alpha.244",
3
+ "version": "3.0.0-alpha.245",
4
4
  "description": "Core package for Reventless framework",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -1,3 +1,5 @@
1
+ let log = Logger.fromEnv()
2
+
1
3
  module type Ops = {
2
4
  let jsonOps: QueryDb.operations<string, JSON.t>
3
5
  }
@@ -72,11 +74,53 @@ module Make = (ReadModelSpec: Reventless.ReadModel.Spec, Ops: Ops) => {
72
74
  // backends — and, unlike a read-time stamp, it is also on the row the live
73
75
  // change channel hands over as raw JSON. A view with no union field is walked
74
76
  // and left exactly as it was.
75
- let stampUnionMembers = dict =>
77
+ // The union fields this view declares, read once. Empty for most views, and
78
+ // the whole diagnostic below costs nothing when it is.
79
+ let declaredUnionFields =
80
+ Reventless.TaggedUnion.fieldsOf(ReadModelSpec.stateSchema->S.castToUnknown)
81
+
82
+ // Logged once per instantiation, and unconditionally: "none" has to be
83
+ // distinguishable from "never instantiated", because a schema that resolves no
84
+ // union fields and a stamp that failed to run leave the same stored row.
85
+ log.debug(
86
+ ~comp="QueryDb",
87
+ `${ReadModelSpec.name}: union fields ${switch declaredUnionFields {
88
+ | [] => "none"
89
+ | fields =>
90
+ fields->Array.map(((f, n)) => `${f}:${n->Option.getOr("<unnamed>")}`)->Array.join(", ")
91
+ }}`,
92
+ )
93
+
94
+ // A union value stored without `__typename` is the failure D1 exists to
95
+ // prevent, and it is silent: the member resolves to null, the null takes its
96
+ // non-nullable parent, and a list loses the row rather than erroring. Nothing
97
+ // downstream can tell that from "no such row", so it is reported here — the
98
+ // one place that knows both the schema and the row.
99
+ let reportUnstamped = dict =>
100
+ declaredUnionFields->Array.forEach(((field, unionName)) =>
101
+ switch dict->Dict.get(field)->Option.flatMap(JSON.Decode.object) {
102
+ | Some(value) if value->Dict.get("TAG")->Option.isSome =>
103
+ if value->Dict.get(Reventless.TaggedUnion.typenameKey)->Option.isNone {
104
+ log.warn(
105
+ ~comp="QueryDb",
106
+ `${ReadModelSpec.name}.${field}: stored a union value with no ${Reventless.TaggedUnion.typenameKey} ` ++
107
+ `(union ${unionName->Option.getOr("<unnamed>")}). The field will resolve to null and take its ` ++
108
+ `parent with it. Union fields seen on this spec: ${declaredUnionFields
109
+ ->Array.map(((f, _)) => f)
110
+ ->Array.join(", ")}`,
111
+ )
112
+ }
113
+ | _ => ()
114
+ }
115
+ )
116
+
117
+ let stampUnionMembers = dict => {
76
118
  Reventless.TaggedUnion.stampInto(
77
119
  ~schema=ReadModelSpec.stateSchema->S.castToUnknown,
78
120
  JSON.Encode.object(dict),
79
121
  )
122
+ reportUnstamped(dict)
123
+ }
80
124
 
81
125
  let save = async (id, state, saveMode, ttl) =>
82
126
  switch state->Message.encode(ReadModelSpec.stateSchema)->JSON.Decode.object {
@@ -7,9 +7,12 @@ import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
7
7
  import * as Effect from "effect/Effect";
8
8
  import * as Stream from "effect/Stream";
9
9
  import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
10
+ import * as Logger$ReventlessCore from "../../util/Logger.res.mjs";
10
11
  import * as Message$ReventlessCore from "../../Message.res.mjs";
11
12
  import * as TaggedUnion$Reventless from "@reventlessdev/reventless-spec/src/components/TaggedUnion.res.mjs";
12
13
 
14
+ let log = Logger$ReventlessCore.fromEnv();
15
+
13
16
  function Make(ReadModelSpec) {
14
17
  return Ops => {
15
18
  let decode = (id, stateJson) => {
@@ -77,14 +80,30 @@ function Make(ReadModelSpec) {
77
80
  dict[attrName$1] = value$1;
78
81
  });
79
82
  };
80
- let stampUnionMembers = dict => TaggedUnion$Reventless.stampInto(ReadModelSpec.stateSchema, dict);
83
+ let declaredUnionFields = TaggedUnion$Reventless.fieldsOf(ReadModelSpec.stateSchema);
84
+ log.debug("QueryDb", undefined, ReadModelSpec.name + `: union fields ` + (
85
+ declaredUnionFields.length !== 0 ? declaredUnionFields.map(param => param[0] + `:` + Stdlib_Option.getOr(param[1], "<unnamed>")).join(", ") : "none"
86
+ ));
87
+ let reportUnstamped = dict => {
88
+ declaredUnionFields.forEach(param => {
89
+ let field = param[0];
90
+ let value = Stdlib_Option.flatMap(dict[field], Stdlib_JSON.Decode.object);
91
+ if (value !== undefined && Stdlib_Option.isSome(value["TAG"]) && Stdlib_Option.isNone(value[TaggedUnion$Reventless.typenameKey])) {
92
+ return log.warn("QueryDb", undefined, ReadModelSpec.name + `.` + field + `: stored a union value with no ` + TaggedUnion$Reventless.typenameKey + ` ` + (`(union ` + Stdlib_Option.getOr(param[1], "<unnamed>") + `). The field will resolve to null and take its `) + (`parent with it. Union fields seen on this spec: ` + declaredUnionFields.map(param => param[0]).join(", ")));
93
+ }
94
+ });
95
+ };
96
+ let stampUnionMembers = dict => {
97
+ TaggedUnion$Reventless.stampInto(ReadModelSpec.stateSchema, dict);
98
+ reportUnstamped(dict);
99
+ };
81
100
  let save = async (id, state, saveMode, ttl) => {
82
101
  let dict = Stdlib_JSON.Decode.object(Message$ReventlessCore.encode(state, ReadModelSpec.stateSchema));
83
102
  if (dict !== undefined) {
84
103
  dict["id"] = Message$ReventlessCore.encode(id, ReadModelSpec.Id.schema);
85
104
  injectSubId(dict, state);
86
105
  injectCompositeIndexAttrs(dict);
87
- TaggedUnion$Reventless.stampInto(ReadModelSpec.stateSchema, dict);
106
+ stampUnionMembers(dict);
88
107
  return await Ops.jsonOps.save(ReadModelSpec.Id.toString(id), dict, saveMode, ttl);
89
108
  } else {
90
109
  return {
@@ -110,7 +129,7 @@ function Make(ReadModelSpec) {
110
129
  dict["id"] = Message$ReventlessCore.encode(id, ReadModelSpec.Id.schema);
111
130
  injectSubId(dict, state);
112
131
  injectCompositeIndexAttrs(dict);
113
- TaggedUnion$Reventless.stampInto(ReadModelSpec.stateSchema, dict);
132
+ stampUnionMembers(dict);
114
133
  return {
115
134
  TAG: "Ok",
116
135
  _0: batch.concat([[
@@ -154,6 +173,8 @@ function Make(ReadModelSpec) {
154
173
  load: load,
155
174
  injectSubId: injectSubId,
156
175
  injectCompositeIndexAttrs: injectCompositeIndexAttrs,
176
+ declaredUnionFields: declaredUnionFields,
177
+ reportUnstamped: reportUnstamped,
157
178
  stampUnionMembers: stampUnionMembers,
158
179
  save: save,
159
180
  saveBatch: saveBatch,
@@ -165,6 +186,7 @@ function Make(ReadModelSpec) {
165
186
  }
166
187
 
167
188
  export {
189
+ log,
168
190
  Make,
169
191
  }
170
- /* effect/Effect Not a pure module */
192
+ /* log Not a pure module */