@thebes/cadmus 0.2.1 → 0.4.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.
Files changed (49) hide show
  1. package/dist/cms/index.cjs +689 -3
  2. package/dist/cms/index.cjs.map +1 -1
  3. package/dist/cms/index.d.cts +2 -2
  4. package/dist/cms/index.d.ts +2 -2
  5. package/dist/cms/index.js +671 -5
  6. package/dist/cms/index.js.map +1 -1
  7. package/dist/email/index.cjs +1 -1
  8. package/dist/email/index.js +1 -1
  9. package/dist/{errors-CW6Lz0AQ.cjs → errors-BhoibM6Z.cjs} +24 -1
  10. package/dist/{errors-CW6Lz0AQ.cjs.map → errors-BhoibM6Z.cjs.map} +1 -1
  11. package/dist/{errors-mZIqZJO4.js → errors-C8SqkFjl.js} +19 -2
  12. package/dist/{errors-mZIqZJO4.js.map → errors-C8SqkFjl.js.map} +1 -1
  13. package/dist/hono/index.cjs +6 -1
  14. package/dist/hono/index.cjs.map +1 -1
  15. package/dist/hono/index.d.cts +1 -1
  16. package/dist/hono/index.d.cts.map +1 -1
  17. package/dist/hono/index.d.ts +1 -1
  18. package/dist/hono/index.d.ts.map +1 -1
  19. package/dist/hono/index.js +6 -1
  20. package/dist/hono/index.js.map +1 -1
  21. package/dist/index-sB3YOadC.d.cts +1304 -0
  22. package/dist/index-sB3YOadC.d.cts.map +1 -0
  23. package/dist/index-sB3YOadC.d.ts +1304 -0
  24. package/dist/index-sB3YOadC.d.ts.map +1 -0
  25. package/dist/index.cjs +22 -1
  26. package/dist/index.d.cts +3 -89
  27. package/dist/index.d.cts.map +1 -1
  28. package/dist/index.d.ts +3 -89
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +3 -3
  31. package/dist/queues/index.cjs +1 -1
  32. package/dist/queues/index.js +1 -1
  33. package/dist/rate-limit/index.cjs +1 -1
  34. package/dist/rate-limit/index.js +1 -1
  35. package/dist/session/index.cjs +1 -1
  36. package/dist/session/index.js +1 -1
  37. package/dist/storage/index.cjs +1 -1
  38. package/dist/storage/index.cjs.map +1 -1
  39. package/dist/storage/index.d.cts +31 -2
  40. package/dist/storage/index.d.cts.map +1 -1
  41. package/dist/storage/index.d.ts +31 -2
  42. package/dist/storage/index.d.ts.map +1 -1
  43. package/dist/storage/index.js +1 -1
  44. package/dist/storage/index.js.map +1 -1
  45. package/package.json +1 -1
  46. package/dist/index-BUrCSGVb.d.cts +0 -616
  47. package/dist/index-BUrCSGVb.d.cts.map +0 -1
  48. package/dist/index-BUrCSGVb.d.ts +0 -616
  49. package/dist/index-BUrCSGVb.d.ts.map +0 -1
