@pramen/cms 0.0.59 → 0.0.60
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/README.md +55 -0
- package/dist/blockkit.d.ts +258 -0
- package/dist/blockkit.js +195 -0
- package/dist/index.d.ts +944 -3
- package/dist/index.js +1896 -70
- package/dist/nav.d.ts +30 -0
- package/dist/nav.js +37 -0
- package/package.json +2 -2
- package/src/blockkit.ts +316 -0
- package/src/index.ts +2018 -83
- package/src/nav.ts +38 -0
package/dist/index.d.ts
CHANGED
|
@@ -43,7 +43,29 @@ export interface FieldDefinition {
|
|
|
43
43
|
*
|
|
44
44
|
* Stored as text. Uniqueness is the schema's job (`unique(t.text())`).
|
|
45
45
|
*/
|
|
46
|
-
| "slug" | "media" | "select"
|
|
46
|
+
| "slug" | "media" | "select"
|
|
47
|
+
/**
|
|
48
|
+
* A pointer to a record that is NOT this row — another pramen row, or a record in a
|
|
49
|
+
* system the CMS does not own. Stored as an OPAQUE id (a string), so the same field
|
|
50
|
+
* links a `cms_pages` row, a collection row and an external CRM record alike.
|
|
51
|
+
*
|
|
52
|
+
* `optionsFrom` on a `select` is most of this already, and stays the ergonomic case for
|
|
53
|
+
* a short closed list: it fetches `{ value, label }[]` ONCE and renders a `<select>`.
|
|
54
|
+
* What it cannot do is scale — no search term, no paging, and no way to render the
|
|
55
|
+
* label of a value whose record is not in the first (only) page. Twenty campaigns are
|
|
56
|
+
* fine; a thousand records are a dropdown nobody can use and a stored id that renders
|
|
57
|
+
* as a uuid.
|
|
58
|
+
*
|
|
59
|
+
* So a reference declares `referenceFrom`, a query handler called with BOTH shapes:
|
|
60
|
+
*
|
|
61
|
+
* { search?: string; limit: number; offset: number } -> browse / search
|
|
62
|
+
* { ids: string[] } -> resolve stored values
|
|
63
|
+
*
|
|
64
|
+
* and returning `{ items: ReferenceOption[]; hasMore?: boolean }` either way. The
|
|
65
|
+
* second shape is what makes an already-stored value renderable without fetching the
|
|
66
|
+
* whole set — the reason this is a field type and not a wider `select`.
|
|
67
|
+
*/
|
|
68
|
+
| "reference" | "repeater" | "group";
|
|
47
69
|
required?: boolean;
|
|
48
70
|
default?: unknown;
|
|
49
71
|
/** repeater/group only — the nested fields. */
|
|
@@ -58,6 +80,28 @@ export interface FieldDefinition {
|
|
|
58
80
|
optionsFrom?: string;
|
|
59
81
|
/** slug only — the sibling field this one is derived from (e.g. `"title"`). */
|
|
60
82
|
from?: string;
|
|
83
|
+
/** reference only — the query handler that resolves this reference. See the `reference`
|
|
84
|
+
* type above for the two request shapes it must answer. */
|
|
85
|
+
referenceFrom?: string;
|
|
86
|
+
/** reference only — store a LIST of ids rather than one. A multiple reference is a
|
|
87
|
+
* `t.json()` column (an array), a single one is `t.text()`; `validateCollections`
|
|
88
|
+
* enforces the difference, because storing an array in a TEXT column is a raw driver
|
|
89
|
+
* error on the first write and nothing earlier. */
|
|
90
|
+
multiple?: boolean;
|
|
91
|
+
}
|
|
92
|
+
/** One option a `reference` field's `referenceFrom` handler returns. `hint` is secondary
|
|
93
|
+
* text (a date, an owner, a status) shown under the label — a picker over a thousand
|
|
94
|
+
* records usually needs more than a name to tell two rows apart. */
|
|
95
|
+
export interface ReferenceOption {
|
|
96
|
+
value: string;
|
|
97
|
+
label: string;
|
|
98
|
+
hint?: string;
|
|
99
|
+
}
|
|
100
|
+
/** What a `reference` field's `referenceFrom` handler returns, for BOTH request shapes.
|
|
101
|
+
* `hasMore` drives the picker's "Load more"; omitting it means "this is everything". */
|
|
102
|
+
export interface ReferenceResult {
|
|
103
|
+
items: ReferenceOption[];
|
|
104
|
+
hasMore?: boolean;
|
|
61
105
|
}
|
|
62
106
|
/** A named region on a content type; `allowedTypes` (block-type slugs) restricts what
|
|
63
107
|
* may be placed there — `null`/omitted means any. */
|
|
@@ -103,7 +147,9 @@ export interface RichTextMark {
|
|
|
103
147
|
export type RichText = RichTextDoc;
|
|
104
148
|
/** Map one FieldDefinition (as a const literal) to the TS type of its RENDERED value.
|
|
105
149
|
* Media resolves to `ResolvedMedia` (the assemble-time shape a component receives). */
|
|
106
|
-
export type FieldTsType<D extends FieldDefinition> = D["type"] extends "text" | "textarea" | "url" | "select" | "date" | "datetime" | "publish" | "slug" ? string : D["type"] extends "richtext" ? RichText : D["type"] extends "number" ? number : D["type"] extends "boolean" ? boolean : D["type"] extends "media" ? ResolvedMedia | null : D["type"] extends "
|
|
150
|
+
export type FieldTsType<D extends FieldDefinition> = D["type"] extends "text" | "textarea" | "url" | "select" | "date" | "datetime" | "publish" | "slug" ? string : D["type"] extends "richtext" ? RichText : D["type"] extends "number" ? number : D["type"] extends "boolean" ? boolean : D["type"] extends "media" ? ResolvedMedia | null : D["type"] extends "reference" ? (D extends {
|
|
151
|
+
multiple: true;
|
|
152
|
+
} ? string[] : string) : D["type"] extends "group" ? InferBlockFields<NonNullable<D["fields"]>> : D["type"] extends "repeater" ? InferBlockFields<NonNullable<D["fields"]>>[] : unknown;
|
|
107
153
|
/** Infer the `fields` object type from a const `FieldDefinition[]`. Required fields are
|
|
108
154
|
* present; optional ones are `| undefined`. */
|
|
109
155
|
export type InferBlockFields<T extends readonly FieldDefinition[]> = {
|
|
@@ -165,6 +211,8 @@ export declare function defineContentType(slug: string, opts: {
|
|
|
165
211
|
regions: readonly RegionDefinition[];
|
|
166
212
|
defaultBlocks?: readonly DefaultBlockDefinition[];
|
|
167
213
|
}): ContentTypeDef;
|
|
214
|
+
/** The default `owner` — see {@link cmsBootstrap}. */
|
|
215
|
+
export declare const CMS_BOOTSTRAP_OWNER = "cms";
|
|
168
216
|
/** Build a pramen `bootstrap` reconciler that upserts code-defined block + content types by
|
|
169
217
|
* `slug` on each boot. Idempotent: inserts a missing type, updates a drifted one, leaves an
|
|
170
218
|
* identical one untouched. Register it on your app:
|
|
@@ -173,10 +221,21 @@ export declare function defineContentType(slug: string, opts: {
|
|
|
173
221
|
* bootstrap: [ cmsBootstrap({ blockTypes: [...], contentTypes: [...] }) ] };
|
|
174
222
|
*
|
|
175
223
|
* Runs with a privileged system Db, so a fresh/reprovisioned database converges to the
|
|
176
|
-
* code-declared types with no manual createContentType/createBlockType call.
|
|
224
|
+
* code-declared types with no manual createContentType/createBlockType call.
|
|
225
|
+
*
|
|
226
|
+
* Every row it writes is stamped `managedBy: owner`, which makes the editor show it
|
|
227
|
+
* read-only — convergence and an editor pointed at the same rows are otherwise a silent
|
|
228
|
+
* data-loss pair (GitHub #48). A type that drops out of the declaration is released back to
|
|
229
|
+
* the editor; a row this owner did not write is never touched, so a second reconciler (a
|
|
230
|
+
* package shipping its own block types, say) composes as long as it passes its own `owner`.
|
|
231
|
+
*
|
|
232
|
+
* The definitions are validated HERE, when the app is constructed, and every problem is
|
|
233
|
+
* reported at once — see {@link validateCmsDefinitions}. */
|
|
177
234
|
export declare function cmsBootstrap(defs: {
|
|
178
235
|
blockTypes?: readonly BlockTypeDef[];
|
|
179
236
|
contentTypes?: readonly ContentTypeDef[];
|
|
237
|
+
}, opts?: {
|
|
238
|
+
owner?: string;
|
|
180
239
|
}): BootstrapFn;
|
|
181
240
|
/** Emit a `.ts` module of per-slug field interfaces + a `BlockFieldsBySlug` registry from
|
|
182
241
|
* DB-stored block types (`{ slug, fieldsSchema }` rows). The runtime counterpart to the
|
|
@@ -187,6 +246,104 @@ export declare function generateBlockTypes(blockTypes: Array<{
|
|
|
187
246
|
}>): string;
|
|
188
247
|
/** The block/page builder tables. All in the default partition (relations can't cross
|
|
189
248
|
* partitions). Prefixed `cms_` to avoid colliding with your own entities. */
|
|
249
|
+
/** What a menu item points at.
|
|
250
|
+
*
|
|
251
|
+
* `custom` is a literal href. The rest are REFERENCES resolved at read time, which is the
|
|
252
|
+
* whole reason the kind exists: a menu that stored `/about` verbatim breaks silently the
|
|
253
|
+
* day the page's slug changes, and the redirect that covers it is a second thing to
|
|
254
|
+
* remember. A `page` item follows the page. */
|
|
255
|
+
export type MenuItemKind = "custom" | "page" | "term" | "collection";
|
|
256
|
+
/** One entry in a menu tree. */
|
|
257
|
+
export interface MenuItem {
|
|
258
|
+
/** Stable within the menu — the editor's list key and the only handle a reorder has. */
|
|
259
|
+
id: string;
|
|
260
|
+
label: string;
|
|
261
|
+
/** Defaults to `"custom"` (a literal `url`). */
|
|
262
|
+
kind?: MenuItemKind;
|
|
263
|
+
/** `page`: a `cms_pages` id · `term`: a `cms_terms` id · `collection`: a collection slug. */
|
|
264
|
+
ref?: string | null;
|
|
265
|
+
/** `custom`: the href, stored verbatim (and `isSafeHref`-checked on write). For the
|
|
266
|
+
* resolved kinds this is FILLED IN by `getMenu` and ignored on write. */
|
|
267
|
+
url?: string;
|
|
268
|
+
target?: string;
|
|
269
|
+
titleAttr?: string;
|
|
270
|
+
cssClasses?: string;
|
|
271
|
+
children?: MenuItem[];
|
|
272
|
+
}
|
|
273
|
+
/** A named navigation menu. Read whole with `getMenu(name)`. */
|
|
274
|
+
export interface Menu {
|
|
275
|
+
id: string;
|
|
276
|
+
name: string;
|
|
277
|
+
label: string;
|
|
278
|
+
items: MenuItem[];
|
|
279
|
+
}
|
|
280
|
+
/** How deep a menu tree may nest. Menus are stored as one document, so without a cap a
|
|
281
|
+
* client could post a tree deep enough to blow the stack in the resolver — and no real
|
|
282
|
+
* navigation is more than three levels anyway. */
|
|
283
|
+
export declare const MAX_MENU_DEPTH = 5;
|
|
284
|
+
/** How many items one menu may hold, at every level combined.
|
|
285
|
+
*
|
|
286
|
+
* Depth alone is not a bound: a FLAT list of 800 `page` items is legal under
|
|
287
|
+
* `MAX_MENU_DEPTH` and turns every anonymous `getMenu` — the read on every page render of
|
|
288
|
+
* the site — into a single `WHERE id IN (?×800)`, which the read engine emits with no
|
|
289
|
+
* chunking. Every other read added alongside this one is bounded (`MAX_TERMS`,
|
|
290
|
+
* `clampLimit`); this one was not, and it is the one on the hot path. */
|
|
291
|
+
export declare const MAX_MENU_ITEMS = 200;
|
|
292
|
+
/** A classification vocabulary — `category`, `tag`, `region`, whatever the site sorts by. */
|
|
293
|
+
export interface Taxonomy {
|
|
294
|
+
id: string;
|
|
295
|
+
slug: string;
|
|
296
|
+
label: string;
|
|
297
|
+
pluralLabel?: string | null;
|
|
298
|
+
description?: string | null;
|
|
299
|
+
/** Terms may declare a `parentId`. A flat vocabulary REJECTS one on write, rather than
|
|
300
|
+
* accepting it and rendering it nowhere. */
|
|
301
|
+
hierarchical: boolean;
|
|
302
|
+
}
|
|
303
|
+
/** One term in a vocabulary. `children` is present only on the tree read (`getTermTree`). */
|
|
304
|
+
export interface Term {
|
|
305
|
+
id: string;
|
|
306
|
+
taxonomyId: string;
|
|
307
|
+
slug: string;
|
|
308
|
+
label: string;
|
|
309
|
+
description?: string | null;
|
|
310
|
+
parentId?: string | null;
|
|
311
|
+
position: number;
|
|
312
|
+
children?: Term[];
|
|
313
|
+
}
|
|
314
|
+
/** How deep a term hierarchy may nest — the same argument as {@link MAX_MENU_DEPTH}, except
|
|
315
|
+
* here the tree is rows and the risk is a parent CYCLE, which `assertTermParent` refuses. */
|
|
316
|
+
export declare const MAX_TERM_DEPTH = 5;
|
|
317
|
+
/** What a widget renders. `component` is the escape hatch: the CMS stores an id + props and
|
|
318
|
+
* the front end maps the id to one of its own components, exactly as `BlockRenderer` maps a
|
|
319
|
+
* block type slug — so a widget area can hold something the CMS has no idea how to draw. */
|
|
320
|
+
export type WidgetType = "content" | "menu" | "component";
|
|
321
|
+
/** One widget in a widget area. */
|
|
322
|
+
export interface Widget {
|
|
323
|
+
id: string;
|
|
324
|
+
type: WidgetType;
|
|
325
|
+
title?: string | null;
|
|
326
|
+
/** `content`: a rich-text document (normalized on write like any other richtext value). */
|
|
327
|
+
content?: RichTextDoc;
|
|
328
|
+
/** `menu`: a `cms_menus.name`. `getWidgetArea` resolves it to `menu` alongside. */
|
|
329
|
+
menuName?: string;
|
|
330
|
+
/** `component`: the front end's own component id + its props. */
|
|
331
|
+
componentId?: string;
|
|
332
|
+
componentProps?: FieldValues;
|
|
333
|
+
}
|
|
334
|
+
/** A widget as READ back: a `menu` widget carries its resolved menu, so a layout renders a
|
|
335
|
+
* whole sidebar from one call rather than one call per widget. */
|
|
336
|
+
export interface ResolvedWidget extends Widget {
|
|
337
|
+
menu?: Menu | null;
|
|
338
|
+
}
|
|
339
|
+
/** A named template region an admin fills without touching code. */
|
|
340
|
+
export interface WidgetArea {
|
|
341
|
+
id: string;
|
|
342
|
+
name: string;
|
|
343
|
+
label: string;
|
|
344
|
+
description?: string | null;
|
|
345
|
+
widgets: ResolvedWidget[];
|
|
346
|
+
}
|
|
190
347
|
export declare const cmsSchema: {
|
|
191
348
|
cms_content_types: import("@pramen/server").EntityDef<{
|
|
192
349
|
id: {
|
|
@@ -221,6 +378,9 @@ export declare const cmsSchema: {
|
|
|
221
378
|
defaultBlocks: {
|
|
222
379
|
readonly type: "json";
|
|
223
380
|
};
|
|
381
|
+
managedBy: {
|
|
382
|
+
readonly type: "text";
|
|
383
|
+
};
|
|
224
384
|
createdAt: {
|
|
225
385
|
readonly type: "text";
|
|
226
386
|
} & {
|
|
@@ -260,6 +420,9 @@ export declare const cmsSchema: {
|
|
|
260
420
|
category: {
|
|
261
421
|
readonly type: "text";
|
|
262
422
|
};
|
|
423
|
+
managedBy: {
|
|
424
|
+
readonly type: "text";
|
|
425
|
+
};
|
|
263
426
|
createdAt: {
|
|
264
427
|
readonly type: "text";
|
|
265
428
|
} & {
|
|
@@ -428,6 +591,13 @@ export declare const cmsSchema: {
|
|
|
428
591
|
readonly target: "cms_page_blocks";
|
|
429
592
|
readonly column: string;
|
|
430
593
|
};
|
|
594
|
+
terms: {
|
|
595
|
+
readonly kind: "manyToMany";
|
|
596
|
+
readonly target: "cms_terms";
|
|
597
|
+
readonly through: string;
|
|
598
|
+
readonly sourceColumn: string;
|
|
599
|
+
readonly targetColumn: string;
|
|
600
|
+
};
|
|
431
601
|
}>;
|
|
432
602
|
cms_page_blocks: import("@pramen/server").EntityDef<{
|
|
433
603
|
id: {
|
|
@@ -600,6 +770,270 @@ export declare const cmsSchema: {
|
|
|
600
770
|
readonly type: "text";
|
|
601
771
|
};
|
|
602
772
|
}, Record<string, never>>;
|
|
773
|
+
cms_menus: import("@pramen/server").EntityDef<{
|
|
774
|
+
id: {
|
|
775
|
+
readonly type: "uuid";
|
|
776
|
+
} & {
|
|
777
|
+
readonly generated: true;
|
|
778
|
+
} & {
|
|
779
|
+
readonly primaryKey: true;
|
|
780
|
+
readonly notNull: true;
|
|
781
|
+
};
|
|
782
|
+
name: {
|
|
783
|
+
readonly type: "text";
|
|
784
|
+
} & {
|
|
785
|
+
readonly notNull: true;
|
|
786
|
+
} & {
|
|
787
|
+
readonly unique: true;
|
|
788
|
+
};
|
|
789
|
+
label: {
|
|
790
|
+
readonly type: "text";
|
|
791
|
+
} & {
|
|
792
|
+
readonly notNull: true;
|
|
793
|
+
};
|
|
794
|
+
items: {
|
|
795
|
+
readonly type: "json";
|
|
796
|
+
};
|
|
797
|
+
version: {
|
|
798
|
+
readonly type: "integer";
|
|
799
|
+
} & {
|
|
800
|
+
readonly default: 1;
|
|
801
|
+
};
|
|
802
|
+
createdAt: {
|
|
803
|
+
readonly type: "text";
|
|
804
|
+
} & {
|
|
805
|
+
readonly defaultExpr: string;
|
|
806
|
+
};
|
|
807
|
+
updatedAt: {
|
|
808
|
+
readonly type: "text";
|
|
809
|
+
} & {
|
|
810
|
+
readonly defaultExpr: string;
|
|
811
|
+
};
|
|
812
|
+
}, Record<string, never>>;
|
|
813
|
+
cms_redirects: import("@pramen/server").EntityDef<{
|
|
814
|
+
id: {
|
|
815
|
+
readonly type: "uuid";
|
|
816
|
+
} & {
|
|
817
|
+
readonly generated: true;
|
|
818
|
+
} & {
|
|
819
|
+
readonly primaryKey: true;
|
|
820
|
+
readonly notNull: true;
|
|
821
|
+
};
|
|
822
|
+
fromPath: {
|
|
823
|
+
readonly type: "text";
|
|
824
|
+
} & {
|
|
825
|
+
readonly notNull: true;
|
|
826
|
+
} & {
|
|
827
|
+
readonly unique: true;
|
|
828
|
+
};
|
|
829
|
+
toPath: {
|
|
830
|
+
readonly type: "text";
|
|
831
|
+
} & {
|
|
832
|
+
readonly notNull: true;
|
|
833
|
+
};
|
|
834
|
+
status: {
|
|
835
|
+
readonly type: "integer";
|
|
836
|
+
} & {
|
|
837
|
+
readonly default: 301;
|
|
838
|
+
};
|
|
839
|
+
enabled: {
|
|
840
|
+
readonly type: "boolean";
|
|
841
|
+
} & {
|
|
842
|
+
readonly default: true;
|
|
843
|
+
};
|
|
844
|
+
note: {
|
|
845
|
+
readonly type: "text";
|
|
846
|
+
};
|
|
847
|
+
createdAt: {
|
|
848
|
+
readonly type: "text";
|
|
849
|
+
} & {
|
|
850
|
+
readonly defaultExpr: string;
|
|
851
|
+
};
|
|
852
|
+
updatedAt: {
|
|
853
|
+
readonly type: "text";
|
|
854
|
+
} & {
|
|
855
|
+
readonly defaultExpr: string;
|
|
856
|
+
};
|
|
857
|
+
}, Record<string, never>>;
|
|
858
|
+
cms_taxonomies: import("@pramen/server").EntityDef<{
|
|
859
|
+
id: {
|
|
860
|
+
readonly type: "uuid";
|
|
861
|
+
} & {
|
|
862
|
+
readonly generated: true;
|
|
863
|
+
} & {
|
|
864
|
+
readonly primaryKey: true;
|
|
865
|
+
readonly notNull: true;
|
|
866
|
+
};
|
|
867
|
+
slug: {
|
|
868
|
+
readonly type: "text";
|
|
869
|
+
} & {
|
|
870
|
+
readonly notNull: true;
|
|
871
|
+
} & {
|
|
872
|
+
readonly unique: true;
|
|
873
|
+
};
|
|
874
|
+
label: {
|
|
875
|
+
readonly type: "text";
|
|
876
|
+
} & {
|
|
877
|
+
readonly notNull: true;
|
|
878
|
+
};
|
|
879
|
+
pluralLabel: {
|
|
880
|
+
readonly type: "text";
|
|
881
|
+
};
|
|
882
|
+
description: {
|
|
883
|
+
readonly type: "text";
|
|
884
|
+
};
|
|
885
|
+
hierarchical: {
|
|
886
|
+
readonly type: "boolean";
|
|
887
|
+
} & {
|
|
888
|
+
readonly default: false;
|
|
889
|
+
};
|
|
890
|
+
createdAt: {
|
|
891
|
+
readonly type: "text";
|
|
892
|
+
} & {
|
|
893
|
+
readonly defaultExpr: string;
|
|
894
|
+
};
|
|
895
|
+
}, Record<string, never>>;
|
|
896
|
+
cms_terms: import("@pramen/server").EntityDef<{
|
|
897
|
+
id: {
|
|
898
|
+
readonly type: "uuid";
|
|
899
|
+
} & {
|
|
900
|
+
readonly generated: true;
|
|
901
|
+
} & {
|
|
902
|
+
readonly primaryKey: true;
|
|
903
|
+
readonly notNull: true;
|
|
904
|
+
};
|
|
905
|
+
taxonomyId: {
|
|
906
|
+
readonly type: "uuid";
|
|
907
|
+
} & {
|
|
908
|
+
readonly notNull: true;
|
|
909
|
+
} & {
|
|
910
|
+
readonly index: true;
|
|
911
|
+
};
|
|
912
|
+
slug: {
|
|
913
|
+
readonly type: "text";
|
|
914
|
+
} & {
|
|
915
|
+
readonly notNull: true;
|
|
916
|
+
};
|
|
917
|
+
label: {
|
|
918
|
+
readonly type: "text";
|
|
919
|
+
} & {
|
|
920
|
+
readonly notNull: true;
|
|
921
|
+
};
|
|
922
|
+
description: {
|
|
923
|
+
readonly type: "text";
|
|
924
|
+
};
|
|
925
|
+
parentId: {
|
|
926
|
+
readonly type: "uuid";
|
|
927
|
+
};
|
|
928
|
+
position: {
|
|
929
|
+
readonly type: "integer";
|
|
930
|
+
} & {
|
|
931
|
+
readonly default: 0;
|
|
932
|
+
};
|
|
933
|
+
createdAt: {
|
|
934
|
+
readonly type: "text";
|
|
935
|
+
} & {
|
|
936
|
+
readonly defaultExpr: string;
|
|
937
|
+
};
|
|
938
|
+
}, {
|
|
939
|
+
taxonomy: {
|
|
940
|
+
readonly kind: "belongsTo";
|
|
941
|
+
readonly target: "cms_taxonomies";
|
|
942
|
+
readonly column: string;
|
|
943
|
+
readonly onDelete: import("@pramen/server").OnDelete | undefined;
|
|
944
|
+
};
|
|
945
|
+
parent: {
|
|
946
|
+
readonly kind: "belongsTo";
|
|
947
|
+
readonly target: "cms_terms";
|
|
948
|
+
readonly column: string;
|
|
949
|
+
readonly onDelete: import("@pramen/server").OnDelete | undefined;
|
|
950
|
+
};
|
|
951
|
+
pages: {
|
|
952
|
+
readonly kind: "hasMany";
|
|
953
|
+
readonly target: "cms_page_terms";
|
|
954
|
+
readonly column: string;
|
|
955
|
+
};
|
|
956
|
+
}>;
|
|
957
|
+
cms_page_terms: import("@pramen/server").EntityDef<{
|
|
958
|
+
id: {
|
|
959
|
+
readonly type: "uuid";
|
|
960
|
+
} & {
|
|
961
|
+
readonly generated: true;
|
|
962
|
+
} & {
|
|
963
|
+
readonly primaryKey: true;
|
|
964
|
+
readonly notNull: true;
|
|
965
|
+
};
|
|
966
|
+
pageId: {
|
|
967
|
+
readonly type: "uuid";
|
|
968
|
+
} & {
|
|
969
|
+
readonly notNull: true;
|
|
970
|
+
} & {
|
|
971
|
+
readonly index: true;
|
|
972
|
+
};
|
|
973
|
+
termId: {
|
|
974
|
+
readonly type: "uuid";
|
|
975
|
+
} & {
|
|
976
|
+
readonly notNull: true;
|
|
977
|
+
} & {
|
|
978
|
+
readonly index: true;
|
|
979
|
+
};
|
|
980
|
+
}, {
|
|
981
|
+
page: {
|
|
982
|
+
readonly kind: "belongsTo";
|
|
983
|
+
readonly target: "cms_pages";
|
|
984
|
+
readonly column: string;
|
|
985
|
+
readonly onDelete: import("@pramen/server").OnDelete | undefined;
|
|
986
|
+
};
|
|
987
|
+
term: {
|
|
988
|
+
readonly kind: "belongsTo";
|
|
989
|
+
readonly target: "cms_terms";
|
|
990
|
+
readonly column: string;
|
|
991
|
+
readonly onDelete: import("@pramen/server").OnDelete | undefined;
|
|
992
|
+
};
|
|
993
|
+
}>;
|
|
994
|
+
cms_widget_areas: import("@pramen/server").EntityDef<{
|
|
995
|
+
id: {
|
|
996
|
+
readonly type: "uuid";
|
|
997
|
+
} & {
|
|
998
|
+
readonly generated: true;
|
|
999
|
+
} & {
|
|
1000
|
+
readonly primaryKey: true;
|
|
1001
|
+
readonly notNull: true;
|
|
1002
|
+
};
|
|
1003
|
+
name: {
|
|
1004
|
+
readonly type: "text";
|
|
1005
|
+
} & {
|
|
1006
|
+
readonly notNull: true;
|
|
1007
|
+
} & {
|
|
1008
|
+
readonly unique: true;
|
|
1009
|
+
};
|
|
1010
|
+
label: {
|
|
1011
|
+
readonly type: "text";
|
|
1012
|
+
} & {
|
|
1013
|
+
readonly notNull: true;
|
|
1014
|
+
};
|
|
1015
|
+
description: {
|
|
1016
|
+
readonly type: "text";
|
|
1017
|
+
};
|
|
1018
|
+
widgets: {
|
|
1019
|
+
readonly type: "json";
|
|
1020
|
+
};
|
|
1021
|
+
version: {
|
|
1022
|
+
readonly type: "integer";
|
|
1023
|
+
} & {
|
|
1024
|
+
readonly default: 1;
|
|
1025
|
+
};
|
|
1026
|
+
createdAt: {
|
|
1027
|
+
readonly type: "text";
|
|
1028
|
+
} & {
|
|
1029
|
+
readonly defaultExpr: string;
|
|
1030
|
+
};
|
|
1031
|
+
updatedAt: {
|
|
1032
|
+
readonly type: "text";
|
|
1033
|
+
} & {
|
|
1034
|
+
readonly defaultExpr: string;
|
|
1035
|
+
};
|
|
1036
|
+
}, Record<string, never>>;
|
|
603
1037
|
cms_media: import("@pramen/server").EntityDef<{
|
|
604
1038
|
id: {
|
|
605
1039
|
readonly type: "uuid";
|
|
@@ -627,6 +1061,28 @@ export declare const cmsSchema: {
|
|
|
627
1061
|
};
|
|
628
1062
|
}, Record<string, never>>;
|
|
629
1063
|
};
|
|
1064
|
+
/** Block Kit — custom admin pages, described as JSON and rendered by the editor. See
|
|
1065
|
+
* `./blockkit`. Re-exported so a host imports `adminPage` beside `collection`. */
|
|
1066
|
+
export { adminPage, createAdminPageHandlers, normalizeAdminResponse, validateAdminPages, MAX_ADMIN_BLOCK_DEPTH, } from "./blockkit";
|
|
1067
|
+
export type { AdminBlock, AdminButton, AdminElement, AdminInput, AdminInteractionType, AdminPageDef, AdminPageHandlerOpts, AdminPageInteraction, AdminPageMeta, AdminPageResponse, AdminText, } from "./blockkit";
|
|
1068
|
+
/**
|
|
1069
|
+
* Columns this package wrote in the pre-ISO space form that the SCHEMA cannot identify.
|
|
1070
|
+
*
|
|
1071
|
+
* `isoTimestampBackfill()` finds every column whose DEFAULT is `expr.now()` on its own.
|
|
1072
|
+
* `cms_pages.publishedAt` has no default at all — `doPublish` stamped it from handler code,
|
|
1073
|
+
* in the same space form, to stay comparable with the `updatedAt` written beside it. There
|
|
1074
|
+
* is nothing on that column to find, so it is named here.
|
|
1075
|
+
*
|
|
1076
|
+
* Spread it into the migration if your store was ever written by a build older than this
|
|
1077
|
+
* one:
|
|
1078
|
+
*
|
|
1079
|
+
* ```ts
|
|
1080
|
+
* migrations: [isoTimestampBackfill({ extraColumns: CMS_LEGACY_TIMESTAMP_COLUMNS })]
|
|
1081
|
+
* ```
|
|
1082
|
+
*
|
|
1083
|
+
* Costs nothing on a store that was not — the UPDATE matches no rows.
|
|
1084
|
+
*/
|
|
1085
|
+
export declare const CMS_LEGACY_TIMESTAMP_COLUMNS: Readonly<Record<string, readonly string[]>>;
|
|
630
1086
|
export interface ValidateOpts {
|
|
631
1087
|
/** Enforce `required` fields (reject when missing). Default true. Editor-facing draft
|
|
632
1088
|
* writes (addBlock/updateBlock/createPage) pass `false` — a DRAFT block may be incomplete;
|
|
@@ -644,6 +1100,43 @@ export interface ValidateOpts {
|
|
|
644
1100
|
legacyBaseline?: FieldValues;
|
|
645
1101
|
}
|
|
646
1102
|
export declare function validateFields(schema: FieldDefinition[] | undefined | null, values: unknown, path?: string, opts?: ValidateOpts): void;
|
|
1103
|
+
/** Every field type the runtime knows. Exported because the editor's type-builder offers
|
|
1104
|
+
* exactly this list — one definition, so a type added here appears there without a second
|
|
1105
|
+
* edit, and a type removed here cannot be authored. */
|
|
1106
|
+
export declare const FIELD_TYPES: readonly FieldDefinition["type"][];
|
|
1107
|
+
/** How deep an authored field schema may nest. A schema is rendered by a recursive
|
|
1108
|
+
* component and validated by a recursive function, so the cap is what keeps both bounded
|
|
1109
|
+
* against a hand-posted document; nothing real nests past two or three. */
|
|
1110
|
+
export declare const MAX_FIELD_DEPTH = 5;
|
|
1111
|
+
/**
|
|
1112
|
+
* Validate and canonicalize an authored `FieldDefinition[]`, throwing a 400 on the first
|
|
1113
|
+
* problem. Returns the CLEANED schema — each field rebuilt from the keys its type actually
|
|
1114
|
+
* uses, so a `select`'s stale `options` cannot ride along on a field someone switched to
|
|
1115
|
+
* `text` and reappear if they switch back.
|
|
1116
|
+
*/
|
|
1117
|
+
export declare function validateFieldSchema(raw: unknown, path?: string, depth?: number): FieldDefinition[];
|
|
1118
|
+
/** Resolve `slug` cross-references inside one schema, now that every sibling is known: a
|
|
1119
|
+
* `slug` field's `from` must name a field that exists AT THE SAME LEVEL (the editor reads
|
|
1120
|
+
* it out of the sibling bag) and holds text. Separate pass because forward references are
|
|
1121
|
+
* legitimate — a slug may precede the title it follows. */
|
|
1122
|
+
export declare function checkSlugSources(schema: readonly FieldDefinition[], path?: string): void;
|
|
1123
|
+
/** Validate + canonicalize an authored field schema end to end. */
|
|
1124
|
+
export declare function normalizeFieldSchema(raw: unknown, path?: string): FieldDefinition[];
|
|
1125
|
+
/**
|
|
1126
|
+
* Validate a content type's `regions`.
|
|
1127
|
+
*
|
|
1128
|
+
* A region NAME is the key `addBlock({ region })` resolves and the key of the assembled
|
|
1129
|
+
* `regions` object a front end reads, so it is held to the same shape as a field name. An
|
|
1130
|
+
* `allowedTypes` entry is a block-type SLUG; it is not checked against the block types that
|
|
1131
|
+
* exist, on purpose — a content type declaring a region for a block type that has not been
|
|
1132
|
+
* created yet is an ordinary order of work, and `assertRegionAllows` is what enforces the
|
|
1133
|
+
* list at placement time.
|
|
1134
|
+
*/
|
|
1135
|
+
export declare function normalizeRegions(raw: unknown): RegionDefinition[];
|
|
1136
|
+
/** Validate a content type's `defaultBlocks` against its own regions. A default block that
|
|
1137
|
+
* names a region the type does not declare is created into nowhere — `createPage` would
|
|
1138
|
+
* place it under a key no renderer reads. */
|
|
1139
|
+
export declare function normalizeDefaultBlocks(raw: unknown, regions: readonly RegionDefinition[]): DefaultBlockDefinition[];
|
|
647
1140
|
/** The node/mark vocabulary a `richtext` value may use. A node entry maps a node type to
|
|
648
1141
|
* the attribute names kept on it; a mark entry does the same for a mark type. */
|
|
649
1142
|
export interface RichTextSchema {
|
|
@@ -760,6 +1253,58 @@ export declare function imageUrl(key: string, opts?: {
|
|
|
760
1253
|
quality?: number;
|
|
761
1254
|
format?: "auto" | "webp" | "avif";
|
|
762
1255
|
}): string;
|
|
1256
|
+
/** Refuse an editor write to a CODE-DEFINED type.
|
|
1257
|
+
*
|
|
1258
|
+
* `cmsBootstrap` reconciles these rows on every boot, so a save here would return 200 and
|
|
1259
|
+
* then be reverted at the next cold start, taking any content authored against the added
|
|
1260
|
+
* field with it. A 409 is the honest answer: the row exists, the edit is well-formed, and
|
|
1261
|
+
* the conflict is with a definition that lives somewhere this request cannot reach.
|
|
1262
|
+
*
|
|
1263
|
+
* The editor renders a managed type read-only, so this is the curl / stale-tab half.
|
|
1264
|
+
*
|
|
1265
|
+
* The caller MUST have read `managedBy` explicitly (`select`), not taken it off a wide read.
|
|
1266
|
+
* Reads are column-projected against the caller's policy, so under a read policy with a
|
|
1267
|
+
* `fields` list the column is simply absent — and a guard written as "absent means editable"
|
|
1268
|
+
* disarms itself for exactly the deployments that restrict fields. `select` fails CLOSED
|
|
1269
|
+
* instead: an unreadable column is a 403 before this runs. */
|
|
1270
|
+
export declare function assertNotManaged(row: Record<string, unknown>, what: string, defineFn: string): void;
|
|
1271
|
+
/** Where a resolved menu item points. Passed to `menuHref` so a deployment can route its
|
|
1272
|
+
* own way without the CMS guessing. */
|
|
1273
|
+
export type MenuHrefTarget = {
|
|
1274
|
+
kind: "page";
|
|
1275
|
+
slug: string;
|
|
1276
|
+
locale: string;
|
|
1277
|
+
} | {
|
|
1278
|
+
kind: "term";
|
|
1279
|
+
taxonomy: string;
|
|
1280
|
+
slug: string;
|
|
1281
|
+
} | {
|
|
1282
|
+
kind: "collection";
|
|
1283
|
+
slug: string;
|
|
1284
|
+
};
|
|
1285
|
+
/** A URL redirect's status. 301/308 are permanent (cached by browsers, and by search
|
|
1286
|
+
* engines as a canonical move); 302/307 are not. Anything else is not a redirect. */
|
|
1287
|
+
export declare const REDIRECT_STATUSES: readonly number[];
|
|
1288
|
+
/**
|
|
1289
|
+
* The stored form of a redirect's `fromPath`: a rooted, query-less, fragment-less path.
|
|
1290
|
+
*
|
|
1291
|
+
* Canonicalized rather than merely validated, because matching is an exact string lookup
|
|
1292
|
+
* against a UNIQUE column. `"/old"` and `"/old/"` are the same URL to a visitor and two
|
|
1293
|
+
* rows here, so the second one is dead the moment the first exists — and which one wins is
|
|
1294
|
+
* whichever the editor happened to type. Trailing slash off (except the root), fragment
|
|
1295
|
+
* and query dropped, percent-encoding left exactly as written (the parser's, and the
|
|
1296
|
+
* request's, canonical form).
|
|
1297
|
+
*/
|
|
1298
|
+
export declare function normalizeRedirectPath(raw: unknown): string;
|
|
1299
|
+
/** A redirect's editable fields, as posted. `requireEnds` is the CREATE case, where both
|
|
1300
|
+
* ends must be present; an update patches whichever keys it sends. */
|
|
1301
|
+
interface RedirectPatch {
|
|
1302
|
+
fromPath?: string;
|
|
1303
|
+
toPath?: string;
|
|
1304
|
+
status?: number;
|
|
1305
|
+
enabled?: boolean;
|
|
1306
|
+
note?: string | null;
|
|
1307
|
+
}
|
|
763
1308
|
/** What a preview link authorizes: one page, in one tenant, until `exp`. */
|
|
764
1309
|
export interface PreviewToken {
|
|
765
1310
|
/** tenant */ t: string;
|
|
@@ -821,6 +1366,14 @@ export interface CmsHandlerOpts {
|
|
|
821
1366
|
* (what the shipped editor produces). Widen it if your editor adds TipTap extensions —
|
|
822
1367
|
* a node type absent from the schema is DROPPED on write, not rejected. */
|
|
823
1368
|
richTextSchema?: RichTextSchema;
|
|
1369
|
+
/** Map a resolved menu reference to a site path. The CMS is headless, so it cannot know
|
|
1370
|
+
* how a deployment routes — this is the same seam `sitemapXml`'s `pageUrl` is.
|
|
1371
|
+
*
|
|
1372
|
+
* Defaults: a page is `/${slug}` on a monolingual deployment and `/${locale}/${slug}`
|
|
1373
|
+
* once `locales` declares more than one; a term is `/${taxonomy}/${term}`; a collection
|
|
1374
|
+
* is `/${slug}/`. Override it and `getMenu` follows — which is the point of storing a
|
|
1375
|
+
* REFERENCE rather than the href an editor typed: change the routing, not the menu. */
|
|
1376
|
+
menuHref?: (target: MenuHrefTarget) => string;
|
|
824
1377
|
}
|
|
825
1378
|
/** Build the CMS handler map. Spread into your app's handlers. Editor mutations are
|
|
826
1379
|
* gated both by `auth` (fast 403 before the body) and by the row ACL (cmsPolicies). */
|
|
@@ -863,6 +1416,188 @@ export declare function createCmsHandlers(opts?: CmsHandlerOpts): {
|
|
|
863
1416
|
fieldsSchema?: FieldDefinition[];
|
|
864
1417
|
defaultBlocks?: DefaultBlockDefinition[];
|
|
865
1418
|
}, Record<string, unknown> | undefined>;
|
|
1419
|
+
/** One menu, references resolved to hrefs. PUBLIC — a menu is site chrome.
|
|
1420
|
+
*
|
|
1421
|
+
* Returns `null` for an unknown name rather than 404ing, because a layout asking for a
|
|
1422
|
+
* menu it has not created yet is the normal state of a site being built, and a thrown
|
|
1423
|
+
* error there takes down every page instead of rendering no nav. */
|
|
1424
|
+
getMenu: import("@pramen/server").Handler<{
|
|
1425
|
+
name: string;
|
|
1426
|
+
}, Menu | null>;
|
|
1427
|
+
/** Every menu, RAW (references unresolved) — the editor's list.
|
|
1428
|
+
*
|
|
1429
|
+
* Capped like every other list here. A site-furniture table is small by nature, which is
|
|
1430
|
+
* an argument for the cap being generous, not for its absence: an unbounded SELECT of a
|
|
1431
|
+
* wide row over RPC is the D1 failure shape from GitHub #22, and "small by nature" is
|
|
1432
|
+
* not a property the query planner knows about. */
|
|
1433
|
+
listMenus: import("@pramen/server").Handler<unknown, Record<string, unknown>[]>;
|
|
1434
|
+
createMenu: import("@pramen/server").Handler<{
|
|
1435
|
+
name: string;
|
|
1436
|
+
label: string;
|
|
1437
|
+
items?: MenuItem[];
|
|
1438
|
+
}, Record<string, unknown>>;
|
|
1439
|
+
/** Patch a menu found by `id` or `name`. `name` is the key layout code resolves and is
|
|
1440
|
+
* NOT mutable — retitling is what `label` is for. */
|
|
1441
|
+
updateMenu: import("@pramen/server").Handler<{
|
|
1442
|
+
id?: string;
|
|
1443
|
+
name?: string;
|
|
1444
|
+
label?: string;
|
|
1445
|
+
items?: MenuItem[];
|
|
1446
|
+
expectedVersion?: number;
|
|
1447
|
+
}, Record<string, unknown> | undefined>;
|
|
1448
|
+
deleteMenu: import("@pramen/server").Handler<{
|
|
1449
|
+
id: string;
|
|
1450
|
+
}, {
|
|
1451
|
+
ok: true;
|
|
1452
|
+
}>;
|
|
1453
|
+
/** Resolve a request path to its redirect, or `null`. PUBLIC and READ-ONLY: this is
|
|
1454
|
+
* what a front end calls on a 404, so it is anonymous traffic on the hot path.
|
|
1455
|
+
*
|
|
1456
|
+
* `enabled` is enforced by the public read POLICY, not by a `where` here, so a disabled
|
|
1457
|
+
* redirect is invisible to every anonymous read (this handler, a future listing, a
|
|
1458
|
+
* relation traversal) rather than to the one call that remembered to filter. */
|
|
1459
|
+
resolveRedirect: import("@pramen/server").Handler<{
|
|
1460
|
+
path: string;
|
|
1461
|
+
}, {
|
|
1462
|
+
to: string;
|
|
1463
|
+
status: number;
|
|
1464
|
+
} | null>;
|
|
1465
|
+
listRedirects: import("@pramen/server").Handler<{
|
|
1466
|
+
limit?: number;
|
|
1467
|
+
offset?: number;
|
|
1468
|
+
}, Record<string, unknown>[]>;
|
|
1469
|
+
createRedirect: import("@pramen/server").Handler<RedirectPatch & {
|
|
1470
|
+
fromPath: string;
|
|
1471
|
+
toPath: string;
|
|
1472
|
+
}, Record<string, unknown>>;
|
|
1473
|
+
updateRedirect: import("@pramen/server").Handler<RedirectPatch & {
|
|
1474
|
+
id: string;
|
|
1475
|
+
}, Record<string, unknown> | undefined>;
|
|
1476
|
+
deleteRedirect: import("@pramen/server").Handler<{
|
|
1477
|
+
id: string;
|
|
1478
|
+
}, {
|
|
1479
|
+
ok: true;
|
|
1480
|
+
}>;
|
|
1481
|
+
/** Every vocabulary. PUBLIC — like content types, a taxonomy's slug is structural (it
|
|
1482
|
+
* is a URL segment) and a front end routes on it. */
|
|
1483
|
+
listTaxonomies: import("@pramen/server").Handler<unknown, Record<string, unknown>[]>;
|
|
1484
|
+
createTaxonomy: import("@pramen/server").Handler<{
|
|
1485
|
+
slug: string;
|
|
1486
|
+
label: string;
|
|
1487
|
+
pluralLabel?: string;
|
|
1488
|
+
description?: string;
|
|
1489
|
+
hierarchical?: boolean;
|
|
1490
|
+
}, Record<string, unknown>>;
|
|
1491
|
+
/** Patch a vocabulary. `slug` is a URL segment and the key `listTerms` resolves, so it
|
|
1492
|
+
* is not mutable — the same rule content types and block types already follow.
|
|
1493
|
+
*
|
|
1494
|
+
* Turning `hierarchical` OFF is refused while any term still has a parent. Allowing it
|
|
1495
|
+
* would leave a stored hierarchy that no reader renders and no writer can clear, and
|
|
1496
|
+
* flattening the terms silently is a destructive edit behind a checkbox. */
|
|
1497
|
+
updateTaxonomy: import("@pramen/server").Handler<{
|
|
1498
|
+
id: string;
|
|
1499
|
+
label?: string;
|
|
1500
|
+
pluralLabel?: string | null;
|
|
1501
|
+
description?: string | null;
|
|
1502
|
+
hierarchical?: boolean;
|
|
1503
|
+
}, Record<string, unknown> | undefined>;
|
|
1504
|
+
/** Delete a vocabulary. Its terms go with it, and their page assignments with those —
|
|
1505
|
+
* both by real `ON DELETE CASCADE`, so the cleanup is the DB's and cannot be half-done
|
|
1506
|
+
* by a handler that threw between two writes. */
|
|
1507
|
+
deleteTaxonomy: import("@pramen/server").Handler<{
|
|
1508
|
+
id: string;
|
|
1509
|
+
}, {
|
|
1510
|
+
ok: true;
|
|
1511
|
+
}>;
|
|
1512
|
+
/** One vocabulary's terms, flat, ordered. PUBLIC. */
|
|
1513
|
+
listTerms: import("@pramen/server").Handler<{
|
|
1514
|
+
taxonomy: string;
|
|
1515
|
+
}, Term[]>;
|
|
1516
|
+
/** One vocabulary's terms as a TREE. PUBLIC. Assembled here rather than by the caller
|
|
1517
|
+
* because a hierarchy is what the nav renders and every consumer would otherwise write
|
|
1518
|
+
* the same fold. */
|
|
1519
|
+
getTermTree: import("@pramen/server").Handler<{
|
|
1520
|
+
taxonomy: string;
|
|
1521
|
+
}, Term[]>;
|
|
1522
|
+
createTerm: import("@pramen/server").Handler<{
|
|
1523
|
+
taxonomy: string;
|
|
1524
|
+
slug: string;
|
|
1525
|
+
label: string;
|
|
1526
|
+
description?: string;
|
|
1527
|
+
parentId?: string | null;
|
|
1528
|
+
position?: number;
|
|
1529
|
+
}, Record<string, unknown>>;
|
|
1530
|
+
updateTerm: import("@pramen/server").Handler<{
|
|
1531
|
+
id: string;
|
|
1532
|
+
slug?: string;
|
|
1533
|
+
label?: string;
|
|
1534
|
+
description?: string | null;
|
|
1535
|
+
parentId?: string | null;
|
|
1536
|
+
position?: number;
|
|
1537
|
+
}, Record<string, unknown> | undefined>;
|
|
1538
|
+
/** Delete a term. Children are promoted to the top level (`ON DELETE SET NULL`) and
|
|
1539
|
+
* page assignments are removed (`ON DELETE CASCADE`) — see `cms_terms.parentId`. */
|
|
1540
|
+
deleteTerm: import("@pramen/server").Handler<{
|
|
1541
|
+
id: string;
|
|
1542
|
+
}, {
|
|
1543
|
+
ok: true;
|
|
1544
|
+
}>;
|
|
1545
|
+
/** A page's assigned terms. PUBLIC (a published page's classification is public). */
|
|
1546
|
+
listPageTerms: import("@pramen/server").Handler<{
|
|
1547
|
+
pageId: string;
|
|
1548
|
+
}, Term[]>;
|
|
1549
|
+
/** Replace a page's term assignments wholesale.
|
|
1550
|
+
*
|
|
1551
|
+
* Set semantics, not add/remove: the editor's panel holds the whole selection, and two
|
|
1552
|
+
* calls that each patch one end of it race into a state neither asked for. Existing
|
|
1553
|
+
* links that survive are LEFT ALONE rather than deleted and reinserted, so the junction
|
|
1554
|
+
* rows (and any future column on them) are stable across a save that changed nothing. */
|
|
1555
|
+
setPageTerms: import("@pramen/server").Handler<{
|
|
1556
|
+
pageId: string;
|
|
1557
|
+
termIds: string[];
|
|
1558
|
+
}, {
|
|
1559
|
+
ok: true;
|
|
1560
|
+
count: number;
|
|
1561
|
+
}>;
|
|
1562
|
+
/** Published pages carrying a term. PUBLIC.
|
|
1563
|
+
*
|
|
1564
|
+
* A relation traversal (`where: { terms: { id } }`), so the page read scope is
|
|
1565
|
+
* AND-merged as it is anywhere else — traversal cannot widen access, and an anonymous
|
|
1566
|
+
* caller sees published pages only. */
|
|
1567
|
+
listPagesByTerm: import("@pramen/server").Handler<{
|
|
1568
|
+
taxonomy: string;
|
|
1569
|
+
term: string;
|
|
1570
|
+
limit?: number;
|
|
1571
|
+
offset?: number;
|
|
1572
|
+
}, Record<string, unknown>[]>;
|
|
1573
|
+
/** One widget area, with `menu` widgets resolved to their menus. PUBLIC.
|
|
1574
|
+
*
|
|
1575
|
+
* `null` for an unknown name, for the same reason `getMenu` returns null: a layout
|
|
1576
|
+
* asking for a sidebar nobody has filled in yet is the normal state of a site under
|
|
1577
|
+
* construction, and throwing there takes down the page. */
|
|
1578
|
+
getWidgetArea: import("@pramen/server").Handler<{
|
|
1579
|
+
name: string;
|
|
1580
|
+
}, WidgetArea | null>;
|
|
1581
|
+
listWidgetAreas: import("@pramen/server").Handler<unknown, Record<string, unknown>[]>;
|
|
1582
|
+
createWidgetArea: import("@pramen/server").Handler<{
|
|
1583
|
+
name: string;
|
|
1584
|
+
label: string;
|
|
1585
|
+
description?: string;
|
|
1586
|
+
widgets?: Widget[];
|
|
1587
|
+
}, Record<string, unknown>>;
|
|
1588
|
+
updateWidgetArea: import("@pramen/server").Handler<{
|
|
1589
|
+
id?: string;
|
|
1590
|
+
name?: string;
|
|
1591
|
+
label?: string;
|
|
1592
|
+
description?: string | null;
|
|
1593
|
+
widgets?: Widget[];
|
|
1594
|
+
expectedVersion?: number;
|
|
1595
|
+
}, Record<string, unknown> | undefined>;
|
|
1596
|
+
deleteWidgetArea: import("@pramen/server").Handler<{
|
|
1597
|
+
id: string;
|
|
1598
|
+
}, {
|
|
1599
|
+
ok: true;
|
|
1600
|
+
}>;
|
|
866
1601
|
/** Mint a signed upload URL for a media blob (keyed under the tenant's `media/`
|
|
867
1602
|
* prefix so the public /media route can serve it). The client PUTs the bytes to
|
|
868
1603
|
* `url`, then calls `createMedia` with the returned `ref`. */
|
|
@@ -1050,6 +1785,9 @@ export declare function createCmsHandlers(opts?: CmsHandlerOpts): {
|
|
|
1050
1785
|
defaultLocale: string;
|
|
1051
1786
|
multilingual: boolean;
|
|
1052
1787
|
pagesByType: true;
|
|
1788
|
+
siteFurniture: true;
|
|
1789
|
+
codeDefinedTypes: true;
|
|
1790
|
+
canEdit: boolean;
|
|
1053
1791
|
}>;
|
|
1054
1792
|
/** Distinct locales present across all pages. NOTE: a DATA query — what is in the
|
|
1055
1793
|
* store — not configuration. `listCmsCapabilities().locales` is what the deployment
|
|
@@ -1277,6 +2015,188 @@ export declare const cmsHandlers: {
|
|
|
1277
2015
|
fieldsSchema?: FieldDefinition[];
|
|
1278
2016
|
defaultBlocks?: DefaultBlockDefinition[];
|
|
1279
2017
|
}, Record<string, unknown> | undefined>;
|
|
2018
|
+
/** One menu, references resolved to hrefs. PUBLIC — a menu is site chrome.
|
|
2019
|
+
*
|
|
2020
|
+
* Returns `null` for an unknown name rather than 404ing, because a layout asking for a
|
|
2021
|
+
* menu it has not created yet is the normal state of a site being built, and a thrown
|
|
2022
|
+
* error there takes down every page instead of rendering no nav. */
|
|
2023
|
+
getMenu: import("@pramen/server").Handler<{
|
|
2024
|
+
name: string;
|
|
2025
|
+
}, Menu | null>;
|
|
2026
|
+
/** Every menu, RAW (references unresolved) — the editor's list.
|
|
2027
|
+
*
|
|
2028
|
+
* Capped like every other list here. A site-furniture table is small by nature, which is
|
|
2029
|
+
* an argument for the cap being generous, not for its absence: an unbounded SELECT of a
|
|
2030
|
+
* wide row over RPC is the D1 failure shape from GitHub #22, and "small by nature" is
|
|
2031
|
+
* not a property the query planner knows about. */
|
|
2032
|
+
listMenus: import("@pramen/server").Handler<unknown, Record<string, unknown>[]>;
|
|
2033
|
+
createMenu: import("@pramen/server").Handler<{
|
|
2034
|
+
name: string;
|
|
2035
|
+
label: string;
|
|
2036
|
+
items?: MenuItem[];
|
|
2037
|
+
}, Record<string, unknown>>;
|
|
2038
|
+
/** Patch a menu found by `id` or `name`. `name` is the key layout code resolves and is
|
|
2039
|
+
* NOT mutable — retitling is what `label` is for. */
|
|
2040
|
+
updateMenu: import("@pramen/server").Handler<{
|
|
2041
|
+
id?: string;
|
|
2042
|
+
name?: string;
|
|
2043
|
+
label?: string;
|
|
2044
|
+
items?: MenuItem[];
|
|
2045
|
+
expectedVersion?: number;
|
|
2046
|
+
}, Record<string, unknown> | undefined>;
|
|
2047
|
+
deleteMenu: import("@pramen/server").Handler<{
|
|
2048
|
+
id: string;
|
|
2049
|
+
}, {
|
|
2050
|
+
ok: true;
|
|
2051
|
+
}>;
|
|
2052
|
+
/** Resolve a request path to its redirect, or `null`. PUBLIC and READ-ONLY: this is
|
|
2053
|
+
* what a front end calls on a 404, so it is anonymous traffic on the hot path.
|
|
2054
|
+
*
|
|
2055
|
+
* `enabled` is enforced by the public read POLICY, not by a `where` here, so a disabled
|
|
2056
|
+
* redirect is invisible to every anonymous read (this handler, a future listing, a
|
|
2057
|
+
* relation traversal) rather than to the one call that remembered to filter. */
|
|
2058
|
+
resolveRedirect: import("@pramen/server").Handler<{
|
|
2059
|
+
path: string;
|
|
2060
|
+
}, {
|
|
2061
|
+
to: string;
|
|
2062
|
+
status: number;
|
|
2063
|
+
} | null>;
|
|
2064
|
+
listRedirects: import("@pramen/server").Handler<{
|
|
2065
|
+
limit?: number;
|
|
2066
|
+
offset?: number;
|
|
2067
|
+
}, Record<string, unknown>[]>;
|
|
2068
|
+
createRedirect: import("@pramen/server").Handler<RedirectPatch & {
|
|
2069
|
+
fromPath: string;
|
|
2070
|
+
toPath: string;
|
|
2071
|
+
}, Record<string, unknown>>;
|
|
2072
|
+
updateRedirect: import("@pramen/server").Handler<RedirectPatch & {
|
|
2073
|
+
id: string;
|
|
2074
|
+
}, Record<string, unknown> | undefined>;
|
|
2075
|
+
deleteRedirect: import("@pramen/server").Handler<{
|
|
2076
|
+
id: string;
|
|
2077
|
+
}, {
|
|
2078
|
+
ok: true;
|
|
2079
|
+
}>;
|
|
2080
|
+
/** Every vocabulary. PUBLIC — like content types, a taxonomy's slug is structural (it
|
|
2081
|
+
* is a URL segment) and a front end routes on it. */
|
|
2082
|
+
listTaxonomies: import("@pramen/server").Handler<unknown, Record<string, unknown>[]>;
|
|
2083
|
+
createTaxonomy: import("@pramen/server").Handler<{
|
|
2084
|
+
slug: string;
|
|
2085
|
+
label: string;
|
|
2086
|
+
pluralLabel?: string;
|
|
2087
|
+
description?: string;
|
|
2088
|
+
hierarchical?: boolean;
|
|
2089
|
+
}, Record<string, unknown>>;
|
|
2090
|
+
/** Patch a vocabulary. `slug` is a URL segment and the key `listTerms` resolves, so it
|
|
2091
|
+
* is not mutable — the same rule content types and block types already follow.
|
|
2092
|
+
*
|
|
2093
|
+
* Turning `hierarchical` OFF is refused while any term still has a parent. Allowing it
|
|
2094
|
+
* would leave a stored hierarchy that no reader renders and no writer can clear, and
|
|
2095
|
+
* flattening the terms silently is a destructive edit behind a checkbox. */
|
|
2096
|
+
updateTaxonomy: import("@pramen/server").Handler<{
|
|
2097
|
+
id: string;
|
|
2098
|
+
label?: string;
|
|
2099
|
+
pluralLabel?: string | null;
|
|
2100
|
+
description?: string | null;
|
|
2101
|
+
hierarchical?: boolean;
|
|
2102
|
+
}, Record<string, unknown> | undefined>;
|
|
2103
|
+
/** Delete a vocabulary. Its terms go with it, and their page assignments with those —
|
|
2104
|
+
* both by real `ON DELETE CASCADE`, so the cleanup is the DB's and cannot be half-done
|
|
2105
|
+
* by a handler that threw between two writes. */
|
|
2106
|
+
deleteTaxonomy: import("@pramen/server").Handler<{
|
|
2107
|
+
id: string;
|
|
2108
|
+
}, {
|
|
2109
|
+
ok: true;
|
|
2110
|
+
}>;
|
|
2111
|
+
/** One vocabulary's terms, flat, ordered. PUBLIC. */
|
|
2112
|
+
listTerms: import("@pramen/server").Handler<{
|
|
2113
|
+
taxonomy: string;
|
|
2114
|
+
}, Term[]>;
|
|
2115
|
+
/** One vocabulary's terms as a TREE. PUBLIC. Assembled here rather than by the caller
|
|
2116
|
+
* because a hierarchy is what the nav renders and every consumer would otherwise write
|
|
2117
|
+
* the same fold. */
|
|
2118
|
+
getTermTree: import("@pramen/server").Handler<{
|
|
2119
|
+
taxonomy: string;
|
|
2120
|
+
}, Term[]>;
|
|
2121
|
+
createTerm: import("@pramen/server").Handler<{
|
|
2122
|
+
taxonomy: string;
|
|
2123
|
+
slug: string;
|
|
2124
|
+
label: string;
|
|
2125
|
+
description?: string;
|
|
2126
|
+
parentId?: string | null;
|
|
2127
|
+
position?: number;
|
|
2128
|
+
}, Record<string, unknown>>;
|
|
2129
|
+
updateTerm: import("@pramen/server").Handler<{
|
|
2130
|
+
id: string;
|
|
2131
|
+
slug?: string;
|
|
2132
|
+
label?: string;
|
|
2133
|
+
description?: string | null;
|
|
2134
|
+
parentId?: string | null;
|
|
2135
|
+
position?: number;
|
|
2136
|
+
}, Record<string, unknown> | undefined>;
|
|
2137
|
+
/** Delete a term. Children are promoted to the top level (`ON DELETE SET NULL`) and
|
|
2138
|
+
* page assignments are removed (`ON DELETE CASCADE`) — see `cms_terms.parentId`. */
|
|
2139
|
+
deleteTerm: import("@pramen/server").Handler<{
|
|
2140
|
+
id: string;
|
|
2141
|
+
}, {
|
|
2142
|
+
ok: true;
|
|
2143
|
+
}>;
|
|
2144
|
+
/** A page's assigned terms. PUBLIC (a published page's classification is public). */
|
|
2145
|
+
listPageTerms: import("@pramen/server").Handler<{
|
|
2146
|
+
pageId: string;
|
|
2147
|
+
}, Term[]>;
|
|
2148
|
+
/** Replace a page's term assignments wholesale.
|
|
2149
|
+
*
|
|
2150
|
+
* Set semantics, not add/remove: the editor's panel holds the whole selection, and two
|
|
2151
|
+
* calls that each patch one end of it race into a state neither asked for. Existing
|
|
2152
|
+
* links that survive are LEFT ALONE rather than deleted and reinserted, so the junction
|
|
2153
|
+
* rows (and any future column on them) are stable across a save that changed nothing. */
|
|
2154
|
+
setPageTerms: import("@pramen/server").Handler<{
|
|
2155
|
+
pageId: string;
|
|
2156
|
+
termIds: string[];
|
|
2157
|
+
}, {
|
|
2158
|
+
ok: true;
|
|
2159
|
+
count: number;
|
|
2160
|
+
}>;
|
|
2161
|
+
/** Published pages carrying a term. PUBLIC.
|
|
2162
|
+
*
|
|
2163
|
+
* A relation traversal (`where: { terms: { id } }`), so the page read scope is
|
|
2164
|
+
* AND-merged as it is anywhere else — traversal cannot widen access, and an anonymous
|
|
2165
|
+
* caller sees published pages only. */
|
|
2166
|
+
listPagesByTerm: import("@pramen/server").Handler<{
|
|
2167
|
+
taxonomy: string;
|
|
2168
|
+
term: string;
|
|
2169
|
+
limit?: number;
|
|
2170
|
+
offset?: number;
|
|
2171
|
+
}, Record<string, unknown>[]>;
|
|
2172
|
+
/** One widget area, with `menu` widgets resolved to their menus. PUBLIC.
|
|
2173
|
+
*
|
|
2174
|
+
* `null` for an unknown name, for the same reason `getMenu` returns null: a layout
|
|
2175
|
+
* asking for a sidebar nobody has filled in yet is the normal state of a site under
|
|
2176
|
+
* construction, and throwing there takes down the page. */
|
|
2177
|
+
getWidgetArea: import("@pramen/server").Handler<{
|
|
2178
|
+
name: string;
|
|
2179
|
+
}, WidgetArea | null>;
|
|
2180
|
+
listWidgetAreas: import("@pramen/server").Handler<unknown, Record<string, unknown>[]>;
|
|
2181
|
+
createWidgetArea: import("@pramen/server").Handler<{
|
|
2182
|
+
name: string;
|
|
2183
|
+
label: string;
|
|
2184
|
+
description?: string;
|
|
2185
|
+
widgets?: Widget[];
|
|
2186
|
+
}, Record<string, unknown>>;
|
|
2187
|
+
updateWidgetArea: import("@pramen/server").Handler<{
|
|
2188
|
+
id?: string;
|
|
2189
|
+
name?: string;
|
|
2190
|
+
label?: string;
|
|
2191
|
+
description?: string | null;
|
|
2192
|
+
widgets?: Widget[];
|
|
2193
|
+
expectedVersion?: number;
|
|
2194
|
+
}, Record<string, unknown> | undefined>;
|
|
2195
|
+
deleteWidgetArea: import("@pramen/server").Handler<{
|
|
2196
|
+
id: string;
|
|
2197
|
+
}, {
|
|
2198
|
+
ok: true;
|
|
2199
|
+
}>;
|
|
1280
2200
|
/** Mint a signed upload URL for a media blob (keyed under the tenant's `media/`
|
|
1281
2201
|
* prefix so the public /media route can serve it). The client PUTs the bytes to
|
|
1282
2202
|
* `url`, then calls `createMedia` with the returned `ref`. */
|
|
@@ -1464,6 +2384,9 @@ export declare const cmsHandlers: {
|
|
|
1464
2384
|
defaultLocale: string;
|
|
1465
2385
|
multilingual: boolean;
|
|
1466
2386
|
pagesByType: true;
|
|
2387
|
+
siteFurniture: true;
|
|
2388
|
+
codeDefinedTypes: true;
|
|
2389
|
+
canEdit: boolean;
|
|
1467
2390
|
}>;
|
|
1468
2391
|
/** Distinct locales present across all pages. NOTE: a DATA query — what is in the
|
|
1469
2392
|
* store — not configuration. `listCmsCapabilities().locales` is what the deployment
|
|
@@ -1668,6 +2591,10 @@ export declare function cmsPolicies(opts?: CmsPolicyOpts): {
|
|
|
1668
2591
|
public: Policy[];
|
|
1669
2592
|
editor: Policy[];
|
|
1670
2593
|
};
|
|
2594
|
+
/** Nav positions for the editor's built-in sections — see `./nav`. Re-exported here so a
|
|
2595
|
+
* host writing `app.ts` imports it from the same place as `collection()`. It LIVES in a leaf
|
|
2596
|
+
* module because `blockkit.ts` needs it too and must not depend on this one. */
|
|
2597
|
+
export { NAV_ORDER } from "./nav";
|
|
1671
2598
|
/** Declares that a pramen entity is editable as a collection in the CMS editor. Both
|
|
1672
2599
|
* halves live here: the runtime facts (entity, idField, validation via `fields`) and the
|
|
1673
2600
|
* UI facts (labels, list columns, ordering). Mirror of {@link ContentTypeDef}. */
|
|
@@ -1699,6 +2626,16 @@ export interface CollectionDef {
|
|
|
1699
2626
|
column: string;
|
|
1700
2627
|
dir?: "asc" | "desc";
|
|
1701
2628
|
};
|
|
2629
|
+
/** Where this collection sits in the editor's primary nav. Lower comes first; ties keep
|
|
2630
|
+
* declaration order. Defaults to `NAV_ORDER.collections`, which is between Pages and
|
|
2631
|
+
* Media — where collections have always rendered.
|
|
2632
|
+
*
|
|
2633
|
+
* The point is not cosmetic. Before this the only extension seam was `extraNav`, which
|
|
2634
|
+
* renders dead LAST and (by default, and for good reason) opens a new tab — so a
|
|
2635
|
+
* project-specific section could only ever be a separate app bolted onto the end of the
|
|
2636
|
+
* admin. A section that belongs beside Pages can now say so. See {@link NAV_ORDER} for
|
|
2637
|
+
* the built-in positions to place against. */
|
|
2638
|
+
readonly navOrder?: number;
|
|
1702
2639
|
/** Workflow features this collection opts into — see {@link CollectionFeature}. Each is
|
|
1703
2640
|
* backed by MANAGED COLUMNS on `entity` that the CMS writes and `fields` may not declare.
|
|
1704
2641
|
* Validated against your schema at `createCollectionHandlers` time (which is why that call
|
|
@@ -1728,6 +2665,10 @@ export interface CollectionMeta {
|
|
|
1728
2665
|
label: string;
|
|
1729
2666
|
pluralLabel: string;
|
|
1730
2667
|
icon?: string;
|
|
2668
|
+
/** Nav position — see {@link CollectionDef.navOrder}. Always present in the meta (the
|
|
2669
|
+
* default is filled here) so the editor sorts one list of numbers rather than deciding
|
|
2670
|
+
* per entry whether a default applies. */
|
|
2671
|
+
navOrder: number;
|
|
1731
2672
|
fields: readonly FieldDefinition[];
|
|
1732
2673
|
list: readonly string[];
|
|
1733
2674
|
titleField: string;
|