@reventlessdev/reventless-spec 3.0.0-alpha.80 → 3.0.0-alpha.82

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,22 @@
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.82 (2026-07-28)
7
+
8
+ ### Features
9
+
10
+ * **spec:** one semantic marker every typed semantic marks itself with ([aa18afc](https://github.com/ReventlessDev/reventless-core/commit/aa18afcf04c8edad9afe27e6fa4261d01e184da7))
11
+ * **spec:** StorageRef — the first semantic type, declared on the field's type ([44f15c3](https://github.com/ReventlessDev/reventless-core/commit/44f15c37de71261d701d18a9f1ada6f481c4a8dc))
12
+
13
+
14
+ # 3.0.0-alpha.81 (2026-07-26)
15
+
16
+ ### Features
17
+
18
+ * **auto-ui:** declare command target state via [@target](https://github.com/target)State ([5fc0374](https://github.com/ReventlessDev/reventless-core/commit/5fc03741a8816c57085b86a4ad7d595e3b690193)), closes [#5](https://github.com/ReventlessDev/reventless-core/issues/5)
19
+ * **auto-ui:** declare field semantics + dashboard metrics ([@semantic](https://github.com/semantic), [@metric](https://github.com/metric)) ([d74ff77](https://github.com/ReventlessDev/reventless-core/commit/d74ff7721e18e8638a82931a370a549b304dac94)), closes [#4](https://github.com/ReventlessDev/reventless-core/issues/4)
20
+
21
+
6
22
  # 3.0.0-alpha.80 (2026-07-22)
7
23
 
8
24
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-spec",
3
- "version": "3.0.0-alpha.80",
3
+ "version": "3.0.0-alpha.82",
4
4
  "description": "Specifications for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {
@@ -177,6 +177,16 @@ type commandDef = {
177
177
  */
178
178
  allowedStates: @s.matches(stringArrayOptionSchema) option<array<string>>,
179
179
  /**
180
+ The single status value this command's handler writes — the command's *to*
181
+ state, sibling of `allowedStates`' *from* set. Source: the
182
+ `@targetState("Shipped")` command-variant annotation. `None` (absent
183
+ annotation) is the back-compat default: AutoUI's board resolver then falls
184
+ back to its name-stem heuristic. `Some("Shipped")` lets the resolver move a
185
+ row by a declared transition instead of a guess. js_nullable for JSON safety,
186
+ same as `allowedStates`.
187
+ */
188
+ targetState: @s.matches(stringOptionSchema) option<string>,
189
+ /**
180
190
  Whether this command variant is exposed in the generated API (a non-`@noApi`
181
191
  variant of a non-`@noApi` command). Dev tooling badges API-exposed commands in
182
192
  the event graph. js_nullable (T | null) so it stays JSON-safe inside the
@@ -102,6 +102,7 @@ let commandDefSchema = S.schema(s => ({
102
102
  mutationField: s.m(S.string),
103
103
  references: s.m(S.array(fieldReferenceSchema)),
104
104
  allowedStates: s.m(stringArrayOptionSchema),
105
+ targetState: s.m(stringOptionSchema),
105
106
  apiExposed: s.m(boolOptionSchema)
106
107
  }));
107
108
 
@@ -1,8 +1,5 @@
1
1
  /** Identifies which entity a reference field points to. */
2
- type target = {entity: string, plugin: option<string>}
3
-
4
- /** Sury metadata ID for entity reference annotation. */
5
- let referenceId: S.Metadata.Id.t<target> = S.Metadata.Id.make(~namespace="reventless", ~name="reference")
2
+ type target = Semantic.referenceTarget
6
3
 
7
4
  /**
8
5
  A sury string schema annotated as an entity reference field.
@@ -28,10 +25,14 @@ Prefer the `@ref("EntityName")` ppx shorthand over writing `@s.matches(...)` by
28
25
  ```
29
26
  */
30
27
  let to_ = (~plugin=?, ~key=?, entity: string): S.t<string> => {
28
+ // Reference-ness and DCB-tagged-ness are two independent facts that happen to
29
+ // co-occur here: this constructor declares both. `toWithoutDcbTag` declares
30
+ // only the first, and plain `DcbTag.string` only the second — so no consumer
31
+ // may infer either one from the other.
31
32
  let base =
32
33
  S.string
33
34
  ->S.Metadata.set(~id=DcbTag.dcbTagId, true)
34
- ->S.Metadata.set(~id=referenceId, {entity, plugin})
35
+ ->Semantic.mark(~id=Semantic.Id.reference, ~payload=ReferenceTo({entity, plugin}))
35
36
  switch key {
36
37
  | Some(k) => base->S.Metadata.set(~id=DcbTag.dcbTagKeyOverrideId, k)
37
38
  | None => base
@@ -40,7 +41,10 @@ let to_ = (~plugin=?, ~key=?, entity: string): S.t<string> => {
40
41
 
41
42
  /** Returns the reference target if the schema carries `Reference.to_(...)` metadata. */
42
43
  let getTarget = (schema: S.t<unknown>): option<target> =>
43
- S.Metadata.get(schema, ~id=referenceId)
44
+ switch Semantic.get(schema) {
45
+ | Some({payload: ReferenceTo(target)}) => Some(target)
46
+ | _ => None
47
+ }
44
48
 
45
49
  /**
46
50
  Like `to_` but does not imply DCB tag semantics.
@@ -48,4 +52,4 @@ Use with `@ref("Entity") @noDcbTag` when the field references another entity
48
52
  but should not participate in content-based event routing.
49
53
  */
50
54
  let toWithoutDcbTag = (~plugin=?, entity: string): S.t<string> =>
51
- S.string->S.Metadata.set(~id=referenceId, {entity, plugin})
55
+ S.string->Semantic.mark(~id=Semantic.Id.reference, ~payload=ReferenceTo({entity, plugin}))
@@ -2,13 +2,15 @@
2
2
 
3
3
  import * as S from "sury/src/S.res.mjs";
4
4
  import * as DcbTag$Reventless from "./DcbTag.res.mjs";
5
-
6
- let referenceId = S.Metadata.Id.make("reventless", "reference");
5
+ import * as Semantic$Reventless from "../semantic/Semantic.res.mjs";
7
6
 
8
7
  function to_(plugin, key, entity) {
9
- let base = S.Metadata.set(S.Metadata.set(S.string, DcbTag$Reventless.dcbTagId, true), referenceId, {
10
- entity: entity,
11
- plugin: plugin
8
+ let base = Semantic$Reventless.mark(S.Metadata.set(S.string, DcbTag$Reventless.dcbTagId, true), Semantic$Reventless.Id.reference, {
9
+ TAG: "ReferenceTo",
10
+ _0: {
11
+ entity: entity,
12
+ plugin: plugin
13
+ }
12
14
  });
13
15
  if (key !== undefined) {
14
16
  return S.Metadata.set(base, DcbTag$Reventless.dcbTagKeyOverrideId, key);
@@ -18,20 +20,31 @@ function to_(plugin, key, entity) {
18
20
  }
19
21
 
20
22
  function getTarget(schema) {
21
- return S.Metadata.get(schema, referenceId);
23
+ let match = Semantic$Reventless.get(schema);
24
+ if (match === undefined) {
25
+ return;
26
+ }
27
+ let target = match.payload;
28
+ if (typeof target !== "object" || target.TAG !== "ReferenceTo") {
29
+ return;
30
+ } else {
31
+ return target._0;
32
+ }
22
33
  }
23
34
 
24
35
  function toWithoutDcbTag(plugin, entity) {
25
- return S.Metadata.set(S.string, referenceId, {
26
- entity: entity,
27
- plugin: plugin
36
+ return Semantic$Reventless.mark(S.string, Semantic$Reventless.Id.reference, {
37
+ TAG: "ReferenceTo",
38
+ _0: {
39
+ entity: entity,
40
+ plugin: plugin
41
+ }
28
42
  });
29
43
  }
30
44
 
31
45
  export {
32
- referenceId,
33
46
  to_,
34
47
  getTarget,
35
48
  toWithoutDcbTag,
36
49
  }
37
- /* referenceId Not a pure module */
50
+ /* S Not a pure module */
@@ -26,6 +26,13 @@ free on the in-memory adapter but is `O(n)` Scan + FilterExpression on
26
26
  DynamoDB-backed adapters — the annotation is the explicit signal that the
27
27
  read model is small enough or the cost is acceptable.
28
28
  */
29
+ /**
30
+ Declared aggregation for a `@metric`-annotated numeric field. `aggregate` is one
31
+ of `"count"`, `"sum"`, `"avg"` (the UI's dashboard vocabulary); `label` is the
32
+ KPI label, or `""` to let the UI derive one from the field name.
33
+ */
34
+ type metricSpec = {aggregate: string, label: string}
35
+
29
36
  type stateAnnotationSpec = {
30
37
  ids: array<string>,
31
38
  compositeIds: array<string>,
@@ -40,6 +47,22 @@ type stateAnnotationSpec = {
40
47
  scan: array<string>,
41
48
  scanSort: array<string>,
42
49
  /**
50
+ Fields annotated `@semantic("<id>")` — `(fieldName, semanticId)` pairs. The
51
+ semantic id is the UI's `AutoSemantics` vocabulary (e.g. `"currency"`,
52
+ `"geo-lat"`); the PPX can't validate it (the UI owns that vocabulary).
53
+ `SuryToJsonSchema` emits `x-reventless-semantic` on the named field, which
54
+ AutoUI reads at `#Annotation` provenance — above its heuristic, below a
55
+ `ui-hints.json` `fields:` override.
56
+ */
57
+ semantic: array<(string, string)>,
58
+ /**
59
+ Fields annotated `@metric(...)` — `(fieldName, metricSpec)` pairs.
60
+ `SuryToJsonSchema` emits `x-reventless-metric: {aggregate, label}` on the named
61
+ field, which AutoUI folds into the dashboard metric list it already builds from
62
+ hints — so a plugin gets a Dashboard page from its schema alone.
63
+ */
64
+ metric: array<(string, metricSpec)>,
65
+ /**
43
66
  Field annotated `@status` on the state record (PPX-emitted). `Some(name)`
44
67
  when one such annotation exists; the PPX errors on duplicate `@status`
45
68
  annotations within the same record. Codegen consumes this to populate
@@ -24,7 +24,7 @@ let name = "CategoriesView"
24
24
  | CategoryRenamed({categoryId: string, name: string})
25
25
  | CategoryArchived({categoryId: string})
26
26
 
27
- let project = event => switch event {
27
+ let project = ({event}) => switch event {
28
28
  | CategoryAdded({categoryId, name}) =>
29
29
  [Set(categoryId, {categoryId, name, archived: false})]
30
30
  | CategoryRenamed({categoryId, name}) =>
@@ -81,6 +81,29 @@ module type Spec = {
81
81
  let visibility: Visibility.t
82
82
  }
83
83
 
84
+ /**
85
+ The envelope a projection receives for each consumed event.
86
+
87
+ Carries the decoded event alongside the metadata the storage layer already
88
+ holds, so a projection can persist producer time or the acting user without the
89
+ command author having to duplicate that framework state into the event payload.
90
+
91
+ `meta.time` and `recordedAt` are **different clocks** — pick deliberately:
92
+
93
+ - `meta.time` — *producer* time, stamped when the command handler created the
94
+ event. This is the domain-meaningful timestamp (when the order was placed,
95
+ when it shipped); it is identical across every path.
96
+ - `recordedAt` — *storage* time, when the event was appended to the DCB log. On
97
+ the AWS (DynamoDB-stream) path this is the authoritative stored column; on the
98
+ local (topic) path it is stamped at publish, so it may sit a few milliseconds
99
+ after the stored value. Use it for storage-lag diagnostics, not domain dates.
100
+ */
101
+ type consumed<'e> = {
102
+ event: 'e,
103
+ meta: Message.meta,
104
+ recordedAt: string,
105
+ }
106
+
84
107
  /**
85
108
  The Projection — the pure projection function from consumed events to actions.
86
109
  */
@@ -89,9 +112,10 @@ module type Projection = {
89
112
 
90
113
  /**
91
114
  Projects one consumed event into read model actions.
92
- Receives only events declared in `Spec.consumedEvent` — no wildcard needed.
115
+ Receives the event wrapped in a `consumed` envelope (event + `meta` +
116
+ `recordedAt`); only events declared in `Spec.consumedEvent` — no wildcard needed.
93
117
  */
94
- let project: Spec.consumedEvent => array<Projection.action<string, Spec.state>>
118
+ let project: consumed<Spec.consumedEvent> => array<Projection.action<string, Spec.state>>
95
119
 
96
120
  /** File URL of this Projection module (`import.meta.url`). */
97
121
  let moduleUrl: string
@@ -0,0 +1,67 @@
1
+ /**
2
+ The one marker every typed semantic marks itself with.
3
+
4
+ A semantic type says what a field's value *is* — a date-time, a reference to
5
+ another entity, a ref into an object store — as a property of the field's
6
+ **type**, not as a string stapled beside it. Every layer downstream then derives
7
+ from that single declaration: validation, the wire contract, the UI widget it
8
+ gets rendered with, and eventually the infrastructure provisioned for it.
9
+
10
+ Typed markers predate this module, and each was bespoke: `DateTime` carried its
11
+ own metadata id, `Reference` carried another, and the schema walk detected both
12
+ by hardcoded special case. That made every new typed marker new detection code.
13
+ One shared marker means the walk reads a semantic generically and a new semantic
14
+ type is a new *value*, not a new branch.
15
+
16
+ The payload is a real variant rather than free-form JSON. The semantic
17
+ vocabulary is framework-owned — an application declares a field *is* a storage
18
+ ref, it does not invent what a storage ref means — so the set is closed, and a
19
+ closed set typed here is one the compiler checks at every producer and consumer.
20
+ It also keeps `Reference.getTarget` a total typed function instead of a decode
21
+ that can fail at runtime.
22
+ */
23
+
24
+ /** Which entity a reference field points to. */
25
+ type referenceTarget = {entity: string, plugin: option<string>}
26
+
27
+ /** Which object store a storage-ref field's value lives in. `plugin` is absent
28
+ when the store belongs to the declaring plugin, which is the common case. */
29
+ type storeTarget = {plugin: option<string>, store: string}
30
+
31
+ /** Per-semantic detail, for the semantics that carry any. */
32
+ type payload =
33
+ | Plain
34
+ | ReferenceTo(referenceTarget)
35
+ | StoredIn(storeTarget)
36
+
37
+ /** A field's semantic: the vocabulary id, plus its detail. */
38
+ type t = {id: string, payload: payload}
39
+
40
+ /**
41
+ The semantic ids the framework itself defines.
42
+
43
+ These strings are the wire vocabulary — they are what `x-reventless-semantic`
44
+ carries, and the same vocabulary the string annotation path already uses, so the
45
+ type path and the annotation path converge on one wire format rather than two.
46
+ */
47
+ module Id = {
48
+ let dateTime = "dateTime"
49
+ let reference = "reference"
50
+ let storageRef = "storageRef"
51
+ }
52
+
53
+ let semanticId: S.Metadata.Id.t<t> = S.Metadata.Id.make(~namespace="reventless", ~name="semantic")
54
+
55
+ /** Mark a schema as carrying a semantic. */
56
+ let mark = (schema: S.t<'a>, ~id: string, ~payload: payload=Plain): S.t<'a> =>
57
+ schema->S.Metadata.set(~id=semanticId, {id, payload})
58
+
59
+ /** The semantic a field's schema carries, if any. */
60
+ let get = (fieldSchema: S.t<'a>): option<t> => S.Metadata.get(fieldSchema, ~id=semanticId)
61
+
62
+ /** Whether a field's schema carries this specific semantic. */
63
+ let has = (fieldSchema: S.t<'a>, ~id: string): bool =>
64
+ switch get(fieldSchema) {
65
+ | Some(s) => s.id === id
66
+ | None => false
67
+ }
@@ -0,0 +1,41 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as S from "sury/src/S.res.mjs";
4
+
5
+ let Id = {
6
+ dateTime: "dateTime",
7
+ reference: "reference",
8
+ storageRef: "storageRef"
9
+ };
10
+
11
+ let semanticId = S.Metadata.Id.make("reventless", "semantic");
12
+
13
+ function mark(schema, id, payloadOpt) {
14
+ let payload = payloadOpt !== undefined ? payloadOpt : "Plain";
15
+ return S.Metadata.set(schema, semanticId, {
16
+ id: id,
17
+ payload: payload
18
+ });
19
+ }
20
+
21
+ function get(fieldSchema) {
22
+ return S.Metadata.get(fieldSchema, semanticId);
23
+ }
24
+
25
+ function has(fieldSchema, id) {
26
+ let s = S.Metadata.get(fieldSchema, semanticId);
27
+ if (s !== undefined) {
28
+ return s.id === id;
29
+ } else {
30
+ return false;
31
+ }
32
+ }
33
+
34
+ export {
35
+ Id,
36
+ semanticId,
37
+ mark,
38
+ get,
39
+ has,
40
+ }
41
+ /* semanticId Not a pure module */
@@ -0,0 +1,124 @@
1
+ /**
2
+ A reference to an object living in one of the platform's object stores.
3
+
4
+ The value is the ref string a store's presign service minted — an origin-relative
5
+ path rooted at the store's served prefix, which the UI renders directly because
6
+ the store is fronted read-only on the app's own origin.
7
+
8
+ ## Why this is a type and not a convention
9
+
10
+ An event log is append-only, so whatever a command accepts into it is permanent.
11
+ Before this type, an `imageUrl: string` field accepted *anything* — including an
12
+ `https://` URL pointing at somebody else's server, or a multi-megabyte `data:`
13
+ URI inlined into the event itself. Both deploy green, both are unfixable after
14
+ the fact, and neither is what the field means. Declaring the field's type makes
15
+ the wrong values unrepresentable at the boundary, before `decide` ever runs.
16
+
17
+ The declaration also states a *requirement*: a field of this type says the
18
+ deployment needs a store called `store` to exist. Nothing provisions that store
19
+ yet, and that is a deliberate resting point — the validation hole is closed and
20
+ the requirement is written down, which is strictly better than the status quo
21
+ even if automatic provisioning never lands.
22
+
23
+ ## The grammar
24
+
25
+ A ref is an absolute, origin-relative path of at least two non-empty segments:
26
+
27
+ /uploads/2f8c1e94-.../photo.jpg
28
+ /uploads/user-42/2f8c1e94-.../photo.jpg
29
+
30
+ Rejected: anything with a scheme (`https://…`, `data:…`), protocol-relative
31
+ `//host/path`, relative paths, empty segments, and `.`/`..` traversal.
32
+
33
+ The framework mints refs in exactly this form — the presign service builds the
34
+ object key as `{servedPrefix}/{identity}{uuid}/{fileName}` and returns `/{key}` —
35
+ so the grammar is the framework's to define, not an application's.
36
+
37
+ Note what is *not* checked: that the ref's prefix belongs to this specific store.
38
+ Today every store shares one served prefix, so there is nothing store-specific to
39
+ check against; the store identity is carried in the field's semantic payload,
40
+ where provisioning and the UI read it. When stores gain per-store prefixes, this
41
+ check tightens from a structural one to a per-store one without the type, the
42
+ wire format, or any stored value changing.
43
+
44
+ @example
45
+ ```rescript
46
+ @schema type command =
47
+ | ChangeProductImage({
48
+ productId: @s.matches(DcbTag.string) string,
49
+ imageUrl: @storageRef("productImages") string,
50
+ })
51
+ ```
52
+ */
53
+
54
+ /** The ref's representation. Transparent `string` on purpose: the marker refines
55
+ an existing `string` field rather than replacing it, so the field's runtime
56
+ representation — and therefore every stored event — is unchanged. A sealed
57
+ type here would defeat that, and would also be unattachable via `@s.matches`,
58
+ which requires the schema's type to match the field's. */
59
+ type t = string
60
+
61
+ external unsafe: string => t = "%identity"
62
+ external toString: t => string = "%identity"
63
+
64
+ let segmentIsSafe = (segment: string) =>
65
+ segment !== "" && segment !== "." && segment !== ".."
66
+
67
+ /**
68
+ Validate a raw string as a storage ref, saying why when it is not one.
69
+
70
+ This is the single definition of the grammar. `forStore`'s sury schema is derived
71
+ from it rather than hand-rolling a second check, so the constructor and the
72
+ schema validation cannot drift apart.
73
+ */
74
+ let fromString = (raw: string): result<t, string> =>
75
+ if !String.startsWith(raw, "/") {
76
+ Error(
77
+ `expected an origin-relative storage ref starting with "/", got ${raw->JSON.Encode.string->JSON.stringify}. External URLs and data: URIs are not storage refs.`,
78
+ )
79
+ } else if String.startsWith(raw, "//") {
80
+ Error(`protocol-relative refs are not storage refs: ${raw}`)
81
+ } else {
82
+ let segments = raw->String.slice(~start=1, ~end=String.length(raw))->String.split("/")
83
+ if segments->Array.length < 2 {
84
+ Error(`a storage ref needs a prefix and an object path, got ${raw}`)
85
+ } else if !(segments->Array.every(segmentIsSafe)) {
86
+ Error(`a storage ref may not contain empty or traversal segments, got ${raw}`)
87
+ } else {
88
+ Ok(raw)
89
+ }
90
+ }
91
+
92
+ /**
93
+ The sury schema for a field holding a ref into a named store.
94
+
95
+ Prefer the `@storageRef("<store>")` ppx shorthand over writing this by hand.
96
+ Qualify the store as `"<plugin>.<store>"` to point at another plugin's store.
97
+ */
98
+ let forStore = (~plugin: option<string>=?, ~store: string): S.t<t> =>
99
+ S.string
100
+ ->S.refine(s => value =>
101
+ // The empty string is admitted as the "no object" sentinel. The fields this
102
+ // marks are non-optional today, and a producer with nothing to reference —
103
+ // a supplier feed carrying no image, say — already writes `""` to mean
104
+ // absence. Rejecting it here would break a legitimate existing value and
105
+ // force an event-schema change, which this marker exists to avoid: it
106
+ // refines an existing `string` field without altering what is stored.
107
+ //
108
+ // Note this is a strictly weaker guarantee than `fromString`, which stays
109
+ // exact. Making these fields properly optional would let the sentinel go.
110
+ if value !== "" {
111
+ switch fromString(value) {
112
+ | Ok(_) => ()
113
+ | Error(why) => s.fail(why)
114
+ }
115
+ }
116
+ )
117
+ ->Semantic.mark(~id=Semantic.Id.storageRef, ~payload=StoredIn({plugin, store}))
118
+
119
+ /** The store a field's schema declares its refs live in, if any. */
120
+ let getStore = (schema: S.t<'a>): option<Semantic.storeTarget> =>
121
+ switch Semantic.get(schema) {
122
+ | Some({payload: StoredIn(target)}) => Some(target)
123
+ | _ => None
124
+ }
@@ -0,0 +1,85 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as S from "sury/src/S.res.mjs";
4
+ import * as Semantic$Reventless from "./Semantic.res.mjs";
5
+
6
+ function segmentIsSafe(segment) {
7
+ if (segment !== "" && segment !== ".") {
8
+ return segment !== "..";
9
+ } else {
10
+ return false;
11
+ }
12
+ }
13
+
14
+ function fromString(raw) {
15
+ if (!raw.startsWith("/")) {
16
+ return {
17
+ TAG: "Error",
18
+ _0: `expected an origin-relative storage ref starting with "/", got ` + JSON.stringify(raw) + `. External URLs and data: URIs are not storage refs.`
19
+ };
20
+ }
21
+ if (raw.startsWith("//")) {
22
+ return {
23
+ TAG: "Error",
24
+ _0: `protocol-relative refs are not storage refs: ` + raw
25
+ };
26
+ }
27
+ let segments = raw.slice(1, raw.length).split("/");
28
+ if (segments.length < 2) {
29
+ return {
30
+ TAG: "Error",
31
+ _0: `a storage ref needs a prefix and an object path, got ` + raw
32
+ };
33
+ } else if (segments.every(segmentIsSafe)) {
34
+ return {
35
+ TAG: "Ok",
36
+ _0: raw
37
+ };
38
+ } else {
39
+ return {
40
+ TAG: "Error",
41
+ _0: `a storage ref may not contain empty or traversal segments, got ` + raw
42
+ };
43
+ }
44
+ }
45
+
46
+ function forStore(plugin, store) {
47
+ return Semantic$Reventless.mark(S.refine(S.string, s => (value => {
48
+ if (value === "") {
49
+ return;
50
+ }
51
+ let why = fromString(value);
52
+ if (why.TAG === "Ok") {
53
+ return;
54
+ } else {
55
+ return s.fail(why._0, undefined);
56
+ }
57
+ })), Semantic$Reventless.Id.storageRef, {
58
+ TAG: "StoredIn",
59
+ _0: {
60
+ plugin: plugin,
61
+ store: store
62
+ }
63
+ });
64
+ }
65
+
66
+ function getStore(schema) {
67
+ let match = Semantic$Reventless.get(schema);
68
+ if (match === undefined) {
69
+ return;
70
+ }
71
+ let target = match.payload;
72
+ if (typeof target !== "object" || target.TAG === "ReferenceTo") {
73
+ return;
74
+ } else {
75
+ return target._0;
76
+ }
77
+ }
78
+
79
+ export {
80
+ segmentIsSafe,
81
+ fromString,
82
+ forStore,
83
+ getStore,
84
+ }
85
+ /* S Not a pure module */
@@ -0,0 +1,29 @@
1
+ /**
2
+ Marks a `string` state field as an ISO-8601 date-time.
3
+
4
+ Sury has no string-typed datetime format (`S.datetime` transforms to `Js.Date.t`,
5
+ changing the field's runtime type), so this mirrors the `DcbTag.string`
6
+ precedent: a `S.t<string>` carrying sury metadata that downstream schema walkers
7
+ detect. `SchemaType`/`SuryToJsonSchema` surface it as `format: "date-time"` on
8
+ the field's JSON Schema, which the AutoUI date heuristics key off (CalendarView,
9
+ TimelineView, date-axis charts).
10
+
11
+ Use on a producer/storage timestamp a projection writes into its state — most
12
+ commonly a `meta.time`-derived field such as `placedAt` / `shippedAt`:
13
+
14
+ @example
15
+ ```rescript
16
+ @schema
17
+ type state = {
18
+ orderId: string,
19
+ placedAt: @s.matches(Reventless.DateTime.string) string,
20
+ }
21
+ ```
22
+ */
23
+
24
+ /** A sury string schema annotated as an ISO-8601 date-time field.
25
+ Use with `@s.matches(Reventless.DateTime.string)`. */
26
+ let string: S.t<string> = S.string->Semantic.mark(~id=Semantic.Id.dateTime)
27
+
28
+ /** Whether a field schema carries the date-time marker. */
29
+ let isDateTime = (fieldSchema: S.t<unknown>) => fieldSchema->Semantic.has(~id=Semantic.Id.dateTime)
@@ -0,0 +1,16 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as S from "sury/src/S.res.mjs";
4
+ import * as Semantic$Reventless from "../semantic/Semantic.res.mjs";
5
+
6
+ let string = Semantic$Reventless.mark(S.string, Semantic$Reventless.Id.dateTime, undefined);
7
+
8
+ function isDateTime(fieldSchema) {
9
+ return Semantic$Reventless.has(fieldSchema, Semantic$Reventless.Id.dateTime);
10
+ }
11
+
12
+ export {
13
+ string,
14
+ isDateTime,
15
+ }
16
+ /* string Not a pure module */