@@ -0,0 +1,1304 @@
1
+ import { BaseSQLiteDatabase, SQLiteTableWithColumns, sqliteTable } from "drizzle-orm/sqlite-core";
2
+ import { InferInsertModel, InferSelectModel, SQL } from "drizzle-orm";
3
+
4
+ //#region src/cms/blocks.d.ts
5
+ /**
6
+ * Renderer registry for block/document content (issue #13) — adopts the
7
+ * Portable Text + `@portabletext/react` pattern (idea, not code): a content
8
+ * document is a serializable **array of typed blocks**, and rendering is a
9
+ * lookup of `block.type → renderer`, not a hand-rolled `switch`. Adding a
10
+ * new block type becomes "register a renderer" with no edit to a central
11
+ * branch.
12
+ *
13
+ * Framework-agnostic on purpose: `TRenderer` is whatever a host wants a
14
+ * block's renderer to be — a string-producing function (SSR/preview HTML),
15
+ * an Astro component, a Solid component, etc. The registry only does lookup,
16
+ * fallback, and introspection; it never assumes a rendering technology.
17
+ */
18
+ /**
19
+ * The minimal shape every block must have: a string discriminant under
20
+ * `type`. (Portable Text uses `_type`; Cadmea content is TipTap-JSON-shaped
21
+ * and already keyed on `type`, so the registry keys on `type` to stay
22
+ * drop-in with stored content — the editor can stay TipTap.)
23
+ */
24
+ interface PortableBlockLike {
25
+ type: string;
26
+ }
27
+ interface BlockRegistry<TRenderer> {
28
+ /** Register (or replace) the renderer for a block type. Chainable. */
29
+ register(type: string, renderer: TRenderer): BlockRegistry<TRenderer>;
30
+ /** Register several at once. Chainable. */
31
+ registerMany(renderers: Record<string, TRenderer>): BlockRegistry<TRenderer>;
32
+ /** The renderer registered for `type`, or `undefined`. */
33
+ get(type: string): TRenderer | undefined;
34
+ /** Whether a renderer is registered for `type`. */
35
+ has(type: string): boolean;
36
+ /** Every registered block type, in registration order. */
37
+ types(): string[];
38
+ /** Set the fallback used when a type has no registered renderer. Chainable. */
39
+ setFallback(renderer: TRenderer): BlockRegistry<TRenderer>;
40
+ /** The renderer for `type`, else the fallback, else `undefined`. */
41
+ resolve(type: string): TRenderer | undefined;
42
+ }
43
+ /**
44
+ * Create a block renderer registry. Seed it with an initial `type → renderer`
45
+ * map and/or an `options.fallback` for unknown types.
46
+ *
47
+ * ```ts
48
+ * const registry = createBlockRegistry<StringBlockRenderer>({
49
+ * divider: () => "<hr>",
50
+ * });
51
+ * registry.register("hero", (b) => `<h1>${b.heading}</h1>`);
52
+ * renderBlocksToString(blocks, registry);
53
+ * ```
54
+ */
55
+ declare function createBlockRegistry<TRenderer>(initial?: Record<string, TRenderer>, options?: {
56
+ fallback?: TRenderer;
57
+ }): BlockRegistry<TRenderer>;
58
+ /**
59
+ * A renderer that turns one block into an HTML string — the registry value
60
+ * type for SSR/preview paths that build markup as strings (e.g. a Hono
61
+ * preview route) rather than mounting components.
62
+ */
63
+ type StringBlockRenderer<TBlock extends PortableBlockLike = PortableBlockLike> = (block: TBlock) => string;
64
+ /**
65
+ * Render an array of blocks to a single HTML string via a registry of
66
+ * {@link StringBlockRenderer}s. Blocks whose type resolves to no renderer
67
+ * (and no fallback) contribute the empty string — the same forgiving
68
+ * behavior the old hand-rolled `switch` had for unknown types.
69
+ */
70
+ declare function renderBlocksToString<TBlock extends PortableBlockLike>(blocks: readonly TBlock[], registry: BlockRegistry<StringBlockRenderer<TBlock>>): string;
71
+ //#endregion
72
+ //#region src/errors.d.ts
73
+ declare global {
74
+ interface ErrorConstructor {
75
+ captureStackTrace?(targetObject: object, constructorOpt?: Function): void;
76
+ }
77
+ }
78
+ /**
79
+ * Base class for all Cadmus errors.
80
+ * All primitives throw CadmusError or a typed subclass — never a raw Error.
81
+ *
82
+ * @example
83
+ * try {
84
+ * await createMagicLink({ kv, email, to })
85
+ * } catch (e) {
86
+ * if (e instanceof CadmusAuthError) {
87
+ * // auth-specific handling
88
+ * } else if (e instanceof CadmusError) {
89
+ * // any cadmus error — e.code tells you which primitive threw
90
+ * } else {
91
+ * throw e // re-throw unknown errors
92
+ * }
93
+ * }
94
+ */
95
+ declare class CadmusError extends Error {
96
+ readonly code: string;
97
+ readonly cause?: unknown | undefined;
98
+ constructor(message: string, code: string, cause?: unknown | undefined);
99
+ }
100
+ /** Thrown by @thebes/cadmus/auth primitives */
101
+ declare class CadmusAuthError extends CadmusError {
102
+ constructor(message: string, cause?: unknown);
103
+ }
104
+ /** Thrown by @thebes/cadmus/db primitives */
105
+ declare class CadmusDbError extends CadmusError {
106
+ constructor(message: string, cause?: unknown);
107
+ }
108
+ /** Thrown by @thebes/cadmus/storage primitives */
109
+ declare class CadmusStorageError extends CadmusError {
110
+ constructor(message: string, cause?: unknown);
111
+ }
112
+ /** Thrown by @thebes/cadmus/cache primitives */
113
+ declare class CadmusCacheError extends CadmusError {
114
+ constructor(message: string, cause?: unknown);
115
+ }
116
+ /** Thrown by @thebes/cadmus/email primitives */
117
+ declare class CadmusEmailError extends CadmusError {
118
+ constructor(message: string, cause?: unknown);
119
+ }
120
+ /** Thrown by @thebes/cadmus/session primitives */
121
+ declare class CadmusSessionError extends CadmusError {
122
+ constructor(message: string, cause?: unknown);
123
+ }
124
+ /** Thrown by @thebes/cadmus/rate-limit primitives */
125
+ declare class CadmusRateLimitError extends CadmusError {
126
+ constructor(message: string, cause?: unknown);
127
+ }
128
+ /** Thrown by @thebes/cadmus/queues primitives */
129
+ declare class CadmusQueueError extends CadmusError {
130
+ constructor(message: string, cause?: unknown);
131
+ }
132
+ /** Thrown by @thebes/cadmus/cms primitives */
133
+ declare class CadmusCmsError extends CadmusError {
134
+ constructor(message: string, cause?: unknown);
135
+ }
136
+ /**
137
+ * Thrown by @thebes/cadmus/cms's createLocalApi when a collection's
138
+ * `access` function rejects an operation. A distinct subclass (rather than
139
+ * a plain CadmusCmsError) so consumers like mountCmsRoutes can map it to
140
+ * 403 by `instanceof`, not by matching on message text.
141
+ */
142
+ declare class CadmusAccessDeniedError extends CadmusCmsError {
143
+ constructor(message: string, cause?: unknown);
144
+ }
145
+ /**
146
+ * One failed field-validation rule (issue #16). `path` is the field's key
147
+ * (flattened, e.g. `shippingAddress_city` for a group subfield). `severity`
148
+ * lets a rule warn without blocking the write — only `"error"` violations
149
+ * cause createLocalApi to throw; `"warning"` ones are carried through for
150
+ * the studio to surface non-blockingly.
151
+ */
152
+ interface ValidationViolation {
153
+ path: string;
154
+ message: string;
155
+ severity: "error" | "warning";
156
+ }
157
+ /**
158
+ * Thrown by createLocalApi when a collection's field-validation rules
159
+ * (Sanity-style chainable `Rule` API — see cms/validation.ts) reject a
160
+ * create/update. Carries the structured `violations` so the studio can
161
+ * surface per-field messages, and `mountCmsRoutes` can map it to HTTP 422
162
+ * by `instanceof` rather than message matching. A subclass of
163
+ * CadmusCmsError, so existing `instanceof CadmusCmsError` handling still
164
+ * catches it. Only `"error"`-severity violations are ever thrown.
165
+ */
166
+ declare class CadmusValidationError extends CadmusCmsError {
167
+ readonly violations: ValidationViolation[];
168
+ constructor(message: string, violations: ValidationViolation[], cause?: unknown);
169
+ }
170
+ /**
171
+ * Thrown by @thebes/cadmus/hono's `createCmsApiClient` when a request
172
+ * against a `mountCmsRoutes` surface returns a non-2xx response. Carries
173
+ * the HTTP status and parsed body so callers can branch on `status`
174
+ * (e.g. 403 → access denied, 404 → not found) instead of re-parsing
175
+ * `{ error: string }` response bodies by hand.
176
+ */
177
+ declare class CadmusApiError extends CadmusError {
178
+ readonly status: number;
179
+ readonly body: unknown;
180
+ constructor(message: string, status: number, body: unknown);
181
+ }
182
+ //#endregion
183
+ //#region src/cms/patch.d.ts
184
+ /**
185
+ * Patch model + field-level diff (issue #14) — adopts Sanity's mutation/patch
186
+ * idea (pattern, not code): represent a content change as a small set of
187
+ * field operations, and compute a field-level diff between two document
188
+ * snapshots. Underpins version-history display (what changed between two
189
+ * versions) and the content-migration runner (#18), which expresses a
190
+ * transform's effect as a {@link Patch} and applies it.
191
+ *
192
+ * Scope: operations are keyed by **top-level field** (a document's own
193
+ * fields), matching "field-level diff" — a changed `blocks` array reads as
194
+ * one changed field, not a deep per-node diff. Deep/array-aware diffing is a
195
+ * deliberate later extension, not built here.
196
+ */
197
+ /** A single field operation. `set` writes a value; `unset` removes the field. */
198
+ type PatchOp = {
199
+ op: "set";
200
+ path: string;
201
+ value: JsonValue;
202
+ } | {
203
+ op: "unset";
204
+ path: string;
205
+ };
206
+ /** An ordered set of field operations transforming one document into another. */
207
+ type Patch = PatchOp[];
208
+ type FieldChangeKind = "added" | "removed" | "changed";
209
+ /** One field's difference between two document snapshots. */
210
+ interface FieldChange {
211
+ /** Top-level field key. */
212
+ path: string;
213
+ kind: FieldChangeKind;
214
+ /** Value in the "before" snapshot (absent for `added`). */
215
+ before?: JsonValue;
216
+ /** Value in the "after" snapshot (absent for `removed`). */
217
+ after?: JsonValue;
218
+ }
219
+ type Doc$1 = Record<string, JsonValue>;
220
+ interface DiffOptions {
221
+ /**
222
+ * Restrict the diff to these field keys. Omit to diff the union of both
223
+ * documents' own keys. Useful for ignoring bookkeeping columns
224
+ * (`id`/`createdAt`/`publishedVersionId`) in a version-history view.
225
+ */
226
+ fields?: readonly string[];
227
+ /** Field keys to skip (e.g. `["id", "createdAt"]`). */
228
+ ignore?: readonly string[];
229
+ }
230
+ /**
231
+ * Field-level diff between two document snapshots — the per-field
232
+ * added/removed/changed list a version-history UI renders. Values are
233
+ * compared structurally (deep-equal), so a field only shows as `changed`
234
+ * when its content actually differs.
235
+ */
236
+ declare function diffDocuments(before: Doc$1, after: Doc$1, options?: DiffOptions): FieldChange[];
237
+ /**
238
+ * The {@link Patch} that transforms `before` into `after`: `set` for each
239
+ * added/changed field, `unset` for each removed field. `applyPatch(before,
240
+ * computePatch(before, after))` deep-equals `after`.
241
+ */
242
+ declare function computePatch(before: Doc$1, after: Doc$1): Patch;
243
+ /**
244
+ * Apply a {@link Patch} to a document, returning a new document (the input is
245
+ * never mutated). Unknown ops are ignored defensively.
246
+ */
247
+ declare function applyPatch(doc: Doc$1, patch: Patch): Doc$1;
248
+ //#endregion
249
+ //#region src/cms/localApi.d.ts
250
+ type AnyTable$1 = SQLiteTableWithColumns<any>;
251
+ /**
252
+ * `TContext` is the per-request value passed to every method and forwarded
253
+ * unchanged to the collection's `access` functions (see {@link CollectionAccess}).
254
+ * Cadmus doesn't standardize its shape — Cadmea types it as `{ session }`,
255
+ * other consumers may type it differently. `context` is a required first
256
+ * argument on every method (not optional) so a call site can't forget it.
257
+ */
258
+ interface LocalApi<TTable extends AnyTable$1, TContext = unknown> {
259
+ /**
260
+ * `depth: 0` (default) returns relationship fields as bare ids; `depth: 1`
261
+ * batch-resolves `hasMany: false` relationship fields into the related
262
+ * row, gated by that collection's own `read` access fn — see
263
+ * `resolveRelationships` below. Requires `createLocalApi`'s `registry`
264
+ * param; throws CadmusCmsError if `depth: 1` is requested without one.
265
+ */
266
+ find(context: TContext, options?: {
267
+ where?: SQL;
268
+ depth?: RelationshipDepth; /** Row cap, applied after `where` — for paginated list views. */
269
+ limit?: number; /** Rows to skip, applied after `where` — pairs with `limit`. */
270
+ offset?: number; /** One or more `asc(table.col)`/`desc(table.col)` expressions. */
271
+ orderBy?: SQL | SQL[];
272
+ }): Promise<InferSelectModel<TTable>[]>;
273
+ findByID(context: TContext, id: number, options?: {
274
+ depth?: RelationshipDepth;
275
+ }): Promise<InferSelectModel<TTable>>;
276
+ /**
277
+ * Total row count for `where` (ignoring `limit`/`offset`) — pairs with
278
+ * `find` to compute page counts/next-page availability without fetching
279
+ * every row. Gated by the same `read` access check as `find`.
280
+ */
281
+ count(context: TContext, options?: {
282
+ where?: SQL;
283
+ }): Promise<number>;
284
+ /**
285
+ * Full-text search over this collection's `search.fields`-configured
286
+ * companion FTS5 table — see types.ts's `CollectionConfig.search` and
287
+ * codegen.ts's `collectionSearchTableSQL`. Gated by `read` access, same
288
+ * as `find`/`findByID`. Throws `CadmusCmsError` if the collection has no
289
+ * `search` config.
290
+ */
291
+ search(context: TContext, query: string, options?: {
292
+ limit?: number;
293
+ }): Promise<InferSelectModel<TTable>[]>;
294
+ create(context: TContext, input: InferInsertModel<TTable>): Promise<InferSelectModel<TTable>>;
295
+ update(context: TContext, id: number, input: Partial<InferInsertModel<TTable>>): Promise<InferSelectModel<TTable>>;
296
+ deleteByID(context: TContext, id: number): Promise<InferSelectModel<TTable>>;
297
+ }
298
+ /**
299
+ * Lets `createLocalApi` resolve `depth: 1` relationship fields without
300
+ * importing every other collection's Local API (which would be a circular
301
+ * dependency the moment two collections relate to each other). The
302
+ * registry is just the raw ingredients — a table and a config per
303
+ * collection slug — built once by the app (e.g. from `cadmeaConfig.collections`)
304
+ * and passed to every `createLocalApi` call that has relationship fields.
305
+ *
306
+ * `apis` is a second, optional registry on the same object, for a
307
+ * different problem: a *hook* (not `createLocalApi` itself) on one
308
+ * collection that needs to write to *another* collection's Local API —
309
+ * e.g. a CRM upsert hook on a lead-capture collection that creates/updates
310
+ * `contacts`/`activities` rows. `tables`/`configs` can't serve this, since
311
+ * a hook needs a real `LocalApi` (with its own access/hooks/search wiring
312
+ * already applied), not raw ingredients to rebuild one from.
313
+ *
314
+ * The chicken-and-egg problem this solves: building collection A's
315
+ * `LocalApi` might need to reference collection B's `LocalApi` (for a
316
+ * hook), but collection B's `LocalApi` doesn't exist yet at the point A's
317
+ * is constructed — and vice versa if B also has a hook referencing A.
318
+ * The fix is **late binding**: build one `CmsRegistry` object, pass the
319
+ * *same reference* into every `createLocalApi` call (so every collection's
320
+ * hooks close over the same mutable object), construct every `LocalApi`,
321
+ * then fill in `registry.apis` afterwards:
322
+ *
323
+ * ```ts
324
+ * const registry: CmsRegistry = { tables, configs, apis: {} };
325
+ * const contactsApi = createLocalApi(db, contactsTable, contactsCollection, registry);
326
+ * const inquiriesApi = createLocalApi(db, inquiriesTable, inquiriesCollection, registry);
327
+ * // populate *after* every createLocalApi call returns — any hook that
328
+ * // reads registry.apis lazily (inside its returned function body, not
329
+ * // at hook-factory-call time) sees the fully-populated map, since hooks
330
+ * // only ever run once real requests start landing.
331
+ * Object.assign(registry.apis!, { contacts: contactsApi, inquiries: inquiriesApi });
332
+ * ```
333
+ *
334
+ * See `getRegisteredApi` for the accessor a hook factory should use to
335
+ * read from this map, rather than indexing `registry.apis` directly.
336
+ */
337
+ interface CmsRegistry {
338
+ tables: Record<string, AnyTable$1>;
339
+ configs: Record<string, CollectionConfig>;
340
+ apis?: Record<string, LocalApi<AnyTable$1, any>>;
341
+ }
342
+ /**
343
+ * Reads collection `slug`'s `LocalApi` out of `registry.apis` — the
344
+ * accessor hook factories should use (see `CmsRegistry`'s doc comment for
345
+ * the late-binding pattern this assumes) instead of indexing
346
+ * `registry.apis` directly, so every caller gets the same clear error if
347
+ * the registry wasn't built/populated correctly. `TContext` is a type-only
348
+ * parameter (the registry itself is stored with `never` to stay variance-
349
+ * safe across collections with different context shapes) — callers assert
350
+ * the context type they expect, the same way `resolveRelationships`'s own
351
+ * registry lookups do.
352
+ */
353
+ declare function getRegisteredApi<TContext>(registry: CmsRegistry | undefined, slug: string): LocalApi<AnyTable$1, TContext>;
354
+ /**
355
+ * Non-throwing counterpart to `checkAccess` below, for UI code that wants
356
+ * to hide/disable an action a context can't perform rather than let it
357
+ * fail server-side after a click (see Phase 6 / issue #26's
358
+ * `getPageCapabilities`). `checkAccess` calls through this same function
359
+ * rather than duplicating the "no access fn = allowed" logic, so `can()`'s
360
+ * answer and the real operation's enforcement can never disagree.
361
+ */
362
+ declare function can<TContext>(config: CollectionConfig, operation: keyof CollectionAccess, context: TContext): Promise<boolean>;
363
+ declare function createLocalApi<TTable extends AnyTable$1, TContext = unknown>(db: BaseSQLiteDatabase<"async", unknown>, table: TTable, config: CollectionConfig, registry?: CmsRegistry): LocalApi<TTable, TContext>;
364
+ /**
365
+ * Extends {@link LocalApi} with draft/publish operations for a collection
366
+ * that opted in via `CollectionConfig.versions.drafts` (see codegen.ts's
367
+ * `collectionVersionsTable`). A separate interface (not a wider
368
+ * `LocalApi`) so non-versioned collections' types don't grow these methods
369
+ * — TypeScript can't conditionally widen `createLocalApi`'s return type
370
+ * off a runtime config value, so this is `createVersionedLocalApi`'s own
371
+ * factory rather than a branch inside `createLocalApi`.
372
+ *
373
+ * Scope, deliberately: a document is always created via the inherited
374
+ * `create()` first (existing behavior, unaffected by versioning) — these
375
+ * methods operate against an *existing* row. `saveDraft` never validates
376
+ * required fields (an incomplete draft is valid); `publish` runs the same
377
+ * full validation `create`/`update` do, since publishing is what makes a
378
+ * version the public-facing document. Plain `find`/`findByID` are
379
+ * unchanged by any of this — they always return the main table's current
380
+ * row regardless of `publishedVersionId`; filtering reads to
381
+ * published-only content is not this phase's concern.
382
+ */
383
+ interface VersionedLocalApi<TTable extends AnyTable$1, TVersionsTable extends AnyTable$1, TContext = unknown> extends LocalApi<TTable, TContext> {
384
+ findVersions(context: TContext, parentId: number): Promise<InferSelectModel<TVersionsTable>[]>;
385
+ /** Inserts a new version row holding `input` as a draft snapshot. */
386
+ saveDraft(context: TContext, id: number, input: Partial<InferInsertModel<TTable>>): Promise<InferSelectModel<TVersionsTable>>;
387
+ /** Copies a version's snapshot onto the main row and marks it published. */
388
+ publish(context: TContext, versionId: number): Promise<InferSelectModel<TTable>>;
389
+ /** Clears the main row's published pointer; the row's data is untouched. */
390
+ unpublish(context: TContext, id: number): Promise<InferSelectModel<TTable>>;
391
+ /**
392
+ * Field-level diff (issue #14) between two version snapshots' `versionData`
393
+ * — the per-field added/removed/changed list a version-history UI renders.
394
+ * Both versions must belong to the same parent. Bookkeeping keys
395
+ * (`id`/`createdAt`/`status`/`publishedVersionId`) are ignored.
396
+ */
397
+ diffVersions(context: TContext, fromVersionId: number, toVersionId: number): Promise<FieldChange[]>;
398
+ }
399
+ declare function createVersionedLocalApi<TTable extends AnyTable$1, TVersionsTable extends AnyTable$1, TContext = unknown>(db: BaseSQLiteDatabase<"async", unknown>, table: TTable, versionsTable: TVersionsTable, config: CollectionConfig, registry?: CmsRegistry): VersionedLocalApi<TTable, TVersionsTable, TContext>;
400
+ //#endregion
401
+ //#region src/cms/validation.d.ts
402
+ type AnyTable = SQLiteTableWithColumns<any>;
403
+ /**
404
+ * Chainable field validation for Cadmea (issue #16) — adopts Sanity's
405
+ * `defineField`/`Rule` validation API (pattern, not code). A field declares
406
+ * `validation: (rule) => rule.required().min(2).custom(...)`; this module
407
+ * turns that chain into a list of declarative checks and evaluates them at
408
+ * write time (server-side, in createLocalApi) as well as anywhere the studio
409
+ * wants synchronous feedback.
410
+ *
411
+ * Design notes:
412
+ * - The builder is **immutable** — every method returns a new {@link Rule}
413
+ * with one more check appended, so a shared base rule can't be mutated by
414
+ * a consumer's chain (mirrors Sanity).
415
+ * - Most checks are synchronous and pure (min/max/regex/custom over the
416
+ * value alone). Two — `unique` and `reference` — need the database and so
417
+ * only run where {@link validateDocument} is given a `db` (i.e. the Local
418
+ * API); they're skipped (not failed) in a pure client-side pass.
419
+ */
420
+ type ValidationSeverity = "error" | "warning";
421
+ /**
422
+ * What a {@link CustomValidator} may return:
423
+ * - `true` / `undefined` → valid
424
+ * - `false` → invalid, generic message
425
+ * - `string` → invalid, that message
426
+ * - `{ message, severity? }` → invalid, that message at the given severity
427
+ */
428
+ type CustomValidatorResult = boolean | undefined | string | {
429
+ message: string;
430
+ severity?: ValidationSeverity;
431
+ };
432
+ interface ValidationFieldContext {
433
+ /** The whole document being validated (nested shape, post-hooks). */
434
+ document: Record<string, unknown>;
435
+ /** This field's flattened key (e.g. `slug`, `shippingAddress_city`). */
436
+ path: string;
437
+ /** Whether this is a create or an update. */
438
+ operation: "create" | "update";
439
+ /** The document's id on update — lets `unique` exclude the row itself. */
440
+ id?: number;
441
+ }
442
+ type CustomValidator = (value: unknown, context: ValidationFieldContext) => CustomValidatorResult | Promise<CustomValidatorResult>;
443
+ type Check = ({
444
+ kind: "required";
445
+ } | {
446
+ kind: "min";
447
+ n: number;
448
+ } | {
449
+ kind: "max";
450
+ n: number;
451
+ } | {
452
+ kind: "length";
453
+ n: number;
454
+ } | {
455
+ kind: "regex";
456
+ re: RegExp;
457
+ label: string;
458
+ } | {
459
+ kind: "integer";
460
+ } | {
461
+ kind: "positive";
462
+ } | {
463
+ kind: "unique";
464
+ } | {
465
+ kind: "reference";
466
+ } | {
467
+ kind: "custom";
468
+ fn: CustomValidator;
469
+ }) & {
470
+ message?: string;
471
+ severity?: ValidationSeverity;
472
+ };
473
+ /**
474
+ * Immutable, chainable rule builder — the value a field's `validation`
475
+ * function receives and returns. Build a `Rule` with the module-level
476
+ * {@link rule} factory, or accept the one passed to your `validation`
477
+ * callback.
478
+ */
479
+ declare class Rule {
480
+ private readonly checks;
481
+ constructor(checks?: readonly Check[]);
482
+ private add;
483
+ /** Override the message of the most recently added check. */
484
+ error(message: string): Rule;
485
+ /**
486
+ * Demote the most recently added check to a warning (non-blocking),
487
+ * optionally with a message. Sanity's `Rule.warning()` analogue.
488
+ */
489
+ warning(message?: string): Rule;
490
+ private withLast;
491
+ required(): Rule;
492
+ /** Minimum string length / array length / numeric value. */
493
+ min(n: number): Rule;
494
+ /** Maximum string length / array length / numeric value. */
495
+ max(n: number): Rule;
496
+ /** Exact string/array length. */
497
+ length(n: number): Rule;
498
+ regex(re: RegExp, label?: string): Rule;
499
+ email(): Rule;
500
+ /** Lowercase kebab-case slug format. Pair with `.unique()` for slugs. */
501
+ slug(): Rule;
502
+ integer(): Rule;
503
+ positive(): Rule;
504
+ /**
505
+ * Value must be unique across the collection (DB-backed; skipped in a
506
+ * pure client-side pass). A first-class rule rather than the hand-rolled
507
+ * column `unique` flag, so the failure is a clear field message instead of
508
+ * a raw UNIQUE-constraint write error.
509
+ */
510
+ unique(): Rule;
511
+ /**
512
+ * For a `relationship` field: the referenced id must exist in the related
513
+ * collection (DB-backed; skipped client-side).
514
+ */
515
+ reference(): Rule;
516
+ custom(fn: CustomValidator): Rule;
517
+ /** Internal: the accumulated checks, read by {@link validateDocument}. */
518
+ toChecks(): readonly Check[];
519
+ }
520
+ /** Fresh, empty rule — the root of a chain. */
521
+ declare function rule(): Rule;
522
+ /**
523
+ * A field's `validation` value: a function from a fresh Rule to the
524
+ * configured chain (Sanity's signature). Returning an array lets a field
525
+ * carry several independent rule chains.
526
+ */
527
+ type ValidationBuilder = (r: Rule) => Rule | Rule[];
528
+ /**
529
+ * Identity helper mirroring Sanity's `defineField` — returns the field
530
+ * config unchanged but gives editors autocomplete and a single, greppable
531
+ * call site for field definitions. Optional: a plain object literal is still
532
+ * a valid field.
533
+ */
534
+ declare function defineField<T extends FieldConfig>(field: T): T;
535
+ interface ValidateDocumentOptions {
536
+ operation: "create" | "update";
537
+ /** Document id (update only) — passed to `unique`/custom validators. */
538
+ id?: number;
539
+ /**
540
+ * Restrict validation to these flattened field keys. Used by update(),
541
+ * which only receives a partial document — validating absent fields would
542
+ * spuriously fail their rules. Omit to validate every field (create).
543
+ */
544
+ onlyFields?: ReadonlySet<string>;
545
+ /**
546
+ * Database handle for DB-backed rules (`unique`, `reference`). When
547
+ * omitted, those rules are skipped — so the same function powers a pure
548
+ * client-side validation pass.
549
+ */
550
+ db?: BaseSQLiteDatabase<"async", unknown>;
551
+ /** This collection's own table (for `unique`). */
552
+ table?: AnyTable;
553
+ /** Registry of tables by slug (for `reference` target lookups). */
554
+ registry?: CmsRegistry;
555
+ }
556
+ /**
557
+ * Evaluate every field's validation rules against `doc`, returning all
558
+ * violations (both errors and warnings). `doc` is the nested document; field
559
+ * values are read from its flattened form so group subfields validate too.
560
+ */
561
+ declare function validateDocument(config: CollectionConfig, doc: Record<string, unknown>, options: ValidateDocumentOptions): Promise<ValidationViolation[]>;
562
+ /**
563
+ * Run {@link validateDocument} and throw {@link CadmusValidationError} if any
564
+ * `"error"`-severity violations are found. Warnings are returned (never
565
+ * thrown) so a caller can still surface them. The thrown error's message is
566
+ * a readable, joined summary of every blocking violation.
567
+ */
568
+ declare function assertValid(config: CollectionConfig, doc: Record<string, unknown>, options: ValidateDocumentOptions): Promise<ValidationViolation[]>;
569
+ //#endregion
570
+ //#region src/cms/types.d.ts
571
+ interface BaseFieldConfig {
572
+ /** column name override; defaults to the config key */
573
+ name?: string;
574
+ required?: boolean;
575
+ unique?: boolean;
576
+ defaultValue?: unknown;
577
+ /**
578
+ * Chainable validation rules (issue #16), Sanity's `defineField`
579
+ * `validation` analogue: `validation: (rule) => rule.required().min(2)`.
580
+ * Evaluated server-side by createLocalApi on every create/update (and by
581
+ * the studio for client-side feedback). Independent of the `required`/
582
+ * `unique` flags above — those still drive the DB schema; these drive
583
+ * value-level checks with clear, per-field error messages. See
584
+ * {@link ValidationBuilder} and `validation.ts`.
585
+ */
586
+ validation?: ValidationBuilder;
587
+ }
588
+ interface TextFieldConfig extends BaseFieldConfig {
589
+ type: "text";
590
+ defaultValue?: string;
591
+ }
592
+ interface SelectFieldConfig<TOption extends string = string> extends BaseFieldConfig {
593
+ type: "select";
594
+ options: readonly TOption[];
595
+ defaultValue?: TOption;
596
+ }
597
+ interface NumberFieldConfig extends BaseFieldConfig {
598
+ type: "number";
599
+ /** marks this field as the table's auto-incrementing primary key */
600
+ autoIncrement?: boolean;
601
+ defaultValue?: number;
602
+ }
603
+ interface DateFieldConfig extends BaseFieldConfig {
604
+ type: "date";
605
+ /** mirrors drizzle's integer(..., { mode: "timestamp" | "timestamp_ms" }) */
606
+ mode?: "timestamp" | "timestamp_ms";
607
+ defaultValue?: "now" | Date;
608
+ }
609
+ interface RichTextFieldConfig extends BaseFieldConfig {
610
+ type: "richText";
611
+ }
612
+ /**
613
+ * The TS type every JSON-mode column (`richText`/`array` fields,
614
+ * `versionData`) is given via drizzle's `.$type<JsonValue>()` — see
615
+ * codegen.ts's and schema-gen.ts's richText/array cases. Without it,
616
+ * drizzle infers a JSON column as `unknown`, which TanStack Start's
617
+ * server-function return-type validator rejects outright (`unknown`
618
+ * doesn't structurally match its `Serializable` check the way a plain
619
+ * object/array/primitive union does). Recursive on purpose — that's what
620
+ * lets the validator recurse through it instead of bottoming out at
621
+ * `unknown`.
622
+ */
623
+ type JsonValue = string | number | boolean | null | JsonValue[] | {
624
+ [key: string]: JsonValue;
625
+ };
626
+ interface CheckboxFieldConfig extends BaseFieldConfig {
627
+ type: "checkbox";
628
+ defaultValue?: boolean;
629
+ }
630
+ interface RelationshipFieldConfig extends BaseFieldConfig {
631
+ type: "relationship";
632
+ relationTo: string;
633
+ /**
634
+ * `false` (default): a plain integer column on this collection's own
635
+ * table storing the related row's id.
636
+ * `true`: no column on this table — represented by a generated join
637
+ * table instead (see codegen.ts's relationshipJoinTables).
638
+ */
639
+ hasMany?: boolean;
640
+ }
641
+ /**
642
+ * `0` (default): a relationship field's column comes back as the bare
643
+ * related-row id. `1`: `createLocalApi`'s `registry` param is used to
644
+ * batch-resolve `hasMany: false` relationship fields into the related
645
+ * row's full document — see localApi.ts's `resolveRelationships`. Depths
646
+ * beyond 1 (resolving a relationship's own relationships) aren't
647
+ * implemented; there's no nested-relationship fixture yet to validate
648
+ * that design against.
649
+ */
650
+ type RelationshipDepth = 0 | 1;
651
+ interface ArrayFieldConfig extends BaseFieldConfig {
652
+ type: "array";
653
+ /**
654
+ * Fields shown for every item, regardless of variant — must include
655
+ * `discriminator.key`'s own field (typically a `select`) if set.
656
+ */
657
+ fields: Record<string, FieldConfig>;
658
+ /**
659
+ * Lets one array field model a union of item shapes (e.g. page-builder
660
+ * blocks: image vs hero vs richText vs...) instead of one fixed field
661
+ * set for every item. `key` names a field already present in `fields`
662
+ * (rendered as the item's type switcher); `variants` maps each of that
663
+ * field's possible values to *additional* fields layered on top, shown
664
+ * only for items whose `key` field currently holds that value. Fields
665
+ * not listed under any variant (i.e. everything in `fields`) render
666
+ * unconditionally — that's the place for fields shared across every
667
+ * variant (e.g. a `caption` every block type has).
668
+ *
669
+ * Storage is unaffected either way — `array` is always one JSON column
670
+ * (see codegen.ts); this only changes what `CollectionEdit` renders.
671
+ */
672
+ discriminator?: {
673
+ key: string;
674
+ variants: Record<string, Record<string, FieldConfig>>;
675
+ };
676
+ }
677
+ interface UploadFieldConfig extends BaseFieldConfig {
678
+ type: "upload";
679
+ defaultValue?: string;
680
+ }
681
+ /**
682
+ * A freeform JSON-blob column — the `json` field type from Section 3 (issue
683
+ * #20-adjacent field-type gap, see DECISIONS.md). Storage-identical to
684
+ * `richText`/`array` (one `.$type<JsonValue>()` text column, see
685
+ * codegen.ts's `fieldToColumn`) but with no TipTap/array-item connotation —
686
+ * use this for genuinely unstructured data (webhook audit payloads, CRM
687
+ * activity metadata), not page-builder content.
688
+ */
689
+ interface JsonFieldConfig extends BaseFieldConfig {
690
+ type: "json";
691
+ defaultValue?: JsonValue;
692
+ }
693
+ /**
694
+ * A fixed-shape, queryable sub-object — the `group` field type from Section
695
+ * 3. Unlike `array` (JSON-blob storage, variable length), `group` flattens
696
+ * to real prefixed columns at the Drizzle level (`<key>_<subKey>`, see
697
+ * codegen.ts's `collectionToTable` and `flattenFields` below) so SQL-level
698
+ * querying/sorting on a subfield (e.g. `shippingAddress.city`) still works.
699
+ * `required`/`unique`/`defaultValue` on the group itself are meaningless —
700
+ * set them on the individual nested `fields` instead; codegen ignores them
701
+ * at the group level.
702
+ */
703
+ interface GroupFieldConfig extends BaseFieldConfig {
704
+ type: "group";
705
+ fields: Record<string, FieldConfig>;
706
+ }
707
+ type FieldConfig = TextFieldConfig | SelectFieldConfig | NumberFieldConfig | DateFieldConfig | RichTextFieldConfig | CheckboxFieldConfig | RelationshipFieldConfig | ArrayFieldConfig | UploadFieldConfig | JsonFieldConfig | GroupFieldConfig;
708
+ /**
709
+ * Expands every `group` field in `fields` into its flattened equivalents
710
+ * (`<key>_<subKey>`, recursively — a group nested inside a group flattens
711
+ * all the way down), and passes every other field through unchanged. This
712
+ * is the single canonicalization step codegen, schema-gen, and the Local
713
+ * API's field-shape validation (`validateRequiredFields`/
714
+ * `rejectUnknownFields`) all run before touching `group` fields, so none of
715
+ * them need their own group-aware branch — see localApi.ts's `flattenDoc`/
716
+ * `nestDoc` for the matching document-level transform.
717
+ *
718
+ * Known limitation: a flattened key can collide if two different group
719
+ * nestings produce the same combined name (e.g. a group `a_b` containing
720
+ * field `c` collides with group `a` containing field `b_c`) — not guarded
721
+ * against, since no current collection nests groups deeply enough to hit
722
+ * it.
723
+ */
724
+ declare function flattenFields(fields: Record<string, FieldConfig>): Record<string, FieldConfig>;
725
+ /**
726
+ * The document-level counterpart to `flattenFields` — turns a `group`
727
+ * field's nested object value (`{ shippingAddress: { city: "..." } }`) into
728
+ * its flattened equivalent (`{ shippingAddress_city: "..." }`) for writing
729
+ * to the DB, recursively. Fields not present in `doc` are simply omitted
730
+ * from the result (lets `update()`'s partial inputs flatten correctly —
731
+ * an absent group means every one of its flattened keys is absent too, not
732
+ * `undefined`-valued). See `nestDoc` for the inverse, used on read.
733
+ */
734
+ declare function flattenDoc(fields: Record<string, FieldConfig>, doc: Record<string, unknown>): Record<string, unknown>;
735
+ /**
736
+ * The inverse of `flattenDoc` — re-nests a flat DB row's `<key>_<subKey>`
737
+ * columns back into `{ key: { subKey: ... } }` for everything the Local
738
+ * API returns to a caller, so a `group` field's document shape always
739
+ * matches its config shape regardless of how it's actually stored.
740
+ */
741
+ declare function nestDoc(fields: Record<string, FieldConfig>, flatRow: Record<string, unknown>): Record<string, unknown>;
742
+ /**
743
+ * Per-operation access check, modeled on Payload's own `access` shape.
744
+ * @returns whether the operation is allowed. Implementations decide their
745
+ * own context shape (auth/session info isn't standardized by Cadmus) — see
746
+ * {@link LocalApi}'s `TContext` generic, which every operation now requires
747
+ * a value for.
748
+ *
749
+ * Enforced by `createLocalApi` since Section 2 — see
750
+ * {@link CollectionConfig.access}.
751
+ */
752
+ type AccessFn<TContext = any> = (context: TContext) => boolean | Promise<boolean>;
753
+ interface CollectionAccess {
754
+ create?: AccessFn;
755
+ read?: AccessFn;
756
+ update?: AccessFn;
757
+ delete?: AccessFn;
758
+ /**
759
+ * Gates `VersionedLocalApi.publish`/`unpublish` (see createVersionedLocalApi
760
+ * in localApi.ts). Separate from `update` — publishing is a distinct
761
+ * privilege from editing a draft, matching Payload's own model.
762
+ */
763
+ publish?: AccessFn;
764
+ }
765
+ /**
766
+ * Lifecycle hooks, modeled on Payload's own hook points. Each is an
767
+ * ordered array, run in sequence. Enforced by `createLocalApi` — see
768
+ * {@link CollectionConfig.hooks}.
769
+ */
770
+ interface CollectionHooks<TDoc = Record<string, unknown>> {
771
+ beforeChange?: Array<(args: {
772
+ data: Partial<TDoc>;
773
+ }) => Partial<TDoc> | Promise<Partial<TDoc>>>;
774
+ /**
775
+ * `operation` distinguishes a freshly-inserted doc from an edited one —
776
+ * `publish()` (versioned collections) counts as `"update"`, since it
777
+ * writes to an already-existing row rather than creating one. Lets
778
+ * webhook config (see `cms/webhooks.ts`) filter which events it fires
779
+ * on without the hook itself tracking state.
780
+ */
781
+ afterChange?: Array<(args: {
782
+ doc: TDoc;
783
+ operation: "create" | "update";
784
+ }) => void | Promise<void>>;
785
+ beforeRead?: Array<(args: {
786
+ doc: TDoc;
787
+ }) => TDoc | Promise<TDoc>>;
788
+ afterRead?: Array<(args: {
789
+ doc: TDoc;
790
+ }) => TDoc | Promise<TDoc>>;
791
+ beforeDelete?: Array<(args: {
792
+ id: number;
793
+ }) => void | Promise<void>>;
794
+ afterDelete?: Array<(args: {
795
+ id: number;
796
+ }) => void | Promise<void>>;
797
+ }
798
+ /**
799
+ * Studio-presentation hints for a collection, modeled on Sanity's Structure
800
+ * Builder (`sanity/structure`). Purely about how the admin sidebar/editor
801
+ * *presents* a collection — never affects the DB schema, the Local API, or
802
+ * access control. Consumed by {@link buildStudioStructure} (see
803
+ * `structure.ts`); a collection with no `admin` block falls back to sensible
804
+ * defaults (visible, editable, listed, grouped under the default group,
805
+ * label = capitalized slug). Plugin-injected collections can't carry an
806
+ * `admin` block in hand-written config, so `buildStudioStructure` also
807
+ * accepts per-slug overrides at the call site — see its `overrides` option.
808
+ */
809
+ interface CollectionAdminConfig {
810
+ /**
811
+ * Sidebar group heading this collection appears under (e.g. "Content",
812
+ * "Store"). Collections without a group fall into the builder's default
813
+ * group. Decoupling nav grouping from the raw collection list is the whole
814
+ * point of the Structure Builder.
815
+ */
816
+ group?: string;
817
+ /**
818
+ * Sort order within a group — lower sorts first. Ties (and the absence of
819
+ * an explicit order) break by the collection's position in the config
820
+ * array, so config order is the stable default.
821
+ */
822
+ order?: number;
823
+ /**
824
+ * Drop this collection from the sidebar entirely. For pure system/log
825
+ * tables a human never browses (e.g. `webhook_events`).
826
+ */
827
+ hidden?: boolean;
828
+ /**
829
+ * Mark as read-only in the studio — still navigable/viewable, but the UI
830
+ * suppresses create/edit/delete affordances. For machine-written tables a
831
+ * human should inspect but never edit (e.g. `payments`).
832
+ */
833
+ readOnly?: boolean;
834
+ /**
835
+ * Singleton: exactly one document. The sidebar links straight to its
836
+ * editor (`/admin/<slug>`) instead of a list-then-create flow — Sanity's
837
+ * singleton-document structure pattern. (Storage is unchanged; this only
838
+ * changes navigation.)
839
+ */
840
+ singleton?: boolean;
841
+ /** Display label override; defaults to a capitalized slug. */
842
+ label?: string;
843
+ /**
844
+ * Optional icon identifier passed through to the sidebar renderer (e.g. a
845
+ * Phosphor icon name). The builder treats it as an opaque string.
846
+ */
847
+ icon?: string;
848
+ }
849
+ interface CollectionConfig {
850
+ /** table name in D1; also the Local API's collection slug (later step) */
851
+ slug: string;
852
+ fields: Record<string, FieldConfig>;
853
+ /**
854
+ * Per-operation access control, enforced by `createLocalApi` (Section 2).
855
+ * Reserved per issue #16 step 7 ("reserve typed config keys now,
856
+ * implementation deferred to Section 2+") — that deferral is over: every
857
+ * `LocalApi` method now requires a `context` argument and runs the
858
+ * matching access function (`read` for `find`/`findByID`, `create` for
859
+ * `create`, etc.) before touching the database. No access function
860
+ * configured for an operation means that operation is unconditionally
861
+ * allowed, matching the pre-enforcement default.
862
+ */
863
+ access?: CollectionAccess;
864
+ /** Lifecycle hooks, enforced by `createLocalApi`. See {@link CollectionHooks}. */
865
+ hooks?: CollectionHooks;
866
+ /**
867
+ * Opts this collection into draft/version history. When `drafts` is
868
+ * true, codegen (see codegen.ts/schema-gen.ts) generates a companion
869
+ * `${slug}_versions` table and a nullable `published_version_id` pointer
870
+ * column on the main table, and `createVersionedLocalApi` (localApi.ts)
871
+ * becomes usable against it. Collections without this stay exactly as
872
+ * before — no versions table, no extra column, only `createLocalApi`.
873
+ */
874
+ versions?: {
875
+ drafts?: boolean; /** Reserved for future pruning of old versions; not enforced yet. */
876
+ maxPerDoc?: number;
877
+ };
878
+ /**
879
+ * Opts this collection into full-text search (issue #29). `fields` names
880
+ * which of this collection's own `text`/`richText`/`upload` fields are
881
+ * indexed — `defineCmsConfig`/`defineCollection` reject any other field
882
+ * type or an unknown key. When set, codegen (see codegen.ts's
883
+ * `collectionSearchTableSQL`) describes a companion `${slug}_fts` SQLite
884
+ * FTS5 virtual table, and `createLocalApi` both becomes able to run
885
+ * `.search()` and keeps that table in sync on every create/update/delete
886
+ * — see localApi.ts's `syncSearchIndex`. `richText` fields are flattened
887
+ * to plain text (TipTap JSON's `text` leaves, concatenated) before being
888
+ * indexed; nested `array`/block content is out of scope for this phase.
889
+ */
890
+ search?: {
891
+ fields: readonly string[];
892
+ };
893
+ /**
894
+ * Studio-presentation hints — grouping, ordering, hidden/read-only,
895
+ * singleton, label, icon. Consumed only by {@link buildStudioStructure}
896
+ * (the Structure Builder); never affects schema, Local API, or access.
897
+ * See {@link CollectionAdminConfig}.
898
+ */
899
+ admin?: CollectionAdminConfig;
900
+ }
901
+ /**
902
+ * A Cadmea plugin — a synchronous transform over the whole CMS config,
903
+ * modeled on Payload's `plugins: [(config) => config]` shape. A plugin may
904
+ * add or modify collections, inject fields, or register lifecycle hooks.
905
+ * `defineCmsConfig` runs plugins in array order, each receiving the output
906
+ * of the previous one, *before* validation — so a plugin's output is held
907
+ * to the same rules as a hand-written config.
908
+ *
909
+ * Synchronous in Section 2 by design: the resolved config is consumed by
910
+ * schema codegen and runtime config loading, both of which are sync. An
911
+ * async variant is a deliberate later extension, not an oversight.
912
+ */
913
+ type CadmeaPlugin = (config: CmsConfig) => CmsConfig;
914
+ interface CmsConfig {
915
+ collections: CollectionConfig[];
916
+ /**
917
+ * Config transforms run in order by `defineCmsConfig` before validation.
918
+ * See {@link CadmeaPlugin}. Omit for a plain, plugin-free config.
919
+ */
920
+ plugins?: CadmeaPlugin[];
921
+ }
922
+ //#endregion
923
+ //#region src/cms/codegen.d.ts
924
+ declare function collectionToTable(config: CollectionConfig): import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
925
+ name: string;
926
+ schema: undefined;
927
+ columns: {
928
+ [x: string]: import("drizzle-orm/sqlite-core").SQLiteColumn<{
929
+ name: string;
930
+ tableName: string;
931
+ dataType: import("drizzle-orm").ColumnDataType;
932
+ columnType: string;
933
+ data: unknown;
934
+ driverParam: unknown;
935
+ notNull: false;
936
+ hasDefault: false;
937
+ isPrimaryKey: false;
938
+ isAutoincrement: false;
939
+ hasRuntimeDefault: false;
940
+ enumValues: string[] | undefined;
941
+ baseColumn: never;
942
+ identity: undefined;
943
+ generated: undefined;
944
+ }, {}, {}>;
945
+ };
946
+ dialect: "sqlite";
947
+ }>;
948
+ declare function collectionVersionsTable(config: CollectionConfig): import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
949
+ name: `${string}_versions`;
950
+ schema: undefined;
951
+ columns: {
952
+ id: import("drizzle-orm/sqlite-core").SQLiteColumn<{
953
+ name: "id";
954
+ tableName: `${string}_versions`;
955
+ dataType: "number";
956
+ columnType: "SQLiteInteger";
957
+ data: number;
958
+ driverParam: number;
959
+ notNull: true;
960
+ hasDefault: true;
961
+ isPrimaryKey: true;
962
+ isAutoincrement: false;
963
+ hasRuntimeDefault: false;
964
+ enumValues: undefined;
965
+ baseColumn: never;
966
+ identity: undefined;
967
+ generated: undefined;
968
+ }, {}, {}>;
969
+ parentId: import("drizzle-orm/sqlite-core").SQLiteColumn<{
970
+ name: "parent_id";
971
+ tableName: `${string}_versions`;
972
+ dataType: "number";
973
+ columnType: "SQLiteInteger";
974
+ data: number;
975
+ driverParam: number;
976
+ notNull: true;
977
+ hasDefault: false;
978
+ isPrimaryKey: false;
979
+ isAutoincrement: false;
980
+ hasRuntimeDefault: false;
981
+ enumValues: undefined;
982
+ baseColumn: never;
983
+ identity: undefined;
984
+ generated: undefined;
985
+ }, {}, {}>;
986
+ versionData: import("drizzle-orm/sqlite-core").SQLiteColumn<{
987
+ name: "version_data";
988
+ tableName: `${string}_versions`;
989
+ dataType: "json";
990
+ columnType: "SQLiteTextJson";
991
+ data: JsonValue;
992
+ driverParam: string;
993
+ notNull: true;
994
+ hasDefault: false;
995
+ isPrimaryKey: false;
996
+ isAutoincrement: false;
997
+ hasRuntimeDefault: false;
998
+ enumValues: undefined;
999
+ baseColumn: never;
1000
+ identity: undefined;
1001
+ generated: undefined;
1002
+ }, {}, {
1003
+ $type: JsonValue;
1004
+ }>;
1005
+ status: import("drizzle-orm/sqlite-core").SQLiteColumn<{
1006
+ name: "status";
1007
+ tableName: `${string}_versions`;
1008
+ dataType: "string";
1009
+ columnType: "SQLiteText";
1010
+ data: "draft" | "published";
1011
+ driverParam: string;
1012
+ notNull: true;
1013
+ hasDefault: false;
1014
+ isPrimaryKey: false;
1015
+ isAutoincrement: false;
1016
+ hasRuntimeDefault: false;
1017
+ enumValues: ["draft", "published"];
1018
+ baseColumn: never;
1019
+ identity: undefined;
1020
+ generated: undefined;
1021
+ }, {}, {
1022
+ length: number | undefined;
1023
+ }>;
1024
+ createdAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
1025
+ name: "created_at";
1026
+ tableName: `${string}_versions`;
1027
+ dataType: "date";
1028
+ columnType: "SQLiteTimestamp";
1029
+ data: Date;
1030
+ driverParam: number;
1031
+ notNull: false;
1032
+ hasDefault: true;
1033
+ isPrimaryKey: false;
1034
+ isAutoincrement: false;
1035
+ hasRuntimeDefault: true;
1036
+ enumValues: undefined;
1037
+ baseColumn: never;
1038
+ identity: undefined;
1039
+ generated: undefined;
1040
+ }, {}, {}>;
1041
+ };
1042
+ dialect: "sqlite";
1043
+ }>;
1044
+ declare function relationshipJoinTables(config: CollectionConfig): Record<string, ReturnType<typeof sqliteTable>>;
1045
+ declare function collectionSearchTableName(config: CollectionConfig): string;
1046
+ declare function collectionSearchTableSQL(config: CollectionConfig): string;
1047
+ declare function extractSearchText(config: CollectionConfig, doc: Record<string, unknown>): string[];
1048
+ declare function cmsConfigToSchema(config: CmsConfig): Record<string, ReturnType<typeof collectionToTable> | ReturnType<typeof collectionVersionsTable>>;
1049
+ //#endregion
1050
+ //#region src/cms/defineCollection.d.ts
1051
+ declare function defineCollection(config: CollectionConfig): CollectionConfig;
1052
+ declare function defineCmsConfig(config: CmsConfig): CmsConfig;
1053
+ //#endregion
1054
+ //#region src/cms/meta.d.ts
1055
+ interface CollectionMeta {
1056
+ slug: string;
1057
+ fields: CollectionConfig["fields"];
1058
+ /** Whether `LocalApi.search()` is usable for this collection — see `CollectionConfig.search`. */
1059
+ searchable: boolean;
1060
+ }
1061
+ declare function getCollectionsMeta(config: CmsConfig): CollectionMeta[];
1062
+ //#endregion
1063
+ //#region src/cms/migrate.d.ts
1064
+ /**
1065
+ * Content-migration runner (issue #18) — adopts Sanity's `sanity/migrate`
1066
+ * idea (pattern, not code): a versioned, repeatable transform over a
1067
+ * collection's stored documents, for reshaping content when a block/field
1068
+ * type changes (distinct from Drizzle *schema* migrations, which only touch
1069
+ * columns — this reshapes the JSON content inside them).
1070
+ *
1071
+ * A migration declares a per-document `document(doc)` transform; the runner
1072
+ * streams every document, computes the {@link Patch} from old→new (reusing
1073
+ * #14's patch model), and either reports it (`dryRun`) or applies it via the
1074
+ * collection's Local API. Idempotent by construction: a transform that's
1075
+ * already been applied produces an empty patch, so re-running changes
1076
+ * nothing.
1077
+ */
1078
+ type Doc = Record<string, JsonValue>;
1079
+ interface Migration<TDoc extends Doc = Doc> {
1080
+ /** Stable identifier — name the checked-in migration file after this. */
1081
+ name: string;
1082
+ /**
1083
+ * Transform one document. Return the reshaped document, or `undefined`
1084
+ * (or the unchanged doc) to leave it as-is. Must be pure and idempotent —
1085
+ * applying it twice yields the same result as once.
1086
+ */
1087
+ document: (doc: TDoc) => TDoc | undefined | Promise<TDoc | undefined>;
1088
+ }
1089
+ /** Identity helper — gives a migration definition its type + a greppable call site. */
1090
+ declare function defineMigration<TDoc extends Doc = Doc>(migration: Migration<TDoc>): Migration<TDoc>;
1091
+ interface MigrationChange {
1092
+ id: number;
1093
+ patch: Patch;
1094
+ }
1095
+ interface MigrationResult {
1096
+ migration: string;
1097
+ dryRun: boolean;
1098
+ scanned: number;
1099
+ changed: number;
1100
+ /** Per-document patches (always populated — the dry-run report). */
1101
+ changes: MigrationChange[];
1102
+ errors: string[];
1103
+ }
1104
+ interface RunMigrationOptions<TContext> {
1105
+ api: LocalApi<any, TContext>;
1106
+ /** Context passed to the Local API's read/update (access + hooks). */
1107
+ context: TContext;
1108
+ /** When true, compute + report patches but write nothing. Default false. */
1109
+ dryRun?: boolean;
1110
+ }
1111
+ /**
1112
+ * Run a migration over every document in a collection. Reads all documents
1113
+ * through `api.find`, applies `migration.document`, and (unless `dryRun`)
1114
+ * writes the resulting patch through `api.update`. Returns a report of what
1115
+ * changed — run it `dryRun` first, then apply.
1116
+ */
1117
+ declare function runMigration<TContext>(migration: Migration, options: RunMigrationOptions<TContext>): Promise<MigrationResult>;
1118
+ //#endregion
1119
+ //#region src/cms/schema-gen.d.ts
1120
+ declare function generateSchemaSource(config: CmsConfig): string;
1121
+ //#endregion
1122
+ //#region src/cms/structure.d.ts
1123
+ /**
1124
+ * Cadmea's Structure Builder — the framework half of issue #12.
1125
+ *
1126
+ * Adopts Sanity's `sanity/structure` idea (pattern, not code): **decouple
1127
+ * the admin nav from the raw collection list.** Instead of mapping every
1128
+ * `config.collections` entry to an `/admin/<slug>` link — which surfaces
1129
+ * system/log tables as editable links and produces dead links — the sidebar
1130
+ * renders from an explicit, grouped structure derived here from each
1131
+ * collection's `admin` hints (see {@link CollectionAdminConfig}) plus
1132
+ * optional per-slug overrides supplied at the call site.
1133
+ *
1134
+ * Pure data in / pure data out: no SolidJS, no DOM, no server imports — so
1135
+ * it's safe to import from a client studio component (e.g. the site's
1136
+ * `PanelNav`) and trivially testable.
1137
+ */
1138
+ /** Default group heading for collections that don't declare `admin.group`. */
1139
+ declare const DEFAULT_STUDIO_GROUP = "Content";
1140
+ /** One navigable collection entry in the studio sidebar. */
1141
+ interface StudioStructureItem {
1142
+ /** The collection's slug. */
1143
+ slug: string;
1144
+ /** Human label — `admin.label`, else the capitalized slug. */
1145
+ label: string;
1146
+ /** Where the sidebar link points (`/admin/<slug>`, configurable prefix). */
1147
+ href: string;
1148
+ /** Read-only collections are viewable but not editable in the studio. */
1149
+ readOnly: boolean;
1150
+ /**
1151
+ * Singletons link straight to their editor rather than a list+create
1152
+ * flow. (The href is identical; the renderer uses this to skip the list.)
1153
+ */
1154
+ singleton: boolean;
1155
+ /** Opaque icon identifier from `admin.icon`, if any. */
1156
+ icon?: string;
1157
+ }
1158
+ /** A titled group of sidebar items, in render order. */
1159
+ interface StudioStructureGroup {
1160
+ title: string;
1161
+ items: StudioStructureItem[];
1162
+ }
1163
+ interface BuildStudioStructureOptions {
1164
+ /**
1165
+ * Per-slug presentation overrides, merged over each collection's own
1166
+ * `admin` block (override keys win). The escape hatch for plugin-injected
1167
+ * collections (`products`, `payments`, `webhook_events`, …) that can't
1168
+ * carry an `admin` block in hand-written config — the studio declares
1169
+ * their presentation here, exactly like Sanity defines structure at the
1170
+ * studio level rather than on the schema.
1171
+ */
1172
+ overrides?: Record<string, CollectionAdminConfig>;
1173
+ /**
1174
+ * Explicit group ordering by title. Groups listed here render first, in
1175
+ * this order; any remaining groups follow in first-appearance order. A
1176
+ * group title absent from `config`'s collections simply doesn't appear.
1177
+ */
1178
+ groupOrder?: readonly string[];
1179
+ /**
1180
+ * Link prefix for each item's `href`. Defaults to `/admin`, producing
1181
+ * `/admin/<slug>`. No trailing slash.
1182
+ */
1183
+ basePath?: string;
1184
+ }
1185
+ /**
1186
+ * Build the studio sidebar structure from a resolved CMS config.
1187
+ *
1188
+ * - Hidden collections (`admin.hidden`) are dropped entirely.
1189
+ * - Each remaining collection is placed in its `admin.group` (or
1190
+ * {@link DEFAULT_STUDIO_GROUP}).
1191
+ * - Within a group, items sort by `admin.order` (ascending; unset sorts
1192
+ * after set), then by their original position in `config.collections` —
1193
+ * so config order is the stable tiebreaker.
1194
+ * - Groups render in `options.groupOrder` first, then first-appearance
1195
+ * order for the rest.
1196
+ *
1197
+ * The input is expected to be the *resolved* config (post-plugins), since
1198
+ * that's what carries plugin-injected collections like `products`.
1199
+ */
1200
+ declare function buildStudioStructure(config: CmsConfig, options?: BuildStudioStructureOptions): StudioStructureGroup[];
1201
+ //#endregion
1202
+ //#region src/cms/visual-editing.d.ts
1203
+ /**
1204
+ * Visual editing / click-to-edit (issue #15) — adopts Sanity's
1205
+ * Presentation/visual-editing idea (pattern, not code): the rendered page
1206
+ * (in a preview context) tags editable regions with the source field they
1207
+ * came from, and an overlay turns those regions into click targets that tell
1208
+ * the studio which field to focus.
1209
+ *
1210
+ * This module ships the two reusable, framework-agnostic primitives:
1211
+ * 1. **Encoding** — `editAttr({ collection, id, field })` produces a data
1212
+ * attribute the server renderer spreads onto an element; `decodeEditRef`
1213
+ * reads it back. Pure, testable.
1214
+ * 2. **Overlay** — `mountVisualEditing()` (browser-only; references `document`
1215
+ * lazily, so importing it server-side is harmless) highlights tagged
1216
+ * elements on hover and, on click, calls `onSelect` and `postMessage`s the
1217
+ * ref to the parent window (the studio shell hosting the preview iframe).
1218
+ *
1219
+ * The studio side listens for that message and navigates to
1220
+ * `/admin/<collection>/<id>` (and may focus `<field>`); that wiring is
1221
+ * consumer-side and not prescribed here.
1222
+ */
1223
+ /** A reference from a rendered region back to the field that produced it. */
1224
+ interface EditRef {
1225
+ collection: string;
1226
+ id: number;
1227
+ field: string;
1228
+ }
1229
+ /** The data attribute editable regions are tagged with. */
1230
+ declare const EDIT_ATTR = "data-cadmus-edit";
1231
+ /** `postMessage` payload type for a click-to-edit selection. */
1232
+ declare const VISUAL_EDIT_MESSAGE = "cadmus:visual-edit";
1233
+ declare function encodeEditRef(ref: EditRef): string;
1234
+ /** Parse an {@link EditRef} string, or null if malformed. */
1235
+ declare function decodeEditRef(value: string): EditRef | null;
1236
+ /**
1237
+ * Attribute object to spread onto a rendered element so the overlay can map
1238
+ * it back to its source field, e.g. `<h1 {...editAttr({collection:'pages',
1239
+ * id, field:'title'})}>`.
1240
+ */
1241
+ declare function editAttr(ref: EditRef): Record<string, string>;
1242
+ interface VisualEditingMessage {
1243
+ type: typeof VISUAL_EDIT_MESSAGE;
1244
+ ref: EditRef;
1245
+ }
1246
+ interface VisualEditingOptions {
1247
+ /** Called with the decoded ref when an editable region is clicked. */
1248
+ onSelect?: (ref: EditRef, element: Element) => void;
1249
+ /**
1250
+ * Origin to `postMessage` the selection to the parent window. Default
1251
+ * `"*"`. Set to the studio origin in production.
1252
+ */
1253
+ targetOrigin?: string;
1254
+ /** Outline color for the hover highlight. Default a teal accent. */
1255
+ highlightColor?: string;
1256
+ }
1257
+ /**
1258
+ * Mount the click-to-edit overlay. Browser-only — call from a preview page's
1259
+ * client script. Highlights `[data-cadmus-edit]` elements on hover and, on
1260
+ * click, calls `onSelect` and posts a {@link VisualEditingMessage} to the
1261
+ * parent window. Returns a cleanup function that removes the listeners.
1262
+ */
1263
+ declare function mountVisualEditing(options?: VisualEditingOptions): () => void;
1264
+ //#endregion
1265
+ //#region src/cms/webhooks.d.ts
1266
+ interface WebhookConfig {
1267
+ /** Endpoint this webhook POSTs to on every matching event. */
1268
+ url: string;
1269
+ /** Restricts delivery to these operations. Default: both. */
1270
+ events?: Array<"create" | "update">;
1271
+ /**
1272
+ * When set, every delivery carries an `X-Cadmus-Signature` header —
1273
+ * HMAC-SHA256 (hex) over the raw JSON body — so the receiver can verify
1274
+ * the payload actually came from this Cadmus instance.
1275
+ */
1276
+ secret?: string;
1277
+ }
1278
+ /** The shape enqueued by `createWebhookHook`, consumed by `deliverWebhookMessage`. */
1279
+ interface WebhookMessage {
1280
+ url: string;
1281
+ secret?: string;
1282
+ event: "create" | "update";
1283
+ doc: Record<string, unknown>;
1284
+ /** ms since epoch, included in the signed/delivered payload. */
1285
+ timestamp: number;
1286
+ }
1287
+ /**
1288
+ * Builds an `afterChange` hook that enqueues a `WebhookMessage` for every
1289
+ * matching write — append the result to a collection's
1290
+ * `hooks.afterChange` array. `queue` is whatever `Queue<WebhookMessage>`
1291
+ * binding the caller's Worker has configured for webhook dispatch (see
1292
+ * wrangler.jsonc's webhook queue producer binding).
1293
+ */
1294
+ declare function createWebhookHook(queue: Queue<WebhookMessage>, config: WebhookConfig): NonNullable<CollectionHooks["afterChange"]>[number];
1295
+ /**
1296
+ * Delivers a single `WebhookMessage` via `fetch()`. Throws
1297
+ * `CadmusQueueError` on any non-2xx response or network failure — meant
1298
+ * to be called from inside `processBatch`'s handler, where a thrown error
1299
+ * becomes a `message.retry()`.
1300
+ */
1301
+ declare function deliverWebhookMessage(message: WebhookMessage): Promise<void>;
1302
+ //#endregion
1303
+ export { RelationshipDepth as $, cmsConfigToSchema as A, PatchOp as At, CadmeaPlugin as B, CadmusEmailError as Bt, RunMigrationOptions as C, createLocalApi as Ct, getCollectionsMeta as D, FieldChange as Dt, CollectionMeta as E, DiffOptions as Et, extractSearchText as F, CadmusApiError as Ft, CollectionConfig as G, CadmusStorageError as Gt, CmsConfig as H, CadmusQueueError as Ht, relationshipJoinTables as I, CadmusAuthError as It, FieldConfig as J, BlockRegistry as Jt, CollectionHooks as K, CadmusValidationError as Kt, AccessFn as L, CadmusCacheError as Lt, collectionSearchTableSQL as M, computePatch as Mt, collectionToTable as N, diffDocuments as Nt, defineCmsConfig as O, FieldChangeKind as Ot, collectionVersionsTable as P, CadmusAccessDeniedError as Pt, NumberFieldConfig as Q, renderBlocksToString as Qt, ArrayFieldConfig as R, CadmusCmsError as Rt, MigrationResult as S, can as St, runMigration as T, getRegisteredApi as Tt, CollectionAccess as U, CadmusRateLimitError as Ut, CheckboxFieldConfig as V, CadmusError as Vt, CollectionAdminConfig as W, CadmusSessionError as Wt, JsonFieldConfig as X, StringBlockRenderer as Xt, GroupFieldConfig as Y, PortableBlockLike as Yt, JsonValue as Z, createBlockRegistry as Zt, StudioStructureItem as _, rule as _t, EDIT_ATTR as a, flattenDoc as at, Migration as b, LocalApi as bt, VisualEditingMessage as c, CustomValidator as ct, editAttr as d, ValidateDocumentOptions as dt, RelationshipFieldConfig as et, encodeEditRef as f, ValidationBuilder as ft, StudioStructureGroup as g, defineField as gt, DEFAULT_STUDIO_GROUP as h, assertValid as ht, deliverWebhookMessage as i, UploadFieldConfig as it, collectionSearchTableName as j, applyPatch as jt, defineCollection as k, Patch as kt, VisualEditingOptions as l, CustomValidatorResult as lt, BuildStudioStructureOptions as m, ValidationSeverity as mt, WebhookMessage as n, SelectFieldConfig as nt, EditRef as o, flattenFields as ot, mountVisualEditing as p, ValidationFieldContext as pt, DateFieldConfig as q, ValidationViolation as qt, createWebhookHook as r, TextFieldConfig as rt, VISUAL_EDIT_MESSAGE as s, nestDoc as st, WebhookConfig as t, RichTextFieldConfig as tt, decodeEditRef as u, Rule as ut, buildStudioStructure as v, validateDocument as vt, defineMigration as w, createVersionedLocalApi as wt, MigrationChange as x, VersionedLocalApi as xt, generateSchemaSource as y, CmsRegistry as yt, BaseFieldConfig as z, CadmusDbError as zt };
1304
+ //# sourceMappingURL=index-sB3YOadC.d.cts.map