@thebes/cadmus 0.2.1 → 0.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.
Files changed (41) hide show
  1. package/dist/cms/index.cjs +458 -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 +451 -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-BUrCSGVb.d.ts → index-BRZrCTsN.d.cts} +641 -145
  22. package/dist/index-BRZrCTsN.d.cts.map +1 -0
  23. package/dist/{index-BUrCSGVb.d.cts → index-BRZrCTsN.d.ts} +641 -145
  24. package/dist/index-BRZrCTsN.d.ts.map +1 -0
  25. package/dist/index.cjs +11 -1
  26. package/dist/index.d.cts +2 -88
  27. package/dist/index.d.cts.map +1 -1
  28. package/dist/index.d.ts +2 -88
  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.js +1 -1
  39. package/package.json +8 -8
  40. package/dist/index-BUrCSGVb.d.cts.map +0 -1
  41. package/dist/index-BUrCSGVb.d.ts.map +0 -1
@@ -1,6 +1,499 @@
1
1
  import { BaseSQLiteDatabase, SQLiteTableWithColumns, sqliteTable } from "drizzle-orm/sqlite-core";
2
2
  import { InferInsertModel, InferSelectModel, SQL } from "drizzle-orm";
3
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/localApi.d.ts
184
+ type AnyTable$1 = SQLiteTableWithColumns<any>;
185
+ /**
186
+ * `TContext` is the per-request value passed to every method and forwarded
187
+ * unchanged to the collection's `access` functions (see {@link CollectionAccess}).
188
+ * Cadmus doesn't standardize its shape — Cadmea types it as `{ session }`,
189
+ * other consumers may type it differently. `context` is a required first
190
+ * argument on every method (not optional) so a call site can't forget it.
191
+ */
192
+ interface LocalApi<TTable extends AnyTable$1, TContext = unknown> {
193
+ /**
194
+ * `depth: 0` (default) returns relationship fields as bare ids; `depth: 1`
195
+ * batch-resolves `hasMany: false` relationship fields into the related
196
+ * row, gated by that collection's own `read` access fn — see
197
+ * `resolveRelationships` below. Requires `createLocalApi`'s `registry`
198
+ * param; throws CadmusCmsError if `depth: 1` is requested without one.
199
+ */
200
+ find(context: TContext, options?: {
201
+ where?: SQL;
202
+ depth?: RelationshipDepth; /** Row cap, applied after `where` — for paginated list views. */
203
+ limit?: number; /** Rows to skip, applied after `where` — pairs with `limit`. */
204
+ offset?: number; /** One or more `asc(table.col)`/`desc(table.col)` expressions. */
205
+ orderBy?: SQL | SQL[];
206
+ }): Promise<InferSelectModel<TTable>[]>;
207
+ findByID(context: TContext, id: number, options?: {
208
+ depth?: RelationshipDepth;
209
+ }): Promise<InferSelectModel<TTable>>;
210
+ /**
211
+ * Total row count for `where` (ignoring `limit`/`offset`) — pairs with
212
+ * `find` to compute page counts/next-page availability without fetching
213
+ * every row. Gated by the same `read` access check as `find`.
214
+ */
215
+ count(context: TContext, options?: {
216
+ where?: SQL;
217
+ }): Promise<number>;
218
+ /**
219
+ * Full-text search over this collection's `search.fields`-configured
220
+ * companion FTS5 table — see types.ts's `CollectionConfig.search` and
221
+ * codegen.ts's `collectionSearchTableSQL`. Gated by `read` access, same
222
+ * as `find`/`findByID`. Throws `CadmusCmsError` if the collection has no
223
+ * `search` config.
224
+ */
225
+ search(context: TContext, query: string, options?: {
226
+ limit?: number;
227
+ }): Promise<InferSelectModel<TTable>[]>;
228
+ create(context: TContext, input: InferInsertModel<TTable>): Promise<InferSelectModel<TTable>>;
229
+ update(context: TContext, id: number, input: Partial<InferInsertModel<TTable>>): Promise<InferSelectModel<TTable>>;
230
+ deleteByID(context: TContext, id: number): Promise<InferSelectModel<TTable>>;
231
+ }
232
+ /**
233
+ * Lets `createLocalApi` resolve `depth: 1` relationship fields without
234
+ * importing every other collection's Local API (which would be a circular
235
+ * dependency the moment two collections relate to each other). The
236
+ * registry is just the raw ingredients — a table and a config per
237
+ * collection slug — built once by the app (e.g. from `cadmeaConfig.collections`)
238
+ * and passed to every `createLocalApi` call that has relationship fields.
239
+ *
240
+ * `apis` is a second, optional registry on the same object, for a
241
+ * different problem: a *hook* (not `createLocalApi` itself) on one
242
+ * collection that needs to write to *another* collection's Local API —
243
+ * e.g. a CRM upsert hook on a lead-capture collection that creates/updates
244
+ * `contacts`/`activities` rows. `tables`/`configs` can't serve this, since
245
+ * a hook needs a real `LocalApi` (with its own access/hooks/search wiring
246
+ * already applied), not raw ingredients to rebuild one from.
247
+ *
248
+ * The chicken-and-egg problem this solves: building collection A's
249
+ * `LocalApi` might need to reference collection B's `LocalApi` (for a
250
+ * hook), but collection B's `LocalApi` doesn't exist yet at the point A's
251
+ * is constructed — and vice versa if B also has a hook referencing A.
252
+ * The fix is **late binding**: build one `CmsRegistry` object, pass the
253
+ * *same reference* into every `createLocalApi` call (so every collection's
254
+ * hooks close over the same mutable object), construct every `LocalApi`,
255
+ * then fill in `registry.apis` afterwards:
256
+ *
257
+ * ```ts
258
+ * const registry: CmsRegistry = { tables, configs, apis: {} };
259
+ * const contactsApi = createLocalApi(db, contactsTable, contactsCollection, registry);
260
+ * const inquiriesApi = createLocalApi(db, inquiriesTable, inquiriesCollection, registry);
261
+ * // populate *after* every createLocalApi call returns — any hook that
262
+ * // reads registry.apis lazily (inside its returned function body, not
263
+ * // at hook-factory-call time) sees the fully-populated map, since hooks
264
+ * // only ever run once real requests start landing.
265
+ * Object.assign(registry.apis!, { contacts: contactsApi, inquiries: inquiriesApi });
266
+ * ```
267
+ *
268
+ * See `getRegisteredApi` for the accessor a hook factory should use to
269
+ * read from this map, rather than indexing `registry.apis` directly.
270
+ */
271
+ interface CmsRegistry {
272
+ tables: Record<string, AnyTable$1>;
273
+ configs: Record<string, CollectionConfig>;
274
+ apis?: Record<string, LocalApi<AnyTable$1, any>>;
275
+ }
276
+ /**
277
+ * Reads collection `slug`'s `LocalApi` out of `registry.apis` — the
278
+ * accessor hook factories should use (see `CmsRegistry`'s doc comment for
279
+ * the late-binding pattern this assumes) instead of indexing
280
+ * `registry.apis` directly, so every caller gets the same clear error if
281
+ * the registry wasn't built/populated correctly. `TContext` is a type-only
282
+ * parameter (the registry itself is stored with `never` to stay variance-
283
+ * safe across collections with different context shapes) — callers assert
284
+ * the context type they expect, the same way `resolveRelationships`'s own
285
+ * registry lookups do.
286
+ */
287
+ declare function getRegisteredApi<TContext>(registry: CmsRegistry | undefined, slug: string): LocalApi<AnyTable$1, TContext>;
288
+ /**
289
+ * Non-throwing counterpart to `checkAccess` below, for UI code that wants
290
+ * to hide/disable an action a context can't perform rather than let it
291
+ * fail server-side after a click (see Phase 6 / issue #26's
292
+ * `getPageCapabilities`). `checkAccess` calls through this same function
293
+ * rather than duplicating the "no access fn = allowed" logic, so `can()`'s
294
+ * answer and the real operation's enforcement can never disagree.
295
+ */
296
+ declare function can<TContext>(config: CollectionConfig, operation: keyof CollectionAccess, context: TContext): Promise<boolean>;
297
+ declare function createLocalApi<TTable extends AnyTable$1, TContext = unknown>(db: BaseSQLiteDatabase<"async", unknown>, table: TTable, config: CollectionConfig, registry?: CmsRegistry): LocalApi<TTable, TContext>;
298
+ /**
299
+ * Extends {@link LocalApi} with draft/publish operations for a collection
300
+ * that opted in via `CollectionConfig.versions.drafts` (see codegen.ts's
301
+ * `collectionVersionsTable`). A separate interface (not a wider
302
+ * `LocalApi`) so non-versioned collections' types don't grow these methods
303
+ * — TypeScript can't conditionally widen `createLocalApi`'s return type
304
+ * off a runtime config value, so this is `createVersionedLocalApi`'s own
305
+ * factory rather than a branch inside `createLocalApi`.
306
+ *
307
+ * Scope, deliberately: a document is always created via the inherited
308
+ * `create()` first (existing behavior, unaffected by versioning) — these
309
+ * methods operate against an *existing* row. `saveDraft` never validates
310
+ * required fields (an incomplete draft is valid); `publish` runs the same
311
+ * full validation `create`/`update` do, since publishing is what makes a
312
+ * version the public-facing document. Plain `find`/`findByID` are
313
+ * unchanged by any of this — they always return the main table's current
314
+ * row regardless of `publishedVersionId`; filtering reads to
315
+ * published-only content is not this phase's concern.
316
+ */
317
+ interface VersionedLocalApi<TTable extends AnyTable$1, TVersionsTable extends AnyTable$1, TContext = unknown> extends LocalApi<TTable, TContext> {
318
+ findVersions(context: TContext, parentId: number): Promise<InferSelectModel<TVersionsTable>[]>;
319
+ /** Inserts a new version row holding `input` as a draft snapshot. */
320
+ saveDraft(context: TContext, id: number, input: Partial<InferInsertModel<TTable>>): Promise<InferSelectModel<TVersionsTable>>;
321
+ /** Copies a version's snapshot onto the main row and marks it published. */
322
+ publish(context: TContext, versionId: number): Promise<InferSelectModel<TTable>>;
323
+ /** Clears the main row's published pointer; the row's data is untouched. */
324
+ unpublish(context: TContext, id: number): Promise<InferSelectModel<TTable>>;
325
+ }
326
+ 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>;
327
+ //#endregion
328
+ //#region src/cms/validation.d.ts
329
+ type AnyTable = SQLiteTableWithColumns<any>;
330
+ /**
331
+ * Chainable field validation for Cadmea (issue #16) — adopts Sanity's
332
+ * `defineField`/`Rule` validation API (pattern, not code). A field declares
333
+ * `validation: (rule) => rule.required().min(2).custom(...)`; this module
334
+ * turns that chain into a list of declarative checks and evaluates them at
335
+ * write time (server-side, in createLocalApi) as well as anywhere the studio
336
+ * wants synchronous feedback.
337
+ *
338
+ * Design notes:
339
+ * - The builder is **immutable** — every method returns a new {@link Rule}
340
+ * with one more check appended, so a shared base rule can't be mutated by
341
+ * a consumer's chain (mirrors Sanity).
342
+ * - Most checks are synchronous and pure (min/max/regex/custom over the
343
+ * value alone). Two — `unique` and `reference` — need the database and so
344
+ * only run where {@link validateDocument} is given a `db` (i.e. the Local
345
+ * API); they're skipped (not failed) in a pure client-side pass.
346
+ */
347
+ type ValidationSeverity = "error" | "warning";
348
+ /**
349
+ * What a {@link CustomValidator} may return:
350
+ * - `true` / `undefined` → valid
351
+ * - `false` → invalid, generic message
352
+ * - `string` → invalid, that message
353
+ * - `{ message, severity? }` → invalid, that message at the given severity
354
+ */
355
+ type CustomValidatorResult = boolean | undefined | string | {
356
+ message: string;
357
+ severity?: ValidationSeverity;
358
+ };
359
+ interface ValidationFieldContext {
360
+ /** The whole document being validated (nested shape, post-hooks). */
361
+ document: Record<string, unknown>;
362
+ /** This field's flattened key (e.g. `slug`, `shippingAddress_city`). */
363
+ path: string;
364
+ /** Whether this is a create or an update. */
365
+ operation: "create" | "update";
366
+ /** The document's id on update — lets `unique` exclude the row itself. */
367
+ id?: number;
368
+ }
369
+ type CustomValidator = (value: unknown, context: ValidationFieldContext) => CustomValidatorResult | Promise<CustomValidatorResult>;
370
+ type Check = ({
371
+ kind: "required";
372
+ } | {
373
+ kind: "min";
374
+ n: number;
375
+ } | {
376
+ kind: "max";
377
+ n: number;
378
+ } | {
379
+ kind: "length";
380
+ n: number;
381
+ } | {
382
+ kind: "regex";
383
+ re: RegExp;
384
+ label: string;
385
+ } | {
386
+ kind: "integer";
387
+ } | {
388
+ kind: "positive";
389
+ } | {
390
+ kind: "unique";
391
+ } | {
392
+ kind: "reference";
393
+ } | {
394
+ kind: "custom";
395
+ fn: CustomValidator;
396
+ }) & {
397
+ message?: string;
398
+ severity?: ValidationSeverity;
399
+ };
400
+ /**
401
+ * Immutable, chainable rule builder — the value a field's `validation`
402
+ * function receives and returns. Build a `Rule` with the module-level
403
+ * {@link rule} factory, or accept the one passed to your `validation`
404
+ * callback.
405
+ */
406
+ declare class Rule {
407
+ private readonly checks;
408
+ constructor(checks?: readonly Check[]);
409
+ private add;
410
+ /** Override the message of the most recently added check. */
411
+ error(message: string): Rule;
412
+ /**
413
+ * Demote the most recently added check to a warning (non-blocking),
414
+ * optionally with a message. Sanity's `Rule.warning()` analogue.
415
+ */
416
+ warning(message?: string): Rule;
417
+ private withLast;
418
+ required(): Rule;
419
+ /** Minimum string length / array length / numeric value. */
420
+ min(n: number): Rule;
421
+ /** Maximum string length / array length / numeric value. */
422
+ max(n: number): Rule;
423
+ /** Exact string/array length. */
424
+ length(n: number): Rule;
425
+ regex(re: RegExp, label?: string): Rule;
426
+ email(): Rule;
427
+ /** Lowercase kebab-case slug format. Pair with `.unique()` for slugs. */
428
+ slug(): Rule;
429
+ integer(): Rule;
430
+ positive(): Rule;
431
+ /**
432
+ * Value must be unique across the collection (DB-backed; skipped in a
433
+ * pure client-side pass). A first-class rule rather than the hand-rolled
434
+ * column `unique` flag, so the failure is a clear field message instead of
435
+ * a raw UNIQUE-constraint write error.
436
+ */
437
+ unique(): Rule;
438
+ /**
439
+ * For a `relationship` field: the referenced id must exist in the related
440
+ * collection (DB-backed; skipped client-side).
441
+ */
442
+ reference(): Rule;
443
+ custom(fn: CustomValidator): Rule;
444
+ /** Internal: the accumulated checks, read by {@link validateDocument}. */
445
+ toChecks(): readonly Check[];
446
+ }
447
+ /** Fresh, empty rule — the root of a chain. */
448
+ declare function rule(): Rule;
449
+ /**
450
+ * A field's `validation` value: a function from a fresh Rule to the
451
+ * configured chain (Sanity's signature). Returning an array lets a field
452
+ * carry several independent rule chains.
453
+ */
454
+ type ValidationBuilder = (r: Rule) => Rule | Rule[];
455
+ /**
456
+ * Identity helper mirroring Sanity's `defineField` — returns the field
457
+ * config unchanged but gives editors autocomplete and a single, greppable
458
+ * call site for field definitions. Optional: a plain object literal is still
459
+ * a valid field.
460
+ */
461
+ declare function defineField<T extends FieldConfig>(field: T): T;
462
+ interface ValidateDocumentOptions {
463
+ operation: "create" | "update";
464
+ /** Document id (update only) — passed to `unique`/custom validators. */
465
+ id?: number;
466
+ /**
467
+ * Restrict validation to these flattened field keys. Used by update(),
468
+ * which only receives a partial document — validating absent fields would
469
+ * spuriously fail their rules. Omit to validate every field (create).
470
+ */
471
+ onlyFields?: ReadonlySet<string>;
472
+ /**
473
+ * Database handle for DB-backed rules (`unique`, `reference`). When
474
+ * omitted, those rules are skipped — so the same function powers a pure
475
+ * client-side validation pass.
476
+ */
477
+ db?: BaseSQLiteDatabase<"async", unknown>;
478
+ /** This collection's own table (for `unique`). */
479
+ table?: AnyTable;
480
+ /** Registry of tables by slug (for `reference` target lookups). */
481
+ registry?: CmsRegistry;
482
+ }
483
+ /**
484
+ * Evaluate every field's validation rules against `doc`, returning all
485
+ * violations (both errors and warnings). `doc` is the nested document; field
486
+ * values are read from its flattened form so group subfields validate too.
487
+ */
488
+ declare function validateDocument(config: CollectionConfig, doc: Record<string, unknown>, options: ValidateDocumentOptions): Promise<ValidationViolation[]>;
489
+ /**
490
+ * Run {@link validateDocument} and throw {@link CadmusValidationError} if any
491
+ * `"error"`-severity violations are found. Warnings are returned (never
492
+ * thrown) so a caller can still surface them. The thrown error's message is
493
+ * a readable, joined summary of every blocking violation.
494
+ */
495
+ declare function assertValid(config: CollectionConfig, doc: Record<string, unknown>, options: ValidateDocumentOptions): Promise<ValidationViolation[]>;
496
+ //#endregion
4
497
  //#region src/cms/types.d.ts
