@pramen/cms 0.0.22 → 0.0.23
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 +39 -1
- package/dist/index.js +63 -0
- package/package.json +2 -2
- package/src/index.ts +104 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { HandlerContext, Policy, FileRef } from "@pramen/server";
|
|
1
|
+
import type { HandlerContext, Policy, FileRef, BootstrapFn } from "@pramen/server";
|
|
2
2
|
/** A field in a block type's (or content type's) field schema. Recursive: a `repeater`
|
|
3
3
|
* or `group` nests `fields`. Mirrors WollyCMS's FieldDefinition. */
|
|
4
4
|
export interface FieldDefinition {
|
|
@@ -72,6 +72,44 @@ export declare function defineBlockType<S extends string, F extends readonly Fie
|
|
|
72
72
|
}): BlockTypeDef<S, F>;
|
|
73
73
|
/** The inferred `fields` type of a `defineBlockType` result. */
|
|
74
74
|
export type BlockFieldsOf<D extends BlockTypeDef> = InferBlockFields<D["fieldsSchema"]>;
|
|
75
|
+
/** A developer-authored content type: a page template. Page-level `fields`, named `regions`
|
|
76
|
+
* (each with an optional block-type allow-list), and optional `defaultBlocks` scaffolded when
|
|
77
|
+
* a page of this type is created. Mirror of `BlockTypeDef`; feed to `cmsBootstrap`. */
|
|
78
|
+
export interface ContentTypeDef {
|
|
79
|
+
readonly slug: string;
|
|
80
|
+
readonly name: string;
|
|
81
|
+
readonly description?: string;
|
|
82
|
+
readonly fields?: readonly FieldDefinition[];
|
|
83
|
+
readonly regions: readonly RegionDefinition[];
|
|
84
|
+
readonly defaultBlocks?: readonly DefaultBlockDefinition[];
|
|
85
|
+
}
|
|
86
|
+
/** Declare a content type in code. Spread the result into `cmsBootstrap({ contentTypes })`:
|
|
87
|
+
*
|
|
88
|
+
* const article = defineContentType("article", {
|
|
89
|
+
* name: "Article",
|
|
90
|
+
* fields: [{ name: "perex", type: "textarea" }, { name: "date", type: "date" }],
|
|
91
|
+
* regions: [{ name: "content", allowedTypes: ["rich_text", "image"] }],
|
|
92
|
+
* }); */
|
|
93
|
+
export declare function defineContentType(slug: string, opts: {
|
|
94
|
+
name?: string;
|
|
95
|
+
description?: string;
|
|
96
|
+
fields?: readonly FieldDefinition[];
|
|
97
|
+
regions: readonly RegionDefinition[];
|
|
98
|
+
defaultBlocks?: readonly DefaultBlockDefinition[];
|
|
99
|
+
}): ContentTypeDef;
|
|
100
|
+
/** Build a pramen `bootstrap` reconciler that upserts code-defined block + content types by
|
|
101
|
+
* `slug` on each boot. Idempotent: inserts a missing type, updates a drifted one, leaves an
|
|
102
|
+
* identical one untouched. Register it on your app:
|
|
103
|
+
*
|
|
104
|
+
* export const app = { schema, handlers, acl, tasks,
|
|
105
|
+
* bootstrap: [ cmsBootstrap({ blockTypes: [...], contentTypes: [...] }) ] };
|
|
106
|
+
*
|
|
107
|
+
* Runs with a privileged system Db, so a fresh/reprovisioned database converges to the
|
|
108
|
+
* code-declared types with no manual createContentType/createBlockType call. */
|
|
109
|
+
export declare function cmsBootstrap(defs: {
|
|
110
|
+
blockTypes?: readonly BlockTypeDef[];
|
|
111
|
+
contentTypes?: readonly ContentTypeDef[];
|
|
112
|
+
}): BootstrapFn;
|
|
75
113
|
/** Emit a `.ts` module of per-slug field interfaces + a `BlockFieldsBySlug` registry from
|
|
76
114
|
* DB-stored block types (`{ slug, fieldsSchema }` rows). The runtime counterpart to the
|
|
77
115
|
* compile-time `InferBlockFields`, for webmaster-authored (data-driven) block types. */
|
package/dist/index.js
CHANGED
|
@@ -40,6 +40,69 @@ import { filterXSS } from "xss";
|
|
|
40
40
|
export function defineBlockType(slug, fields, opts = {}) {
|
|
41
41
|
return { slug, name: opts.name ?? slug, fieldsSchema: fields, description: opts.description, icon: opts.icon, category: opts.category };
|
|
42
42
|
}
|
|
43
|
+
/** Declare a content type in code. Spread the result into `cmsBootstrap({ contentTypes })`:
|
|
44
|
+
*
|
|
45
|
+
* const article = defineContentType("article", {
|
|
46
|
+
* name: "Article",
|
|
47
|
+
* fields: [{ name: "perex", type: "textarea" }, { name: "date", type: "date" }],
|
|
48
|
+
* regions: [{ name: "content", allowedTypes: ["rich_text", "image"] }],
|
|
49
|
+
* }); */
|
|
50
|
+
export function defineContentType(slug, opts) {
|
|
51
|
+
return { slug, name: opts.name ?? slug, description: opts.description, fields: opts.fields, regions: opts.regions, defaultBlocks: opts.defaultBlocks };
|
|
52
|
+
}
|
|
53
|
+
const sameJson = (a, b) => JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
|
|
54
|
+
/** Insert `values` if no row has this `slug`, else patch only the columns that drifted
|
|
55
|
+
* (never `id`/`slug`/`createdAt`). Idempotent — an identical definition is a no-op. */
|
|
56
|
+
async function upsertBySlug(db, table, slug, values) {
|
|
57
|
+
const existing = (await db.find({ from: table, where: { slug }, limit: 1 }))[0];
|
|
58
|
+
if (!existing) {
|
|
59
|
+
await db.insert(table, values);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const patch = {};
|
|
63
|
+
for (const [k, v] of Object.entries(values)) {
|
|
64
|
+
if (k === "slug")
|
|
65
|
+
continue;
|
|
66
|
+
if (!sameJson(existing[k], v))
|
|
67
|
+
patch[k] = v;
|
|
68
|
+
}
|
|
69
|
+
if (Object.keys(patch).length)
|
|
70
|
+
await db.update(table, String(existing.id), patch);
|
|
71
|
+
}
|
|
72
|
+
/** Build a pramen `bootstrap` reconciler that upserts code-defined block + content types by
|
|
73
|
+
* `slug` on each boot. Idempotent: inserts a missing type, updates a drifted one, leaves an
|
|
74
|
+
* identical one untouched. Register it on your app:
|
|
75
|
+
*
|
|
76
|
+
* export const app = { schema, handlers, acl, tasks,
|
|
77
|
+
* bootstrap: [ cmsBootstrap({ blockTypes: [...], contentTypes: [...] }) ] };
|
|
78
|
+
*
|
|
79
|
+
* Runs with a privileged system Db, so a fresh/reprovisioned database converges to the
|
|
80
|
+
* code-declared types with no manual createContentType/createBlockType call. */
|
|
81
|
+
export function cmsBootstrap(defs) {
|
|
82
|
+
return async ({ db }) => {
|
|
83
|
+
const sys = db;
|
|
84
|
+
for (const bt of defs.blockTypes ?? []) {
|
|
85
|
+
await upsertBySlug(sys, "cms_block_types", bt.slug, {
|
|
86
|
+
name: bt.name,
|
|
87
|
+
slug: bt.slug,
|
|
88
|
+
description: bt.description ?? null,
|
|
89
|
+
fieldsSchema: bt.fieldsSchema ?? [],
|
|
90
|
+
icon: bt.icon ?? null,
|
|
91
|
+
category: bt.category ?? null,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
for (const ct of defs.contentTypes ?? []) {
|
|
95
|
+
await upsertBySlug(sys, "cms_content_types", ct.slug, {
|
|
96
|
+
name: ct.name,
|
|
97
|
+
slug: ct.slug,
|
|
98
|
+
description: ct.description ?? null,
|
|
99
|
+
fieldsSchema: ct.fields ?? [],
|
|
100
|
+
regions: ct.regions ?? [],
|
|
101
|
+
defaultBlocks: ct.defaultBlocks ?? [],
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
43
106
|
// --- codegen: emit .ts field interfaces from DB-stored block schemas ------------------
|
|
44
107
|
//
|
|
45
108
|
// The data-driven half (webmaster-created block types) has no static type. This is the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pramen/cms",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.23",
|
|
4
4
|
"description": "Optional block/page builder for pramen — Drupal-Paragraphs-style typed blocks in named regions, reusable blocks, scheduled publishing, built entirely from pramen primitives.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"access": "public"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@pramen/server": "0.0.
|
|
44
|
+
"@pramen/server": "0.0.23",
|
|
45
45
|
"xss": "^1.0.15"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
package/src/index.ts
CHANGED
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
Forbidden,
|
|
43
43
|
PramenError,
|
|
44
44
|
} from "@pramen/server";
|
|
45
|
-
import type { HandlerContext, Policy, FileRef } from "@pramen/server";
|
|
45
|
+
import type { HandlerContext, Policy, FileRef, BootstrapFn } from "@pramen/server";
|
|
46
46
|
import { filterXSS } from "xss";
|
|
47
47
|
|
|
48
48
|
// --- field schema DSL (the block-editor field language) ---------------------
|
|
@@ -159,6 +159,109 @@ export function defineBlockType<S extends string, F extends readonly FieldDefini
|
|
|
159
159
|
/** The inferred `fields` type of a `defineBlockType` result. */
|
|
160
160
|
export type BlockFieldsOf<D extends BlockTypeDef> = InferBlockFields<D["fieldsSchema"]>;
|
|
161
161
|
|
|
162
|
+
// --- code-defined content types + bootstrap reconcile ---------------------------------
|
|
163
|
+
//
|
|
164
|
+
// Block/content types are runtime rows (a webmaster can add one with no deploy), but a repo
|
|
165
|
+
// that hard-depends on a fixed shape (e.g. an Astro site whose build fails unless an
|
|
166
|
+
// `article` type with certain fields exists) wants them CODE-DEFINED and auto-applied. These
|
|
167
|
+
// helpers let you declare types in code and converge them into the store on boot via pramen's
|
|
168
|
+
// `app.bootstrap` — so a fresh / reprovisioned database has them without a manual
|
|
169
|
+
// createContentType/createBlockType call.
|
|
170
|
+
|
|
171
|
+
/** A developer-authored content type: a page template. Page-level `fields`, named `regions`
|
|
172
|
+
* (each with an optional block-type allow-list), and optional `defaultBlocks` scaffolded when
|
|
173
|
+
* a page of this type is created. Mirror of `BlockTypeDef`; feed to `cmsBootstrap`. */
|
|
174
|
+
export interface ContentTypeDef {
|
|
175
|
+
readonly slug: string;
|
|
176
|
+
readonly name: string;
|
|
177
|
+
readonly description?: string;
|
|
178
|
+
readonly fields?: readonly FieldDefinition[];
|
|
179
|
+
readonly regions: readonly RegionDefinition[];
|
|
180
|
+
readonly defaultBlocks?: readonly DefaultBlockDefinition[];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Declare a content type in code. Spread the result into `cmsBootstrap({ contentTypes })`:
|
|
184
|
+
*
|
|
185
|
+
* const article = defineContentType("article", {
|
|
186
|
+
* name: "Article",
|
|
187
|
+
* fields: [{ name: "perex", type: "textarea" }, { name: "date", type: "date" }],
|
|
188
|
+
* regions: [{ name: "content", allowedTypes: ["rich_text", "image"] }],
|
|
189
|
+
* }); */
|
|
190
|
+
export function defineContentType(
|
|
191
|
+
slug: string,
|
|
192
|
+
opts: {
|
|
193
|
+
name?: string;
|
|
194
|
+
description?: string;
|
|
195
|
+
fields?: readonly FieldDefinition[];
|
|
196
|
+
regions: readonly RegionDefinition[];
|
|
197
|
+
defaultBlocks?: readonly DefaultBlockDefinition[];
|
|
198
|
+
},
|
|
199
|
+
): ContentTypeDef {
|
|
200
|
+
return { slug, name: opts.name ?? slug, description: opts.description, fields: opts.fields, regions: opts.regions, defaultBlocks: opts.defaultBlocks };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** The narrow slice of the system Db a reconcile needs. `cmsBootstrap` runs with a SYSTEM
|
|
204
|
+
* Db (ACL bypassed), so these calls are unrestricted; kept loose to avoid threading the
|
|
205
|
+
* host app's schema generic through a library helper. */
|
|
206
|
+
interface ReconcileDb {
|
|
207
|
+
find(q: { from: string; where?: Record<string, unknown>; limit?: number }): Promise<Record<string, unknown>[]>;
|
|
208
|
+
insert(table: string, values: Record<string, unknown>): Promise<unknown>;
|
|
209
|
+
update(table: string, id: string, patch: Record<string, unknown>): Promise<unknown>;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const sameJson = (a: unknown, b: unknown): boolean => JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
|
|
213
|
+
|
|
214
|
+
/** Insert `values` if no row has this `slug`, else patch only the columns that drifted
|
|
215
|
+
* (never `id`/`slug`/`createdAt`). Idempotent — an identical definition is a no-op. */
|
|
216
|
+
async function upsertBySlug(db: ReconcileDb, table: string, slug: string, values: Record<string, unknown>): Promise<void> {
|
|
217
|
+
const existing = (await db.find({ from: table, where: { slug }, limit: 1 }))[0];
|
|
218
|
+
if (!existing) {
|
|
219
|
+
await db.insert(table, values);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const patch: Record<string, unknown> = {};
|
|
223
|
+
for (const [k, v] of Object.entries(values)) {
|
|
224
|
+
if (k === "slug") continue;
|
|
225
|
+
if (!sameJson(existing[k], v)) patch[k] = v;
|
|
226
|
+
}
|
|
227
|
+
if (Object.keys(patch).length) await db.update(table, String(existing.id), patch);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Build a pramen `bootstrap` reconciler that upserts code-defined block + content types by
|
|
231
|
+
* `slug` on each boot. Idempotent: inserts a missing type, updates a drifted one, leaves an
|
|
232
|
+
* identical one untouched. Register it on your app:
|
|
233
|
+
*
|
|
234
|
+
* export const app = { schema, handlers, acl, tasks,
|
|
235
|
+
* bootstrap: [ cmsBootstrap({ blockTypes: [...], contentTypes: [...] }) ] };
|
|
236
|
+
*
|
|
237
|
+
* Runs with a privileged system Db, so a fresh/reprovisioned database converges to the
|
|
238
|
+
* code-declared types with no manual createContentType/createBlockType call. */
|
|
239
|
+
export function cmsBootstrap(defs: { blockTypes?: readonly BlockTypeDef[]; contentTypes?: readonly ContentTypeDef[] }): BootstrapFn {
|
|
240
|
+
return async ({ db }) => {
|
|
241
|
+
const sys = db as unknown as ReconcileDb;
|
|
242
|
+
for (const bt of defs.blockTypes ?? []) {
|
|
243
|
+
await upsertBySlug(sys, "cms_block_types", bt.slug, {
|
|
244
|
+
name: bt.name,
|
|
245
|
+
slug: bt.slug,
|
|
246
|
+
description: bt.description ?? null,
|
|
247
|
+
fieldsSchema: bt.fieldsSchema ?? [],
|
|
248
|
+
icon: bt.icon ?? null,
|
|
249
|
+
category: bt.category ?? null,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
for (const ct of defs.contentTypes ?? []) {
|
|
253
|
+
await upsertBySlug(sys, "cms_content_types", ct.slug, {
|
|
254
|
+
name: ct.name,
|
|
255
|
+
slug: ct.slug,
|
|
256
|
+
description: ct.description ?? null,
|
|
257
|
+
fieldsSchema: ct.fields ?? [],
|
|
258
|
+
regions: ct.regions ?? [],
|
|
259
|
+
defaultBlocks: ct.defaultBlocks ?? [],
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
162
265
|
// --- codegen: emit .ts field interfaces from DB-stored block schemas ------------------
|
|
163
266
|
//
|
|
164
267
|
// The data-driven half (webmaster-created block types) has no static type. This is the
|