@reventlessdev/reventless-spec 3.0.0-alpha.130 → 3.0.0-alpha.132

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.
@@ -0,0 +1,66 @@
1
+ // Reading a built plugin's declared structure.
2
+ //
3
+ // A plugin's generated definition module is a functor over the platform type
4
+ // that exposes `pluginStructure` as an ordinary value, and the local platform's
5
+ // `Make` takes no configuration — so applying one to the other is enough to hold
6
+ // a real structure. No deploy program, no database, no server, nothing left
7
+ // running.
8
+ //
9
+ // This lives in a companion `.mjs` because it is untyped reflection: `import` is
10
+ // syntax rather than a value, so a computed specifier cannot be bound as an
11
+ // external, and the value that comes back is shaped by the plugin rather than by
12
+ // anything this repository can name.
13
+ //
14
+ // The structure is handed back as plain JSON and read field by field on the
15
+ // ReScript side, rather than parsed through the published schema. A
16
+ // `pluginStructure` has two legitimate representations — an absent optional is
17
+ // `undefined` in memory and an explicit `null` on the wire — and the published
18
+ // schema describes the wire form, so validating an in-memory structure against
19
+ // it fails on every optional that happens to be empty.
20
+ import { existsSync } from "node:fs";
21
+ import { join } from "node:path";
22
+
23
+ const localPlatformPath = (pluginDir) =>
24
+ join(
25
+ pluginDir,
26
+ "node_modules/@reventlessdev/reventless-local/src/Platform.res.mjs",
27
+ );
28
+
29
+ const pluginEntryPath = (pluginDir) => join(pluginDir, "src/Plugin.res.mjs");
30
+
31
+ /**
32
+ * @returns {Promise<{ok: true, structure: unknown} | {ok: false, error: string}>}
33
+ */
34
+ export const loadPluginStructure = async (pluginDir) => {
35
+ const entry = pluginEntryPath(pluginDir);
36
+ const local = localPlatformPath(pluginDir);
37
+ if (!existsSync(entry)) {
38
+ return {
39
+ ok: false,
40
+ error: `no compiled plugin at ${entry} — build the plugin first`,
41
+ };
42
+ }
43
+ if (!existsSync(local)) {
44
+ return {
45
+ ok: false,
46
+ error: `no local platform installed at ${local}`,
47
+ };
48
+ }
49
+ try {
50
+ const localMod = await import(local);
51
+ const pluginMod = await import(entry);
52
+ const structure = pluginMod.Make(localMod.Make()).pluginStructure;
53
+ // A structure whose component arrays are absent came from a release this
54
+ // reader cannot understand. Saying so beats reporting a model that is
55
+ // quietly half-empty.
56
+ const present = ["readModels", "stateViewSlices", "stateChangeSlices", "aggregates"].every(
57
+ (k) => Array.isArray(structure?.[k]),
58
+ );
59
+ return present
60
+ ? { ok: true, structure }
61
+ : { ok: false, error: "the plugin structure declares no component arrays" };
62
+ } catch (e) {
63
+ const message = e && (e.message || e.RE_EXN_ID) ? String(e.message || e.RE_EXN_ID) : "unknown error";
64
+ return { ok: false, error: `reading the plugin structure: ${message}` };
65
+ }
66
+ };
@@ -0,0 +1,58 @@
1
+ /**
2
+ Marks a `string` field as a calendar day — `2026-03-02`, with no instant in it.
3
+
4
+ ## Why this is not a `DateTime`
5
+
6
+ A birth date, a due date, an invoice date, an `effectiveFrom` — the value has no
7
+ time of day, and storing one as an instant creates the midnight bug: a renderer
8
+ that localizes `2026-03-02T00:00:00Z` shows *March 1st* to every reader west of
9
+ Greenwich. That is a wrong date on a screen produced by a correct renderer, and
10
+ no annotation can repair it, because the value itself has lost the distinction.
11
+
12
+ ## The grammar
13
+
14
+ Sury's `S.isoDate`: `2026-03-02` accepts, `2026-13-02`, `09:00:00`,
15
+ `2026-03-02T09:00:00Z` and the empty string all reject. It emits
16
+ `format: "date"`, the standard JSON Schema keyword a consumer already reads.
17
+
18
+ ## Why `CalendarDate` and not `Date`
19
+
20
+ `reventless-spec` compiles with `-open RescriptCore`, and its own modules resolve
21
+ unqualified inside the package — so a `Date.res` here would shadow the stdlib
22
+ `Date` for every file in it. The wire vocabulary id stays the short `"date"`;
23
+ only the module name carries the qualifier.
24
+
25
+ @example
26
+ ```rescript
27
+ @schema
28
+ type state = {
29
+ invoiceId: string,
30
+ issuedOn: Reventless.CalendarDate.t,
31
+ }
32
+ ```
33
+ */
34
+
35
+ /** The day's representation. Transparent `string`, like the rest of the branded
36
+ scalars: the marker refines an existing field rather than replacing it. */
37
+ type t = string
38
+
39
+ external unsafe: string => t = "%identity"
40
+ external toString: t => string = "%identity"
41
+
42
+ // Sury's rule, held once. `fromString` runs it rather than restating it, and
43
+ // `schema` is built from `fromString`, so there is exactly one grammar here.
44
+ let grammar: S.t<string> = S.isoDate
45
+
46
+ /** Validate a raw string as an ISO-8601 calendar day, saying why when it is not one. */
47
+ let fromString = (raw: string): result<t, string> =>
48
+ switch raw->S.parseOrThrow(~to=grammar) {
49
+ | value => Ok(value)
50
+ | exception _ => Error(`expected a calendar date (YYYY-MM-DD), got ${Semantic.showString(raw)}`)
51
+ }
52
+
53
+ /** The sury schema for a calendar-date field. Write the field's type as
54
+ `Reventless.CalendarDate.t` and sury-ppx resolves it. */
55
+ let schema: S.t<t> = S.string->Semantic.refined(~id=Semantic.Id.date, ~check=fromString)
56
+
57
+ /** Whether a field schema carries the calendar-date marker. */
58
+ let isCalendarDate = (fieldSchema: S.t<unknown>) => fieldSchema->Semantic.has(~id=Semantic.Id.date)
@@ -0,0 +1,37 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as S from "sury/src/S.res.mjs";
4
+ import * as Sury from "sury";
5
+ import * as Semantic$Reventless from "./Semantic.res.mjs";
6
+
7
+ let grammar = Sury.isoDate;
8
+
9
+ function fromString(raw) {
10
+ let value;
11
+ try {
12
+ value = S.parseOrThrow(raw, grammar);
13
+ } catch (exn) {
14
+ return {
15
+ TAG: "Error",
16
+ _0: `expected a calendar date (YYYY-MM-DD), got ` + Semantic$Reventless.showString(raw)
17
+ };
18
+ }
19
+ return {
20
+ TAG: "Ok",
21
+ _0: value
22
+ };
23
+ }
24
+
25
+ let schema = Semantic$Reventless.refined(Sury.string, Semantic$Reventless.Id.date, fromString);
26
+
27
+ function isCalendarDate(fieldSchema) {
28
+ return Semantic$Reventless.has(fieldSchema, Semantic$Reventless.Id.date);
29
+ }
30
+
31
+ export {
32
+ grammar,
33
+ fromString,
34
+ schema,
35
+ isCalendarDate,
36
+ }
37
+ /* grammar Not a pure module */
@@ -41,10 +41,10 @@ who assumes parity with `Money` assumes wrong. When sury fixes the record
41
41
  refinement the rule moves into the schema and `validate` stays as its single