5
498
  interface BaseFieldConfig {
6
499
  /** column name override; defaults to the config key */
@@ -8,6 +501,16 @@ interface BaseFieldConfig {
8
501
  required?: boolean;
9
502
  unique?: boolean;
10
503
  defaultValue?: unknown;
504
+ /**
505
+ * Chainable validation rules (issue #16), Sanity's `defineField`
506
+ * `validation` analogue: `validation: (rule) => rule.required().min(2)`.
507
+ * Evaluated server-side by createLocalApi on every create/update (and by
508
+ * the studio for client-side feedback). Independent of the `required`/
509
+ * `unique` flags above — those still drive the DB schema; these drive
510
+ * value-level checks with clear, per-field error messages. See
511
+ * {@link ValidationBuilder} and `validation.ts`.
512
+ */
513
+ validation?: ValidationBuilder;
11
514
  }
12
515
  interface TextFieldConfig extends BaseFieldConfig {
13
516
  type: "text";
@@ -219,6 +722,57 @@ interface CollectionHooks<TDoc = Record<string, unknown>> {
219
722
  id: number;
220
723
  }) => void | Promise<void>>;
221
724
  }
725
+ /**
726
+ * Studio-presentation hints for a collection, modeled on Sanity's Structure
727
+ * Builder (`sanity/structure`). Purely about how the admin sidebar/editor
728
+ * *presents* a collection — never affects the DB schema, the Local API, or
729
+ * access control. Consumed by {@link buildStudioStructure} (see
730
+ * `structure.ts`); a collection with no `admin` block falls back to sensible
731
+ * defaults (visible, editable, listed, grouped under the default group,
732
+ * label = capitalized slug). Plugin-injected collections can't carry an
733
+ * `admin` block in hand-written config, so `buildStudioStructure` also
734
+ * accepts per-slug overrides at the call site — see its `overrides` option.
735
+ */
736
+ interface CollectionAdminConfig {
737
+ /**
738
+ * Sidebar group heading this collection appears under (e.g. "Content",
739
+ * "Store"). Collections without a group fall into the builder's default
740
+ * group. Decoupling nav grouping from the raw collection list is the whole
741
+ * point of the Structure Builder.
742
+ */
743
+ group?: string;
744
+ /**
745
+ * Sort order within a group — lower sorts first. Ties (and the absence of
746
+ * an explicit order) break by the collection's position in the config
747
+ * array, so config order is the stable default.
748
+ */
749
+ order?: number;
750
+ /**
751
+ * Drop this collection from the sidebar entirely. For pure system/log
752
+ * tables a human never browses (e.g. `webhook_events`).
753
+ */
754
+ hidden?: boolean;
755
+ /**
756
+ * Mark as read-only in the studio — still navigable/viewable, but the UI
757
+ * suppresses create/edit/delete affordances. For machine-written tables a
758
+ * human should inspect but never edit (e.g. `payments`).
759
+ */
760
+ readOnly?: boolean;
761
+ /**
762
+ * Singleton: exactly one document. The sidebar links straight to its
763
+ * editor (`/admin/<slug>`) instead of a list-then-create flow — Sanity's
764
+ * singleton-document structure pattern. (Storage is unchanged; this only
765
+ * changes navigation.)
766
+ */
767
+ singleton?: boolean;
768
+ /** Display label override; defaults to a capitalized slug. */
769
+ label?: string;
770
+ /**
771
+ * Optional icon identifier passed through to the sidebar renderer (e.g. a
772
+ * Phosphor icon name). The builder treats it as an opaque string.
773
+ */
774
+ icon?: string;
775
+ }
222
776
  interface CollectionConfig {
223
777
  /** table name in D1; also the Local API's collection slug (later step) */
224
778
  slug: string;
@@ -263,6 +817,13 @@ interface CollectionConfig {
263
817
  search?: {
264
818
  fields: readonly string[];
265
819
  };
820
+ /**
821
+ * Studio-presentation hints — grouping, ordering, hidden/read-only,
822
+ * singleton, label, icon. Consumed only by {@link buildStudioStructure}
823
+ * (the Structure Builder); never affects schema, Local API, or access.
824
+ * See {@link CollectionAdminConfig}.
825
+ */
826
+ admin?: CollectionAdminConfig;
266
827
  }
267
828
  /**
268
829
  * A Cadmea plugin — a synchronous transform over the whole CMS config,
@@ -417,162 +978,97 @@ declare function cmsConfigToSchema(config: CmsConfig): Record<string, ReturnType
417
978
  declare function defineCollection(config: CollectionConfig): CollectionConfig;
418
979
  declare function defineCmsConfig(config: CmsConfig): CmsConfig;
419
980
  //#endregion
420
- //#region src/cms/localApi.d.ts
421
- type AnyTable = SQLiteTableWithColumns<any>;
981
+ //#region src/cms/meta.d.ts
982
+ interface CollectionMeta {
983
+ slug: string;
984
+ fields: CollectionConfig["fields"];
985
+ /** Whether `LocalApi.search()` is usable for this collection — see `CollectionConfig.search`. */
986
+ searchable: boolean;
987
+ }
988
+ declare function getCollectionsMeta(config: CmsConfig): CollectionMeta[];
989
+ //#endregion
990
+ //#region src/cms/schema-gen.d.ts
991
+ declare function generateSchemaSource(config: CmsConfig): string;
992
+ //#endregion
993
+ //#region src/cms/structure.d.ts
422
994
  /**
423
- * `TContext` is the per-request value passed to every method and forwarded
424
- * unchanged to the collection's `access` functions (see {@link CollectionAccess}).
425
- * Cadmus doesn't standardize its shape Cadmea types it as `{ session }`,
426
- * other consumers may type it differently. `context` is a required first
427
- * argument on every method (not optional) so a call site can't forget it.
995
+ * Cadmea's Structure Builder the framework half of issue #12.
996
+ *
997
+ * Adopts Sanity's `sanity/structure` idea (pattern, not code): **decouple
998
+ * the admin nav from the raw collection list.** Instead of mapping every
999
+ * `config.collections` entry to an `/admin/<slug>` link which surfaces
1000
+ * system/log tables as editable links and produces dead links — the sidebar
1001
+ * renders from an explicit, grouped structure derived here from each
1002
+ * collection's `admin` hints (see {@link CollectionAdminConfig}) plus
1003
+ * optional per-slug overrides supplied at the call site.
1004
+ *
1005
+ * Pure data in / pure data out: no SolidJS, no DOM, no server imports — so
1006
+ * it's safe to import from a client studio component (e.g. the site's
1007
+ * `PanelNav`) and trivially testable.
428
1008
  */
