@pramen/cms 0.0.14 → 0.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1078 -0
- package/dist/index.js +1342 -0
- package/dist/react.d.ts +41 -0
- package/dist/react.js +30 -0
- package/package.json +6 -3
- package/src/index.ts +14 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,1078 @@
|
|
|
1
|
+
import type { HandlerContext, Policy, FileRef } from "@pramen/server";
|
|
2
|
+
/** A field in a block type's (or content type's) field schema. Recursive: a `repeater`
|
|
3
|
+
* or `group` nests `fields`. Mirrors WollyCMS's FieldDefinition. */
|
|
4
|
+
export interface FieldDefinition {
|
|
5
|
+
name: string;
|
|
6
|
+
label?: string;
|
|
7
|
+
type: "text" | "textarea" | "richtext" | "url" | "number" | "boolean" | "media" | "select" | "repeater" | "group";
|
|
8
|
+
required?: boolean;
|
|
9
|
+
default?: unknown;
|
|
10
|
+
/** repeater/group only — the nested fields. */
|
|
11
|
+
fields?: FieldDefinition[];
|
|
12
|
+
/** repeater only — item count bounds. */
|
|
13
|
+
min?: number;
|
|
14
|
+
max?: number;
|
|
15
|
+
/** select only. */
|
|
16
|
+
options?: string[];
|
|
17
|
+
}
|
|
18
|
+
/** A named region on a content type; `allowedTypes` (block-type slugs) restricts what
|
|
19
|
+
* may be placed there — `null`/omitted means any. */
|
|
20
|
+
export interface RegionDefinition {
|
|
21
|
+
name: string;
|
|
22
|
+
label?: string;
|
|
23
|
+
allowedTypes?: string[] | null;
|
|
24
|
+
}
|
|
25
|
+
/** A block auto-created in a region when a page of this content type is created. */
|
|
26
|
+
export interface DefaultBlockDefinition {
|
|
27
|
+
region: string;
|
|
28
|
+
blockTypeSlug: string;
|
|
29
|
+
fields?: Record<string, unknown>;
|
|
30
|
+
}
|
|
31
|
+
/** A rich-text value — a serialized editor document (or a plain string). */
|
|
32
|
+
export type RichText = string | {
|
|
33
|
+
type: string;
|
|
34
|
+
content?: unknown[];
|
|
35
|
+
};
|
|
36
|
+
/** Map one FieldDefinition (as a const literal) to the TS type of its RENDERED value.
|
|
37
|
+
* Media resolves to `ResolvedMedia` (the assemble-time shape a component receives). */
|
|
38
|
+
export type FieldTsType<D extends FieldDefinition> = D["type"] extends "text" | "textarea" | "url" | "select" ? 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 "group" ? InferBlockFields<NonNullable<D["fields"]>> : D["type"] extends "repeater" ? InferBlockFields<NonNullable<D["fields"]>>[] : unknown;
|
|
39
|
+
/** Infer the `fields` object type from a const `FieldDefinition[]`. Required fields are
|
|
40
|
+
* present; optional ones are `| undefined`. */
|
|
41
|
+
export type InferBlockFields<T extends readonly FieldDefinition[]> = {
|
|
42
|
+
[D in T[number] as D["name"]]: D extends {
|
|
43
|
+
required: true;
|
|
44
|
+
} ? FieldTsType<D> : FieldTsType<D> | undefined;
|
|
45
|
+
};
|
|
46
|
+
/** A developer-authored block type: a slug + a const field schema, carrying enough type
|
|
47
|
+
* info to (a) create the DB block type and (b) type a rendering component's `fields`. */
|
|
48
|
+
export interface BlockTypeDef<S extends string = string, F extends readonly FieldDefinition[] = readonly FieldDefinition[]> {
|
|
49
|
+
readonly slug: S;
|
|
50
|
+
readonly name: string;
|
|
51
|
+
readonly fieldsSchema: F;
|
|
52
|
+
readonly description?: string;
|
|
53
|
+
readonly icon?: string;
|
|
54
|
+
readonly category?: string;
|
|
55
|
+
}
|
|
56
|
+
/** Declare a typed block type. Pass `fields as const` to preserve the literals so
|
|
57
|
+
* `BlockFieldsOf<typeof def>` infers the field shape:
|
|
58
|
+
*
|
|
59
|
+
* const hero = defineBlockType("hero", [
|
|
60
|
+
* { name: "heading", type: "text", required: true },
|
|
61
|
+
* { name: "image", type: "media" },
|
|
62
|
+
* ] as const);
|
|
63
|
+
* type HeroFields = BlockFieldsOf<typeof hero>; // { heading: string; image: ResolvedMedia|null|undefined }
|
|
64
|
+
*
|
|
65
|
+
* Spread `hero` (minus fieldsSchema key naming) into `createBlockType`, and use
|
|
66
|
+
* `BlockFieldsOf<typeof hero>` to type the block's React component. */
|
|
67
|
+
export declare function defineBlockType<S extends string, F extends readonly FieldDefinition[]>(slug: S, fields: F, opts?: {
|
|
68
|
+
name?: string;
|
|
69
|
+
description?: string;
|
|
70
|
+
icon?: string;
|
|
71
|
+
category?: string;
|
|
72
|
+
}): BlockTypeDef<S, F>;
|
|
73
|
+
/** The inferred `fields` type of a `defineBlockType` result. */
|
|
74
|
+
export type BlockFieldsOf<D extends BlockTypeDef> = InferBlockFields<D["fieldsSchema"]>;
|
|
75
|
+
/** Emit a `.ts` module of per-slug field interfaces + a `BlockFieldsBySlug` registry from
|
|
76
|
+
* DB-stored block types (`{ slug, fieldsSchema }` rows). The runtime counterpart to the
|
|
77
|
+
* compile-time `InferBlockFields`, for webmaster-authored (data-driven) block types. */
|
|
78
|
+
export declare function generateBlockTypes(blockTypes: Array<{
|
|
79
|
+
slug: string;
|
|
80
|
+
fieldsSchema?: FieldDefinition[] | null;
|
|
81
|
+
}>): string;
|
|
82
|
+
/** The block/page builder tables. All in the default partition (relations can't cross
|
|
83
|
+
* partitions). Prefixed `cms_` to avoid colliding with your own entities. */
|
|
84
|
+
export declare const cmsSchema: {
|
|
85
|
+
cms_content_types: import("@pramen/server").EntityDef<{
|
|
86
|
+
id: {
|
|
87
|
+
readonly type: "uuid";
|
|
88
|
+
} & {
|
|
89
|
+
readonly generated: true;
|
|
90
|
+
} & {
|
|
91
|
+
readonly primaryKey: true;
|
|
92
|
+
readonly notNull: true;
|
|
93
|
+
};
|
|
94
|
+
name: {
|
|
95
|
+
readonly type: "text";
|
|
96
|
+
} & {
|
|
97
|
+
readonly notNull: true;
|
|
98
|
+
};
|
|
99
|
+
slug: {
|
|
100
|
+
readonly type: "text";
|
|
101
|
+
} & {
|
|
102
|
+
readonly notNull: true;
|
|
103
|
+
} & {
|
|
104
|
+
readonly unique: true;
|
|
105
|
+
};
|
|
106
|
+
description: {
|
|
107
|
+
readonly type: "text";
|
|
108
|
+
};
|
|
109
|
+
fieldsSchema: {
|
|
110
|
+
readonly type: "json";
|
|
111
|
+
};
|
|
112
|
+
regions: {
|
|
113
|
+
readonly type: "json";
|
|
114
|
+
};
|
|
115
|
+
defaultBlocks: {
|
|
116
|
+
readonly type: "json";
|
|
117
|
+
};
|
|
118
|
+
createdAt: {
|
|
119
|
+
readonly type: "text";
|
|
120
|
+
} & {
|
|
121
|
+
readonly defaultExpr: string;
|
|
122
|
+
};
|
|
123
|
+
}, Record<string, never>>;
|
|
124
|
+
cms_block_types: import("@pramen/server").EntityDef<{
|
|
125
|
+
id: {
|
|
126
|
+
readonly type: "uuid";
|
|
127
|
+
} & {
|
|
128
|
+
readonly generated: true;
|
|
129
|
+
} & {
|
|
130
|
+
readonly primaryKey: true;
|
|
131
|
+
readonly notNull: true;
|
|
132
|
+
};
|
|
133
|
+
name: {
|
|
134
|
+
readonly type: "text";
|
|
135
|
+
} & {
|
|
136
|
+
readonly notNull: true;
|
|
137
|
+
};
|
|
138
|
+
slug: {
|
|
139
|
+
readonly type: "text";
|
|
140
|
+
} & {
|
|
141
|
+
readonly notNull: true;
|
|
142
|
+
} & {
|
|
143
|
+
readonly unique: true;
|
|
144
|
+
};
|
|
145
|
+
description: {
|
|
146
|
+
readonly type: "text";
|
|
147
|
+
};
|
|
148
|
+
fieldsSchema: {
|
|
149
|
+
readonly type: "json";
|
|
150
|
+
};
|
|
151
|
+
icon: {
|
|
152
|
+
readonly type: "text";
|
|
153
|
+
};
|
|
154
|
+
category: {
|
|
155
|
+
readonly type: "text";
|
|
156
|
+
};
|
|
157
|
+
createdAt: {
|
|
158
|
+
readonly type: "text";
|
|
159
|
+
} & {
|
|
160
|
+
readonly defaultExpr: string;
|
|
161
|
+
};
|
|
162
|
+
}, Record<string, never>>;
|
|
163
|
+
cms_blocks: import("@pramen/server").EntityDef<{
|
|
164
|
+
id: {
|
|
165
|
+
readonly type: "uuid";
|
|
166
|
+
} & {
|
|
167
|
+
readonly generated: true;
|
|
168
|
+
} & {
|
|
169
|
+
readonly primaryKey: true;
|
|
170
|
+
readonly notNull: true;
|
|
171
|
+
};
|
|
172
|
+
typeId: {
|
|
173
|
+
readonly type: "uuid";
|
|
174
|
+
} & {
|
|
175
|
+
readonly notNull: true;
|
|
176
|
+
};
|
|
177
|
+
title: {
|
|
178
|
+
readonly type: "text";
|
|
179
|
+
};
|
|
180
|
+
fields: {
|
|
181
|
+
readonly type: "json";
|
|
182
|
+
};
|
|
183
|
+
isReusable: {
|
|
184
|
+
readonly type: "boolean";
|
|
185
|
+
} & {
|
|
186
|
+
readonly default: false;
|
|
187
|
+
};
|
|
188
|
+
createdAt: {
|
|
189
|
+
readonly type: "text";
|
|
190
|
+
} & {
|
|
191
|
+
readonly defaultExpr: string;
|
|
192
|
+
};
|
|
193
|
+
updatedAt: {
|
|
194
|
+
readonly type: "text";
|
|
195
|
+
} & {
|
|
196
|
+
readonly defaultExpr: string;
|
|
197
|
+
};
|
|
198
|
+
}, {
|
|
199
|
+
type: {
|
|
200
|
+
readonly kind: "belongsTo";
|
|
201
|
+
readonly target: "cms_block_types";
|
|
202
|
+
readonly column: string;
|
|
203
|
+
};
|
|
204
|
+
}>;
|
|
205
|
+
cms_pages: import("@pramen/server").EntityDef<{
|
|
206
|
+
id: {
|
|
207
|
+
readonly type: "uuid";
|
|
208
|
+
} & {
|
|
209
|
+
readonly generated: true;
|
|
210
|
+
} & {
|
|
211
|
+
readonly primaryKey: true;
|
|
212
|
+
readonly notNull: true;
|
|
213
|
+
};
|
|
214
|
+
typeId: {
|
|
215
|
+
readonly type: "uuid";
|
|
216
|
+
} & {
|
|
217
|
+
readonly notNull: true;
|
|
218
|
+
};
|
|
219
|
+
title: {
|
|
220
|
+
readonly type: "text";
|
|
221
|
+
} & {
|
|
222
|
+
readonly notNull: true;
|
|
223
|
+
};
|
|
224
|
+
slug: {
|
|
225
|
+
readonly type: "text";
|
|
226
|
+
} & {
|
|
227
|
+
readonly notNull: true;
|
|
228
|
+
} & {
|
|
229
|
+
readonly index: true;
|
|
230
|
+
};
|
|
231
|
+
status: {
|
|
232
|
+
readonly type: "text";
|
|
233
|
+
} & {
|
|
234
|
+
readonly default: "draft";
|
|
235
|
+
};
|
|
236
|
+
locale: {
|
|
237
|
+
readonly type: "text";
|
|
238
|
+
} & {
|
|
239
|
+
readonly default: "en";
|
|
240
|
+
};
|
|
241
|
+
translationGroupId: {
|
|
242
|
+
readonly type: "uuid";
|
|
243
|
+
} & {
|
|
244
|
+
readonly generated: true;
|
|
245
|
+
};
|
|
246
|
+
fields: {
|
|
247
|
+
readonly type: "json";
|
|
248
|
+
};
|
|
249
|
+
publishedAt: {
|
|
250
|
+
readonly type: "text";
|
|
251
|
+
};
|
|
252
|
+
scheduledAt: {
|
|
253
|
+
readonly type: "text";
|
|
254
|
+
};
|
|
255
|
+
unpublishAt: {
|
|
256
|
+
readonly type: "text";
|
|
257
|
+
};
|
|
258
|
+
currentRevisionId: {
|
|
259
|
+
readonly type: "uuid";
|
|
260
|
+
};
|
|
261
|
+
metaTitle: {
|
|
262
|
+
readonly type: "text";
|
|
263
|
+
};
|
|
264
|
+
metaDescription: {
|
|
265
|
+
readonly type: "text";
|
|
266
|
+
};
|
|
267
|
+
canonicalUrl: {
|
|
268
|
+
readonly type: "text";
|
|
269
|
+
};
|
|
270
|
+
robots: {
|
|
271
|
+
readonly type: "text";
|
|
272
|
+
};
|
|
273
|
+
ogTitle: {
|
|
274
|
+
readonly type: "text";
|
|
275
|
+
};
|
|
276
|
+
ogDescription: {
|
|
277
|
+
readonly type: "text";
|
|
278
|
+
};
|
|
279
|
+
ogImage: {
|
|
280
|
+
readonly type: "uuid";
|
|
281
|
+
};
|
|
282
|
+
structuredData: {
|
|
283
|
+
readonly type: "json";
|
|
284
|
+
};
|
|
285
|
+
createdAt: {
|
|
286
|
+
readonly type: "text";
|
|
287
|
+
} & {
|
|
288
|
+
readonly defaultExpr: string;
|
|
289
|
+
};
|
|
290
|
+
updatedAt: {
|
|
291
|
+
readonly type: "text";
|
|
292
|
+
} & {
|
|
293
|
+
readonly defaultExpr: string;
|
|
294
|
+
};
|
|
295
|
+
}, {
|
|
296
|
+
type: {
|
|
297
|
+
readonly kind: "belongsTo";
|
|
298
|
+
readonly target: "cms_content_types";
|
|
299
|
+
readonly column: string;
|
|
300
|
+
};
|
|
301
|
+
placements: {
|
|
302
|
+
readonly kind: "hasMany";
|
|
303
|
+
readonly target: "cms_page_blocks";
|
|
304
|
+
readonly column: string;
|
|
305
|
+
};
|
|
306
|
+
}>;
|
|
307
|
+
cms_page_blocks: import("@pramen/server").EntityDef<{
|
|
308
|
+
id: {
|
|
309
|
+
readonly type: "uuid";
|
|
310
|
+
} & {
|
|
311
|
+
readonly generated: true;
|
|
312
|
+
} & {
|
|
313
|
+
readonly primaryKey: true;
|
|
314
|
+
readonly notNull: true;
|
|
315
|
+
};
|
|
316
|
+
pageId: {
|
|
317
|
+
readonly type: "uuid";
|
|
318
|
+
} & {
|
|
319
|
+
readonly notNull: true;
|
|
320
|
+
};
|
|
321
|
+
blockId: {
|
|
322
|
+
readonly type: "uuid";
|
|
323
|
+
} & {
|
|
324
|
+
readonly notNull: true;
|
|
325
|
+
};
|
|
326
|
+
region: {
|
|
327
|
+
readonly type: "text";
|
|
328
|
+
} & {
|
|
329
|
+
readonly notNull: true;
|
|
330
|
+
};
|
|
331
|
+
position: {
|
|
332
|
+
readonly type: "integer";
|
|
333
|
+
} & {
|
|
334
|
+
readonly notNull: true;
|
|
335
|
+
};
|
|
336
|
+
isShared: {
|
|
337
|
+
readonly type: "boolean";
|
|
338
|
+
} & {
|
|
339
|
+
readonly default: false;
|
|
340
|
+
};
|
|
341
|
+
overrides: {
|
|
342
|
+
readonly type: "json";
|
|
343
|
+
};
|
|
344
|
+
}, {
|
|
345
|
+
page: {
|
|
346
|
+
readonly kind: "belongsTo";
|
|
347
|
+
readonly target: "cms_pages";
|
|
348
|
+
readonly column: string;
|
|
349
|
+
};
|
|
350
|
+
block: {
|
|
351
|
+
readonly kind: "belongsTo";
|
|
352
|
+
readonly target: "cms_blocks";
|
|
353
|
+
readonly column: string;
|
|
354
|
+
};
|
|
355
|
+
}>;
|
|
356
|
+
cms_page_revisions: import("@pramen/server").EntityDef<{
|
|
357
|
+
id: {
|
|
358
|
+
readonly type: "uuid";
|
|
359
|
+
} & {
|
|
360
|
+
readonly generated: true;
|
|
361
|
+
} & {
|
|
362
|
+
readonly primaryKey: true;
|
|
363
|
+
readonly notNull: true;
|
|
364
|
+
};
|
|
365
|
+
pageId: {
|
|
366
|
+
readonly type: "uuid";
|
|
367
|
+
} & {
|
|
368
|
+
readonly notNull: true;
|
|
369
|
+
};
|
|
370
|
+
title: {
|
|
371
|
+
readonly type: "text";
|
|
372
|
+
};
|
|
373
|
+
status: {
|
|
374
|
+
readonly type: "text";
|
|
375
|
+
};
|
|
376
|
+
snapshot: {
|
|
377
|
+
readonly type: "json";
|
|
378
|
+
};
|
|
379
|
+
note: {
|
|
380
|
+
readonly type: "text";
|
|
381
|
+
};
|
|
382
|
+
actor: {
|
|
383
|
+
readonly type: "text";
|
|
384
|
+
};
|
|
385
|
+
createdAt: {
|
|
386
|
+
readonly type: "text";
|
|
387
|
+
} & {
|
|
388
|
+
readonly defaultExpr: string;
|
|
389
|
+
};
|
|
390
|
+
}, {
|
|
391
|
+
page: {
|
|
392
|
+
readonly kind: "belongsTo";
|
|
393
|
+
readonly target: "cms_pages";
|
|
394
|
+
readonly column: string;
|
|
395
|
+
};
|
|
396
|
+
}>;
|
|
397
|
+
cms_audit: import("@pramen/server").EntityDef<{
|
|
398
|
+
id: {
|
|
399
|
+
readonly type: "uuid";
|
|
400
|
+
} & {
|
|
401
|
+
readonly generated: true;
|
|
402
|
+
} & {
|
|
403
|
+
readonly primaryKey: true;
|
|
404
|
+
readonly notNull: true;
|
|
405
|
+
};
|
|
406
|
+
pageId: {
|
|
407
|
+
readonly type: "uuid";
|
|
408
|
+
} & {
|
|
409
|
+
readonly index: true;
|
|
410
|
+
};
|
|
411
|
+
action: {
|
|
412
|
+
readonly type: "text";
|
|
413
|
+
} & {
|
|
414
|
+
readonly notNull: true;
|
|
415
|
+
};
|
|
416
|
+
fromStatus: {
|
|
417
|
+
readonly type: "text";
|
|
418
|
+
};
|
|
419
|
+
toStatus: {
|
|
420
|
+
readonly type: "text";
|
|
421
|
+
};
|
|
422
|
+
actor: {
|
|
423
|
+
readonly type: "text";
|
|
424
|
+
};
|
|
425
|
+
note: {
|
|
426
|
+
readonly type: "text";
|
|
427
|
+
};
|
|
428
|
+
createdAt: {
|
|
429
|
+
readonly type: "text";
|
|
430
|
+
} & {
|
|
431
|
+
readonly defaultExpr: string;
|
|
432
|
+
};
|
|
433
|
+
}, Record<string, never>>;
|
|
434
|
+
cms_media: import("@pramen/server").EntityDef<{
|
|
435
|
+
id: {
|
|
436
|
+
readonly type: "uuid";
|
|
437
|
+
} & {
|
|
438
|
+
readonly generated: true;
|
|
439
|
+
} & {
|
|
440
|
+
readonly primaryKey: true;
|
|
441
|
+
readonly notNull: true;
|
|
442
|
+
};
|
|
443
|
+
file: {
|
|
444
|
+
readonly type: "fileRef";
|
|
445
|
+
};
|
|
446
|
+
alt: {
|
|
447
|
+
readonly type: "text";
|
|
448
|
+
};
|
|
449
|
+
createdAt: {
|
|
450
|
+
readonly type: "text";
|
|
451
|
+
} & {
|
|
452
|
+
readonly defaultExpr: string;
|
|
453
|
+
};
|
|
454
|
+
}, Record<string, never>>;
|
|
455
|
+
};
|
|
456
|
+
export interface ValidateOpts {
|
|
457
|
+
/** Enforce `required` fields (reject when missing). Default true. Editor-facing draft
|
|
458
|
+
* writes (addBlock/updateBlock/createPage) pass `false` — a DRAFT block may be incomplete;
|
|
459
|
+
* required is only mandatory when publishing. Type checks always run. */
|
|
460
|
+
requireRequired?: boolean;
|
|
461
|
+
}
|
|
462
|
+
/** Validate a block/page's `fields` payload against a field schema, throwing a 400 on
|
|
463
|
+
* the first violation. Recursive (repeater/group). Lenient on unknown field types. */
|
|
464
|
+
export declare function validateFields(schema: FieldDefinition[] | undefined | null, values: unknown, path?: string, opts?: ValidateOpts): void;
|
|
465
|
+
export interface RenderedBlock {
|
|
466
|
+
/** The placement id (cms_page_blocks) — stable per position; used for reorder/remove. */
|
|
467
|
+
id: string;
|
|
468
|
+
/** The underlying block instance id (cms_blocks) — used to edit the block's content. */
|
|
469
|
+
block_id: string;
|
|
470
|
+
block_type: string;
|
|
471
|
+
title: string | null;
|
|
472
|
+
fields: Record<string, unknown>;
|
|
473
|
+
is_shared: boolean;
|
|
474
|
+
}
|
|
475
|
+
export interface PageTranslation {
|
|
476
|
+
locale: string;
|
|
477
|
+
slug: string;
|
|
478
|
+
}
|
|
479
|
+
export interface PageSeo {
|
|
480
|
+
metaTitle: string | null;
|
|
481
|
+
metaDescription: string | null;
|
|
482
|
+
canonicalUrl: string | null;
|
|
483
|
+
robots: string | null;
|
|
484
|
+
ogTitle: string | null;
|
|
485
|
+
ogDescription: string | null;
|
|
486
|
+
ogImage: ResolvedMedia | null;
|
|
487
|
+
structuredData: unknown | null;
|
|
488
|
+
}
|
|
489
|
+
export interface AssembledPage {
|
|
490
|
+
page: {
|
|
491
|
+
id: string;
|
|
492
|
+
title: string;
|
|
493
|
+
slug: string;
|
|
494
|
+
status: string;
|
|
495
|
+
locale: string;
|
|
496
|
+
translationGroupId: string | null;
|
|
497
|
+
/** Published sibling locales of this page (for hreflang alternates). */
|
|
498
|
+
translations: PageTranslation[];
|
|
499
|
+
fields: Record<string, unknown> | null;
|
|
500
|
+
/** Back-compat: mirrors seo.metaTitle/metaDescription. */
|
|
501
|
+
metaTitle: string | null;
|
|
502
|
+
metaDescription: string | null;
|
|
503
|
+
seo: PageSeo;
|
|
504
|
+
};
|
|
505
|
+
regions: Record<string, RenderedBlock[]>;
|
|
506
|
+
}
|
|
507
|
+
/** A `"media"` block field, resolved from a stored media id to a servable shape at
|
|
508
|
+
* assemble time. `url` is the raw (full-size) serving path; pass `key` to `imageUrl()`
|
|
509
|
+
* for on-the-fly transforms. `null` when the referenced media was deleted. */
|
|
510
|
+
export interface ResolvedMedia {
|
|
511
|
+
id: string;
|
|
512
|
+
key: string;
|
|
513
|
+
url: string;
|
|
514
|
+
alt: string | null;
|
|
515
|
+
contentType: string | null;
|
|
516
|
+
filename: string | null;
|
|
517
|
+
}
|
|
518
|
+
/** The public serving path for a media blob (relative; the client resolves it against
|
|
519
|
+
* its base). Served by the Worker's public `GET /media/<key>` route. */
|
|
520
|
+
export declare function mediaPath(key: string): string;
|
|
521
|
+
/** Build a URL for a media blob, optionally with Cloudflare Image Resizing transforms
|
|
522
|
+
* (`/cdn-cgi/image/<opts>/…`). With no transform opts it's just `mediaPath` (optionally
|
|
523
|
+
* prefixed by `origin`). Transforms need the deploy's origin to resolve the source path. */
|
|
524
|
+
export declare function imageUrl(key: string, opts?: {
|
|
525
|
+
origin?: string;
|
|
526
|
+
width?: number;
|
|
527
|
+
height?: number;
|
|
528
|
+
quality?: number;
|
|
529
|
+
format?: "auto" | "webp" | "avif";
|
|
530
|
+
}): string;
|
|
531
|
+
export interface CmsHandlerOpts {
|
|
532
|
+
/** Roles permitted to call the editor mutations (also enforced by the ACL). Default
|
|
533
|
+
* `["editor", "admin"]`. */
|
|
534
|
+
editorRoles?: readonly string[];
|
|
535
|
+
/** Max accepted media upload size in bytes (enforced at the Worker). Default 25 MB. */
|
|
536
|
+
mediaMaxSize?: number;
|
|
537
|
+
/** Default locale used when `getPage`/`createPage` omit one. Default `"en"`. */
|
|
538
|
+
defaultLocale?: string;
|
|
539
|
+
/** Roles permitted to approve/reject a page in review and publish (the editorial gate).
|
|
540
|
+
* Default `["reviewer", "admin"]`. */
|
|
541
|
+
reviewerRoles?: readonly string[];
|
|
542
|
+
}
|
|
543
|
+
/** Build the CMS handler map. Spread into your app's handlers. Editor mutations are
|
|
544
|
+
* gated both by `auth` (fast 403 before the body) and by the row ACL (cmsPolicies). */
|
|
545
|
+
export declare function createCmsHandlers(opts?: CmsHandlerOpts): {
|
|
546
|
+
listBlockTypes: import("@pramen/server").Handler<unknown, Record<string, unknown>[]>;
|
|
547
|
+
createBlockType: import("@pramen/server").Handler<{
|
|
548
|
+
name: string;
|
|
549
|
+
slug: string;
|
|
550
|
+
fieldsSchema?: FieldDefinition[];
|
|
551
|
+
icon?: string;
|
|
552
|
+
category?: string;
|
|
553
|
+
description?: string;
|
|
554
|
+
}, Record<string, unknown>>;
|
|
555
|
+
createContentType: import("@pramen/server").Handler<{
|
|
556
|
+
name: string;
|
|
557
|
+
slug: string;
|
|
558
|
+
regions: RegionDefinition[];
|
|
559
|
+
fieldsSchema?: FieldDefinition[];
|
|
560
|
+
defaultBlocks?: DefaultBlockDefinition[];
|
|
561
|
+
}, Record<string, unknown>>;
|
|
562
|
+
/** Mint a signed upload URL for a media blob (keyed under the tenant's `media/`
|
|
563
|
+
* prefix so the public /media route can serve it). The client PUTs the bytes to
|
|
564
|
+
* `url`, then calls `createMedia` with the returned `ref`. */
|
|
565
|
+
signMediaUpload: import("@pramen/server").Handler<{
|
|
566
|
+
contentType: string;
|
|
567
|
+
filename?: string;
|
|
568
|
+
}, {
|
|
569
|
+
url: string;
|
|
570
|
+
ref: FileRef;
|
|
571
|
+
}>;
|
|
572
|
+
/** Confirm an uploaded blob is really in storage (capturing its true size) and
|
|
573
|
+
* persist a `cms_media` row. Mirrors the notes attach flow. */
|
|
574
|
+
createMedia: import("@pramen/server").Handler<{
|
|
575
|
+
ref: FileRef;
|
|
576
|
+
alt?: string;
|
|
577
|
+
}, Record<string, unknown>>;
|
|
578
|
+
listMedia: import("@pramen/server").Handler<{
|
|
579
|
+
limit?: number;
|
|
580
|
+
offset?: number;
|
|
581
|
+
}, Record<string, unknown>[]>;
|
|
582
|
+
getMedia: import("@pramen/server").Handler<{
|
|
583
|
+
id: string;
|
|
584
|
+
}, Record<string, unknown>>;
|
|
585
|
+
/** Edit a media asset's metadata (currently just `alt` text). Editor-gated. */
|
|
586
|
+
updateMedia: import("@pramen/server").Handler<{
|
|
587
|
+
id: string;
|
|
588
|
+
alt: string | null;
|
|
589
|
+
}, Record<string, unknown>>;
|
|
590
|
+
/** Delete a media row AND its R2 blob. (Automatic orphan sweeping — media no longer
|
|
591
|
+
* referenced by any block — is future work; refs live inside opaque block JSON.) */
|
|
592
|
+
deleteMedia: import("@pramen/server").Handler<{
|
|
593
|
+
id: string;
|
|
594
|
+
}, {
|
|
595
|
+
ok: boolean;
|
|
596
|
+
}>;
|
|
597
|
+
listContentTypes: import("@pramen/server").Handler<unknown, Record<string, unknown>[]>;
|
|
598
|
+
getContentType: import("@pramen/server").Handler<{
|
|
599
|
+
id: string;
|
|
600
|
+
}, Record<string, unknown>>;
|
|
601
|
+
listPages: import("@pramen/server").Handler<unknown, Record<string, unknown>[]>;
|
|
602
|
+
/** Public: list published pages (slug, locale, updatedAt) for sitemap generation. The
|
|
603
|
+
* anonymous ACL scopes cms_pages reads to status=published, so this is safe to expose. */
|
|
604
|
+
listPublishedPages: import("@pramen/server").Handler<unknown, {
|
|
605
|
+
slug: string;
|
|
606
|
+
locale: string;
|
|
607
|
+
updatedAt: string;
|
|
608
|
+
}[]>;
|
|
609
|
+
/** Update a page's SEO fields (meta/canonical/robots/OpenGraph/JSON-LD). Editor-gated. */
|
|
610
|
+
updatePageSeo: import("@pramen/server").Handler<{
|
|
611
|
+
pageId: string;
|
|
612
|
+
}, {
|
|
613
|
+
ok: boolean;
|
|
614
|
+
page: Record<string, unknown>;
|
|
615
|
+
}>;
|
|
616
|
+
/** Create a page and auto-scaffold its content type's default blocks. */
|
|
617
|
+
createPage: import("@pramen/server").Handler<{
|
|
618
|
+
typeId: string;
|
|
619
|
+
title: string;
|
|
620
|
+
slug: string;
|
|
621
|
+
locale?: string;
|
|
622
|
+
fields?: Record<string, unknown>;
|
|
623
|
+
}, Record<string, unknown>>;
|
|
624
|
+
/** Create a translation of an existing page: a new page in `locale` sharing the
|
|
625
|
+
* source's translationGroupId (and content type). Content starts empty — the editor
|
|
626
|
+
* fills in the translated blocks. Slug defaults to the source's (allowed in a new locale). */
|
|
627
|
+
createTranslation: import("@pramen/server").Handler<{
|
|
628
|
+
pageId: string;
|
|
629
|
+
locale: string;
|
|
630
|
+
title?: string;
|
|
631
|
+
slug?: string;
|
|
632
|
+
}, Record<string, unknown>>;
|
|
633
|
+
/** List all locales of a page (the translation group), including the page itself. */
|
|
634
|
+
listTranslations: import("@pramen/server").Handler<{
|
|
635
|
+
pageId: string;
|
|
636
|
+
}, {
|
|
637
|
+
id: string;
|
|
638
|
+
locale: string;
|
|
639
|
+
slug: string;
|
|
640
|
+
title: string;
|
|
641
|
+
status: string;
|
|
642
|
+
}[]>;
|
|
643
|
+
/** Distinct locales present across all pages. */
|
|
644
|
+
listLocales: import("@pramen/server").Handler<unknown, string[]>;
|
|
645
|
+
/** Create a block instance and place it into a page region in one call (the common
|
|
646
|
+
* editor action). Validates the fields against the block type's schema and the region
|
|
647
|
+
* against the content type's allow-list. */
|
|
648
|
+
addBlock: import("@pramen/server").Handler<{
|
|
649
|
+
pageId: string;
|
|
650
|
+
blockTypeSlug: string;
|
|
651
|
+
region: string;
|
|
652
|
+
fields?: Record<string, unknown>;
|
|
653
|
+
title?: string;
|
|
654
|
+
position?: number;
|
|
655
|
+
isReusable?: boolean;
|
|
656
|
+
}, {
|
|
657
|
+
block: Record<string, unknown>;
|
|
658
|
+
placement: Record<string, unknown>;
|
|
659
|
+
}>;
|
|
660
|
+
/** Place an EXISTING (typically reusable) block into a page region as a SHARED
|
|
661
|
+
* placement, with optional per-placement `overrides` merged over the block's fields at
|
|
662
|
+
* read time. This is the "edit once, appear on many pages" workflow — the same block id
|
|
663
|
+
* can be placed on several pages; editing it updates them all, while `overrides` let one
|
|
664
|
+
* placement diverge. The merged (base + overrides) result is validated against the
|
|
665
|
+
* block type's field schema. */
|
|
666
|
+
placeBlock: import("@pramen/server").Handler<{
|
|
667
|
+
pageId: string;
|
|
668
|
+
blockId: string;
|
|
669
|
+
region: string;
|
|
670
|
+
position?: number;
|
|
671
|
+
overrides?: Record<string, unknown>;
|
|
672
|
+
}, Record<string, unknown>>;
|
|
673
|
+
/** Fetch a block's RAW content (media fields as ids, not resolved) — for editing. */
|
|
674
|
+
getBlock: import("@pramen/server").Handler<{
|
|
675
|
+
blockId: string;
|
|
676
|
+
}, Record<string, unknown>>;
|
|
677
|
+
/** Update a block's content (re-validated against its type's field schema). */
|
|
678
|
+
updateBlock: import("@pramen/server").Handler<{
|
|
679
|
+
blockId: string;
|
|
680
|
+
fields?: Record<string, unknown>;
|
|
681
|
+
title?: string;
|
|
682
|
+
}, Record<string, unknown> | undefined>;
|
|
683
|
+
/** Reorder a region: `order` is the page_block ids in their new order. It must cover
|
|
684
|
+
* EXACTLY the region's current placements (same set, no dups) — otherwise a partial or
|
|
685
|
+
* stale list would leave untouched placements colliding at a shared position. */
|
|
686
|
+
reorderRegion: import("@pramen/server").Handler<{
|
|
687
|
+
pageId: string;
|
|
688
|
+
region: string;
|
|
689
|
+
order: string[];
|
|
690
|
+
}, {
|
|
691
|
+
ok: boolean;
|
|
692
|
+
count: number;
|
|
693
|
+
}>;
|
|
694
|
+
/** Remove a placement. A reusable/shared block stays in the library (it may be placed
|
|
695
|
+
* elsewhere); a non-reusable block with no remaining placements is deleted too, so
|
|
696
|
+
* add/remove churn doesn't accumulate unreachable block rows. */
|
|
697
|
+
removeBlock: import("@pramen/server").Handler<{
|
|
698
|
+
pageBlockId: string;
|
|
699
|
+
}, {
|
|
700
|
+
ok: boolean;
|
|
701
|
+
}>;
|
|
702
|
+
/** Move a draft (or rejected) page into review. Editor-gated. */
|
|
703
|
+
submitForReview: import("@pramen/server").Handler<{
|
|
704
|
+
pageId: string;
|
|
705
|
+
note?: string;
|
|
706
|
+
}, {
|
|
707
|
+
ok: boolean;
|
|
708
|
+
page: Record<string, unknown> | undefined;
|
|
709
|
+
}>;
|
|
710
|
+
/** Approve a page in review → publish it (snapshot + currentRevisionId). Reviewer-gated. */
|
|
711
|
+
approve: import("@pramen/server").Handler<{
|
|
712
|
+
pageId: string;
|
|
713
|
+
note?: string;
|
|
714
|
+
}, {
|
|
715
|
+
ok: boolean;
|
|
716
|
+
page: Record<string, unknown> | undefined;
|
|
717
|
+
}>;
|
|
718
|
+
/** Reject a page in review → back to draft. Reviewer-gated. */
|
|
719
|
+
reject: import("@pramen/server").Handler<{
|
|
720
|
+
pageId: string;
|
|
721
|
+
note?: string;
|
|
722
|
+
}, {
|
|
723
|
+
ok: boolean;
|
|
724
|
+
page: Record<string, unknown> | undefined;
|
|
725
|
+
}>;
|
|
726
|
+
/** The workflow audit trail for a page (most recent first). Handler-gated to editors;
|
|
727
|
+
* reads the append-only log via `exec` (a plain admin-scoped read of a gated log). */
|
|
728
|
+
listPageAudit: import("@pramen/server").Handler<{
|
|
729
|
+
pageId: string;
|
|
730
|
+
limit?: number;
|
|
731
|
+
}, Record<string, unknown>[]>;
|
|
732
|
+
/** Publish a page directly: snapshot the assembled page into a revision and flip
|
|
733
|
+
* status to `published` (records an audit entry). The public content API serves the
|
|
734
|
+
* snapshot. `approve` is the review-gated path to the same outcome. */
|
|
735
|
+
publishPage: import("@pramen/server").Handler<{
|
|
736
|
+
pageId: string;
|
|
737
|
+
note?: string;
|
|
738
|
+
}, {
|
|
739
|
+
ok: boolean;
|
|
740
|
+
page: Record<string, unknown> | undefined;
|
|
741
|
+
}>;
|
|
742
|
+
unpublishPage: import("@pramen/server").Handler<{
|
|
743
|
+
pageId: string;
|
|
744
|
+
}, {
|
|
745
|
+
ok: boolean;
|
|
746
|
+
page: Record<string, unknown>;
|
|
747
|
+
}>;
|
|
748
|
+
/** Schedule a page to publish at `publishAt` (epoch ms), and optionally unpublish at
|
|
749
|
+
* `unpublishAt`. Enqueues delayed outbox tasks (atomic with this write).
|
|
750
|
+
*
|
|
751
|
+
* Outbox tasks can't be recalled, so cancellation/rescheduling is handled by INTENT
|
|
752
|
+
* TOKENS: the page stores the scheduled times (`scheduledAt`/`unpublishAt`, ISO), and
|
|
753
|
+
* each task carries the token it was enqueued for. At fire time the task acts ONLY if
|
|
754
|
+
* its token still equals the page's current token — so rescheduling (new token),
|
|
755
|
+
* manual publish/unpublish (token cleared), and duplicate deliveries all make a stale
|
|
756
|
+
* task a no-op. `unpublishAt` must be after `publishAt`. */
|
|
757
|
+
schedulePage: import("@pramen/server").Handler<{
|
|
758
|
+
pageId: string;
|
|
759
|
+
publishAt: number;
|
|
760
|
+
unpublishAt?: number;
|
|
761
|
+
}, {
|
|
762
|
+
ok: boolean;
|
|
763
|
+
publishInMs: number;
|
|
764
|
+
}>;
|
|
765
|
+
/** Fetch an assembled page by slug (+ locale). Anonymous callers get the published
|
|
766
|
+
* snapshot (the ACL scopes `cms_pages` reads to `status = published`). Editors may pass
|
|
767
|
+
* `preview: true` to assemble the current DRAFT live from the tables. `locale` defaults
|
|
768
|
+
* to the configured default locale; a slug is unique per locale. */
|
|
769
|
+
getPage: import("@pramen/server").Handler<{
|
|
770
|
+
slug: string;
|
|
771
|
+
locale?: string;
|
|
772
|
+
preview?: boolean;
|
|
773
|
+
}, AssembledPage>;
|
|
774
|
+
};
|
|
775
|
+
/** The default CMS handlers (editor roles `["editor", "admin"]`). */
|
|
776
|
+
export declare const cmsHandlers: {
|
|
777
|
+
listBlockTypes: import("@pramen/server").Handler<unknown, Record<string, unknown>[]>;
|
|
778
|
+
createBlockType: import("@pramen/server").Handler<{
|
|
779
|
+
name: string;
|
|
780
|
+
slug: string;
|
|
781
|
+
fieldsSchema?: FieldDefinition[];
|
|
782
|
+
icon?: string;
|
|
783
|
+
category?: string;
|
|
784
|
+
description?: string;
|
|
785
|
+
}, Record<string, unknown>>;
|
|
786
|
+
createContentType: import("@pramen/server").Handler<{
|
|
787
|
+
name: string;
|
|
788
|
+
slug: string;
|
|
789
|
+
regions: RegionDefinition[];
|
|
790
|
+
fieldsSchema?: FieldDefinition[];
|
|
791
|
+
defaultBlocks?: DefaultBlockDefinition[];
|
|
792
|
+
}, Record<string, unknown>>;
|
|
793
|
+
/** Mint a signed upload URL for a media blob (keyed under the tenant's `media/`
|
|
794
|
+
* prefix so the public /media route can serve it). The client PUTs the bytes to
|
|
795
|
+
* `url`, then calls `createMedia` with the returned `ref`. */
|
|
796
|
+
signMediaUpload: import("@pramen/server").Handler<{
|
|
797
|
+
contentType: string;
|
|
798
|
+
filename?: string;
|
|
799
|
+
}, {
|
|
800
|
+
url: string;
|
|
801
|
+
ref: FileRef;
|
|
802
|
+
}>;
|
|
803
|
+
/** Confirm an uploaded blob is really in storage (capturing its true size) and
|
|
804
|
+
* persist a `cms_media` row. Mirrors the notes attach flow. */
|
|
805
|
+
createMedia: import("@pramen/server").Handler<{
|
|
806
|
+
ref: FileRef;
|
|
807
|
+
alt?: string;
|
|
808
|
+
}, Record<string, unknown>>;
|
|
809
|
+
listMedia: import("@pramen/server").Handler<{
|
|
810
|
+
limit?: number;
|
|
811
|
+
offset?: number;
|
|
812
|
+
}, Record<string, unknown>[]>;
|
|
813
|
+
getMedia: import("@pramen/server").Handler<{
|
|
814
|
+
id: string;
|
|
815
|
+
}, Record<string, unknown>>;
|
|
816
|
+
/** Edit a media asset's metadata (currently just `alt` text). Editor-gated. */
|
|
817
|
+
updateMedia: import("@pramen/server").Handler<{
|
|
818
|
+
id: string;
|
|
819
|
+
alt: string | null;
|
|
820
|
+
}, Record<string, unknown>>;
|
|
821
|
+
/** Delete a media row AND its R2 blob. (Automatic orphan sweeping — media no longer
|
|
822
|
+
* referenced by any block — is future work; refs live inside opaque block JSON.) */
|
|
823
|
+
deleteMedia: import("@pramen/server").Handler<{
|
|
824
|
+
id: string;
|
|
825
|
+
}, {
|
|
826
|
+
ok: boolean;
|
|
827
|
+
}>;
|
|
828
|
+
listContentTypes: import("@pramen/server").Handler<unknown, Record<string, unknown>[]>;
|
|
829
|
+
getContentType: import("@pramen/server").Handler<{
|
|
830
|
+
id: string;
|
|
831
|
+
}, Record<string, unknown>>;
|
|
832
|
+
listPages: import("@pramen/server").Handler<unknown, Record<string, unknown>[]>;
|
|
833
|
+
/** Public: list published pages (slug, locale, updatedAt) for sitemap generation. The
|
|
834
|
+
* anonymous ACL scopes cms_pages reads to status=published, so this is safe to expose. */
|
|
835
|
+
listPublishedPages: import("@pramen/server").Handler<unknown, {
|
|
836
|
+
slug: string;
|
|
837
|
+
locale: string;
|
|
838
|
+
updatedAt: string;
|
|
839
|
+
}[]>;
|
|
840
|
+
/** Update a page's SEO fields (meta/canonical/robots/OpenGraph/JSON-LD). Editor-gated. */
|
|
841
|
+
updatePageSeo: import("@pramen/server").Handler<{
|
|
842
|
+
pageId: string;
|
|
843
|
+
}, {
|
|
844
|
+
ok: boolean;
|
|
845
|
+
page: Record<string, unknown>;
|
|
846
|
+
}>;
|
|
847
|
+
/** Create a page and auto-scaffold its content type's default blocks. */
|
|
848
|
+
createPage: import("@pramen/server").Handler<{
|
|
849
|
+
typeId: string;
|
|
850
|
+
title: string;
|
|
851
|
+
slug: string;
|
|
852
|
+
locale?: string;
|
|
853
|
+
fields?: Record<string, unknown>;
|
|
854
|
+
}, Record<string, unknown>>;
|
|
855
|
+
/** Create a translation of an existing page: a new page in `locale` sharing the
|
|
856
|
+
* source's translationGroupId (and content type). Content starts empty — the editor
|
|
857
|
+
* fills in the translated blocks. Slug defaults to the source's (allowed in a new locale). */
|
|
858
|
+
createTranslation: import("@pramen/server").Handler<{
|
|
859
|
+
pageId: string;
|
|
860
|
+
locale: string;
|
|
861
|
+
title?: string;
|
|
862
|
+
slug?: string;
|
|
863
|
+
}, Record<string, unknown>>;
|
|
864
|
+
/** List all locales of a page (the translation group), including the page itself. */
|
|
865
|
+
listTranslations: import("@pramen/server").Handler<{
|
|
866
|
+
pageId: string;
|
|
867
|
+
}, {
|
|
868
|
+
id: string;
|
|
869
|
+
locale: string;
|
|
870
|
+
slug: string;
|
|
871
|
+
title: string;
|
|
872
|
+
status: string;
|
|
873
|
+
}[]>;
|
|
874
|
+
/** Distinct locales present across all pages. */
|
|
875
|
+
listLocales: import("@pramen/server").Handler<unknown, string[]>;
|
|
876
|
+
/** Create a block instance and place it into a page region in one call (the common
|
|
877
|
+
* editor action). Validates the fields against the block type's schema and the region
|
|
878
|
+
* against the content type's allow-list. */
|
|
879
|
+
addBlock: import("@pramen/server").Handler<{
|
|
880
|
+
pageId: string;
|
|
881
|
+
blockTypeSlug: string;
|
|
882
|
+
region: string;
|
|
883
|
+
fields?: Record<string, unknown>;
|
|
884
|
+
title?: string;
|
|
885
|
+
position?: number;
|
|
886
|
+
isReusable?: boolean;
|
|
887
|
+
}, {
|
|
888
|
+
block: Record<string, unknown>;
|
|
889
|
+
placement: Record<string, unknown>;
|
|
890
|
+
}>;
|
|
891
|
+
/** Place an EXISTING (typically reusable) block into a page region as a SHARED
|
|
892
|
+
* placement, with optional per-placement `overrides` merged over the block's fields at
|
|
893
|
+
* read time. This is the "edit once, appear on many pages" workflow — the same block id
|
|
894
|
+
* can be placed on several pages; editing it updates them all, while `overrides` let one
|
|
895
|
+
* placement diverge. The merged (base + overrides) result is validated against the
|
|
896
|
+
* block type's field schema. */
|
|
897
|
+
placeBlock: import("@pramen/server").Handler<{
|
|
898
|
+
pageId: string;
|
|
899
|
+
blockId: string;
|
|
900
|
+
region: string;
|
|
901
|
+
position?: number;
|
|
902
|
+
overrides?: Record<string, unknown>;
|
|
903
|
+
}, Record<string, unknown>>;
|
|
904
|
+
/** Fetch a block's RAW content (media fields as ids, not resolved) — for editing. */
|
|
905
|
+
getBlock: import("@pramen/server").Handler<{
|
|
906
|
+
blockId: string;
|
|
907
|
+
}, Record<string, unknown>>;
|
|
908
|
+
/** Update a block's content (re-validated against its type's field schema). */
|
|
909
|
+
updateBlock: import("@pramen/server").Handler<{
|
|
910
|
+
blockId: string;
|
|
911
|
+
fields?: Record<string, unknown>;
|
|
912
|
+
title?: string;
|
|
913
|
+
}, Record<string, unknown> | undefined>;
|
|
914
|
+
/** Reorder a region: `order` is the page_block ids in their new order. It must cover
|
|
915
|
+
* EXACTLY the region's current placements (same set, no dups) — otherwise a partial or
|
|
916
|
+
* stale list would leave untouched placements colliding at a shared position. */
|
|
917
|
+
reorderRegion: import("@pramen/server").Handler<{
|
|
918
|
+
pageId: string;
|
|
919
|
+
region: string;
|
|
920
|
+
order: string[];
|
|
921
|
+
}, {
|
|
922
|
+
ok: boolean;
|
|
923
|
+
count: number;
|
|
924
|
+
}>;
|
|
925
|
+
/** Remove a placement. A reusable/shared block stays in the library (it may be placed
|
|
926
|
+
* elsewhere); a non-reusable block with no remaining placements is deleted too, so
|
|
927
|
+
* add/remove churn doesn't accumulate unreachable block rows. */
|
|
928
|
+
removeBlock: import("@pramen/server").Handler<{
|
|
929
|
+
pageBlockId: string;
|
|
930
|
+
}, {
|
|
931
|
+
ok: boolean;
|
|
932
|
+
}>;
|
|
933
|
+
/** Move a draft (or rejected) page into review. Editor-gated. */
|
|
934
|
+
submitForReview: import("@pramen/server").Handler<{
|
|
935
|
+
pageId: string;
|
|
936
|
+
note?: string;
|
|
937
|
+
}, {
|
|
938
|
+
ok: boolean;
|
|
939
|
+
page: Record<string, unknown> | undefined;
|
|
940
|
+
}>;
|
|
941
|
+
/** Approve a page in review → publish it (snapshot + currentRevisionId). Reviewer-gated. */
|
|
942
|
+
approve: import("@pramen/server").Handler<{
|
|
943
|
+
pageId: string;
|
|
944
|
+
note?: string;
|
|
945
|
+
}, {
|
|
946
|
+
ok: boolean;
|
|
947
|
+
page: Record<string, unknown> | undefined;
|
|
948
|
+
}>;
|
|
949
|
+
/** Reject a page in review → back to draft. Reviewer-gated. */
|
|
950
|
+
reject: import("@pramen/server").Handler<{
|
|
951
|
+
pageId: string;
|
|
952
|
+
note?: string;
|
|
953
|
+
}, {
|
|
954
|
+
ok: boolean;
|
|
955
|
+
page: Record<string, unknown> | undefined;
|
|
956
|
+
}>;
|
|
957
|
+
/** The workflow audit trail for a page (most recent first). Handler-gated to editors;
|
|
958
|
+
* reads the append-only log via `exec` (a plain admin-scoped read of a gated log). */
|
|
959
|
+
listPageAudit: import("@pramen/server").Handler<{
|
|
960
|
+
pageId: string;
|
|
961
|
+
limit?: number;
|
|
962
|
+
}, Record<string, unknown>[]>;
|
|
963
|
+
/** Publish a page directly: snapshot the assembled page into a revision and flip
|
|
964
|
+
* status to `published` (records an audit entry). The public content API serves the
|
|
965
|
+
* snapshot. `approve` is the review-gated path to the same outcome. */
|
|
966
|
+
publishPage: import("@pramen/server").Handler<{
|
|
967
|
+
pageId: string;
|
|
968
|
+
note?: string;
|
|
969
|
+
}, {
|
|
970
|
+
ok: boolean;
|
|
971
|
+
page: Record<string, unknown> | undefined;
|
|
972
|
+
}>;
|
|
973
|
+
unpublishPage: import("@pramen/server").Handler<{
|
|
974
|
+
pageId: string;
|
|
975
|
+
}, {
|
|
976
|
+
ok: boolean;
|
|
977
|
+
page: Record<string, unknown>;
|
|
978
|
+
}>;
|
|
979
|
+
/** Schedule a page to publish at `publishAt` (epoch ms), and optionally unpublish at
|
|
980
|
+
* `unpublishAt`. Enqueues delayed outbox tasks (atomic with this write).
|
|
981
|
+
*
|
|
982
|
+
* Outbox tasks can't be recalled, so cancellation/rescheduling is handled by INTENT
|
|
983
|
+
* TOKENS: the page stores the scheduled times (`scheduledAt`/`unpublishAt`, ISO), and
|
|
984
|
+
* each task carries the token it was enqueued for. At fire time the task acts ONLY if
|
|
985
|
+
* its token still equals the page's current token — so rescheduling (new token),
|
|
986
|
+
* manual publish/unpublish (token cleared), and duplicate deliveries all make a stale
|
|
987
|
+
* task a no-op. `unpublishAt` must be after `publishAt`. */
|
|
988
|
+
schedulePage: import("@pramen/server").Handler<{
|
|
989
|
+
pageId: string;
|
|
990
|
+
publishAt: number;
|
|
991
|
+
unpublishAt?: number;
|
|
992
|
+
}, {
|
|
993
|
+
ok: boolean;
|
|
994
|
+
publishInMs: number;
|
|
995
|
+
}>;
|
|
996
|
+
/** Fetch an assembled page by slug (+ locale). Anonymous callers get the published
|
|
997
|
+
* snapshot (the ACL scopes `cms_pages` reads to `status = published`). Editors may pass
|
|
998
|
+
* `preview: true` to assemble the current DRAFT live from the tables. `locale` defaults
|
|
999
|
+
* to the configured default locale; a slug is unique per locale. */
|
|
1000
|
+
getPage: import("@pramen/server").Handler<{
|
|
1001
|
+
slug: string;
|
|
1002
|
+
locale?: string;
|
|
1003
|
+
preview?: boolean;
|
|
1004
|
+
}, AssembledPage>;
|
|
1005
|
+
};
|
|
1006
|
+
export interface CmsPolicyOpts {
|
|
1007
|
+
/** Prefix for policy names (unique across roles). Default `cms`. */
|
|
1008
|
+
prefix?: string;
|
|
1009
|
+
}
|
|
1010
|
+
/** ACL fragments. Spread `public` into your anonymous role and `editor` into your
|
|
1011
|
+
* editor/admin role:
|
|
1012
|
+
*
|
|
1013
|
+
* role("anonymous", [...cmsPolicies().public])
|
|
1014
|
+
* role("editor", [...cmsPolicies().editor])
|
|
1015
|
+
*
|
|
1016
|
+
* `public` grants read of PUBLISHED pages + their revision snapshots only (the public
|
|
1017
|
+
* content API reads the snapshot, so unpublished block rows are never exposed).
|
|
1018
|
+
* `editor` grants full CRUD across every cms_ table. */
|
|
1019
|
+
export declare function cmsPolicies(opts?: CmsPolicyOpts): {
|
|
1020
|
+
public: Policy[];
|
|
1021
|
+
editor: Policy[];
|
|
1022
|
+
};
|
|
1023
|
+
/** Task handlers backing `schedulePage`. Register via `app.tasks = { ...cmsTasks }`.
|
|
1024
|
+
* They run with a privileged, system-scoped ctx off the write path (the outbox drain).
|
|
1025
|
+
*
|
|
1026
|
+
* Each task validates its INTENT TOKEN against the page's current `scheduledAt`/`unpublishAt`
|
|
1027
|
+
* (set by `schedulePage`, cleared/overwritten by a manual publish/unpublish or a reschedule).
|
|
1028
|
+
* A task whose token no longer matches is a no-op — that's how a superseded/cancelled
|
|
1029
|
+
* schedule, and an at-least-once duplicate delivery, are neutralized (outbox tasks can't be
|
|
1030
|
+
* recalled). NOTE on atomicity: unlike the interactive `publishPage` (one mutation
|
|
1031
|
+
* transaction), the drain runs a handler WITHOUT a surrounding transaction, so a crash
|
|
1032
|
+
* between the revision insert and the page update leaves the page unpublished with an orphan
|
|
1033
|
+
* revision until the next at-least-once redelivery re-runs (the token still matches, so it
|
|
1034
|
+
* completes). Acceptable for a scheduled job; the interactive path is atomic. */
|
|
1035
|
+
export declare const cmsTasks: {
|
|
1036
|
+
"cms:publish": (ctx: HandlerContext, payload: unknown) => Promise<void>;
|
|
1037
|
+
"cms:unpublish": (ctx: HandlerContext, payload: unknown) => Promise<void>;
|
|
1038
|
+
};
|
|
1039
|
+
export interface SitemapEntry {
|
|
1040
|
+
slug: string;
|
|
1041
|
+
locale: string;
|
|
1042
|
+
updatedAt?: string;
|
|
1043
|
+
}
|
|
1044
|
+
export interface SitemapOpts {
|
|
1045
|
+
origin: string;
|
|
1046
|
+
/** Map an entry → its absolute URL. Default `${origin}/${locale}/${slug}`. */
|
|
1047
|
+
pageUrl?: (e: SitemapEntry, origin: string) => string;
|
|
1048
|
+
}
|
|
1049
|
+
/** Build a sitemap.xml body from published-page entries. */
|
|
1050
|
+
export declare function sitemapXml(entries: SitemapEntry[], opts: SitemapOpts): string;
|
|
1051
|
+
/** Build a robots.txt body pointing at the sitemap. */
|
|
1052
|
+
export declare function robotsTxt(opts: {
|
|
1053
|
+
origin: string;
|
|
1054
|
+
disallow?: string[];
|
|
1055
|
+
}): string;
|
|
1056
|
+
interface RouteCtx {
|
|
1057
|
+
callPrivileged: (opts: {
|
|
1058
|
+
name: string;
|
|
1059
|
+
input?: unknown;
|
|
1060
|
+
tenant?: string;
|
|
1061
|
+
roles?: string[];
|
|
1062
|
+
}) => Promise<Response>;
|
|
1063
|
+
}
|
|
1064
|
+
interface CmsRoute {
|
|
1065
|
+
method: string;
|
|
1066
|
+
path: string;
|
|
1067
|
+
handler: (request: Request, env: Readonly<Record<string, unknown>>, ctx: RouteCtx) => Promise<Response>;
|
|
1068
|
+
}
|
|
1069
|
+
/** Turnkey public routes for `GET /sitemap.xml` and `GET /robots.txt`. Spread into
|
|
1070
|
+
* `app.routes`. The sitemap pulls published pages via `callPrivileged(listPublishedPages)`.
|
|
1071
|
+
* `origin` defaults to the request's origin; `pageUrl` customizes the URL shape. */
|
|
1072
|
+
export declare function cmsRoutes(opts?: {
|
|
1073
|
+
origin?: string;
|
|
1074
|
+
tenant?: string;
|
|
1075
|
+
pageUrl?: SitemapOpts["pageUrl"];
|
|
1076
|
+
disallow?: string[];
|
|
1077
|
+
}): CmsRoute[];
|
|
1078
|
+
export {};
|