42
42
  definition — the relationship `Money.validateAmount` has with `amountSchema`.
43
43
 
44
- Parsing is `Date.fromString` on each instant. A range whose strings do not parse
45
- is a decode-time problem the `DateTime` marker does not currently catch either,
46
- so there is no second validation layer here — a reversed *parseable* range is
47
- what `validate` catches, and an unparseable one is out of both their scope.
44
+ The *parts* are checked, though, and by their own type rather than by anything
45
+ here: each is a `DateTime`, so a string that is not a UTC instant is rejected at
46
+ decode. What is left for `validate` is the one rule that relates the two — a
47
+ range whose instants both parse, the earlier one second.
48
48
 
49
49
  ## How a field declares it
50
50
 
@@ -70,12 +70,12 @@ projection rebuild. It costs a log something only if it *collapses* an existing
70
70
  @schema
71
71
  type t = {
72
72
  /** The instant the range opens, inclusive. */
73
- start: @s.matches(DateTime.string) string,
73
+ start: DateTime.t,
74
74
  /** The instant the range closes, **exclusive** — the range does not contain
75
75
  it. `@as("end")` puts `end` on the wire (where the UI's own `GanttChart`
76
76
  already spells it that way); `end_` is the source spelling because `end`
77
77
  is awkward as a bare ReScript field. */
78
- @as("end") end_: @s.matches(DateTime.string) string,
78
+ @as("end") end_: DateTime.t,
79
79
  }
