@voltro/cms 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/LICENSE +57 -0
- package/README.md +26 -0
- package/SECURITY.md +56 -0
- package/THIRD-PARTY-NOTICES.md +347 -0
- package/dist/ContentForm-D0YGVkRu.js +278 -0
- package/dist/index.d.ts +589 -0
- package/dist/index.js +376 -0
- package/dist/web.d.ts +312 -0
- package/dist/web.js +2 -0
- package/package.json +55 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
import { DataStore } from '@voltro/database';
|
|
2
|
+
import { Effect } from 'effect';
|
|
3
|
+
import { ReactNode } from 'react';
|
|
4
|
+
import { Row } from '@voltro/database';
|
|
5
|
+
import { Schema as Schema_2 } from 'effect';
|
|
6
|
+
import { TableLike } from '@voltro/database';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Compute every `derivedFrom` field from its source value. Returns a NEW
|
|
10
|
+
* row — the input is not mutated. A caller-provided value for a derived
|
|
11
|
+
* field is OVERWRITTEN (derived values are the content type's opinion,
|
|
12
|
+
* mirroring the column `.computed()` semantics). Derivations run in
|
|
13
|
+
* declaration order against the progressively-updated row, so a derived
|
|
14
|
+
* field may source another, earlier-declared derived field.
|
|
15
|
+
*/
|
|
16
|
+
export declare const applyDerivations: (type: Pick<ContentType, "fields" | "derivedFields">, row: Readonly<Record<string, unknown>>) => Record<string, unknown>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Archive a content row: set `status: 'archived'` on the draft AND the
|
|
20
|
+
* published copy (whichever exist). Archived rows stay in their tables
|
|
21
|
+
* but disappear from the read surface (`ctx.cms` excludes `archived`).
|
|
22
|
+
* Atomic per row. Throws `ContentNotFound` when the id exists in neither
|
|
23
|
+
* table within the caller's scope.
|
|
24
|
+
*/
|
|
25
|
+
export declare const archive: (store: DataStore, type: ContentType, id: string) => Promise<void>;
|
|
26
|
+
|
|
27
|
+
/** Effect-native `archive` (see {@link saveDraftEffect}). */
|
|
28
|
+
export declare const archiveEffect: (store: DataStore, type: ContentType, id: string) => Effect.Effect<void, ContentNotFound>;
|
|
29
|
+
|
|
30
|
+
/** The CMS read surface attached to `ctx.cms` (and the standalone client). */
|
|
31
|
+
export declare interface CmsApi {
|
|
32
|
+
/** Open a query over a content type's PUBLISHED rows (archived excluded). */
|
|
33
|
+
contentType(name: string): ContentQuery;
|
|
34
|
+
/**
|
|
35
|
+
* Resolve a single row honouring a preview token. With a valid token
|
|
36
|
+
* for `{type, id}`, reads the DRAFT row; otherwise reads the published
|
|
37
|
+
* row by id. Returns null when nothing matches / the token is invalid.
|
|
38
|
+
*/
|
|
39
|
+
preview(type: string, id: string, token: string | undefined): Promise<Row | null>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Build the `ctx.cms` surface over a request-scoped store. Attach the
|
|
44
|
+
* result to your `AppContext` slot (or read it via `useCms(ctx)`).
|
|
45
|
+
*/
|
|
46
|
+
export declare const cmsContext: (store: DataStore, types: ReadonlyArray<ContentType>, options?: {
|
|
47
|
+
secret?: string;
|
|
48
|
+
}) => CmsApi;
|
|
49
|
+
|
|
50
|
+
export declare interface CmsRestOptions {
|
|
51
|
+
readonly cms: CmsApi;
|
|
52
|
+
readonly types: ReadonlyArray<ContentType>;
|
|
53
|
+
/**
|
|
54
|
+
* API keys allowed to read. A key may be scoped to specific content
|
|
55
|
+
* types; `'*'` (or omitting `types`) grants all. Reads
|
|
56
|
+
* `Authorization: Bearer <key>` or `x-api-key: <key>`.
|
|
57
|
+
*/
|
|
58
|
+
readonly apiKeys: ReadonlyArray<{
|
|
59
|
+
key: string;
|
|
60
|
+
types?: ReadonlyArray<string>;
|
|
61
|
+
}>;
|
|
62
|
+
/** Default page size when `?limit` is absent. */
|
|
63
|
+
readonly defaultLimit?: number;
|
|
64
|
+
/** Maximum page size — `?limit` is clamped to this. */
|
|
65
|
+
readonly maxLimit?: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** A minimal HTTP request the REST handler reads. */
|
|
69
|
+
export declare interface CmsRestRequest {
|
|
70
|
+
readonly method: string;
|
|
71
|
+
/** Path AFTER the mount prefix, e.g. `/v1/cms/blogPost` or `/v1/cms/blogPost/my-slug`. */
|
|
72
|
+
readonly path: string;
|
|
73
|
+
readonly headers: Readonly<Record<string, string | undefined>>;
|
|
74
|
+
readonly query?: Readonly<Record<string, string | undefined>>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The JSON response the handler returns. */
|
|
78
|
+
export declare interface CmsRestResponse {
|
|
79
|
+
readonly status: number;
|
|
80
|
+
readonly body: unknown;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Everything the write pipeline can fail with on the typed channel. */
|
|
84
|
+
export declare type CmsWriteError = ContentValidationFailed | ContentNotFound | ContentIdConflict;
|
|
85
|
+
|
|
86
|
+
/** The lifecycle states a content row moves through. */
|
|
87
|
+
export declare const CONTENT_STATUSES: readonly ["draft", "published", "archived"];
|
|
88
|
+
|
|
89
|
+
/** The pair of tables a content type compiles to. */
|
|
90
|
+
export declare interface ContentEntities {
|
|
91
|
+
readonly draft: TableLike;
|
|
92
|
+
readonly published: TableLike;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Auto-generated content form. Renders one labelled widget per field,
|
|
97
|
+
* driven entirely by each field's `editorHint`. Stateless — the caller
|
|
98
|
+
* owns `value` + `onChange`.
|
|
99
|
+
*/
|
|
100
|
+
export declare const ContentForm: (props: ContentFormProps) => ReactNode;
|
|
101
|
+
|
|
102
|
+
export declare interface ContentFormProps {
|
|
103
|
+
readonly contentType: ContentFormType;
|
|
104
|
+
readonly value: ContentValue;
|
|
105
|
+
readonly onChange: (next: ContentValue) => void;
|
|
106
|
+
/** Override the widget for one or more editor hints (e.g. a TipTap richText). */
|
|
107
|
+
readonly widgets?: Partial<Record<EditorHint, Widget>>;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The content-type shape ContentForm reads (structural — avoids a value import). */
|
|
111
|
+
export declare interface ContentFormType {
|
|
112
|
+
readonly fields: Readonly<Record<string, FieldSchema>>;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Raised by `saveDraft` when the caller supplies an explicit `id` that
|
|
117
|
+
* already exists OUTSIDE the caller's scope (another tenant's row).
|
|
118
|
+
* Refusing here keeps a caller-pinned id from ever updating — or, on a
|
|
119
|
+
* store without DB-enforced PKs, overwriting — a foreign row.
|
|
120
|
+
*/
|
|
121
|
+
export declare class ContentIdConflict extends ContentIdConflict_base {
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
declare const ContentIdConflict_base: Schema_2.TaggedErrorClass<ContentIdConflict, "ContentIdConflict", {
|
|
125
|
+
readonly _tag: Schema_2.tag<"ContentIdConflict">;
|
|
126
|
+
} & {
|
|
127
|
+
contentType: typeof Schema_2.String;
|
|
128
|
+
id: typeof Schema_2.String;
|
|
129
|
+
}>;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Raised by `publish` / `unpublish` / `archive` when the row id doesn't
|
|
133
|
+
* exist in the caller's scope. On a tenant-scoped request store this is
|
|
134
|
+
* ALSO the cross-tenant answer: another tenant's row is indistinguishable
|
|
135
|
+
* from a missing one (the scoped read never sees it).
|
|
136
|
+
*/
|
|
137
|
+
export declare class ContentNotFound extends ContentNotFound_base {
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
declare const ContentNotFound_base: Schema_2.TaggedErrorClass<ContentNotFound, "ContentNotFound", {
|
|
141
|
+
readonly _tag: Schema_2.tag<"ContentNotFound">;
|
|
142
|
+
} & {
|
|
143
|
+
contentType: typeof Schema_2.String;
|
|
144
|
+
id: typeof Schema_2.String;
|
|
145
|
+
/** Which lifecycle table the id was looked up in. */
|
|
146
|
+
stage: Schema_2.Literal<["draft", "published"]>;
|
|
147
|
+
}>;
|
|
148
|
+
|
|
149
|
+
/** A chainable read over one content type's published rows. */
|
|
150
|
+
export declare interface ContentQuery {
|
|
151
|
+
where(field: string, op: WhereOp, value: unknown): ContentQuery;
|
|
152
|
+
orderBy(field: string, dir?: 'asc' | 'desc'): ContentQuery;
|
|
153
|
+
limit(n: number): ContentQuery;
|
|
154
|
+
offset(n: number): ContentQuery;
|
|
155
|
+
/** Resolve all matching rows. */
|
|
156
|
+
all(): Promise<ReadonlyArray<Row>>;
|
|
157
|
+
/** Resolve the first matching row, or null. */
|
|
158
|
+
one(): Promise<Row | null>;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export declare type ContentStatus = (typeof CONTENT_STATUSES)[number];
|
|
162
|
+
|
|
163
|
+
/** A registered content type. */
|
|
164
|
+
export declare interface ContentType {
|
|
165
|
+
readonly name: string;
|
|
166
|
+
readonly displayName: string;
|
|
167
|
+
readonly pluralName: string;
|
|
168
|
+
readonly fields: Readonly<Record<string, FieldSchema>>;
|
|
169
|
+
readonly list?: ListConfig;
|
|
170
|
+
readonly idPrefix?: string;
|
|
171
|
+
/** Field names whose value is computed-on-save (derivedFrom). */
|
|
172
|
+
readonly derivedFields: ReadonlyArray<string>;
|
|
173
|
+
/** Field names marked unique-per-tenant (composite `(tenantId, field)` UNIQUE). */
|
|
174
|
+
readonly uniqueFields: ReadonlyArray<string>;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** The user-authored spec passed to `defineContentType`. */
|
|
178
|
+
export declare interface ContentTypeSpec {
|
|
179
|
+
readonly name: string;
|
|
180
|
+
readonly displayName: string;
|
|
181
|
+
readonly pluralName: string;
|
|
182
|
+
readonly fields: Readonly<Record<string, FieldSchema | PipeableField>>;
|
|
183
|
+
readonly list?: ListConfig;
|
|
184
|
+
/** Override the TypeID prefix for awkward plurals / long names. */
|
|
185
|
+
readonly idPrefix?: string;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Compile a content type to its `<type>_drafts` + `<type>_published`
|
|
190
|
+
* tables. Drafts hold in-progress edits; publishing copies a row to the
|
|
191
|
+
* published table (the one the rest of the app reads). Both carry the
|
|
192
|
+
* lifecycle `status` column + audit timestamps and are `.reactive()` so
|
|
193
|
+
* a publish wakes live consumer subscriptions. The draft table also
|
|
194
|
+
* carries a nullable `publishAt` (scheduled publishing). A field marked
|
|
195
|
+
* `unique()` adds a composite `(tenantId, field)` DB UNIQUE to BOTH
|
|
196
|
+
* tables — per-tenant uniqueness enforced by the database.
|
|
197
|
+
*/
|
|
198
|
+
export declare const contentTypeToEntities: (ct: ContentType) => ContentEntities;
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Raised by `validateContent` / `saveDraft` when a row violates the
|
|
202
|
+
* content type's field rules (pattern / maxLength / typed kinds /
|
|
203
|
+
* literal sets / rich-text content policy). Carries EVERY violation,
|
|
204
|
+
* per field, so an editor can annotate the whole form in one pass.
|
|
205
|
+
*/
|
|
206
|
+
export declare class ContentValidationFailed extends ContentValidationFailed_base {
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
declare const ContentValidationFailed_base: Schema_2.TaggedErrorClass<ContentValidationFailed, "ContentValidationFailed", {
|
|
210
|
+
readonly _tag: Schema_2.tag<"ContentValidationFailed">;
|
|
211
|
+
} & {
|
|
212
|
+
contentType: typeof Schema_2.String;
|
|
213
|
+
violations: Schema_2.Array$<Schema_2.Struct<{
|
|
214
|
+
field: typeof Schema_2.String;
|
|
215
|
+
rule: typeof Schema_2.String;
|
|
216
|
+
message: typeof Schema_2.String;
|
|
217
|
+
}>>;
|
|
218
|
+
}>;
|
|
219
|
+
|
|
220
|
+
export declare type ContentValue = Record<string, unknown>;
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Collect every save-time rule violation for `row` against `type` —
|
|
224
|
+
* required presence, per-kind type checks, `maxLength`, `pattern`,
|
|
225
|
+
* `Literal` value sets, and the rich-text content policy
|
|
226
|
+
* (`allowImages` / `allowEmbeds: false` reject image/embed nodes or the
|
|
227
|
+
* `<img>` / `<iframe>` / `<embed>` markers in string values).
|
|
228
|
+
*
|
|
229
|
+
* Pure and never throws — usable in a browser form ("annotate every
|
|
230
|
+
* invalid field") as well as on the server. `id` is allowed (must be a
|
|
231
|
+
* string); the lifecycle/mixin columns the pipeline owns (`status`,
|
|
232
|
+
* `tenantId`, audit who/when) are tolerated on input and ignored; any
|
|
233
|
+
* OTHER undeclared key is an `unknown` violation (usually a typo'd
|
|
234
|
+
* field name).
|
|
235
|
+
*
|
|
236
|
+
* NOTE on ordering: run `applyDerivations` first when validating a row
|
|
237
|
+
* that is about to be written — a derived field's rules apply to the
|
|
238
|
+
* derived value, not to a caller value that's about to be overwritten.
|
|
239
|
+
*/
|
|
240
|
+
export declare const contentViolations: (type: Pick<ContentType, "name" | "fields">, row: Readonly<Record<string, unknown>>) => ReadonlyArray<FieldViolation>;
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* A standalone CMS client for build-time consumption (the blog
|
|
244
|
+
* template's CMS mode), where there is no request handle. Same surface
|
|
245
|
+
* as `ctx.cms`; reads the published tables directly.
|
|
246
|
+
*/
|
|
247
|
+
export declare const createCmsClient: (config: {
|
|
248
|
+
store: DataStore;
|
|
249
|
+
types: ReadonlyArray<ContentType>;
|
|
250
|
+
secret?: string;
|
|
251
|
+
}) => CmsApi;
|
|
252
|
+
|
|
253
|
+
export declare const defineContentType: (spec: ContentTypeSpec) => ContentType;
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Mark a field computed-on-save from another field. The transform runs
|
|
257
|
+
* before the row is written — `saveDraft` applies it (via
|
|
258
|
+
* `applyDerivations`) ahead of validation, and `publish` copies the
|
|
259
|
+
* derived value. A caller-provided value is overwritten, mirroring the
|
|
260
|
+
* column `.computed()` semantics.
|
|
261
|
+
*
|
|
262
|
+
* ```ts
|
|
263
|
+
* slug: Schema.String.pipe(pattern(/^[a-z0-9-]+$/), derivedFrom('title', slugify))
|
|
264
|
+
* ```
|
|
265
|
+
*/
|
|
266
|
+
export declare const derivedFrom: (source: string, transform: (value: unknown, row: Record<string, unknown>) => unknown) => FieldRefinement;
|
|
267
|
+
|
|
268
|
+
/** A field marked computed-on-save from another field's value. */
|
|
269
|
+
export declare interface DerivedSpec {
|
|
270
|
+
readonly source: string;
|
|
271
|
+
readonly transform: (value: unknown, row: Record<string, unknown>) => unknown;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** The widget the editor renders for a field — drives ContentForm. */
|
|
275
|
+
export declare type EditorHint = 'text' | 'textarea' | 'number' | 'boolean' | 'date' | 'datetime' | 'select' | 'richText' | 'media' | 'reference' | 'array' | 'struct';
|
|
276
|
+
|
|
277
|
+
/** Discriminator for how a field compiles to a column + serialises. */
|
|
278
|
+
export declare type FieldKind = 'string' | 'number' | 'boolean' | 'date' | 'datetime' | 'literal' | 'richText' | 'media' | 'reference' | 'array' | 'struct';
|
|
279
|
+
|
|
280
|
+
/** A refinement applied through `.pipe(...)`. */
|
|
281
|
+
export declare type FieldRefinement = (field: FieldSchema) => FieldSchema;
|
|
282
|
+
|
|
283
|
+
/** A fully-resolved field descriptor stored on a content type. */
|
|
284
|
+
export declare interface FieldSchema {
|
|
285
|
+
readonly kind: FieldKind;
|
|
286
|
+
readonly editorHint: EditorHint;
|
|
287
|
+
/** When true the field is nullable in the DB + optional in the editor. */
|
|
288
|
+
readonly isOptional: boolean;
|
|
289
|
+
/** Max string length — drives textarea-vs-input + save-time validation. */
|
|
290
|
+
readonly maxLength?: number;
|
|
291
|
+
/** Source-form regex (string) for save-time validation. */
|
|
292
|
+
readonly pattern?: string;
|
|
293
|
+
/** Closed value set for `literal` fields → `<select>` + CHECK constraint. */
|
|
294
|
+
readonly literals?: ReadonlyArray<string>;
|
|
295
|
+
/**
|
|
296
|
+
* When true, the field is unique PER TENANT — the derived tables carry a
|
|
297
|
+
* composite `(tenantId, <field>)` DB UNIQUE constraint (see contentType.ts).
|
|
298
|
+
* Only meaningful on a `string` field (typically a slug).
|
|
299
|
+
*/
|
|
300
|
+
readonly unique?: boolean;
|
|
301
|
+
/** RichText options. */
|
|
302
|
+
readonly richText?: RichTextOptions;
|
|
303
|
+
/** Referenced content-type name for `reference` fields. */
|
|
304
|
+
readonly referenceType?: string;
|
|
305
|
+
/** Element field for `array` fields. */
|
|
306
|
+
readonly element?: FieldSchema;
|
|
307
|
+
/** Nested field map for `struct` fields. */
|
|
308
|
+
readonly struct?: Readonly<Record<string, FieldSchema>>;
|
|
309
|
+
/** Computed-on-save derivation (set via `derivedFrom`). */
|
|
310
|
+
readonly derived?: DerivedSpec;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** One save-time rule violation, addressed to a field path. */
|
|
314
|
+
export declare interface FieldViolation {
|
|
315
|
+
/** Dotted/indexed field path, e.g. `title`, `meta.ogTitle`, `tags[2]`. */
|
|
316
|
+
readonly field: string;
|
|
317
|
+
/** The rule that failed: `required` | `type` | `maxLength` | `pattern` | `literal` | `richText` | `unknown`. */
|
|
318
|
+
readonly rule: string;
|
|
319
|
+
readonly message: string;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Handle one CMS REST request. Returns 401 on a missing/unknown key,
|
|
324
|
+
* 403 when the key isn't scoped to the requested type, 404 for unknown
|
|
325
|
+
* types or paths, 405 for non-GET, and 200 with the published rows
|
|
326
|
+
* otherwise.
|
|
327
|
+
*/
|
|
328
|
+
export declare const handleCmsRest: (req: CmsRestRequest, options: CmsRestOptions) => Promise<CmsRestResponse>;
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* List-view configuration for the consuming editor/template: which
|
|
332
|
+
* columns its list page shows and the default sort. Validated by
|
|
333
|
+
* `defineContentType` — every referenced name must be a declared field
|
|
334
|
+
* or a lifecycle column, so a typo fails at definition time instead of
|
|
335
|
+
* rendering an empty column.
|
|
336
|
+
*/
|
|
337
|
+
export declare interface ListConfig {
|
|
338
|
+
readonly columns: ReadonlyArray<string>;
|
|
339
|
+
readonly defaultSort?: {
|
|
340
|
+
readonly field: string;
|
|
341
|
+
readonly dir: 'asc' | 'desc';
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Cap a string field's length. Past `value` the editor renders a textarea. */
|
|
346
|
+
export declare const maxLength: (value: number) => FieldRefinement;
|
|
347
|
+
|
|
348
|
+
/** The media field names on a content type (top level) — handy for a
|
|
349
|
+
* consumer that wants to know whether resolution is even needed. */
|
|
350
|
+
export declare const mediaFields: (type: Pick<ContentType, "fields">) => ReadonlyArray<string>;
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Resolve a stored media KEY to a served URL. The app builds this from its
|
|
354
|
+
* storage layer, e.g. `(key) => Effect.runPromise(storage.getUrl(key, { tenantId }))`
|
|
355
|
+
* or, for an access-checked URL, `(key) => Effect.runPromise(storage.mintUrl(key, subject))`.
|
|
356
|
+
* Returning `null` leaves the field as its stored key (unresolved).
|
|
357
|
+
*/
|
|
358
|
+
export declare type MediaResolver = (key: string) => Promise<string | null>;
|
|
359
|
+
|
|
360
|
+
/** Constrain a string field to a regex (source form, validated on save). */
|
|
361
|
+
export declare const pattern: (re: RegExp) => FieldRefinement;
|
|
362
|
+
|
|
363
|
+
export declare interface PipeableField extends FieldSchema {
|
|
364
|
+
readonly isOptional: boolean;
|
|
365
|
+
pipe(...refinements: ReadonlyArray<FieldRefinement>): PipeableField;
|
|
366
|
+
/** Mark the field optional → nullable column + non-required editor input. */
|
|
367
|
+
optional(): PipeableField;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** A verified preview claim. */
|
|
371
|
+
export declare interface PreviewClaim {
|
|
372
|
+
readonly type: string;
|
|
373
|
+
readonly id: string;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** Mint a signed preview token for a draft row. */
|
|
377
|
+
export declare const previewToken: (type: string, id: string, options?: {
|
|
378
|
+
ttlSeconds?: number;
|
|
379
|
+
secret?: string;
|
|
380
|
+
}) => string;
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Publish a draft: copy the draft row's declared fields into
|
|
384
|
+
* `<type>_published` (insert on first publish, keyed update after — the
|
|
385
|
+
* published row keeps the SAME id) and mark the draft `status:
|
|
386
|
+
* 'published'`. Runs inside `store.transactional`, so the copy + the
|
|
387
|
+
* draft status flip land atomically per row. Both tables are
|
|
388
|
+
* `.reactive()`, so the publish wakes live consumer subscriptions.
|
|
389
|
+
*
|
|
390
|
+
* A missing draft id — including another tenant's draft, which a scoped
|
|
391
|
+
* store reads as absent — throws `ContentNotFound`.
|
|
392
|
+
*
|
|
393
|
+
* Returns the published row.
|
|
394
|
+
*/
|
|
395
|
+
export declare const publish: (store: DataStore, type: ContentType, id: string) => Promise<Row>;
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Publish every draft of `type` whose scheduled `publishAt` has passed
|
|
399
|
+
* (and that is still a draft). Returns the ids that were published. A
|
|
400
|
+
* driver calls this on an interval from a `*.cron.tsx` schedule (see the
|
|
401
|
+
* README/docs recipe) — it is idempotent: `publish` flips the draft
|
|
402
|
+
* `status` to `'published'`, so a row is never double-published, and
|
|
403
|
+
* `publishAt` is cleared so a later re-save doesn't spuriously re-fire.
|
|
404
|
+
*
|
|
405
|
+
* Runs over the store it is handed: a request-scoped store publishes one
|
|
406
|
+
* tenant's due drafts; a background/root store sweeps every tenant.
|
|
407
|
+
*/
|
|
408
|
+
export declare const publishDue: (store: DataStore, type: ContentType, now?: Date) => Promise<ReadonlyArray<string>>;
|
|
409
|
+
|
|
410
|
+
/** Effect-native `publishDue` — the sweep never fails typed (a store/db error
|
|
411
|
+
* is a defect), so it carries `never` on the error channel. */
|
|
412
|
+
export declare const publishDueEffect: (store: DataStore, type: ContentType, now?: Date) => Effect.Effect<ReadonlyArray<string>, never>;
|
|
413
|
+
|
|
414
|
+
/** Effect-native `publish` (see {@link saveDraftEffect}). */
|
|
415
|
+
export declare const publishEffect: (store: DataStore, type: ContentType, id: string) => Effect.Effect<Row, ContentNotFound>;
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Resolve every `media` field on ONE row (including media nested inside
|
|
419
|
+
* `Array` / `Struct` fields) from its stored storage key to a served URL,
|
|
420
|
+
* using the caller-supplied `resolve`. Returns a NEW row; the input is not
|
|
421
|
+
* mutated. A key the resolver returns `null` for is left as-is.
|
|
422
|
+
*
|
|
423
|
+
* A content type with no media fields returns the row unchanged (no work).
|
|
424
|
+
*/
|
|
425
|
+
export declare const resolveMedia: (type: Pick<ContentType, "fields">, row: Readonly<Record<string, unknown>>, resolve: MediaResolver) => Promise<Record<string, unknown>>;
|
|
426
|
+
|
|
427
|
+
/** Resolve media across MANY rows in one call (the list-read case). */
|
|
428
|
+
export declare const resolveMediaAll: (type: Pick<ContentType, "fields">, rows: ReadonlyArray<Readonly<Record<string, unknown>>>, resolve: MediaResolver) => Promise<ReadonlyArray<Record<string, unknown>>>;
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Options carried by a RichText field, enforced at save time by
|
|
432
|
+
* `validateContent`: an explicit `false` REJECTS the content class
|
|
433
|
+
* (image/embed nodes in a structured document, or `<img>` / `<iframe>` /
|
|
434
|
+
* `<embed>` markers in a string value); `true` or omitted allows it.
|
|
435
|
+
*/
|
|
436
|
+
export declare interface RichTextOptions {
|
|
437
|
+
readonly allowImages?: boolean;
|
|
438
|
+
readonly allowEmbeds?: boolean;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Save a row into `<type>_drafts`: apply the `derivedFrom` derivations
|
|
443
|
+
* (caller-provided values for derived fields are overwritten), validate
|
|
444
|
+
* the derived row against the field rules (throws
|
|
445
|
+
* `ContentValidationFailed` with per-field violations), then write.
|
|
446
|
+
*
|
|
447
|
+
* Without `row.id` a new draft is inserted (the id scheme generates the
|
|
448
|
+
* TypeID). With `row.id`, the caller's OWN draft is updated in place; an
|
|
449
|
+
* id that exists only outside the caller's tenant scope throws
|
|
450
|
+
* `ContentIdConflict` — it is never updated or overwritten. The row is
|
|
451
|
+
* written with `status: 'draft'`; through a request-scoped `ctx.store`
|
|
452
|
+
* the tenant/audit columns are stamped by the store spine.
|
|
453
|
+
*
|
|
454
|
+
* Returns the written draft row.
|
|
455
|
+
*/
|
|
456
|
+
export declare const saveDraft: (store: DataStore, type: ContentType, row: Readonly<Record<string, unknown>>) => Promise<Row>;
|
|
457
|
+
|
|
458
|
+
/** Effect-native `saveDraft` for handlers written in `Effect.gen` — the
|
|
459
|
+
* typed failures ride the error channel (`Effect.catchTag('ContentValidationFailed', …)`). */
|
|
460
|
+
export declare const saveDraftEffect: (store: DataStore, type: ContentType, row: Readonly<Record<string, unknown>>) => Effect.Effect<Row, ContentValidationFailed | ContentIdConflict>;
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Mark a draft to publish automatically at `at`. Sets the draft's
|
|
464
|
+
* `publishAt` timestamp WITHOUT publishing now — a later `publishDue`
|
|
465
|
+
* sweep flips it live once `at` has passed. Keys off the caller's own
|
|
466
|
+
* draft (a foreign / missing id throws `ContentNotFound`); the draft's
|
|
467
|
+
* `status` stays `'draft'` until it goes live.
|
|
468
|
+
*
|
|
469
|
+
* Returns the updated draft row.
|
|
470
|
+
*/
|
|
471
|
+
export declare const schedulePublish: (store: DataStore, type: ContentType, id: string, at: Date) => Promise<Row>;
|
|
472
|
+
|
|
473
|
+
/** Effect-native `schedulePublish` — the `ContentNotFound` failure rides the
|
|
474
|
+
* error channel (`Effect.catchTag('ContentNotFound', …)`). */
|
|
475
|
+
export declare const schedulePublishEffect: (store: DataStore, type: ContentType, id: string, at: Date) => Effect.Effect<Row, ContentNotFound>;
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* The CMS field namespace. Scalars mirror the effect/Schema names the
|
|
479
|
+
* docs use; the CMS-only constructors (RichText / Media / Reference /
|
|
480
|
+
* Array / Struct) carry their editor metadata. `optional()` is exposed
|
|
481
|
+
* as a property accessor on each scalar through the underlying field.
|
|
482
|
+
*/
|
|
483
|
+
export declare const Schema: {
|
|
484
|
+
/** Single-line text. `.pipe(maxLength(N>120))` switches it to a textarea. */
|
|
485
|
+
readonly String: PipeableField;
|
|
486
|
+
/** Numeric input. */
|
|
487
|
+
readonly Number: PipeableField;
|
|
488
|
+
/** Toggle. */
|
|
489
|
+
readonly Boolean: PipeableField;
|
|
490
|
+
/** Date-only picker. */
|
|
491
|
+
readonly Date: PipeableField;
|
|
492
|
+
/** Date + time picker. */
|
|
493
|
+
readonly DateTime: PipeableField;
|
|
494
|
+
readonly maxLength: (value: number) => FieldRefinement;
|
|
495
|
+
readonly pattern: (re: RegExp) => FieldRefinement;
|
|
496
|
+
readonly unique: () => FieldRefinement;
|
|
497
|
+
/** Closed value set → `<select>` widget + a CHECK constraint. */
|
|
498
|
+
readonly Literal: (values_0: string, ...values: string[]) => PipeableField;
|
|
499
|
+
/**
|
|
500
|
+
* Rich text, stored as a JSON document (e.g. a TipTap doc). The shipped
|
|
501
|
+
* default widget is a plain textarea placeholder — swap in a real editor
|
|
502
|
+
* via the `ContentForm` `widgets` override. `allowImages` /
|
|
503
|
+
* `allowEmbeds: false` reject that content class at save time.
|
|
504
|
+
*/
|
|
505
|
+
readonly RichText: (options?: RichTextOptions) => PipeableField;
|
|
506
|
+
/**
|
|
507
|
+
* A storage object key (e.g. one issued by `@voltro/plugin-storage`),
|
|
508
|
+
* stored as text. Resolve the key to a served URL after a read with
|
|
509
|
+
* `resolveMedia(type, row, resolver)` — the app supplies the resolver
|
|
510
|
+
* (built from its storage layer's `getUrl`/`mintUrl`), so the core
|
|
511
|
+
* package stays decoupled from any storage plugin.
|
|
512
|
+
*/
|
|
513
|
+
readonly Media: () => PipeableField;
|
|
514
|
+
/** A reference to another content type's published rows. */
|
|
515
|
+
readonly Reference: (type: string) => PipeableField;
|
|
516
|
+
/** Repeatable field. Stored as a JSON array. */
|
|
517
|
+
readonly Array: (element: FieldSchema) => PipeableField;
|
|
518
|
+
/** Nested object. Stored as a JSON document. */
|
|
519
|
+
readonly Struct: (fields: Readonly<Record<string, FieldSchema>>) => PipeableField;
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
/** The canonical slug helper: lowercase, hyphenate, strip non-url chars. */
|
|
523
|
+
export declare const slugify: (value: unknown) => string;
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Mark a string field unique PER TENANT. The derived `<type>_drafts` +
|
|
527
|
+
* `<type>_published` tables get a real composite `(tenantId, <field>)`
|
|
528
|
+
* DB UNIQUE constraint (the tenant() mixin's `tenantId` is NOT NULL, so
|
|
529
|
+
* no partial/NULL machinery is needed) — the database rejects a second
|
|
530
|
+
* row with the same slug in the same tenant, not the application. Pair it
|
|
531
|
+
* with `derivedFrom('title', slugify)` for a per-tenant-unique slug.
|
|
532
|
+
*/
|
|
533
|
+
export declare const unique: () => FieldRefinement;
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Take a published row down: delete it from `<type>_published` and flip
|
|
537
|
+
* the draft (when present) back to `status: 'draft'`. Atomic per row via
|
|
538
|
+
* `store.transactional`. Throws `ContentNotFound` when the id has no
|
|
539
|
+
* published row in the caller's scope.
|
|
540
|
+
*/
|
|
541
|
+
export declare const unpublish: (store: DataStore, type: ContentType, id: string) => Promise<void>;
|
|
542
|
+
|
|
543
|
+
/** Effect-native `unpublish` (see {@link saveDraftEffect}). */
|
|
544
|
+
export declare const unpublishEffect: (store: DataStore, type: ContentType, id: string) => Effect.Effect<void, ContentNotFound>;
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Typed accessor for handlers — reads the `cms` slot the app attached to
|
|
548
|
+
* `AppContext`. Throws a clear message when the CMS isn't wired. Mirrors
|
|
549
|
+
* the `useWebhooks(ctx)` pattern (slot typed `unknown` to avoid a
|
|
550
|
+
* circular dep with @voltro/runtime).
|
|
551
|
+
*/
|
|
552
|
+
export declare const useCms: (ctx: {
|
|
553
|
+
readonly cms?: unknown;
|
|
554
|
+
}) => CmsApi;
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Validate a row against a content type's save-time rules. Throws
|
|
558
|
+
* `ContentValidationFailed` carrying EVERY violation (per-field detail)
|
|
559
|
+
* when any rule breaks; returns nothing on success. The throwing face of
|
|
560
|
+
* `contentViolations` — `saveDraft` calls this after derivation.
|
|
561
|
+
*/
|
|
562
|
+
export declare const validateContent: (type: Pick<ContentType, "name" | "fields">, row: Readonly<Record<string, unknown>>) => void;
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Verify a preview token. Returns the `{ type, id }` claim on success,
|
|
566
|
+
* `null` on any failure (malformed, signature mismatch, expired). Never
|
|
567
|
+
* throws. Constant-time signature compare.
|
|
568
|
+
*/
|
|
569
|
+
export declare const verifyPreviewToken: (token: string, options?: {
|
|
570
|
+
secret?: string;
|
|
571
|
+
}) => PreviewClaim | null;
|
|
572
|
+
|
|
573
|
+
/** Comparison operators accepted by `.where(field, op, value)`. */
|
|
574
|
+
export declare type WhereOp = '=' | '!=' | '<' | '<=' | '>' | '>=';
|
|
575
|
+
|
|
576
|
+
export declare type Widget = (props: WidgetProps) => ReactNode;
|
|
577
|
+
|
|
578
|
+
/** Resolve the widget for a field, honouring `widgets` overrides. */
|
|
579
|
+
export declare const widgetFor: (hint: EditorHint, overrides?: Partial<Record<EditorHint, Widget>>) => Widget;
|
|
580
|
+
|
|
581
|
+
/** Props for a single field widget. */
|
|
582
|
+
export declare interface WidgetProps {
|
|
583
|
+
readonly name: string;
|
|
584
|
+
readonly field: FieldSchema;
|
|
585
|
+
readonly value: unknown;
|
|
586
|
+
readonly onChange: (value: unknown) => void;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
export { }
|