429
- interface LocalApi<TTable extends AnyTable, TContext = unknown> {
1009
+ /** Default group heading for collections that don't declare `admin.group`. */
1010
+ declare const DEFAULT_STUDIO_GROUP = "Content";
1011
+ /** One navigable collection entry in the studio sidebar. */
1012
+ interface StudioStructureItem {
1013
+ /** The collection's slug. */
1014
+ slug: string;
1015
+ /** Human label — `admin.label`, else the capitalized slug. */
1016
+ label: string;
1017
+ /** Where the sidebar link points (`/admin/<slug>`, configurable prefix). */
1018
+ href: string;
1019
+ /** Read-only collections are viewable but not editable in the studio. */
1020
+ readOnly: boolean;
430
1021
  /**
431
- * `depth: 0` (default) returns relationship fields as bare ids; `depth: 1`
432
- * batch-resolves `hasMany: false` relationship fields into the related
433
- * row, gated by that collection's own `read` access fn — see
434
- * `resolveRelationships` below. Requires `createLocalApi`'s `registry`
435
- * param; throws CadmusCmsError if `depth: 1` is requested without one.
1022
+ * Singletons link straight to their editor rather than a list+create
1023
+ * flow. (The href is identical; the renderer uses this to skip the list.)
436
1024
  */
437
- find(context: TContext, options?: {
438
- where?: SQL;
439
- depth?: RelationshipDepth; /** Row cap, applied after `where` — for paginated list views. */
440
- limit?: number; /** Rows to skip, applied after `where` — pairs with `limit`. */
441
- offset?: number; /** One or more `asc(table.col)`/`desc(table.col)` expressions. */
442
- orderBy?: SQL | SQL[];
443
- }): Promise<InferSelectModel<TTable>[]>;
444
- findByID(context: TContext, id: number, options?: {
445
- depth?: RelationshipDepth;
446
- }): Promise<InferSelectModel<TTable>>;
1025
+ singleton: boolean;
1026
+ /** Opaque icon identifier from `admin.icon`, if any. */
1027
+ icon?: string;
1028
+ }
1029
+ /** A titled group of sidebar items, in render order. */
1030
+ interface StudioStructureGroup {
1031
+ title: string;
1032
+ items: StudioStructureItem[];
1033
+ }
1034
+ interface BuildStudioStructureOptions {
447
1035
  /**
448
- * Total row count for `where` (ignoring `limit`/`offset`) — pairs with
449
- * `find` to compute page counts/next-page availability without fetching
450
- * every row. Gated by the same `read` access check as `find`.
1036
+ * Per-slug presentation overrides, merged over each collection's own
1037
+ * `admin` block (override keys win). The escape hatch for plugin-injected
1038
+ * collections (`products`, `payments`, `webhook_events`, …) that can't
1039
+ * carry an `admin` block in hand-written config — the studio declares
1040
+ * their presentation here, exactly like Sanity defines structure at the
1041
+ * studio level rather than on the schema.
451
1042
  */
452
- count(context: TContext, options?: {
453
- where?: SQL;
454
- }): Promise<number>;
1043
+ overrides?: Record<string, CollectionAdminConfig>;
455
1044
  /**
456
- * Full-text search over this collection's `search.fields`-configured
457
- * companion FTS5 table see types.ts's `CollectionConfig.search` and
458
- * codegen.ts's `collectionSearchTableSQL`. Gated by `read` access, same
459
- * as `find`/`findByID`. Throws `CadmusCmsError` if the collection has no
460
- * `search` config.
1045
+ * Explicit group ordering by title. Groups listed here render first, in
1046
+ * this order; any remaining groups follow in first-appearance order. A
1047
+ * group title absent from `config`'s collections simply doesn't appear.
461
1048
  */
462
- search(context: TContext, query: string, options?: {
463
- limit?: number;
464
- }): Promise<InferSelectModel<TTable>[]>;
465
- create(context: TContext, input: InferInsertModel<TTable>): Promise<InferSelectModel<TTable>>;
466
- update(context: TContext, id: number, input: Partial<InferInsertModel<TTable>>): Promise<InferSelectModel<TTable>>;
467
- deleteByID(context: TContext, id: number): Promise<InferSelectModel<TTable>>;
1049
+ groupOrder?: readonly string[];
1050
+ /**
1051
+ * Link prefix for each item's `href`. Defaults to `/admin`, producing
1052
+ * `/admin/<slug>`. No trailing slash.
1053
+ */
1054
+ basePath?: string;
468
1055
  }
469
1056
  /**
470
- * Lets `createLocalApi` resolve `depth: 1` relationship fields without
471
- * importing every other collection's Local API (which would be a circular
472
- * dependency the moment two collections relate to each other). The
473
- * registry is just the raw ingredients — a table and a config per
474
- * collection slug — built once by the app (e.g. from `cadmeaConfig.collections`)
475
- * and passed to every `createLocalApi` call that has relationship fields.
476
- *
477
- * `apis` is a second, optional registry on the same object, for a
478
- * different problem: a *hook* (not `createLocalApi` itself) on one
479
- * collection that needs to write to *another* collection's Local API —
480
- * e.g. a CRM upsert hook on a lead-capture collection that creates/updates
481
- * `contacts`/`activities` rows. `tables`/`configs` can't serve this, since
482
- * a hook needs a real `LocalApi` (with its own access/hooks/search wiring
483
- * already applied), not raw ingredients to rebuild one from.
484
- *
485
- * The chicken-and-egg problem this solves: building collection A's
486
- * `LocalApi` might need to reference collection B's `LocalApi` (for a
487
- * hook), but collection B's `LocalApi` doesn't exist yet at the point A's
488
- * is constructed — and vice versa if B also has a hook referencing A.
489
- * The fix is **late binding**: build one `CmsRegistry` object, pass the
490
- * *same reference* into every `createLocalApi` call (so every collection's
491
- * hooks close over the same mutable object), construct every `LocalApi`,
492
- * then fill in `registry.apis` afterwards:
493
- *
494
- * ```ts
495
- * const registry: CmsRegistry = { tables, configs, apis: {} };
496
- * const contactsApi = createLocalApi(db, contactsTable, contactsCollection, registry);
497
- * const inquiriesApi = createLocalApi(db, inquiriesTable, inquiriesCollection, registry);
498
- * // populate *after* every createLocalApi call returns — any hook that
499
- * // reads registry.apis lazily (inside its returned function body, not
500
- * // at hook-factory-call time) sees the fully-populated map, since hooks
501
- * // only ever run once real requests start landing.
502
- * Object.assign(registry.apis!, { contacts: contactsApi, inquiries: inquiriesApi });
503
- * ```
1057
+ * Build the studio sidebar structure from a resolved CMS config.
504
1058
  *
505
- * See `getRegisteredApi` for the accessor a hook factory should use to
506
- * read from this map, rather than indexing `registry.apis` directly.
507
- */
508
- interface CmsRegistry {
509
- tables: Record<string, AnyTable>;
510
- configs: Record<string, CollectionConfig>;
511
- apis?: Record<string, LocalApi<AnyTable, any>>;
512
- }
513
- /**
514
- * Reads collection `slug`'s `LocalApi` out of `registry.apis` — the
515
- * accessor hook factories should use (see `CmsRegistry`'s doc comment for
516
- * the late-binding pattern this assumes) instead of indexing
517
- * `registry.apis` directly, so every caller gets the same clear error if
518
- * the registry wasn't built/populated correctly. `TContext` is a type-only
519
- * parameter (the registry itself is stored with `never` to stay variance-
520
- * safe across collections with different context shapes) — callers assert
521
- * the context type they expect, the same way `resolveRelationships`'s own
522
- * registry lookups do.
523
- */
524
- declare function getRegisteredApi<TContext>(registry: CmsRegistry | undefined, slug: string): LocalApi<AnyTable, TContext>;
525
- /**
526
- * Non-throwing counterpart to `checkAccess` below, for UI code that wants
527
- * to hide/disable an action a context can't perform rather than let it
528
- * fail server-side after a click (see Phase 6 / issue #26's
529
- * `getPageCapabilities`). `checkAccess` calls through this same function
530
- * rather than duplicating the "no access fn = allowed" logic, so `can()`'s
531
- * answer and the real operation's enforcement can never disagree.
532
- */
533
- declare function can<TContext>(config: CollectionConfig, operation: keyof CollectionAccess, context: TContext): Promise<boolean>;
534
- declare function createLocalApi<TTable extends AnyTable, TContext = unknown>(db: BaseSQLiteDatabase<"async", unknown>, table: TTable, config: CollectionConfig, registry?: CmsRegistry): LocalApi<TTable, TContext>;
535
- /**
536
- * Extends {@link LocalApi} with draft/publish operations for a collection
537
- * that opted in via `CollectionConfig.versions.drafts` (see codegen.ts's
538
- * `collectionVersionsTable`). A separate interface (not a wider
539
- * `LocalApi`) so non-versioned collections' types don't grow these methods
540
- * — TypeScript can't conditionally widen `createLocalApi`'s return type
541
- * off a runtime config value, so this is `createVersionedLocalApi`'s own
542
- * factory rather than a branch inside `createLocalApi`.
1059
+ * - Hidden collections (`admin.hidden`) are dropped entirely.
1060
+ * - Each remaining collection is placed in its `admin.group` (or
1061
+ * {@link DEFAULT_STUDIO_GROUP}).
1062
+ * - Within a group, items sort by `admin.order` (ascending; unset sorts
1063
+ * after set), then by their original position in `config.collections` —
1064
+ * so config order is the stable tiebreaker.
1065
+ * - Groups render in `options.groupOrder` first, then first-appearance
1066
+ * order for the rest.
543
1067
  *
544
- * Scope, deliberately: a document is always created via the inherited
545
- * `create()` first (existing behavior, unaffected by versioning) — these
546
- * methods operate against an *existing* row. `saveDraft` never validates
547
- * required fields (an incomplete draft is valid); `publish` runs the same
548
- * full validation `create`/`update` do, since publishing is what makes a
549
- * version the public-facing document. Plain `find`/`findByID` are
550
- * unchanged by any of this — they always return the main table's current
551
- * row regardless of `publishedVersionId`; filtering reads to
552
- * published-only content is not this phase's concern.
1068
+ * The input is expected to be the *resolved* config (post-plugins), since
1069
+ * that's what carries plugin-injected collections like `products`.
553
1070
  */
554
- interface VersionedLocalApi<TTable extends AnyTable, TVersionsTable extends AnyTable, TContext = unknown> extends LocalApi<TTable, TContext> {
555
- findVersions(context: TContext, parentId: number): Promise<InferSelectModel<TVersionsTable>[]>;
556
- /** Inserts a new version row holding `input` as a draft snapshot. */
557
- saveDraft(context: TContext, id: number, input: Partial<InferInsertModel<TTable>>): Promise<InferSelectModel<TVersionsTable>>;
558
- /** Copies a version's snapshot onto the main row and marks it published. */
559
- publish(context: TContext, versionId: number): Promise<InferSelectModel<TTable>>;
560
- /** Clears the main row's published pointer; the row's data is untouched. */
561
- unpublish(context: TContext, id: number): Promise<InferSelectModel<TTable>>;
562
- }
563
- declare function createVersionedLocalApi<TTable extends AnyTable, TVersionsTable extends AnyTable, TContext = unknown>(db: BaseSQLiteDatabase<"async", unknown>, table: TTable, versionsTable: TVersionsTable, config: CollectionConfig, registry?: CmsRegistry): VersionedLocalApi<TTable, TVersionsTable, TContext>;
564
- //#endregion
565
- //#region src/cms/meta.d.ts
566
- interface CollectionMeta {
567
- slug: string;
568
- fields: CollectionConfig["fields"];
569
- /** Whether `LocalApi.search()` is usable for this collection — see `CollectionConfig.search`. */
570
- searchable: boolean;
571
- }
572
- declare function getCollectionsMeta(config: CmsConfig): CollectionMeta[];
573
- //#endregion
574
- //#region src/cms/schema-gen.d.ts
575
- declare function generateSchemaSource(config: CmsConfig): string;
1071
+ declare function buildStudioStructure(config: CmsConfig, options?: BuildStudioStructureOptions): StudioStructureGroup[];
576
1072
  //#endregion
577
1073
  //#region src/cms/webhooks.d.ts
578
1074
  interface WebhookConfig {
@@ -612,5 +1108,5 @@ declare function createWebhookHook(queue: Queue<WebhookMessage>, config: Webhook
612
1108
  */
613
1109
  declare function deliverWebhookMessage(message: WebhookMessage): Promise<void>;
614
1110
  //#endregion
615
- export { CollectionAccess as A, RelationshipFieldConfig as B, relationshipJoinTables as C, CadmeaPlugin as D, BaseFieldConfig as E, GroupFieldConfig as F, flattenDoc as G, SelectFieldConfig as H, JsonFieldConfig as I, flattenFields as K, JsonValue as L, CollectionHooks as M, DateFieldConfig as N, CheckboxFieldConfig as O, FieldConfig as P, NumberFieldConfig as R, extractSearchText as S, ArrayFieldConfig as T, TextFieldConfig as U, RichTextFieldConfig as V, UploadFieldConfig as W, cmsConfigToSchema as _, generateSchemaSource as a, collectionToTable as b, CmsRegistry as c, can as d, createLocalApi as f, defineCollection as g, defineCmsConfig as h, deliverWebhookMessage as i, CollectionConfig as j, CmsConfig as k, LocalApi as l, getRegisteredApi as m, WebhookMessage as n, CollectionMeta as o, createVersionedLocalApi as p, nestDoc as q, createWebhookHook as r, getCollectionsMeta as s, WebhookConfig as t, VersionedLocalApi as u, collectionSearchTableName as v, AccessFn as w, collectionVersionsTable as x, collectionSearchTableSQL as y, RelationshipDepth as z };
616
- //# sourceMappingURL=index-BUrCSGVb.d.cts.map
1111
+ export { ValidationSeverity as $, CollectionConfig as A, RichTextFieldConfig as B, ArrayFieldConfig as C, CadmusValidationError as Ct, CmsConfig as D, StringBlockRenderer as Dt, CheckboxFieldConfig as E, PortableBlockLike as Et, JsonFieldConfig as F, flattenFields as G, TextFieldConfig as H, JsonValue as I, CustomValidatorResult as J, nestDoc as K, NumberFieldConfig as L, DateFieldConfig as M, FieldConfig as N, CollectionAccess as O, createBlockRegistry as Ot, GroupFieldConfig as P, ValidationFieldContext as Q, RelationshipDepth as R, AccessFn as S, CadmusStorageError as St, CadmeaPlugin as T, BlockRegistry as Tt, UploadFieldConfig as U, SelectFieldConfig as V, flattenDoc as W, ValidateDocumentOptions as X, Rule as Y, ValidationBuilder as Z, collectionSearchTableSQL as _, CadmusEmailError as _t, BuildStudioStructureOptions as a, LocalApi as at, extractSearchText as b, CadmusRateLimitError as bt, StudioStructureItem as c, createLocalApi as ct, CollectionMeta as d, CadmusAccessDeniedError as dt, assertValid as et, getCollectionsMeta as f, CadmusApiError as ft, collectionSearchTableName as g, CadmusDbError as gt, cmsConfigToSchema as h, CadmusCmsError as ht, deliverWebhookMessage as i, CmsRegistry as it, CollectionHooks as j, CollectionAdminConfig as k, renderBlocksToString as kt, buildStudioStructure as l, createVersionedLocalApi as lt, defineCollection as m, CadmusCacheError as mt, WebhookMessage as n, rule as nt, DEFAULT_STUDIO_GROUP as o, VersionedLocalApi as ot, defineCmsConfig as p, CadmusAuthError as pt, CustomValidator as q, createWebhookHook as r, validateDocument as rt, StudioStructureGroup as s, can as st, WebhookConfig as t, defineField as tt, generateSchemaSource as u, getRegisteredApi as ut, collectionToTable as v, CadmusError as vt, BaseFieldConfig as w, ValidationViolation as wt, relationshipJoinTables as x, CadmusSessionError as xt, collectionVersionsTable as y, CadmusQueueError as yt, RelationshipFieldConfig as z };
1112
+ //# sourceMappingURL=index-BRZrCTsN.d.ts.map