@graview/core 0.1.41 → 0.1.43
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/dist/answer-seen.js +1 -1
- package/dist/answer-seen.js.map +1 -1
- package/dist/capabilities.js +1 -1
- package/dist/capabilities.js.map +1 -1
- package/dist/cli/check/brand.d.ts.map +1 -1
- package/dist/cli/check/brand.js +39 -2
- package/dist/cli/check/brand.js.map +1 -1
- package/dist/cli/document-schema.d.ts.map +1 -1
- package/dist/cli/document-schema.js +8 -5
- package/dist/cli/document-schema.js.map +1 -1
- package/dist/document/brand-check.d.ts +2 -0
- package/dist/document/brand-check.d.ts.map +1 -1
- package/dist/document/brand-check.js +8 -1
- package/dist/document/brand-check.js.map +1 -1
- package/dist/document/brand.d.ts.map +1 -1
- package/dist/document/brand.js +3 -2
- package/dist/document/brand.js.map +1 -1
- package/dist/document/connection-spec.d.ts +2 -2
- package/dist/document/connection-spec.d.ts.map +1 -1
- package/dist/document/connection-spec.js +40 -34
- package/dist/document/connection-spec.js.map +1 -1
- package/dist/document/describe-place.d.ts.map +1 -1
- package/dist/document/describe-place.js +3 -2
- package/dist/document/describe-place.js.map +1 -1
- package/dist/document/diff.d.ts.map +1 -1
- package/dist/document/diff.js +7 -2
- package/dist/document/diff.js.map +1 -1
- package/dist/document/edit.d.ts.map +1 -1
- package/dist/document/edit.js +150 -128
- package/dist/document/edit.js.map +1 -1
- package/dist/document/graview-template.d.ts +97 -43
- package/dist/document/graview-template.d.ts.map +1 -1
- package/dist/document/graview-template.js +77 -66
- package/dist/document/graview-template.js.map +1 -1
- package/dist/document/logo.d.ts +12 -0
- package/dist/document/logo.d.ts.map +1 -0
- package/dist/document/logo.js +36 -0
- package/dist/document/logo.js.map +1 -0
- package/dist/document/schema.d.ts +51 -57
- package/dist/document/schema.d.ts.map +1 -1
- package/dist/document/schema.js +372 -338
- package/dist/document/schema.js.map +1 -1
- package/dist/document.schema.json +30 -2
- package/dist/formats.d.ts +10 -4
- package/dist/formats.d.ts.map +1 -1
- package/dist/formats.js +17 -8
- package/dist/formats.js.map +1 -1
- package/dist/history.d.ts +32 -3
- package/dist/history.d.ts.map +1 -1
- package/dist/history.js.map +1 -1
- package/dist/import/rows.d.ts.map +1 -1
- package/dist/import/rows.js +0 -1
- package/dist/import/rows.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/installation.d.ts +1 -1
- package/dist/installation.d.ts.map +1 -1
- package/dist/installation.js +1 -1
- package/dist/installation.js.map +1 -1
- package/dist/integrity.d.ts.map +1 -1
- package/dist/integrity.js +2 -1
- package/dist/integrity.js.map +1 -1
- package/dist/mutations/node-ref.d.ts +1 -1
- package/dist/mutations/node-ref.d.ts.map +1 -1
- package/dist/mutations/node-ref.js +1 -1
- package/dist/mutations/node-ref.js.map +1 -1
- package/dist/ops/inverse.d.ts +23 -0
- package/dist/ops/inverse.d.ts.map +1 -0
- package/dist/ops/inverse.js +62 -0
- package/dist/ops/inverse.js.map +1 -0
- package/dist/ops/log.d.ts.map +1 -1
- package/dist/ops/log.js +3 -1
- package/dist/ops/log.js.map +1 -1
- package/dist/ops/revisions.d.ts +19 -4
- package/dist/ops/revisions.d.ts.map +1 -1
- package/dist/ops/revisions.js +23 -8
- package/dist/ops/revisions.js.map +1 -1
- package/dist/ops/types.d.ts +11 -2
- package/dist/ops/types.d.ts.map +1 -1
- package/dist/ops/undo.d.ts +3 -3
- package/dist/ops/undo.d.ts.map +1 -1
- package/dist/ops/undo.js +8 -6
- package/dist/ops/undo.js.map +1 -1
- package/dist/ops/withheld.d.ts +1 -1
- package/dist/ops/withheld.d.ts.map +1 -1
- package/dist/ops/withheld.js +12 -4
- package/dist/ops/withheld.js.map +1 -1
- package/dist/past.d.ts +15 -0
- package/dist/past.d.ts.map +1 -1
- package/dist/past.js +150 -19
- package/dist/past.js.map +1 -1
- package/dist/schema/app-link-field.d.ts +1 -1
- package/dist/schema/app-link-field.d.ts.map +1 -1
- package/dist/schema/app-link-field.js +1 -1
- package/dist/schema/app-link-field.js.map +1 -1
- package/dist/schema/file-field.d.ts +1 -1
- package/dist/schema/file-field.d.ts.map +1 -1
- package/dist/schema/file-field.js +1 -1
- package/dist/schema/file-field.js.map +1 -1
- package/dist/schema/linked-field.d.ts +1 -1
- package/dist/schema/linked-field.d.ts.map +1 -1
- package/dist/schema/linked-field.js +1 -1
- package/dist/schema/linked-field.js.map +1 -1
- package/dist/schema/markdown-field.d.ts +1 -1
- package/dist/schema/markdown-field.d.ts.map +1 -1
- package/dist/schema/markdown-field.js +1 -1
- package/dist/schema/markdown-field.js.map +1 -1
- package/dist/schema/repeat-field.d.ts +1 -1
- package/dist/schema/repeat-field.d.ts.map +1 -1
- package/dist/schema/repeat-field.js +1 -1
- package/dist/schema/repeat-field.js.map +1 -1
- package/dist/schema/typed-field-builders.d.ts +1 -1
- package/dist/schema/typed-field-builders.d.ts.map +1 -1
- package/dist/schema/typed-field-builders.js +1 -1
- package/dist/schema/typed-field-builders.js.map +1 -1
- package/dist/store.d.ts +7 -8
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +12 -15
- package/dist/store.js.map +1 -1
- package/dist/temporal/effectivity.d.ts +1 -1
- package/dist/temporal/effectivity.d.ts.map +1 -1
- package/dist/temporal/effectivity.js +1 -1
- package/dist/temporal/effectivity.js.map +1 -1
- package/dist/testing.js +1 -1
- package/dist/testing.js.map +1 -1
- package/dist/theme/marks.d.ts +13 -2
- package/dist/theme/marks.d.ts.map +1 -1
- package/dist/theme/marks.js +16 -0
- package/dist/theme/marks.js.map +1 -1
- package/dist/theme/types.d.ts +17 -2
- package/dist/theme/types.d.ts.map +1 -1
- package/dist/theme/types.js.map +1 -1
- package/dist/tidying.d.ts.map +1 -1
- package/dist/tidying.js +0 -1
- package/dist/tidying.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/document/schema.js
CHANGED
|
@@ -21,367 +21,401 @@ export const NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
|
21
21
|
/** Field names are identifiers so a rule can say them bare: `quote != null`. */
|
|
22
22
|
export const FIELD_NAME = /^[a-z][A-Za-z0-9]*$/;
|
|
23
23
|
const RESERVED_FIELDS = new Set(["id", "kind"]);
|
|
24
|
-
/**
|
|
25
|
-
* What every object already answers to — `constructor`, `toString` and the
|
|
26
|
-
* rest of `Object.prototype` — is no field's or relation's name: a record's
|
|
27
|
-
* fields and a kind's relations are looked up by name, and one of these is
|
|
28
|
-
* "there" on every record whether it was declared or not.
|
|
29
|
-
*/
|
|
30
|
-
const inherited = (n) => n in Object.prototype;
|
|
31
|
-
const notInherited = z.refine((n) => !inherited(n), "every object already has this name — choose another");
|
|
32
|
-
/** At least `min` and at most `max` long: a string's characters, a list's items. */
|
|
33
|
-
const sized = (min, max) => [...(min === undefined ? [] : [z.minLength(min)]), ...(max === undefined ? [] : [z.maxLength(max)])];
|
|
34
|
-
const str = (min, max) => z.string().check(...sized(min, max));
|
|
35
|
-
const some = (of, min, max) => z.array(of).check(...sized(min, max));
|
|
36
|
-
const name = (what) => z.string().check(z.regex(NAME, `${what} names are kebab-case, lower-case words joined by hyphens like "tool-note" (field and relation names are camelCase, like "supersededBy")`));
|
|
37
24
|
/**
|
|
38
25
|
* A RELATION'S NAME may be kebab-case or one camelCase word. camelCase is
|
|
39
26
|
* recommended: a rule can then walk it bare (`tendedBy.name`), where
|
|
40
27
|
* `tended-by.name` would read as a subtraction.
|
|
41
28
|
*/
|
|
42
29
|
export const EDGE_NAME = /^(?:[a-z][a-z0-9]*(?:-[a-z0-9]+)*|[a-z][A-Za-z0-9]*)$/;
|
|
43
|
-
const edgeName = z.string().check(z.regex(EDGE_NAME, 'relation names are one camelCase word, like "supersededBy" (kind names are kebab-case, like "tool-note")'), notInherited);
|
|
44
|
-
const fieldName = z
|
|
45
|
-
.string()
|
|
46
|
-
.check(z.regex(FIELD_NAME, 'field names are one word or camelCase, like "dueDate", so rules say them bare (kind names are kebab-case, like "tool-note")'), z.refine((n) => !RESERVED_FIELDS.has(n), 'a field cannot be called "id" or "kind" — the framework keeps those; try "type" or "category"'), notInherited);
|
|
47
|
-
const sentence = str(1, 500);
|
|
48
|
-
const expression = str(1, 2000);
|
|
49
|
-
const template = str(1, 300);
|
|
50
30
|
/**
|
|
51
31
|
* `image` and `file` hold what the host stored (FR-162): a `FileValue`, its
|
|
52
32
|
* `href` a path on the host. `phone` and `place` are words drawn as a link to
|
|
53
33
|
* call and to a map; `duration` is a whole number of minutes, said "1 h 30 min".
|
|
54
34
|
*/
|
|
55
35
|
export const FIELD_TYPES = ["string", "text", "number", "integer", "boolean", "date", "datetime", "enum", "list", "url", "email", "image", "video", "file", "app-link", "phone", "place", "duration"];
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
options: z.optional(some(str(1, 80), 1, 100)),
|
|
64
|
-
of: z.optional(z.enum(["string", "number", "date", "image", "video", "file"])),
|
|
65
|
-
default: z.optional(z.unknown()),
|
|
66
|
-
unit: z.optional(str(undefined, 12)),
|
|
67
|
-
/** How a number is shown (`money`, `percent`, `duration`), or that a `text` field is written in Markdown (FR-180; the checker's `field-format` says which goes where). */
|
|
68
|
-
format: z.optional(z.enum(["money", "percent", "duration", "markdown"])),
|
|
69
|
-
/** A number's range (FR-114): the least and the most it may be, and the step its values are whole multiples of (JSON Schema's `multipleOf`). */
|
|
70
|
-
min: z.optional(z.number()),
|
|
71
|
-
max: z.optional(z.number()),
|
|
72
|
-
step: z.optional(z.number()),
|
|
73
|
-
/** The name this field had in the previous version: its values move instead of being dropped. */
|
|
74
|
-
renamedFrom: z.optional(z.string().check(z.regex(FIELD_NAME))),
|
|
75
|
-
/**
|
|
76
|
-
* A date that repeats (`repeat.ts`): a record may keep, beside the date
|
|
77
|
-
* (its first occurrence), how it repeats under the date's name and
|
|
78
|
-
* `Repeat` — `"startsAtRepeat": { "every": "week", "on": ["tue"] }`.
|
|
79
|
-
*/
|
|
80
|
-
repeats: z.optional(z.boolean()),
|
|
81
|
-
})
|
|
82
|
-
.check(z.superRefine((f, ctx) => {
|
|
83
|
-
if (f.type === "enum" && !f.options)
|
|
84
|
-
ctx.addIssue({ code: "custom", message: 'an enum field lists its "options"', path: ["options"] });
|
|
85
|
-
if (f.type !== "enum" && f.options)
|
|
86
|
-
ctx.addIssue({ code: "custom", message: '"options" belongs only on an enum field', path: ["options"] });
|
|
87
|
-
if (f.type !== "list" && f.of)
|
|
88
|
-
ctx.addIssue({ code: "custom", message: '"of" belongs only on a list field', path: ["of"] });
|
|
89
|
-
if (f.repeats && f.type !== "date" && f.type !== "datetime")
|
|
90
|
-
ctx.addIssue({ code: "custom", message: '"repeats" belongs only on a date or datetime field', path: ["repeats"] });
|
|
91
|
-
}));
|
|
92
|
-
/**
|
|
93
|
-
* WHERE A RELATION GOES: a kind of this document (`"house"`), or a kind in
|
|
94
|
-
* ANOTHER APP the viewer can open (FR-171) — `@` + the app's address + `/`
|
|
95
|
-
* + its kind: `"@nick/house/house"`, `"@app_kf5eydr9z58jzf/house"`. A link
|
|
96
|
-
* to another app's record picks one there and leaves that app untouched.
|
|
36
|
+
/*
|
|
37
|
+
* THE SHAPES ARE BUILT ON FIRST USE. Some hundred and fifty zod schemas make
|
|
38
|
+
* up the document's shape, and building them is work a Worker's cold isolate
|
|
39
|
+
* or a page would do the moment it imported `@graview/core/document`, whether
|
|
40
|
+
* or not it ever read a document. Each exported shape is `z.lazy` over the one
|
|
41
|
+
* set `build` makes the first time any of them parses: the same names, the
|
|
42
|
+
* same parse, the same issues, and nothing done at import.
|
|
97
43
|
*/
|
|
98
|
-
|
|
99
|
-
export const EdgeSpec = z.strictObject({
|
|
100
|
-
to: z.union([some(edgeTarget, 1), z.literal("*")]),
|
|
101
|
-
cardinality: z.optional(z.enum(["one", "many"])),
|
|
102
|
-
description: z.optional(sentence),
|
|
103
|
-
inverse: z.optional(str(1, 80)),
|
|
104
|
-
appendOnly: z.optional(z.boolean()),
|
|
105
|
-
/** The name this relation had in the previous version: its links move. */
|
|
106
|
-
renamedFrom: z.optional(z.string()),
|
|
107
|
-
/** `"never"`: no automated author makes or breaks this relation on its own (`@graview/core/tidying`). */
|
|
108
|
-
tidying: z.optional(z.literal("never")),
|
|
109
|
-
});
|
|
110
|
-
/** A computed field: its expression, or the expression with the words it is shown by (FR-83). */
|
|
111
|
-
export const ComputedSpec = z.union([expression, z.strictObject({ expr: expression, label: z.optional(str(1, 60)), description: z.optional(sentence) })]);
|
|
112
|
-
export const KindSpec = z.strictObject({
|
|
113
|
-
noun: z.optional(str(1, 40)),
|
|
114
|
-
plural: z.optional(str(1, 40)),
|
|
115
|
-
description: z.optional(sentence),
|
|
116
|
-
fields: z.record(fieldName, FieldSpec).check(z.refine((f) => Object.keys(f).length > 0, "a kind has at least one field")),
|
|
117
|
-
label: z.optional(template),
|
|
118
|
-
describe: z.optional(template),
|
|
119
|
-
lifecycle: z.optional(z.strictObject({ field: fieldName, retired: some(z.union([z.string(), z.number(), z.boolean()]), 1) })),
|
|
120
|
-
figure: z.optional(str(undefined, 20_000)),
|
|
121
|
-
/** What a glance at one says — a card, a row, a hit in Find — first, in this order: the definition's `display.glance` (FR-39). */
|
|
122
|
-
glance: z.optional(some(fieldName, 1, 20)),
|
|
44
|
+
function build() {
|
|
123
45
|
/**
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
46
|
+
* What every object already answers to — `constructor`, `toString` and the
|
|
47
|
+
* rest of `Object.prototype` — is no field's or relation's name: a record's
|
|
48
|
+
* fields and a kind's relations are looked up by name, and one of these is
|
|
49
|
+
* "there" on every record whether it was declared or not.
|
|
128
50
|
*/
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
51
|
+
const inherited = (n) => n in Object.prototype;
|
|
52
|
+
const notInherited = z.refine((n) => !inherited(n), "every object already has this name — choose another");
|
|
53
|
+
/** At least `min` and at most `max` long: a string's characters, a list's items. */
|
|
54
|
+
const sized = (min, max) => [...(min === undefined ? [] : [z.minLength(min)]), ...(max === undefined ? [] : [z.maxLength(max)])];
|
|
55
|
+
const str = (min, max) => z.string().check(...sized(min, max));
|
|
56
|
+
const some = (of, min, max) => z.array(of).check(...sized(min, max));
|
|
57
|
+
const name = (what) => z.string().check(z.regex(NAME, `${what} names are kebab-case, lower-case words joined by hyphens like "tool-note" (field and relation names are camelCase, like "supersededBy")`));
|
|
58
|
+
const edgeName = z.string().check(z.regex(EDGE_NAME, 'relation names are one camelCase word, like "supersededBy" (kind names are kebab-case, like "tool-note")'), notInherited);
|
|
59
|
+
const fieldName = z
|
|
60
|
+
.string()
|
|
61
|
+
.check(z.regex(FIELD_NAME, 'field names are one word or camelCase, like "dueDate", so rules say them bare (kind names are kebab-case, like "tool-note")'), z.refine((n) => !RESERVED_FIELDS.has(n), 'a field cannot be called "id" or "kind" — the framework keeps those; try "type" or "category"'), notInherited);
|
|
62
|
+
const sentence = str(1, 500);
|
|
63
|
+
const expression = str(1, 2000);
|
|
64
|
+
const template = str(1, 300);
|
|
65
|
+
const FieldSpec = z
|
|
66
|
+
.strictObject({
|
|
67
|
+
type: z.enum(FIELD_TYPES),
|
|
68
|
+
required: z.optional(z.boolean()),
|
|
69
|
+
description: z.optional(sentence),
|
|
70
|
+
/** What the field is called on screen, where its name does not say it. */
|
|
71
|
+
label: z.optional(str(1, 60)),
|
|
72
|
+
options: z.optional(some(str(1, 80), 1, 100)),
|
|
73
|
+
of: z.optional(z.enum(["string", "number", "date", "image", "video", "file"])),
|
|
74
|
+
default: z.optional(z.unknown()),
|
|
75
|
+
unit: z.optional(str(undefined, 12)),
|
|
76
|
+
/** How a number is shown (`money`, `percent`, `duration`), or that a `text` field is written in Markdown (FR-180; the checker's `field-format` says which goes where). */
|
|
77
|
+
format: z.optional(z.enum(["money", "percent", "duration", "markdown"])),
|
|
78
|
+
/** A number's range (FR-114): the least and the most it may be, and the step its values are whole multiples of (JSON Schema's `multipleOf`). */
|
|
79
|
+
min: z.optional(z.number()),
|
|
80
|
+
max: z.optional(z.number()),
|
|
81
|
+
step: z.optional(z.number()),
|
|
82
|
+
/** The name this field had in the previous version: its values move instead of being dropped. */
|
|
83
|
+
renamedFrom: z.optional(z.string().check(z.regex(FIELD_NAME))),
|
|
84
|
+
/**
|
|
85
|
+
* A date that repeats (`repeat.ts`): a record may keep, beside the date
|
|
86
|
+
* (its first occurrence), how it repeats under the date's name and
|
|
87
|
+
* `Repeat` — `"startsAtRepeat": { "every": "week", "on": ["tue"] }`.
|
|
88
|
+
*/
|
|
89
|
+
repeats: z.optional(z.boolean()),
|
|
90
|
+
})
|
|
91
|
+
.check(z.superRefine((f, ctx) => {
|
|
92
|
+
if (f.type === "enum" && !f.options)
|
|
93
|
+
ctx.addIssue({ code: "custom", message: 'an enum field lists its "options"', path: ["options"] });
|
|
94
|
+
if (f.type !== "enum" && f.options)
|
|
95
|
+
ctx.addIssue({ code: "custom", message: '"options" belongs only on an enum field', path: ["options"] });
|
|
96
|
+
if (f.type !== "list" && f.of)
|
|
97
|
+
ctx.addIssue({ code: "custom", message: '"of" belongs only on a list field', path: ["of"] });
|
|
98
|
+
if (f.repeats && f.type !== "date" && f.type !== "datetime")
|
|
99
|
+
ctx.addIssue({ code: "custom", message: '"repeats" belongs only on a date or datetime field', path: ["repeats"] });
|
|
100
|
+
}));
|
|
134
101
|
/**
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
102
|
+
* WHERE A RELATION GOES: a kind of this document (`"house"`), or a kind in
|
|
103
|
+
* ANOTHER APP the viewer can open (FR-171) — `@` + the app's address + `/`
|
|
104
|
+
* + its kind: `"@nick/house/house"`, `"@app_kf5eydr9z58jzf/house"`. A link
|
|
105
|
+
* to another app's record picks one there and leaves that app untouched.
|
|
138
106
|
*/
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
107
|
+
const edgeTarget = z.string().check(z.regex(new RegExp(`${NAME.source}|${LINKED_TARGET.source}`), 'a relation goes to a kind of this document, like "house", or of another app, like "@nick/house/house"'));
|
|
108
|
+
const EdgeSpec = z.strictObject({
|
|
109
|
+
to: z.union([some(edgeTarget, 1), z.literal("*")]),
|
|
110
|
+
cardinality: z.optional(z.enum(["one", "many"])),
|
|
111
|
+
description: z.optional(sentence),
|
|
112
|
+
inverse: z.optional(str(1, 80)),
|
|
113
|
+
appendOnly: z.optional(z.boolean()),
|
|
114
|
+
/** The name this relation had in the previous version: its links move. */
|
|
115
|
+
renamedFrom: z.optional(z.string()),
|
|
116
|
+
/** `"never"`: no automated author makes or breaks this relation on its own (`@graview/core/tidying`). */
|
|
117
|
+
tidying: z.optional(z.literal("never")),
|
|
118
|
+
});
|
|
119
|
+
/** A computed field: its expression, or the expression with the words it is shown by (FR-83). */
|
|
120
|
+
const ComputedSpec = z.union([expression, z.strictObject({ expr: expression, label: z.optional(str(1, 60)), description: z.optional(sentence) })]);
|
|
121
|
+
const KindSpec = z.strictObject({
|
|
122
|
+
noun: z.optional(str(1, 40)),
|
|
123
|
+
plural: z.optional(str(1, 40)),
|
|
124
|
+
description: z.optional(sentence),
|
|
125
|
+
fields: z.record(fieldName, FieldSpec).check(z.refine((f) => Object.keys(f).length > 0, "a kind has at least one field")),
|
|
126
|
+
label: z.optional(template),
|
|
127
|
+
describe: z.optional(template),
|
|
128
|
+
lifecycle: z.optional(z.strictObject({ field: fieldName, retired: some(z.union([z.string(), z.number(), z.boolean()]), 1) })),
|
|
129
|
+
figure: z.optional(str(undefined, 20_000)),
|
|
130
|
+
/** What a glance at one says — a card, a row, a hit in Find — first, in this order: the definition's `display.glance` (FR-39). */
|
|
131
|
+
glance: z.optional(some(fieldName, 1, 20)),
|
|
132
|
+
/**
|
|
133
|
+
* How a record's page orders and groups its facts (FR-148): `fields`
|
|
134
|
+
* first, in this order, then each group under its title; the rest follow
|
|
135
|
+
* in declared order, under "Details" when there are groups. The
|
|
136
|
+
* definition's `display.page`.
|
|
137
|
+
*/
|
|
138
|
+
page: z.optional(z.strictObject({
|
|
139
|
+
fields: z.optional(some(fieldName, 1, 40)),
|
|
140
|
+
groups: z.optional(some(z.strictObject({ title: str(1, 60), fields: some(fieldName, 1, 40) }), 1, 12)),
|
|
141
|
+
})),
|
|
142
|
+
edges: z.optional(z.record(edgeName, EdgeSpec)),
|
|
143
|
+
/**
|
|
144
|
+
* Values worked out, not stored (FR-83): a name and a rule-language
|
|
145
|
+
* expression read like a field — `"net": "sum(out('includes'), list * units)"` —
|
|
146
|
+
* or `{ expr, label?, description? }`. Never written by an act.
|
|
147
|
+
*/
|
|
148
|
+
computed: z.optional(z.record(z.string(), ComputedSpec).check(z.refine((c) => Object.keys(c).length <= 20, "a kind works out at most 20 computed fields"))),
|
|
149
|
+
/**
|
|
150
|
+
* Which field fills which role (the definition's `fieldRoles`): `when`, a
|
|
151
|
+
* record's natural time, which lists open newest first by and a timeline
|
|
152
|
+
* or calendar draws at when its lens binds no start; `who`, its author,
|
|
153
|
+
* which a glance says; and any a lens binds by name (`start`, `end`,
|
|
154
|
+
* `column`, `order`).
|
|
155
|
+
*/
|
|
156
|
+
fieldRoles: z.optional(z.record(z.string(), z.string())),
|
|
157
|
+
/** The name this kind had in the previous version: its records move. */
|
|
158
|
+
renamedFrom: z.optional(z.string().check(z.regex(NAME))),
|
|
159
|
+
/** This kind stands for the people: a record of it whose id is a member's principal id is that member (`$me`, `self`, `own`, `via`, `ownEnd`). */
|
|
160
|
+
people: z.optional(z.boolean()),
|
|
161
|
+
/** `false`: its records are kept or removed, never archived — the kind is given no `archive-`, `unarchive-` or `merge-<kind>` (FR-173). */
|
|
162
|
+
archives: z.optional(z.literal(false)),
|
|
163
|
+
/** What the app's own actor may do to its records on its own: `"never"` nothing, `"no-links"` all but link and unlink them (`@graview/core/tidying`). */
|
|
164
|
+
tidying: z.optional(z.enum(["never", "no-links"])),
|
|
165
|
+
});
|
|
166
|
+
/** A value an effect writes: a literal, `$arg`, `$subject`, `$new`, `$now`, `$today`, or `{ expr }`. */
|
|
167
|
+
const ValueSpec = z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(z.unknown()), z.strictObject({ expr: expression })]);
|
|
168
|
+
const ref = z.string().check(z.regex(/^\$[A-Za-z][A-Za-z0-9]*$/, 'a reference is "$" and a name, like "$subject" or "$new"'));
|
|
169
|
+
const EffectSpec = z.union([
|
|
170
|
+
z.strictObject({ create: name("kind"), as: z.optional(z.string().check(z.regex(FIELD_NAME))), set: z.optional(z.record(fieldName, ValueSpec)) }),
|
|
171
|
+
z.strictObject({ set: z.record(fieldName, ValueSpec), target: z.optional(ref) }),
|
|
172
|
+
z.strictObject({ connect: edgeName, from: ref, to: ref }),
|
|
173
|
+
z.strictObject({ sever: edgeName, from: ref, to: ref }),
|
|
174
|
+
z.strictObject({ remove: ref }),
|
|
175
|
+
]);
|
|
176
|
+
const ActSpec = z.strictObject({
|
|
177
|
+
title: z.optional(str(1, 80)),
|
|
178
|
+
description: z.optional(sentence),
|
|
179
|
+
/** How the act reads standing on the far end of the relation it makes or breaks. */
|
|
180
|
+
fromTheOtherEnd: z.optional(str(1, 80)),
|
|
181
|
+
on: z.optional(z.union([name("kind"), some(name("kind"), 1)])),
|
|
182
|
+
creates: z.optional(name("kind")),
|
|
183
|
+
sets: z.optional(z.record(fieldName, ValueSpec)),
|
|
184
|
+
writes: z.optional(some(fieldName, 1)),
|
|
185
|
+
connects: z.optional(edgeName),
|
|
186
|
+
severs: z.optional(edgeName),
|
|
187
|
+
/**
|
|
188
|
+
* Fields set on the record at the OTHER end of what the act connects or
|
|
189
|
+
* severs — its `$to` (FR-115): `"setsOther": { "ownership": "shared" }`.
|
|
190
|
+
* Values as `sets` takes them; an expression reads that record's fields.
|
|
191
|
+
*/
|
|
192
|
+
setsOther: z.optional(z.record(fieldName, ValueSpec)),
|
|
193
|
+
/**
|
|
194
|
+
* The subject's links severed before it connects (FR-115): `true`, those
|
|
195
|
+
* of the relation it connects; or the relations named, each at the
|
|
196
|
+
* subject's own end. The link being made is kept.
|
|
197
|
+
*/
|
|
198
|
+
replaces: z.optional(z.union([z.literal(true), some(edgeName, 1, 10)])),
|
|
199
|
+
removes: z.optional(z.literal(true)),
|
|
200
|
+
args: z.optional(z.record(fieldName, FieldSpec)),
|
|
201
|
+
effects: z.optional(some(EffectSpec, 1, 20)),
|
|
202
|
+
allowedWhen: z.optional(expression),
|
|
203
|
+
refusal: z.optional(template),
|
|
204
|
+
destructive: z.optional(z.boolean()),
|
|
205
|
+
});
|
|
206
|
+
const RepairSpec = z.strictObject({
|
|
207
|
+
act: name("act"),
|
|
208
|
+
label: z.optional(str(1, 80)),
|
|
209
|
+
args: z.optional(z.record(z.string(), ValueSpec)),
|
|
210
|
+
});
|
|
211
|
+
const RuleSpec = z.strictObject({
|
|
212
|
+
title: z.optional(str(1, 120)),
|
|
213
|
+
description: z.optional(sentence),
|
|
214
|
+
over: z.union([z.literal("graph"), name("kind")]),
|
|
215
|
+
when: z.optional(expression),
|
|
216
|
+
require: expression,
|
|
217
|
+
says: z.optional(template),
|
|
218
|
+
repairs: z.optional(some(RepairSpec, undefined, 10)),
|
|
219
|
+
});
|
|
220
|
+
const GrantSpec = z.strictObject({
|
|
221
|
+
roles: z.union([some(name("role"), 1), z.literal("*")]),
|
|
222
|
+
mutations: z.union([some(name("act"), 1), z.literal("*")]),
|
|
223
|
+
kinds: z.optional(z.union([z.array(name("kind")), z.literal("*")])),
|
|
224
|
+
describe: z.optional(sentence),
|
|
225
|
+
self: z.optional(z.boolean()),
|
|
226
|
+
/** Only on the principal's own end of the link the act connects or severs. */
|
|
227
|
+
ownEnd: z.optional(z.boolean()),
|
|
228
|
+
});
|
|
178
229
|
/**
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
230
|
+
* Who sees which kinds — the framework's `Sight`, one meaning (FR-02).
|
|
231
|
+
* Absent: everyone sees everything. Present: a kind no sight names is seen
|
|
232
|
+
* by nobody but the system (a host that gives its owners everything serves
|
|
233
|
+
* them as the system, or names them in a sight).
|
|
182
234
|
*/
|
|
183
|
-
|
|
235
|
+
const fieldNames = z.optional(some(name("field"), 1));
|
|
236
|
+
const SightSpec = z.strictObject({
|
|
237
|
+
roles: z.union([some(name("role"), 1), z.literal("*")]),
|
|
238
|
+
kinds: some(name("kind"), 1),
|
|
239
|
+
/** Only the principal's own records: the ones they made, their own record, and what an edge joins to it. */
|
|
240
|
+
own: z.optional(z.boolean()),
|
|
241
|
+
/** Only the records this relation joins to the principal's own record of the people kind. */
|
|
242
|
+
via: z.optional(edgeName),
|
|
243
|
+
/** Only these fields of the records it shows (Roadmap f-field-sight); sights add up. */
|
|
244
|
+
fields: fieldNames,
|
|
245
|
+
/** Every field but these of the records it shows: kept from the roles, on every surface, and not theirs to write. */
|
|
246
|
+
hide: fieldNames,
|
|
247
|
+
});
|
|
248
|
+
const PolicySpec = z.strictObject({
|
|
249
|
+
grants: z.array(GrantSpec),
|
|
250
|
+
sees: z.optional(z.array(SightSpec)),
|
|
251
|
+
});
|
|
184
252
|
/**
|
|
185
|
-
* The
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
args: z.optional(z.record(fieldName, FieldSpec)),
|
|
192
|
-
effects: z.optional(some(EffectSpec, 1, 20)),
|
|
193
|
-
allowedWhen: z.optional(expression),
|
|
194
|
-
refusal: z.optional(template),
|
|
195
|
-
destructive: z.optional(z.boolean()),
|
|
196
|
-
});
|
|
197
|
-
export const RepairSpec = z.strictObject({
|
|
198
|
-
act: name("act"),
|
|
199
|
-
label: z.optional(str(1, 80)),
|
|
200
|
-
args: z.optional(z.record(z.string(), ValueSpec)),
|
|
201
|
-
});
|
|
202
|
-
export const RuleSpec = z.strictObject({
|
|
203
|
-
title: z.optional(str(1, 120)),
|
|
204
|
-
description: z.optional(sentence),
|
|
205
|
-
over: z.union([z.literal("graph"), name("kind")]),
|
|
206
|
-
when: z.optional(expression),
|
|
207
|
-
require: expression,
|
|
208
|
-
says: z.optional(template),
|
|
209
|
-
repairs: z.optional(some(RepairSpec, undefined, 10)),
|
|
210
|
-
});
|
|
211
|
-
export const GrantSpec = z.strictObject({
|
|
212
|
-
roles: z.union([some(name("role"), 1), z.literal("*")]),
|
|
213
|
-
mutations: z.union([some(name("act"), 1), z.literal("*")]),
|
|
214
|
-
kinds: z.optional(z.union([z.array(name("kind")), z.literal("*")])),
|
|
215
|
-
describe: z.optional(sentence),
|
|
216
|
-
self: z.optional(z.boolean()),
|
|
217
|
-
/** Only on the principal's own end of the link the act connects or severs. */
|
|
218
|
-
ownEnd: z.optional(z.boolean()),
|
|
219
|
-
});
|
|
220
|
-
/**
|
|
221
|
-
* Who sees which kinds — the framework's `Sight`, one meaning (FR-02).
|
|
222
|
-
* Absent: everyone sees everything. Present: a kind no sight names is seen
|
|
223
|
-
* by nobody but the system (a host that gives its owners everything serves
|
|
224
|
-
* them as the system, or names them in a sight).
|
|
225
|
-
*/
|
|
226
|
-
const fieldNames = z.optional(some(name("field"), 1));
|
|
227
|
-
export const SightSpec = z.strictObject({
|
|
228
|
-
roles: z.union([some(name("role"), 1), z.literal("*")]),
|
|
229
|
-
kinds: some(name("kind"), 1),
|
|
230
|
-
/** Only the principal's own records: the ones they made, their own record, and what an edge joins to it. */
|
|
231
|
-
own: z.optional(z.boolean()),
|
|
232
|
-
/** Only the records this relation joins to the principal's own record of the people kind. */
|
|
233
|
-
via: z.optional(edgeName),
|
|
234
|
-
/** Only these fields of the records it shows (Roadmap f-field-sight); sights add up. */
|
|
235
|
-
fields: fieldNames,
|
|
236
|
-
/** Every field but these of the records it shows: kept from the roles, on every surface, and not theirs to write. */
|
|
237
|
-
hide: fieldNames,
|
|
238
|
-
});
|
|
239
|
-
export const PolicySpec = z.strictObject({
|
|
240
|
-
grants: z.array(GrantSpec),
|
|
241
|
-
sees: z.optional(z.array(SightSpec)),
|
|
242
|
-
});
|
|
243
|
-
/**
|
|
244
|
-
* The installation's look and its money: an accent color the palette is
|
|
245
|
-
* derived from, and the currency and locale a sum is said in wherever a
|
|
246
|
-
* block names none (FR-100) — the locale its days, times and numbers are
|
|
247
|
-
* said in too (FR-178), "en-US" when absent — and the time zone its
|
|
248
|
-
* instants are said in (Roadmap f-app-time-zone), each reader's own clock
|
|
249
|
-
* when absent. Any of them may stand alone.
|
|
250
|
-
*/
|
|
251
|
-
export const BrandSpec = z.strictObject({
|
|
252
|
-
accent: z.optional(z.string().check(z.regex(/^#[0-9a-fA-F]{6}$/, 'an accent is a color like "#c2577a"'))),
|
|
253
|
-
name: z.optional(str(1, 60)),
|
|
254
|
-
currency: z.optional(z.string().check(z.regex(/^[A-Z]{3}$/, 'a currency is its three-letter code, like "USD" or "EUR"'))),
|
|
255
|
-
// A language tag ("en-US", "de-DE"); `graview check` says when it is none (`brand-locale`).
|
|
256
|
-
locale: z.optional(str(2, 35)),
|
|
257
|
-
// An IANA time zone ("America/New_York"); `graview check` says when it is none (`brand-time-zone`).
|
|
258
|
-
timeZone: z.optional(str(1, 64)),
|
|
259
|
-
/*
|
|
260
|
-
* THE REST OF THE BRAND (FR-124), each optional and each checked by
|
|
261
|
-
* `graview check` (`brand-mark`, `brand-font`): a logo and a page icon —
|
|
262
|
-
* inline SVG, or a path on the app's own host — the faces words are set
|
|
263
|
-
* in, how round and how roomy, a hue per kind, and the scheme it prefers.
|
|
264
|
-
*/
|
|
265
|
-
logo: z.optional(z.union([str(1, MARK_MAX), z.strictObject({ src: str(1, MARK_MAX), alt: z.optional(str(1, 120)) })])),
|
|
266
|
-
// A page icon: a mark as the logo is one, or `{ "emoji": "🏡" }` — one standard emoji, the app's mark where it has no logo (`brand-emoji`).
|
|
267
|
-
favicon: z.optional(z.union([str(1, MARK_MAX), z.strictObject({ emoji: z.string() })])),
|
|
268
|
-
typography: z.optional(z.strictObject({ display: z.optional(str(1, 200)), body: z.optional(str(1, 200)), mono: z.optional(str(1, 200)) })),
|
|
269
|
-
shape: z.optional(z.strictObject({
|
|
270
|
-
radius: z.optional(z.number().check(z.minimum(0), z.maximum(32))),
|
|
271
|
-
density: z.optional(z.number().check(z.minimum(0.75), z.maximum(1.5))),
|
|
272
|
-
})),
|
|
273
|
-
accents: z.optional(z.record(z.string(), z.number().check(z.minimum(0), z.maximum(360)))),
|
|
274
|
-
scheme: z.optional(z.enum(["light", "dark", "auto"])),
|
|
275
|
-
});
|
|
276
|
-
const loose = z.record(z.string(), z.unknown());
|
|
277
|
-
/** A grid lens's order (`grid-sort.ts`): a field, or { by, direction }. */
|
|
278
|
-
const GridSortSpec = z.union([str(1, 120), z.strictObject({ by: str(1, 120), direction: z.optional(z.enum(["asc", "desc"])) })]);
|
|
279
|
-
const Year = z.number().check(z.int(), z.minimum(1), z.maximum(9999));
|
|
280
|
-
/**
|
|
281
|
-
* A lens: checked by `graview check` against the lenses the framework ships
|
|
282
|
-
* (`declaredLenses`), so loose here but for what its page is: `create`,
|
|
283
|
-
* false where the place's page leaves off the forms that begin its kind —
|
|
284
|
-
* and the options whose shape is the same on every lens that takes them:
|
|
285
|
-
* a coverage's `sort` and `columnSort` and a status board's `sort` (the
|
|
286
|
-
* order of its rows, columns or cards), and a calendar's `horizon` (its
|
|
287
|
-
* span of years, reaching its records' first and last unless `from` and
|
|
288
|
-
* `to` pin it). `search` and `filters` are fields, here only as lists of
|
|
289
|
-
* names: which ones the kind has is the checker's to say, as is a status
|
|
290
|
-
* board's `collapsed` and `hidden` columns.
|
|
291
|
-
*/
|
|
292
|
-
const LensSpec = z.looseObject({
|
|
293
|
-
create: z.optional(z.boolean()),
|
|
294
|
-
options: z.optional(z.looseObject({
|
|
295
|
-
sort: z.optional(GridSortSpec),
|
|
296
|
-
columnSort: z.optional(GridSortSpec),
|
|
297
|
-
search: z.optional(some(str(1, 120), undefined, 40)),
|
|
298
|
-
filters: z.optional(some(str(1, 120), undefined, 40)),
|
|
299
|
-
horizon: z.optional(z.union([z.number().check(z.int(), z.minimum(1), z.maximum(200)), z.looseObject({ years: z.number().check(z.int(), z.minimum(1), z.maximum(200)), title: str(1, 80), from: z.optional(Year), to: z.optional(Year) })])),
|
|
300
|
-
})),
|
|
301
|
-
});
|
|
302
|
-
/** A kind's view specs: which slots it fills. The blocks are checked in views.ts, sentence by sentence. */
|
|
303
|
-
const ViewSpecsSpec = z.strictObject({
|
|
304
|
-
card: z.optional(z.array(z.unknown())),
|
|
305
|
-
row: z.optional(z.array(z.unknown())),
|
|
306
|
-
page: z.optional(z.array(z.unknown())),
|
|
307
|
-
});
|
|
308
|
-
/** A kind's list's declared order (`pages.lists.<kind>.sort`): a key, or a key with a direction and a tiebreak. */
|
|
309
|
-
const ListSortSpec = () => {
|
|
310
|
-
const direction = z.optional(z.enum(["asc", "desc"]));
|
|
311
|
-
return z.union([
|
|
312
|
-
str(1, 120),
|
|
313
|
-
z.strictObject({ by: str(1, 120), direction, then: z.optional(z.union([str(1, 120), z.strictObject({ by: str(1, 120), direction })])) }),
|
|
314
|
-
]);
|
|
315
|
-
};
|
|
316
|
-
/** A kind's list's declared grouping (`pages.lists.<kind>.group`): a field, or a field with its groups' order and the closed ones. */
|
|
317
|
-
const ListGroupSpec = () => z.union([str(1, 120), z.strictObject({ by: str(1, 120), order: z.optional(some(str(1, 80), undefined, 40)), closed: z.optional(some(str(1, 80), undefined, 40)) })]);
|
|
318
|
-
/**
|
|
319
|
-
* THE ARRANGEMENT (FR-80): the kinds in order, the kinds the home leaves
|
|
320
|
-
* off, the place the app opens on. Loose enough that a document which
|
|
321
|
-
* carried anything else under `pages` before FR-80 — when it was accepted
|
|
322
|
-
* and ignored — still parses; `graview check` says what it cannot honor.
|
|
323
|
-
*/
|
|
324
|
-
export const PagesSpec = z.looseObject({
|
|
325
|
-
order: z.optional(z.array(z.string())),
|
|
326
|
-
hide: z.optional(z.array(z.string())),
|
|
327
|
-
first: z.optional(z.string()),
|
|
328
|
-
/* The places that stand on the bar beside Home (FR-145, ranked): kinds by name or plural, pictures by title. */
|
|
329
|
-
primary: z.optional(some(str(1, 80), undefined, 40)),
|
|
330
|
-
/*
|
|
331
|
-
* What the bar's switch calls the scene and the pages (FR-137); "Scene" and "Pages" when unsaid.
|
|
332
|
-
* At most 40 characters, as `pages.overview` was (FR-132), which a stored document says for `scene`
|
|
333
|
-
* (RESPELLED): a respelling never narrows what the key may hold. The bar cuts a long word, whole on hover.
|
|
334
|
-
*/
|
|
335
|
-
scene: z.optional(str(1, 40)),
|
|
336
|
-
pages: z.optional(str(1, 40)),
|
|
337
|
-
/*
|
|
338
|
-
* What each kind's list offers to filter by, by kind (`pages.lists.request.filters`): fields, relations,
|
|
339
|
-
* `is`, and `<relation>.<field>` for a choice at a relation's far end. Its choice fields stand as chips.
|
|
253
|
+
* The installation's look and its money: an accent color the palette is
|
|
254
|
+
* derived from, and the currency and locale a sum is said in wherever a
|
|
255
|
+
* block names none (FR-100) — the locale its days, times and numbers are
|
|
256
|
+
* said in too (FR-178), "en-US" when absent — and the time zone its
|
|
257
|
+
* instants are said in (Roadmap f-app-time-zone), each reader's own clock
|
|
258
|
+
* when absent. Any of them may stand alone.
|
|
340
259
|
*/
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
260
|
+
const BrandSpec = z.strictObject({
|
|
261
|
+
accent: z.optional(z.string().check(z.regex(/^#[0-9a-fA-F]{6}$/, 'an accent is a color like "#c2577a"'))),
|
|
262
|
+
name: z.optional(str(1, 60)),
|
|
263
|
+
currency: z.optional(z.string().check(z.regex(/^[A-Z]{3}$/, 'a currency is its three-letter code, like "USD" or "EUR"'))),
|
|
264
|
+
// A language tag ("en-US", "de-DE"); `graview check` says when it is none (`brand-locale`).
|
|
265
|
+
locale: z.optional(str(2, 35)),
|
|
266
|
+
// An IANA time zone ("America/New_York"); `graview check` says when it is none (`brand-time-zone`).
|
|
267
|
+
timeZone: z.optional(str(1, 64)),
|
|
268
|
+
/*
|
|
269
|
+
* THE REST OF THE BRAND (FR-124), each optional and each checked by
|
|
270
|
+
* `graview check` (`brand-mark`, `brand-font`): a logo and a page icon —
|
|
271
|
+
* inline SVG, or a path on the app's own host — the faces words are set
|
|
272
|
+
* in, how round and how roomy, a hue per kind, and the scheme it prefers.
|
|
273
|
+
*/
|
|
274
|
+
logo: z.optional(z.union([
|
|
275
|
+
str(1, MARK_MAX),
|
|
276
|
+
z.strictObject({ src: str(1, MARK_MAX), alt: z.optional(str(1, 120)) }),
|
|
277
|
+
// One for each scheme: the dark one drawn in the dark; with none, `darkFallback: "invert"` asks for the light one inverted there (never guessed).
|
|
278
|
+
z.strictObject({ light: str(1, MARK_MAX), dark: z.optional(str(1, MARK_MAX)), alt: z.optional(str(1, 120)), darkFallback: z.optional(z.literal("invert")) }),
|
|
279
|
+
])),
|
|
280
|
+
// A page icon: a mark as the logo is one, or `{ "emoji": "🏡" }` — one standard emoji, the app's mark where it has no logo (`brand-emoji`).
|
|
281
|
+
favicon: z.optional(z.union([str(1, MARK_MAX), z.strictObject({ emoji: z.string() })])),
|
|
282
|
+
typography: z.optional(z.strictObject({ display: z.optional(str(1, 200)), body: z.optional(str(1, 200)), mono: z.optional(str(1, 200)) })),
|
|
283
|
+
shape: z.optional(z.strictObject({
|
|
284
|
+
radius: z.optional(z.number().check(z.minimum(0), z.maximum(32))),
|
|
285
|
+
density: z.optional(z.number().check(z.minimum(0.75), z.maximum(1.5))),
|
|
286
|
+
})),
|
|
287
|
+
accents: z.optional(z.record(z.string(), z.number().check(z.minimum(0), z.maximum(360)))),
|
|
288
|
+
scheme: z.optional(z.enum(["light", "dark", "auto"])),
|
|
289
|
+
});
|
|
290
|
+
const loose = z.record(z.string(), z.unknown());
|
|
291
|
+
/** A grid lens's order (`grid-sort.ts`): a field, or { by, direction }. */
|
|
292
|
+
const GridSortSpec = z.union([str(1, 120), z.strictObject({ by: str(1, 120), direction: z.optional(z.enum(["asc", "desc"])) })]);
|
|
293
|
+
const Year = z.number().check(z.int(), z.minimum(1), z.maximum(9999));
|
|
360
294
|
/**
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
295
|
+
* A lens: checked by `graview check` against the lenses the framework ships
|
|
296
|
+
* (`declaredLenses`), so loose here but for what its page is: `create`,
|
|
297
|
+
* false where the place's page leaves off the forms that begin its kind —
|
|
298
|
+
* and the options whose shape is the same on every lens that takes them:
|
|
299
|
+
* a coverage's `sort` and `columnSort` and a status board's `sort` (the
|
|
300
|
+
* order of its rows, columns or cards), and a calendar's `horizon` (its
|
|
301
|
+
* span of years, reaching its records' first and last unless `from` and
|
|
302
|
+
* `to` pin it). `search` and `filters` are fields, here only as lists of
|
|
303
|
+
* names: which ones the kind has is the checker's to say, as is a status
|
|
304
|
+
* board's `collapsed` and `hidden` columns.
|
|
365
305
|
*/
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
306
|
+
const LensSpec = z.looseObject({
|
|
307
|
+
create: z.optional(z.boolean()),
|
|
308
|
+
options: z.optional(z.looseObject({
|
|
309
|
+
sort: z.optional(GridSortSpec),
|
|
310
|
+
columnSort: z.optional(GridSortSpec),
|
|
311
|
+
search: z.optional(some(str(1, 120), undefined, 40)),
|
|
312
|
+
filters: z.optional(some(str(1, 120), undefined, 40)),
|
|
313
|
+
horizon: z.optional(z.union([z.number().check(z.int(), z.minimum(1), z.maximum(200)), z.looseObject({ years: z.number().check(z.int(), z.minimum(1), z.maximum(200)), title: str(1, 80), from: z.optional(Year), to: z.optional(Year) })])),
|
|
314
|
+
})),
|
|
315
|
+
});
|
|
316
|
+
/** A kind's view specs: which slots it fills. The blocks are checked in views.ts, sentence by sentence. */
|
|
317
|
+
const ViewSpecsSpec = z.strictObject({
|
|
318
|
+
card: z.optional(z.array(z.unknown())),
|
|
319
|
+
row: z.optional(z.array(z.unknown())),
|
|
320
|
+
page: z.optional(z.array(z.unknown())),
|
|
321
|
+
});
|
|
322
|
+
/** A kind's list's declared order (`pages.lists.<kind>.sort`): a key, or a key with a direction and a tiebreak. */
|
|
323
|
+
const ListSortSpec = () => {
|
|
324
|
+
const direction = z.optional(z.enum(["asc", "desc"]));
|
|
325
|
+
return z.union([
|
|
326
|
+
str(1, 120),
|
|
327
|
+
z.strictObject({ by: str(1, 120), direction, then: z.optional(z.union([str(1, 120), z.strictObject({ by: str(1, 120), direction })])) }),
|
|
328
|
+
]);
|
|
329
|
+
};
|
|
330
|
+
/** A kind's list's declared grouping (`pages.lists.<kind>.group`): a field, or a field with its groups' order and the closed ones. */
|
|
331
|
+
const ListGroupSpec = () => z.union([str(1, 120), z.strictObject({ by: str(1, 120), order: z.optional(some(str(1, 80), undefined, 40)), closed: z.optional(some(str(1, 80), undefined, 40)) })]);
|
|
379
332
|
/**
|
|
380
|
-
*
|
|
381
|
-
*
|
|
382
|
-
*
|
|
383
|
-
*
|
|
333
|
+
* THE ARRANGEMENT (FR-80): the kinds in order, the kinds the home leaves
|
|
334
|
+
* off, the place the app opens on. Loose enough that a document which
|
|
335
|
+
* carried anything else under `pages` before FR-80 — when it was accepted
|
|
336
|
+
* and ignored — still parses; `graview check` says what it cannot honor.
|
|
384
337
|
*/
|
|
385
|
-
|
|
386
|
-
|
|
338
|
+
const PagesSpec = z.looseObject({
|
|
339
|
+
order: z.optional(z.array(z.string())),
|
|
340
|
+
hide: z.optional(z.array(z.string())),
|
|
341
|
+
first: z.optional(z.string()),
|
|
342
|
+
/* The places that stand on the bar beside Home (FR-145, ranked): kinds by name or plural, pictures by title. */
|
|
343
|
+
primary: z.optional(some(str(1, 80), undefined, 40)),
|
|
344
|
+
/*
|
|
345
|
+
* What the bar's switch calls the scene and the pages (FR-137); "Scene" and "Pages" when unsaid.
|
|
346
|
+
* At most 40 characters, as `pages.overview` was (FR-132), which a stored document says for `scene`
|
|
347
|
+
* (RESPELLED): a respelling never narrows what the key may hold. The bar cuts a long word, whole on hover.
|
|
348
|
+
*/
|
|
349
|
+
scene: z.optional(str(1, 40)),
|
|
350
|
+
pages: z.optional(str(1, 40)),
|
|
351
|
+
/*
|
|
352
|
+
* What each kind's list offers to filter by, by kind (`pages.lists.request.filters`): fields, relations,
|
|
353
|
+
* `is`, and `<relation>.<field>` for a choice at a relation's far end. Its choice fields stand as chips.
|
|
354
|
+
*/
|
|
355
|
+
lists: z.optional(z.record(z.string(), z.strictObject({
|
|
356
|
+
filters: z.optional(some(str(1, 120), undefined, 40)),
|
|
357
|
+
/* The order it opens in: a field, a computed field or `label`, lowest first — or { by, direction, then }. */
|
|
358
|
+
sort: z.optional(ListSortSpec()),
|
|
359
|
+
/* How it opens grouped: a choice or yes-or-no field — or { by, order, closed }, the closed groups last and shut. */
|
|
360
|
+
group: z.optional(ListGroupSpec()),
|
|
361
|
+
}))),
|
|
362
|
+
});
|
|
363
|
+
const DocumentSpec = z.strictObject({
|
|
364
|
+
/* Where an editor finds this shape as JSON Schema (`@graview/core/document.schema.json`); the compiler reads past it. */
|
|
365
|
+
$schema: z.optional(z.string()),
|
|
366
|
+
format: z.literal(FORMAT),
|
|
367
|
+
formatVersion: z.literal(FORMAT_VERSION),
|
|
368
|
+
name: str(1, 80),
|
|
369
|
+
description: z.optional(sentence),
|
|
370
|
+
version: z.optional(z.number().check(z.int(), z.minimum(1))),
|
|
371
|
+
kinds: z
|
|
372
|
+
.record(name("kind"), KindSpec)
|
|
373
|
+
.check(z.refine((k) => Object.keys(k).length > 0, "a document declares at least one kind"), z.refine((k) => Object.keys(k).length <= 40, "a document declares at most 40 kinds")),
|
|
374
|
+
/**
|
|
375
|
+
* FACTS OF THE WHOLE GRAPH: a name and a rule-language expression with no
|
|
376
|
+
* record as its subject — `"mapProposed": "every(all('area'), proposed)"`
|
|
377
|
+
* — or `{ expr, label?, description? }`. Read anywhere the language is, as
|
|
378
|
+
* `fact('mapProposed')`; worked out on demand under the language's budget.
|
|
379
|
+
*/
|
|
380
|
+
computed: z.optional(z.record(z.string(), ComputedSpec)),
|
|
381
|
+
acts: z.optional(z.record(name("act"), ActSpec)),
|
|
382
|
+
rules: z.optional(z.record(name("rule"), RuleSpec)),
|
|
383
|
+
roles: z.optional(z.array(name("role"))),
|
|
384
|
+
policy: z.optional(PolicySpec),
|
|
385
|
+
modules: z.optional(loose),
|
|
386
|
+
lenses: z.optional(z.array(LensSpec)),
|
|
387
|
+
pages: z.optional(PagesSpec),
|
|
388
|
+
/** Each kind's views by kind, and `home`: the home view, a list of blocks about no one record (FR-81). */
|
|
389
|
+
views: z.optional(z.record(z.string(), z.union([ViewSpecsSpec, z.array(z.unknown())]))),
|
|
390
|
+
brand: z.optional(BrandSpec),
|
|
391
|
+
settings: z.optional(z.array(loose)),
|
|
392
|
+
migrations: z.optional(z.array(loose)),
|
|
393
|
+
/**
|
|
394
|
+
* Other systems this app is kept in step with, by name: today, Google
|
|
395
|
+
* calendars. Read loosely here, as a page that draws the app never runs
|
|
396
|
+
* one; their shape (`ConnectionSpec`, `./connection-spec.ts`) is the
|
|
397
|
+
* checker's to judge, and the JSON schema says it to an editor.
|
|
398
|
+
*/
|
|
399
|
+
connections: z.optional(z.record(name("connection"), loose)),
|
|
400
|
+
});
|
|
401
|
+
return { FieldSpec, EdgeSpec, ComputedSpec, KindSpec, ValueSpec, EffectSpec, ActSpec, RepairSpec, RuleSpec, GrantSpec, SightSpec, PolicySpec, BrandSpec, PagesSpec, DocumentSpec };
|
|
402
|
+
}
|
|
403
|
+
let built;
|
|
404
|
+
/** The document's shapes, built once, the first time one is read. */
|
|
405
|
+
const shapes = () => (built ??= build());
|
|
406
|
+
export const FieldSpec = z.lazy(() => shapes().FieldSpec);
|
|
407
|
+
export const EdgeSpec = z.lazy(() => shapes().EdgeSpec);
|
|
408
|
+
export const ComputedSpec = z.lazy(() => shapes().ComputedSpec);
|
|
409
|
+
export const KindSpec = z.lazy(() => shapes().KindSpec);
|
|
410
|
+
export const ValueSpec = z.lazy(() => shapes().ValueSpec);
|
|
411
|
+
export const EffectSpec = z.lazy(() => shapes().EffectSpec);
|
|
412
|
+
export const ActSpec = z.lazy(() => shapes().ActSpec);
|
|
413
|
+
export const RepairSpec = z.lazy(() => shapes().RepairSpec);
|
|
414
|
+
export const RuleSpec = z.lazy(() => shapes().RuleSpec);
|
|
415
|
+
export const GrantSpec = z.lazy(() => shapes().GrantSpec);
|
|
416
|
+
export const SightSpec = z.lazy(() => shapes().SightSpec);
|
|
417
|
+
export const PolicySpec = z.lazy(() => shapes().PolicySpec);
|
|
418
|
+
export const BrandSpec = z.lazy(() => shapes().BrandSpec);
|
|
419
|
+
export const PagesSpec = z.lazy(() => shapes().PagesSpec);
|
|
420
|
+
export const DocumentSpec = z.lazy(() => shapes().DocumentSpec);
|
|
387
421
|
//# sourceMappingURL=schema.js.map
|