@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.
- package/CHANGELOG.md +17 -0
- package/package.json +3 -2
- package/run-check-lifecycle.mjs +2 -0
- package/schema/platform-api.graphql +1 -0
- package/src/components/ComponentKind.res +14 -8
- package/src/components/ComponentKind.res.mjs +8 -6
- package/src/lifecycle/CheckLifecycleModel.res +1353 -0
- package/src/lifecycle/CheckLifecycleModel.res.mjs +1134 -0
- package/src/lifecycle/loadPluginStructure.mjs +66 -0
- package/src/semantic/CalendarDate.res +58 -0
- package/src/semantic/CalendarDate.res.mjs +37 -0
- package/src/semantic/DateRange.res +10 -10
- package/src/semantic/DateRange.res.mjs +12 -14
- package/src/semantic/DateTime.res +92 -0
- package/src/semantic/DateTime.res.mjs +65 -0
- package/src/semantic/RowImage.res +122 -0
- package/src/semantic/RowImage.res.mjs +101 -0
- package/src/semantic/Semantic.res +8 -0
- package/src/semantic/Semantic.res.mjs +2 -0
- package/src/types/Lifecycle.res +106 -0
- package/src/types/Lifecycle.res.mjs +115 -0
- package/src/types/DateTime.res +0 -29
- package/src/types/DateTime.res.mjs +0 -16
|
@@ -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
|
-
|
|
45
|
-
is a
|
|
46
|
-
|
|
47
|
-
|
|
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:
|
|
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_:
|
|
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
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
let millis =
|
|
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 "
|
|
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.
|
|
9
|
-
end: s.m(DateTime$Reventless.
|
|
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 (
|
|
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((
|
|
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 =
|
|
45
|
-
if (t >=
|
|
46
|
-
return t <
|
|
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 (
|
|
54
|
-
return
|
|
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"
|