80
80
 
81
81
  /** The sury schema for a date-range field, carrying the `dateRange` semantic.
@@ -85,10 +85,10 @@ type t = {
85
85
  is deliberately *not* refined in here — see the module doc. */
86
86
  let schema: S.t<t> = schema->Semantic.mark(~id=Semantic.Id.dateRange)
87
87
 
88
- /** An instant as milliseconds since the epoch — `NaN` if it does not parse. The
89
- one place a range's strings become numbers, so end-exclusivity and the
90
- ordering rule are all expressed against a single parse. */
91
- let millis = (instant: string): float => instant->Date.fromString->Date.getTime
88
+ /** An instant as milliseconds since the epoch. `DateTime`'s, so end-exclusivity
89
+ and the ordering rule are expressed against the same single parse the type
90
+ itself uses rather than a second copy of it here. */
91
+ let millis = DateTime.millis
92
92
 
93
93
  /**
94
94
  Validate a range's ordering, saying why when it is reversed.
@@ -1,22 +1,18 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as Sury from "sury";
4
- import * as DateTime$Reventless from "../types/DateTime.res.mjs";
4
+ import * as DateTime$Reventless from "./DateTime.res.mjs";
5
5
  import * as Semantic$Reventless from "./Semantic.res.mjs";
6
6
 
7
7
  let schema = Sury.$schema(s => ({
8
- start: s.m(DateTime$Reventless.string),
9
- end: s.m(DateTime$Reventless.string)
8
+ start: s.m(DateTime$Reventless.schema),
9
+ end: s.m(DateTime$Reventless.schema)
10
10
  }));
11
11
 
12
12
  let schema$1 = Semantic$Reventless.mark(schema, Semantic$Reventless.Id.dateRange, undefined);
13
13
 
14
- function millis(instant) {
15
- return new Date(instant).getTime();
16
- }
17
-
18
14
  function validate(range) {
19
- if (new Date(range.start).getTime() > new Date(range.end).getTime()) {
15
+ if (DateTime$Reventless.millis(range.start) > DateTime$Reventless.millis(range.end)) {
20
16
  return {
21
17
  TAG: "Error",
22
18
  _0: `a range ends before it starts: ` + range.start + ` is after ` + range.end + `. A range is [start, end) — the start is the earlier instant.`
@@ -37,21 +33,21 @@ function make(start, end_) {
37
33
  }
38
34
 
39
35
  function duration(range) {
40
- return Math.trunc((new Date(range.end).getTime() - new Date(range.start).getTime()) / 1000.0) | 0;
36
+ return Math.trunc((DateTime$Reventless.millis(range.end) - DateTime$Reventless.millis(range.start)) / 1000.0) | 0;
41
37
  }
42
38
 
43
39
  function contains(range, instant) {
44
- let t = new Date(instant).getTime();
45
- if (t >= new Date(range.start).getTime()) {
46
- return t < new Date(range.end).getTime();
40
+ let t = DateTime$Reventless.millis(instant);
41
+ if (t >= DateTime$Reventless.millis(range.start)) {
42
+ return t < DateTime$Reventless.millis(range.end);
47
43
  } else {
48
44
  return false;
49
45
  }
50
46
  }
51
47
 
52
48
  function overlaps(a, b) {
53
- if (new Date(a.start).getTime() < new Date(b.end).getTime()) {
54
- return new Date(b.start).getTime() < new Date(a.end).getTime();
49
+ if (DateTime$Reventless.millis(a.start) < DateTime$Reventless.millis(b.end)) {
50
+ return DateTime$Reventless.millis(b.start) < DateTime$Reventless.millis(a.end);
55
51
  } else {
56
52
  return false;
57
53
  }
@@ -61,6 +57,8 @@ function format(range) {
61
57
  return range.start + ` – ` + range.end;
62
58
  }
63
59
 
60
+ let millis = DateTime$Reventless.millis;
61
+
64
62
  export {
65
63
  schema$1 as schema,
66
64
  millis,
@@ -0,0 +1,92 @@
1
+ /**
2
+ Marks a `string` field as an ISO-8601 instant.
3
+
4
+ ## The grammar
5
+
6
+ Sury's `S.isoDateTime`, and nothing added — the same rule the branded scalars
7
+ follow. `S.datetime` is the other binding and is not this one: it *transforms* to
8
+ `Js.Date.t`, changing the field's runtime type, where this keeps the string the
9
+ projection wrote.
10
+
11
+ ## UTC only
12
+
13
+ `S.isoDateTime` accepts a `Z` instant and rejects an offset — `2026-03-02T09:00:00+01:00`
14
+ and a bare `2026-03-02T09:00:00` both fail. That is stricter than RFC 3339 and it
15
+ is the strictness worth adopting: every instant the framework produces comes from
16
+ `Message.nowAsISOString`, which is always `Z`. "Instants are stored in UTC" is a
17
+ fact a reader can rely on; "we accept whatever offset arrived" pushes the zone
18
+ question onto every consumer.
19
+
20
+ An inbound feed carrying a supplier's local offset converts at the boundary,
21
+ which is what an `InboundTranslation` slice is for — `Date.fromString` then
22
+ `toISOString`. Forgetting it is now a rejection rather than a row nobody can sort.
23
+
24
+ `SchemaType`/`SuryToJsonSchema` surface the marker as `format: "date-time"` on
25
+ the field's JSON Schema, which the AutoUI date heuristics key off (CalendarView,
26
+ TimelineView, date-axis charts).
27
+
28
+ A date with no instant in it — a birth date, a due date — is `CalendarDate`, not
29
+ this. Storing one as an instant creates the midnight bug.
30
+
31
+ @example
32
+ ```rescript
33
+ @schema
34
+ type state = {
35
+ orderId: string,
36
+ placedAt: Reventless.DateTime.t,
37
+ shippedAt: option<Reventless.DateTime.t>,
38
+ }
39
+ ```
40
+ */
41
+
42
+ /** The instant's representation. Transparent `string`: the marker refines an
43
+ existing field rather than replacing it, so nothing stored changes, and
44
+ `placedAt: meta.time` keeps compiling. */
45
+ type t = string
46
+
47
+ external unsafe: string => t = "%identity"
48
+ external toString: t => string = "%identity"
49
+
50
+ // Sury's rule, held once. `fromString` runs it rather than restating it, and
51
+ // `schema` is built from `fromString`, so there is exactly one grammar here.
52
+ let grammar: S.t<string> = S.isoDateTime
53
+
54
+ /** Validate a raw string as a UTC ISO-8601 instant, saying why when it is not one. */
55
+ let fromString = (raw: string): result<t, string> =>
56
+ switch raw->S.parseOrThrow(~to=grammar) {
57
+ | value => Ok(value)
58
+ | exception _ =>
59
+ Error(`expected a UTC ISO-8601 instant, got ${Semantic.showString(raw)}`)
60
+ }
61
+
62
+ /** The sury schema for an instant field. Use with `@s.matches(Reventless.DateTime.schema)`,
63
+ or write the field's type as `Reventless.DateTime.t` and let sury-ppx resolve it. */
64
+ let schema: S.t<t> = S.string->Semantic.refined(~id=Semantic.Id.dateTime, ~check=fromString)
65
+
66
+ /** A sury string schema annotated as an instant, without the grammar.
67
+
68
+ @deprecated Use `schema`, which carries the same marker and checks the value. */
69
+ let string: S.t<string> = S.string->Semantic.mark(~id=Semantic.Id.dateTime)
70
+
71
+ /** Whether a field schema carries the date-time marker. */
72
+ let isDateTime = (fieldSchema: S.t<unknown>) => fieldSchema->Semantic.has(~id=Semantic.Id.dateTime)
73
+
74
+ /** An instant as milliseconds since the epoch — `NaN` if it does not parse. The
75
+ one place an instant becomes a number, so every comparison here and in
76
+ `DateRange` is expressed against a single parse. */
77
+ let millis = (instant: t): float => instant->Date.fromString->Date.getTime
78
+
79
+ /** Whether one instant is strictly earlier than another. */
80
+ let isBefore = (a: t, b: t): bool => millis(a) < millis(b)
81
+
82
+ /** Order two instants, oldest first. */
83
+ let compare = (a: t, b: t) => Float.compare(millis(a), millis(b))
84
+
85
+ /** The instant as text, to the minute, in UTC — `"2026-03-02 09:00"`.
86
+ Locale-independent, the way `Money.format` and `Duration.format` are: the
87
+ same value reads the same in every log line and every test. A value that is
88
+ not the shape the grammar admits reads back unchanged. */
89
+ let format = (instant: t): string =>
90
+ String.length(instant) >= 16
91
+ ? String.slice(instant, ~start=0, ~end=10) ++ " " ++ String.slice(instant, ~start=11, ~end=16)
92
+ : instant
@@ -0,0 +1,65 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as S from "sury/src/S.res.mjs";
4
+ import * as Sury from "sury";
5
+ import * as Primitive_float from "@rescript/runtime/lib/es6/Primitive_float.js";
6
+ import * as Semantic$Reventless from "./Semantic.res.mjs";
7
+
8
+ let grammar = Sury.isoDateTime;
9
+
10
+ function fromString(raw) {
11
+ let value;
12
+ try {
13
+ value = S.parseOrThrow(raw, grammar);
14
+ } catch (exn) {
15
+ return {
16
+ TAG: "Error",
17
+ _0: `expected a UTC ISO-8601 instant, got ` + Semantic$Reventless.showString(raw)
18
+ };
19
+ }
20
+ return {
21
+ TAG: "Ok",
22
+ _0: value
23
+ };
24
+ }
25
+
26
+ let schema = Semantic$Reventless.refined(Sury.string, Semantic$Reventless.Id.dateTime, fromString);
27
+
28
+ let string = Semantic$Reventless.mark(Sury.string, Semantic$Reventless.Id.dateTime, undefined);
29
+
30
+ function isDateTime(fieldSchema) {
31
+ return Semantic$Reventless.has(fieldSchema, Semantic$Reventless.Id.dateTime);
32
+ }
33
+
34
+ function millis(instant) {
35
+ return new Date(instant).getTime();
36
+ }
37
+
38
+ function isBefore(a, b) {
39
+ return new Date(a).getTime() < new Date(b).getTime();
40
+ }
41
+
42
+ function compare(a, b) {
43
+ return Primitive_float.compare(new Date(a).getTime(), new Date(b).getTime());
44
+ }
45
+
46
+ function format(instant) {
47
+ if (instant.length >= 16) {
48
+ return instant.slice(0, 10) + " " + instant.slice(11, 16);
49
+ } else {
50
+ return instant;
51
+ }
52
+ }
53
+
54
+ export {
55
+ grammar,
56
+ fromString,
57
+ schema,
58
+ string,
59
+ isDateTime,
60
+ millis,
61
+ isBefore,
62
+ compare,
63
+ format,
64
+ }
65
+ /* grammar Not a pure module */
@@ -0,0 +1,122 @@
1
+ /**
2
+ The picture a row is shown by, wherever a row is shown as a reference to it.
3
+
4
+ ## Why this is derived once, here
5
+
6
+ The reference door (`{list}Refs`) names a row for a caller holding a pointer to
7
+ it, and a card or a tile that draws such a reference wants its picture for the
8
+ same reason it wants its name. Three backends answer that door — an AppSync
9
+ resolver over DynamoDB, the Postgres query Lambda, and the local GraphQL server
10
+ — and a rule about "where a row keeps its picture" spelled three times is a rule
11
+ the three would eventually disagree about. So the *field* is found once, from the
12
+ state schema, and each backend only reads it out.
13
+
14
+ ## What counts as the picture
15
+
16
+ The first field, in declaration order, carrying an image semantic:
17
+ `uploadableImage` or `imageRef` (the field's value IS the ref) or
18
+ `captionedImage` (the ref sits inside the record, beside its text). A field
19
+ holding a set of them is read at `[0]`, because a set puts the primary member
20
+ first — the same member a card, a gallery tile and a list cell already draw.
21
+
22
+ Declaration order rather than a name rule: a view with two image fields has
23
+ already said which one comes first, and guessing from names would let a field
24
+ called `thumbnail` outrank the one the author put at the top.
25
+ */
26
+
27
+ /** Where the ref string sits, relative to the field that carries it. */
28
+ type shape =
29
+ | /** The field's own value is the ref. */ Scalar
30
+ | /** The ref is one member of the field's record. */ Member(string)
31
+
32
+ type source = {
33
+ field: string,
34
+ shape: shape,
35
+ /** The field holds a set, and the row is shown by its first member. */
36
+ fromArray: bool,
37
+ }
38
+
39
+ /** `CaptionedImage`'s own name for the reference it wraps. Named here rather
40
+ than spelled at each reader: the record is this package's, so a rename would
41
+ otherwise be caught nowhere. */
42
+ let captionedImageMember = "ref"
43
+
44
+ let shapeOf = (schema: S.t<unknown>): option<shape> =>
45
+ switch Semantic.getFrom(schema) {
46
+ | Some({id}) if id === Semantic.Id.uploadableImage || id === Semantic.Id.imageRef => Some(Scalar)
47
+ | Some({id}) if id === Semantic.Id.captionedImage => Some(Member(captionedImageMember))
48
+ | _ => None
49
+ }
50
+
51
+ // sury keeps a homogeneous element schema in `additionalItems` and a tuple's
52
+ // positional schemas in `items`; both are read, as `StorageRef.getFieldStore`
53
+ // does, so the marker is found wherever the element sits.
54
+ let elementOf = (schema: S.t<unknown>): option<S.t<unknown>> =>
55
+ switch schema {
56
+ | Array({items, additionalItems}) =>
57
+ switch items->Array.get(0) {
58
+ | Some(_) as element => element
59
+ | None =>
60
+ switch additionalItems {
61
+ | Schema(element) => Some(element)
62
+ | _ => None
63
+ }
64
+ }
65
+ | _ => None
66
+ }
67
+
68
+ let sourceOfField = (~field: string, schema: S.t<unknown>): option<source> => {
69
+ // The marker on an optional field lives inside the wrapper, and an array's
70
+ // lives on its element — so both are stepped through before asking.
71
+ let unwrapped = schema->Semantic.unwrapOptional->Option.getOr(schema)
72
+ switch shapeOf(unwrapped) {
73
+ | Some(shape) => Some({field, shape, fromArray: false})
74
+ | None =>
75
+ unwrapped
76
+ ->elementOf
77
+ ->Option.flatMap(shapeOf)
78
+ ->Option.map(shape => {field, shape, fromArray: true})
79
+ }
80
+ }
81
+
82
+ /** The field a reference to this row shows it by, or nothing where the view
83
+ declares no picture — which is most of them. */
84
+ let sourceFrom = (stateSchema: S.t<unknown>): option<source> =>
85
+ switch stateSchema {
86
+ | Object({properties}) =>
87
+ properties
88
+ ->Dict.toArray
89
+ ->Array.filterMap(((field, schema)) => sourceOfField(~field, schema))
90
+ ->Array.get(0)
91
+ | _ => None
92
+ }
93
+
94
+ /** The ref one row carries, read out of the stored item. An empty string is read
95
+ as absent: a projection that wrote a placeholder said nothing about a
96
+ picture, and a consumer resolving `""` against a store gets a broken tile. */
97
+ let refFrom = (row: dict<JSON.t>, source: source): option<string> => {
98
+ let value = row->Dict.get(source.field)
99
+ let value = source.fromArray
100
+ ? value->Option.flatMap(JSON.Decode.array)->Option.flatMap(members => members->Array.get(0))
101
+ : value
102
+ switch source.shape {
103
+ | Scalar => value->Option.flatMap(JSON.Decode.string)
104
+ | Member(member) =>
105
+ value
106
+ ->Option.flatMap(JSON.Decode.object)
107
+ ->Option.flatMap(d => d->Dict.get(member))
108
+ ->Option.flatMap(JSON.Decode.string)
109
+ }->Option.filter(ref => ref != "")
110
+ }
111
+
112
+ /** The same read as a JavaScript expression, for the one backend whose resolver
113
+ is generated source rather than a function: the AppSync template. `null`
114
+ where the row carries nothing, which is what the field's type promises. */
115
+ let jsExpr = (~row: string, source: source): string => {
116
+ let at = `${row}['${source.field}']`
117
+ let at = source.fromArray ? `(${at} ?? [])[0]` : at
118
+ switch source.shape {
119
+ | Scalar => `${at} ?? null`
120
+ | Member(member) => `${at}?.['${member}'] ?? null`
121
+ }
122
+ }
@@ -0,0 +1,101 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
4
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
5
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
+ import * as Semantic$Reventless from "./Semantic.res.mjs";
7
+
8
+ let captionedImageMember = "ref";
9
+
10
+ function shapeOf(schema) {
11
+ let match = Semantic$Reventless.getFrom(schema);
12
+ if (match === undefined) {
13
+ return;
14
+ }
15
+ let id = match.id;
16
+ if (id === Semantic$Reventless.Id.uploadableImage || id === Semantic$Reventless.Id.imageRef) {
17
+ return "Scalar";
18
+ } else if (id === Semantic$Reventless.Id.captionedImage) {
19
+ return {
20
+ TAG: "Member",
21
+ _0: captionedImageMember
22
+ };
23
+ } else {
24
+ return;
25
+ }
26
+ }
27
+
28
+ function elementOf(schema) {
29
+ if (schema.type !== "array") {
30
+ return;
31
+ }
32
+ let additionalItems = schema.additionalItems;
33
+ let element = schema.items[0];
34
+ if (element !== undefined) {
35
+ return element;
36
+ } else if (additionalItems === "strip" || additionalItems === "strict") {
37
+ return;
38
+ } else {
39
+ return additionalItems;
40
+ }
41
+ }
42
+
43
+ function sourceOfField(field, schema) {
44
+ let unwrapped = Stdlib_Option.getOr(Semantic$Reventless.unwrapOptional(schema), schema);
45
+ let shape = shapeOf(unwrapped);
46
+ if (shape !== undefined) {
47
+ return {
48
+ field: field,
49
+ shape: shape,
50
+ fromArray: false
51
+ };
52
+ } else {
53
+ return Stdlib_Option.map(Stdlib_Option.flatMap(elementOf(unwrapped), shapeOf), shape => ({
54
+ field: field,
55
+ shape: shape,
56
+ fromArray: true
57
+ }));
58
+ }
59
+ }
60
+
61
+ function sourceFrom(stateSchema) {
62
+ if (stateSchema.type === "object") {
63
+ return Stdlib_Array.filterMap(Object.entries(stateSchema.properties), param => sourceOfField(param[0], param[1]))[0];
64
+ }
65
+ }
66
+
67
+ function refFrom(row, source) {
68
+ let value = row[source.field];
69
+ let value$1 = source.fromArray ? Stdlib_Option.flatMap(Stdlib_Option.flatMap(value, Stdlib_JSON.Decode.array), members => members[0]) : value;
70
+ let member = source.shape;
71
+ let tmp;
72
+ if (typeof member !== "object") {
73
+ tmp = Stdlib_Option.flatMap(value$1, Stdlib_JSON.Decode.string);
74
+ } else {
75
+ let member$1 = member._0;
76
+ tmp = Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(value$1, Stdlib_JSON.Decode.object), d => d[member$1]), Stdlib_JSON.Decode.string);
77
+ }
78
+ return Stdlib_Option.filter(tmp, ref => ref !== "");
79
+ }
80
+
81
+ function jsExpr(row, source) {
82
+ let at = row + `['` + source.field + `']`;
83
+ let at$1 = source.fromArray ? `(` + at + ` ?? [])[0]` : at;
84
+ let member = source.shape;
85
+ if (typeof member !== "object") {
86
+ return at$1 + ` ?? null`;
87
+ } else {
88
+ return at$1 + `?.['` + member._0 + `'] ?? null`;
89
+ }
90
+ }
91
+
92
+ export {
93
+ captionedImageMember,
94
+ shapeOf,
95
+ elementOf,
96
+ sourceOfField,
97
+ sourceFrom,
98
+ refFrom,
99
+ jsExpr,
100
+ }
101
+ /* Semantic-Reventless Not a pure module */
@@ -59,6 +59,9 @@ type t = {id: string, payload: payload}
59
59
  `x-reventless-semantic` carries, shared with the annotation path. */
60
60
  module Id = {
61
61
  let dateTime = "dateTime"
62
+ // A day with no instant in it. Separate from `dateTime` because storing one as
63
+ // an instant is what produces the midnight bug — see `CalendarDate`.
64
+ let date = "date"
62
65
  let reference = "reference"
63
66
  let storageRef = "storageRef"
64
67
  // Like `storageRef`, but inline-or-reference rather than always a ref path.
@@ -101,6 +104,11 @@ module Id = {
101
104
  // A lat/lng pair. Cheapest to adopt: `{lat, lng}` is already the stored shape.
102
105
  let geoPoint = "geoPoint"
103
106
 
107
+ // The ordered record of the states a row has been through. Marks a shape
108
+ // rather than a scalar's meaning, and rides here anyway: this is the marker
109
+ // the schema walk emits, so anything else would be invisible on the wire.
110
+ let lifecycleTrail = "lifecycleTrail"
111
+
104
112
  // The first composite that is a union rather than an object. Collapses fields,
105
113
  // so adopting it changes the wire and rebuilds a derived view.
106
114
  let geolocation = "geolocation"