@abraca/schema 2.3.0
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/abracadabra-schema.cjs +853 -0
- package/dist/abracadabra-schema.cjs.map +1 -0
- package/dist/abracadabra-schema.esm.js +781 -0
- package/dist/abracadabra-schema.esm.js.map +1 -0
- package/dist/index.d.ts +2538 -0
- package/package.json +41 -0
- package/src/crdt.ts +63 -0
- package/src/generated/calendar.ts +35 -0
- package/src/generated/chart.ts +35 -0
- package/src/generated/checklist.ts +35 -0
- package/src/generated/dashboard.ts +35 -0
- package/src/generated/doc.ts +35 -0
- package/src/generated/gallery.ts +35 -0
- package/src/generated/graph.ts +35 -0
- package/src/generated/json-schema/calendar.json +140 -0
- package/src/generated/json-schema/chart.json +164 -0
- package/src/generated/json-schema/checklist.json +138 -0
- package/src/generated/json-schema/dashboard.json +122 -0
- package/src/generated/json-schema/doc.json +122 -0
- package/src/generated/json-schema/gallery.json +157 -0
- package/src/generated/json-schema/graph.json +125 -0
- package/src/generated/json-schema/kanban.json +145 -0
- package/src/generated/json-schema/map.json +125 -0
- package/src/generated/json-schema/outline.json +122 -0
- package/src/generated/json-schema/overview.json +122 -0
- package/src/generated/json-schema/plugin-manifest.json +221 -0
- package/src/generated/json-schema/prose.json +122 -0
- package/src/generated/json-schema/sheets.json +135 -0
- package/src/generated/json-schema/slides.json +129 -0
- package/src/generated/json-schema/table.json +136 -0
- package/src/generated/json-schema/timeline.json +122 -0
- package/src/generated/kanban.ts +35 -0
- package/src/generated/map.ts +35 -0
- package/src/generated/markdown/calendar.md +59 -0
- package/src/generated/markdown/chart.md +62 -0
- package/src/generated/markdown/checklist.md +58 -0
- package/src/generated/markdown/dashboard.md +56 -0
- package/src/generated/markdown/doc.md +56 -0
- package/src/generated/markdown/gallery.md +61 -0
- package/src/generated/markdown/graph.md +57 -0
- package/src/generated/markdown/kanban.md +62 -0
- package/src/generated/markdown/map.md +57 -0
- package/src/generated/markdown/outline.md +56 -0
- package/src/generated/markdown/overview.md +56 -0
- package/src/generated/markdown/prose.md +56 -0
- package/src/generated/markdown/sheets.md +59 -0
- package/src/generated/markdown/slides.md +57 -0
- package/src/generated/markdown/table.md +58 -0
- package/src/generated/markdown/timeline.md +56 -0
- package/src/generated/outline.ts +35 -0
- package/src/generated/overview.ts +35 -0
- package/src/generated/prose.ts +35 -0
- package/src/generated/rust/calendar.rs +125 -0
- package/src/generated/rust/chart.rs +151 -0
- package/src/generated/rust/checklist.rs +123 -0
- package/src/generated/rust/dashboard.rs +101 -0
- package/src/generated/rust/doc.rs +101 -0
- package/src/generated/rust/gallery.rs +146 -0
- package/src/generated/rust/graph.rs +104 -0
- package/src/generated/rust/kanban.rs +127 -0
- package/src/generated/rust/map.rs +104 -0
- package/src/generated/rust/outline.rs +101 -0
- package/src/generated/rust/overview.rs +101 -0
- package/src/generated/rust/prose.rs +101 -0
- package/src/generated/rust/sheets.rs +110 -0
- package/src/generated/rust/slides.rs +111 -0
- package/src/generated/rust/table.rs +121 -0
- package/src/generated/rust/timeline.rs +101 -0
- package/src/generated/sheets.ts +35 -0
- package/src/generated/slides.ts +35 -0
- package/src/generated/table.ts +35 -0
- package/src/generated/timeline.ts +35 -0
- package/src/index.ts +389 -0
- package/src/manifest/plugin-manifest.ts +212 -0
- package/src/query.ts +150 -0
- package/src/types/calendar.ts +23 -0
- package/src/types/chart.ts +36 -0
- package/src/types/checklist.ts +22 -0
- package/src/types/dashboard.ts +18 -0
- package/src/types/doc.ts +19 -0
- package/src/types/gallery.ts +24 -0
- package/src/types/graph.ts +21 -0
- package/src/types/kanban.ts +27 -0
- package/src/types/map.ts +21 -0
- package/src/types/outline.ts +17 -0
- package/src/types/overview.ts +17 -0
- package/src/types/prose.ts +19 -0
- package/src/types/sheets.ts +24 -0
- package/src/types/slides.ts +22 -0
- package/src/types/table.ts +22 -0
- package/src/types/timeline.ts +18 -0
- package/src/types/universal.ts +59 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AUTO-GENERATED by @abraca/schema (slides).
|
|
3
|
+
*
|
|
4
|
+
* Source: ../types/slides.ts
|
|
5
|
+
* Run `pnpm schema:gen` to regenerate. Do not edit by hand —
|
|
6
|
+
* CI verifies that this file matches the generator output.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type * as Y from "yjs";
|
|
10
|
+
import type { z } from "zod";
|
|
11
|
+
import type { SlidesMeta } from "../types/slides.ts";
|
|
12
|
+
|
|
13
|
+
export type DocTypeName = "slides";
|
|
14
|
+
|
|
15
|
+
export type DocMetaMap = {
|
|
16
|
+
"slides": z.infer<typeof SlidesMeta>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type DocBodyMap = {
|
|
20
|
+
"slides": never;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type DocChildrenMap = {
|
|
24
|
+
"slides": never;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export interface DocOf<N extends DocTypeName> {
|
|
28
|
+
readonly id: string;
|
|
29
|
+
readonly type: N;
|
|
30
|
+
readonly meta: DocMetaMap[N];
|
|
31
|
+
readonly body: DocBodyMap[N];
|
|
32
|
+
readonly children: ReadonlyArray<DocOf<DocChildrenMap[N]>>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type Doc = { [N in DocTypeName]: DocOf<N> }[DocTypeName];
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AUTO-GENERATED by @abraca/schema (table).
|
|
3
|
+
*
|
|
4
|
+
* Source: ../types/table.ts
|
|
5
|
+
* Run `pnpm schema:gen` to regenerate. Do not edit by hand —
|
|
6
|
+
* CI verifies that this file matches the generator output.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type * as Y from "yjs";
|
|
10
|
+
import type { z } from "zod";
|
|
11
|
+
import type { TableMeta } from "../types/table.ts";
|
|
12
|
+
|
|
13
|
+
export type DocTypeName = "table";
|
|
14
|
+
|
|
15
|
+
export type DocMetaMap = {
|
|
16
|
+
"table": z.infer<typeof TableMeta>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type DocBodyMap = {
|
|
20
|
+
"table": never;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type DocChildrenMap = {
|
|
24
|
+
"table": never;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export interface DocOf<N extends DocTypeName> {
|
|
28
|
+
readonly id: string;
|
|
29
|
+
readonly type: N;
|
|
30
|
+
readonly meta: DocMetaMap[N];
|
|
31
|
+
readonly body: DocBodyMap[N];
|
|
32
|
+
readonly children: ReadonlyArray<DocOf<DocChildrenMap[N]>>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type Doc = { [N in DocTypeName]: DocOf<N> }[DocTypeName];
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AUTO-GENERATED by @abraca/schema (timeline).
|
|
3
|
+
*
|
|
4
|
+
* Source: ../types/timeline.ts
|
|
5
|
+
* Run `pnpm schema:gen` to regenerate. Do not edit by hand —
|
|
6
|
+
* CI verifies that this file matches the generator output.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type * as Y from "yjs";
|
|
10
|
+
import type { z } from "zod";
|
|
11
|
+
import type { TimelineMeta } from "../types/timeline.ts";
|
|
12
|
+
|
|
13
|
+
export type DocTypeName = "timeline";
|
|
14
|
+
|
|
15
|
+
export type DocMetaMap = {
|
|
16
|
+
"timeline": z.infer<typeof TimelineMeta>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type DocBodyMap = {
|
|
20
|
+
"timeline": never;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type DocChildrenMap = {
|
|
24
|
+
"timeline": never;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export interface DocOf<N extends DocTypeName> {
|
|
28
|
+
readonly id: string;
|
|
29
|
+
readonly type: N;
|
|
30
|
+
readonly meta: DocMetaMap[N];
|
|
31
|
+
readonly body: DocBodyMap[N];
|
|
32
|
+
readonly children: ReadonlyArray<DocOf<DocChildrenMap[N]>>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type Doc = { [N in DocTypeName]: DocOf<N> }[DocTypeName];
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @abraca/schema — public surface.
|
|
3
|
+
*
|
|
4
|
+
* The package exposes:
|
|
5
|
+
* 1. A small DSL (`defineDocType`, `defineSchema`, `ref`) for declaring
|
|
6
|
+
* doc-types backed by Zod meta schemas.
|
|
7
|
+
* 2. CRDT primitive markers (`ytext`, `yarray`, `ymap`) — see `./crdt.ts`.
|
|
8
|
+
* 3. A library of independently-importable per-page-type schemas (see
|
|
9
|
+
* `./types/`). Each file exports:
|
|
10
|
+
* - the doc-type value (e.g. `Kanban`)
|
|
11
|
+
* - the meta Zod schema (e.g. `KanbanMeta`)
|
|
12
|
+
* - a single-type `SchemaRegistry` (e.g. `kanbanSchema`)
|
|
13
|
+
* Consumers compose registries by hand:
|
|
14
|
+
* `defineSchema({ types: [Kanban, Calendar, Doc] })`.
|
|
15
|
+
* 4. A shared `UniversalMeta` zod object for cross-cutting meta keys.
|
|
16
|
+
*
|
|
17
|
+
* No aggregate "all page types" schema is shipped — each app picks the
|
|
18
|
+
* subset it needs.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { z, ZodType } from "zod";
|
|
22
|
+
|
|
23
|
+
export { ytext, yarray, ymap, crdtMetaOf } from "./crdt.ts";
|
|
24
|
+
export type { CrdtKind, CrdtMeta } from "./crdt.ts";
|
|
25
|
+
|
|
26
|
+
export { UniversalMeta } from "./types/universal.ts";
|
|
27
|
+
|
|
28
|
+
export {
|
|
29
|
+
META_COLUMNS,
|
|
30
|
+
WhereClauseSchema,
|
|
31
|
+
parseWhereClause,
|
|
32
|
+
} from "./query.ts";
|
|
33
|
+
export type { MetaColumn, WhereClause } from "./query.ts";
|
|
34
|
+
|
|
35
|
+
export { Doc, DocMeta, docSchema } from "./types/doc.ts";
|
|
36
|
+
export { Prose, ProseMeta, proseSchema } from "./types/prose.ts";
|
|
37
|
+
export { Kanban, KanbanMeta, kanbanSchema } from "./types/kanban.ts";
|
|
38
|
+
export { Gallery, GalleryMeta, gallerySchema } from "./types/gallery.ts";
|
|
39
|
+
export { Table, TableMeta, tableSchema } from "./types/table.ts";
|
|
40
|
+
export { Outline, OutlineMeta, outlineSchema } from "./types/outline.ts";
|
|
41
|
+
export { Checklist, ChecklistMeta, checklistSchema } from "./types/checklist.ts";
|
|
42
|
+
export { Graph, GraphMeta, graphSchema } from "./types/graph.ts";
|
|
43
|
+
export { Timeline, TimelineMeta, timelineSchema } from "./types/timeline.ts";
|
|
44
|
+
export { Calendar, CalendarMeta, calendarSchema } from "./types/calendar.ts";
|
|
45
|
+
export { MapDocType, MapMeta, mapSchema } from "./types/map.ts";
|
|
46
|
+
export { Dashboard, DashboardMeta, dashboardSchema } from "./types/dashboard.ts";
|
|
47
|
+
export { Chart, ChartMeta, chartSchema } from "./types/chart.ts";
|
|
48
|
+
export { Sheets, SheetsMeta, sheetsSchema } from "./types/sheets.ts";
|
|
49
|
+
export { Slides, SlidesMeta, slidesSchema } from "./types/slides.ts";
|
|
50
|
+
export { Overview, OverviewMeta, overviewSchema } from "./types/overview.ts";
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Plugin manifest validation — opt-in runtime validator for @abraca/plugin's
|
|
54
|
+
// `PluginManifest` type. Used by the registry server, the plugin CLI, and
|
|
55
|
+
// hosts that want defence-in-depth before instantiating external plugins.
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
export {
|
|
59
|
+
PluginManifestSchema,
|
|
60
|
+
PluginManifestAuthorSchema,
|
|
61
|
+
PluginManifestContributesSchema,
|
|
62
|
+
PluginCapabilitySchema,
|
|
63
|
+
PluginIdSchema,
|
|
64
|
+
SemverSchema,
|
|
65
|
+
SemverRangeSchema,
|
|
66
|
+
IntegritySchema,
|
|
67
|
+
RelativePathSchema,
|
|
68
|
+
PluginPricingSchema,
|
|
69
|
+
PluginVersionStatusSchema,
|
|
70
|
+
validatePluginManifest,
|
|
71
|
+
} from "./manifest/plugin-manifest.ts";
|
|
72
|
+
export type {
|
|
73
|
+
ManifestValidationIssue,
|
|
74
|
+
ManifestValidationResult,
|
|
75
|
+
} from "./manifest/plugin-manifest.ts";
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Public types
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
/** A reference to another doc-type by name. Resolved at registry build time. */
|
|
82
|
+
export interface DocTypeRef {
|
|
83
|
+
readonly __ref: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Declaration handed to {@link defineDocType}. */
|
|
87
|
+
export interface DocTypeSpec<
|
|
88
|
+
Name extends string = string,
|
|
89
|
+
MetaSchema extends ZodType = ZodType,
|
|
90
|
+
BodySchema extends ZodType | undefined = ZodType | undefined,
|
|
91
|
+
> {
|
|
92
|
+
readonly name: Name;
|
|
93
|
+
readonly version: number;
|
|
94
|
+
readonly meta: MetaSchema;
|
|
95
|
+
/**
|
|
96
|
+
* Body slot — the document's primary content. Typically a CRDT marker
|
|
97
|
+
* (`ytext()`, `yarray(...)`, `ymap(...)`) but can be any Zod schema for
|
|
98
|
+
* documents whose body is read as a POJO snapshot.
|
|
99
|
+
*/
|
|
100
|
+
readonly body?: BodySchema;
|
|
101
|
+
/** Allowed child doc-types, by ref. */
|
|
102
|
+
readonly children?: ReadonlyArray<DocTypeRef>;
|
|
103
|
+
/** Reserved for Phase 2 — declarative permissions per field. */
|
|
104
|
+
readonly permissions?: Record<string, unknown>;
|
|
105
|
+
/**
|
|
106
|
+
* Forward migrations between versions. Each entry's key is the
|
|
107
|
+
* **source** version: `migrations[1]` migrates a v1 meta to v2.
|
|
108
|
+
* `runMigrations` chains them up to `version`. Reserved versions
|
|
109
|
+
* with no migration entry stop the chain; the meta is returned at
|
|
110
|
+
* the highest version reached.
|
|
111
|
+
*/
|
|
112
|
+
readonly migrations?: {
|
|
113
|
+
readonly [fromVersion: number]: (oldMeta: any) => any;
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Resolved doc-type after registry build. */
|
|
118
|
+
export interface DocType<
|
|
119
|
+
Name extends string = string,
|
|
120
|
+
MetaSchema extends ZodType = ZodType,
|
|
121
|
+
BodySchema extends ZodType | undefined = ZodType | undefined,
|
|
122
|
+
> extends DocTypeSpec<Name, MetaSchema, BodySchema> {
|
|
123
|
+
readonly __resolved: true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface SchemaSpec {
|
|
127
|
+
readonly types: ReadonlyArray<DocType>;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Phantom meta-type witness on the registry. Carrying the meta map at the
|
|
132
|
+
* type level lets consumers (e.g. `provider.docs(schema).get(type, id)`)
|
|
133
|
+
* extract per-doc-type meta types via {@link DocMetaOf}/{@link DocTypeNameOf}
|
|
134
|
+
* without re-importing the source declarations.
|
|
135
|
+
*
|
|
136
|
+
* Default `Record<string, unknown>` keeps existing untyped consumers working
|
|
137
|
+
* unchanged (Rule 4).
|
|
138
|
+
*/
|
|
139
|
+
export interface SchemaRegistry<
|
|
140
|
+
TMap extends Record<string, unknown> = Record<string, unknown>,
|
|
141
|
+
> {
|
|
142
|
+
readonly types: ReadonlyMap<string, DocType>;
|
|
143
|
+
get(name: string): DocType | undefined;
|
|
144
|
+
validateMeta(name: string, value: unknown): ValidationResult;
|
|
145
|
+
/**
|
|
146
|
+
* Returns ok if `child` is allowed under `parent`, err otherwise.
|
|
147
|
+
* `parent === null` means the document tree root — only doc-types
|
|
148
|
+
* declared as root-eligible (none today; reserved) are accepted there.
|
|
149
|
+
*/
|
|
150
|
+
validateChildType(
|
|
151
|
+
parent: string | null,
|
|
152
|
+
child: string,
|
|
153
|
+
): ValidationResult;
|
|
154
|
+
/**
|
|
155
|
+
* Phantom-only field. Exists for type-inference (`infer` patterns in
|
|
156
|
+
* {@link DocTypeNameOf}/{@link DocMetaOf}). Always `undefined` at runtime.
|
|
157
|
+
*/
|
|
158
|
+
readonly __metaMap?: TMap;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Extract the doc-type names from a typed registry. Works against any
|
|
163
|
+
* `SchemaRegistry<TMap>` value.
|
|
164
|
+
*/
|
|
165
|
+
export type DocTypeNameOf<S> = S extends SchemaRegistry<infer M>
|
|
166
|
+
? keyof M & string
|
|
167
|
+
: never;
|
|
168
|
+
|
|
169
|
+
/** Extract the meta type for a given doc-type name from a typed registry. */
|
|
170
|
+
export type DocMetaOf<S, N extends string> = S extends SchemaRegistry<infer M>
|
|
171
|
+
? N extends keyof M
|
|
172
|
+
? M[N]
|
|
173
|
+
: never
|
|
174
|
+
: never;
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Internal: derive the meta-map type from a `SchemaSpec` so `defineSchema`
|
|
178
|
+
* can return `SchemaRegistry<MetaMapFor<typeof spec>>` and consumers get
|
|
179
|
+
* fully-inferred per-doc-type meta types at the call site.
|
|
180
|
+
*/
|
|
181
|
+
type MetaMapFor<S extends SchemaSpec> = {
|
|
182
|
+
[T in S["types"][number] as T["name"]]: T extends DocType<
|
|
183
|
+
string,
|
|
184
|
+
infer M extends ZodType
|
|
185
|
+
>
|
|
186
|
+
? z.infer<M>
|
|
187
|
+
: never;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
export type ValidationResult =
|
|
191
|
+
| { ok: true; value: unknown }
|
|
192
|
+
| { ok: false; errors: ReadonlyArray<ValidationIssue> };
|
|
193
|
+
|
|
194
|
+
export interface ValidationIssue {
|
|
195
|
+
readonly path: ReadonlyArray<PropertyKey>;
|
|
196
|
+
readonly message: string;
|
|
197
|
+
readonly code?: string;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// Public API
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Declare a doc-type. The returned value carries enough type information for
|
|
206
|
+
* downstream codegen and is a `DocType` once registered.
|
|
207
|
+
*/
|
|
208
|
+
export function defineDocType<
|
|
209
|
+
Name extends string,
|
|
210
|
+
MetaSchema extends ZodType,
|
|
211
|
+
BodySchema extends ZodType | undefined = undefined,
|
|
212
|
+
>(
|
|
213
|
+
spec: DocTypeSpec<Name, MetaSchema, BodySchema>,
|
|
214
|
+
): DocType<Name, MetaSchema, BodySchema> {
|
|
215
|
+
return { ...spec, __resolved: true };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Reference another doc-type by name. Resolution happens in {@link defineSchema}. */
|
|
219
|
+
export function ref(name: string): DocTypeRef {
|
|
220
|
+
return { __ref: name };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Build a registry from a list of doc-types. Validates that all `ref(...)`
|
|
225
|
+
* targets exist; throws on dangling references.
|
|
226
|
+
*
|
|
227
|
+
* The returned registry carries a phantom `__metaMap` witness inferred from
|
|
228
|
+
* `spec`, so call sites like `provider.docs(schema).get("kanban", id)`
|
|
229
|
+
* see fully-typed meta even though the registry's runtime shape is generic.
|
|
230
|
+
*/
|
|
231
|
+
export function defineSchema<S extends SchemaSpec>(
|
|
232
|
+
spec: S,
|
|
233
|
+
): SchemaRegistry<MetaMapFor<S>> {
|
|
234
|
+
const types = new Map<string, DocType>();
|
|
235
|
+
for (const t of spec.types) {
|
|
236
|
+
if (types.has(t.name)) {
|
|
237
|
+
throw new Error(`Duplicate doc-type name: ${t.name}`);
|
|
238
|
+
}
|
|
239
|
+
types.set(t.name, t);
|
|
240
|
+
}
|
|
241
|
+
for (const t of spec.types) {
|
|
242
|
+
for (const child of t.children ?? []) {
|
|
243
|
+
if (!types.has(child.__ref)) {
|
|
244
|
+
throw new Error(
|
|
245
|
+
`Doc-type "${t.name}" references unknown child doc-type "${child.__ref}"`,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
types,
|
|
252
|
+
get(name) {
|
|
253
|
+
return types.get(name);
|
|
254
|
+
},
|
|
255
|
+
validateChildType(parent, child) {
|
|
256
|
+
if (!types.has(child)) {
|
|
257
|
+
return {
|
|
258
|
+
ok: false,
|
|
259
|
+
errors: [{ path: [], message: `Unknown doc-type: ${child}` }],
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
if (parent === null) {
|
|
263
|
+
return {
|
|
264
|
+
ok: false,
|
|
265
|
+
errors: [
|
|
266
|
+
{
|
|
267
|
+
path: [],
|
|
268
|
+
message: `Doc-type "${child}" cannot be a root document — root-eligibility is reserved for Phase 2`,
|
|
269
|
+
},
|
|
270
|
+
],
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
const parentType = types.get(parent);
|
|
274
|
+
if (!parentType) {
|
|
275
|
+
return {
|
|
276
|
+
ok: false,
|
|
277
|
+
errors: [{ path: [], message: `Unknown parent doc-type: ${parent}` }],
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
const allowed = (parentType.children ?? []).some((c) => c.__ref === child);
|
|
281
|
+
if (allowed) {
|
|
282
|
+
return { ok: true, value: undefined };
|
|
283
|
+
}
|
|
284
|
+
return {
|
|
285
|
+
ok: false,
|
|
286
|
+
errors: [
|
|
287
|
+
{
|
|
288
|
+
path: [],
|
|
289
|
+
message: `Doc-type "${child}" is not an allowed child of "${parent}"`,
|
|
290
|
+
},
|
|
291
|
+
],
|
|
292
|
+
};
|
|
293
|
+
},
|
|
294
|
+
validateMeta(name, value) {
|
|
295
|
+
const t = types.get(name);
|
|
296
|
+
// Rule 4 (feedback_schema_free_core): doc-types not in this
|
|
297
|
+
// registry are unconstrained. Existing-app traffic for types the
|
|
298
|
+
// registry doesn't know about is never rejected — callers can
|
|
299
|
+
// check membership explicitly via `registry.get(name)` if they
|
|
300
|
+
// need strict semantics.
|
|
301
|
+
if (!t) {
|
|
302
|
+
return { ok: true, value };
|
|
303
|
+
}
|
|
304
|
+
const parsed = t.meta.safeParse(value);
|
|
305
|
+
if (parsed.success) {
|
|
306
|
+
return { ok: true, value: parsed.data };
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
ok: false,
|
|
310
|
+
errors: parsed.error.issues.map((i) => ({
|
|
311
|
+
path: i.path,
|
|
312
|
+
message: i.message,
|
|
313
|
+
code: i.code,
|
|
314
|
+
})),
|
|
315
|
+
};
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Apply forward migrations to a meta object.
|
|
322
|
+
*
|
|
323
|
+
* Reads `meta.__schemaVersion` (defaults to `1` if absent) and applies
|
|
324
|
+
* each `migrations[v]` from there up to the schema's declared `version`.
|
|
325
|
+
* Each migration function receives the previous version's meta and
|
|
326
|
+
* returns the next version's meta. The final value carries
|
|
327
|
+
* `__schemaVersion = <highest version reached>`.
|
|
328
|
+
*
|
|
329
|
+
* Behaviour:
|
|
330
|
+
* - **Unknown doc-type**: returns `meta` unchanged. Rule 4 — registries
|
|
331
|
+
* only constrain the types they declare.
|
|
332
|
+
* - **No migrations declared**: returns `meta` unchanged. Useful for
|
|
333
|
+
* freshly-introduced doc-types still at v1.
|
|
334
|
+
* - **Missing migration entry mid-chain**: stops at the highest
|
|
335
|
+
* reachable version. Caller can detect by inspecting
|
|
336
|
+
* `result.__schemaVersion` against `schema.get(typeName)?.version`.
|
|
337
|
+
* - **Non-object meta** (null, undefined, primitives): returns
|
|
338
|
+
* unchanged. Migrations only operate on object meta.
|
|
339
|
+
*
|
|
340
|
+
* Migrations are pure: they receive a meta value and return a new
|
|
341
|
+
* one. They MUST NOT mutate the input (callers may pass shared
|
|
342
|
+
* references).
|
|
343
|
+
*
|
|
344
|
+
* @example
|
|
345
|
+
* // v2 of kanban introduces a new required-shape `boardLayout` key.
|
|
346
|
+
* const Kanban = defineDocType({
|
|
347
|
+
* name: "kanban",
|
|
348
|
+
* version: 2,
|
|
349
|
+
* meta: KanbanMetaV2,
|
|
350
|
+
* migrations: {
|
|
351
|
+
* 1: (oldMeta) => ({ ...oldMeta, boardLayout: "default" }),
|
|
352
|
+
* },
|
|
353
|
+
* });
|
|
354
|
+
* const fresh = runMigrations(kanbanSchema, "kanban", { kanbanShowIcon: true });
|
|
355
|
+
* // fresh.boardLayout === "default"
|
|
356
|
+
* // fresh.__schemaVersion === 2
|
|
357
|
+
*/
|
|
358
|
+
export function runMigrations(
|
|
359
|
+
registry: SchemaRegistry,
|
|
360
|
+
typeName: string,
|
|
361
|
+
meta: unknown,
|
|
362
|
+
): unknown {
|
|
363
|
+
if (meta === null || typeof meta !== "object") return meta;
|
|
364
|
+
const t = registry.get(typeName);
|
|
365
|
+
if (!t) return meta; // Rule 4
|
|
366
|
+
const target = t.version;
|
|
367
|
+
const migrations = t.migrations;
|
|
368
|
+
|
|
369
|
+
const m = meta as Record<string, unknown>;
|
|
370
|
+
let currentVersion: number =
|
|
371
|
+
typeof m.__schemaVersion === "number" && m.__schemaVersion >= 1
|
|
372
|
+
? m.__schemaVersion
|
|
373
|
+
: 1;
|
|
374
|
+
if (currentVersion >= target) return meta;
|
|
375
|
+
if (!migrations) return meta;
|
|
376
|
+
|
|
377
|
+
let working: unknown = meta;
|
|
378
|
+
let migrated = false;
|
|
379
|
+
while (currentVersion < target) {
|
|
380
|
+
const fn = migrations[currentVersion];
|
|
381
|
+
if (typeof fn !== "function") break;
|
|
382
|
+
working = fn(working);
|
|
383
|
+
currentVersion += 1;
|
|
384
|
+
migrated = true;
|
|
385
|
+
}
|
|
386
|
+
if (!migrated) return meta; // no migration ran → return original unchanged
|
|
387
|
+
if (working === null || typeof working !== "object") return working;
|
|
388
|
+
return { ...(working as Record<string, unknown>), __schemaVersion: currentVersion };
|
|
389
|
+
}
|