@pramen/cms 0.0.15 → 0.0.17
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 +46 -4
- package/dist/index.js +67 -0
- package/package.json +2 -2
- package/src/index.ts +64 -1
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { HandlerContext, Policy, FileRef } from "@pramen/server";
|
|
|
4
4
|
export interface FieldDefinition {
|
|
5
5
|
name: string;
|
|
6
6
|
label?: string;
|
|
7
|
-
type: "text" | "textarea" | "richtext" | "url" | "number" | "boolean" | "media" | "select" | "repeater" | "group";
|
|
7
|
+
type: "text" | "textarea" | "richtext" | "url" | "number" | "boolean" | "date" | "datetime" | "media" | "select" | "repeater" | "group";
|
|
8
8
|
required?: boolean;
|
|
9
9
|
default?: unknown;
|
|
10
10
|
/** repeater/group only — the nested fields. */
|
|
@@ -35,7 +35,7 @@ export type RichText = string | {
|
|
|
35
35
|
};
|
|
36
36
|
/** Map one FieldDefinition (as a const literal) to the TS type of its RENDERED value.
|
|
37
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;
|
|
38
|
+
export type FieldTsType<D extends FieldDefinition> = D["type"] extends "text" | "textarea" | "url" | "select" | "date" | "datetime" ? 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
39
|
/** Infer the `fields` object type from a const `FieldDefinition[]`. Required fields are
|
|
40
40
|
* present; optional ones are `| undefined`. */
|
|
41
41
|
export type InferBlockFields<T extends readonly FieldDefinition[]> = {
|
|
@@ -459,8 +459,6 @@ export interface ValidateOpts {
|
|
|
459
459
|
* required is only mandatory when publishing. Type checks always run. */
|
|
460
460
|
requireRequired?: boolean;
|
|
461
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
462
|
export declare function validateFields(schema: FieldDefinition[] | undefined | null, values: unknown, path?: string, opts?: ValidateOpts): void;
|
|
465
463
|
export interface RenderedBlock {
|
|
466
464
|
/** The placement id (cms_page_blocks) — stable per position; used for reorder/remove. */
|
|
@@ -559,6 +557,28 @@ export declare function createCmsHandlers(opts?: CmsHandlerOpts): {
|
|
|
559
557
|
fieldsSchema?: FieldDefinition[];
|
|
560
558
|
defaultBlocks?: DefaultBlockDefinition[];
|
|
561
559
|
}, Record<string, unknown>>;
|
|
560
|
+
/** Update a block type (found by `id` or `slug`). `slug` is the stable key and is
|
|
561
|
+
* NOT mutable; only the metadata + `fieldsSchema` are patched. Editor-gated. This is
|
|
562
|
+
* what lets a schema evolve (e.g. a field text → date) without recreating the type. */
|
|
563
|
+
updateBlockType: import("@pramen/server").Handler<{
|
|
564
|
+
id?: string;
|
|
565
|
+
slug?: string;
|
|
566
|
+
name?: string;
|
|
567
|
+
fieldsSchema?: FieldDefinition[];
|
|
568
|
+
icon?: string | null;
|
|
569
|
+
category?: string | null;
|
|
570
|
+
description?: string | null;
|
|
571
|
+
}, Record<string, unknown> | undefined>;
|
|
572
|
+
/** Update a content type (found by `id` or `slug`). `slug` is the stable key and is
|
|
573
|
+
* NOT mutable; `name`/`regions`/`fieldsSchema`/`defaultBlocks` are patched. Editor-gated. */
|
|
574
|
+
updateContentType: import("@pramen/server").Handler<{
|
|
575
|
+
id?: string;
|
|
576
|
+
slug?: string;
|
|
577
|
+
name?: string;
|
|
578
|
+
regions?: RegionDefinition[];
|
|
579
|
+
fieldsSchema?: FieldDefinition[];
|
|
580
|
+
defaultBlocks?: DefaultBlockDefinition[];
|
|
581
|
+
}, Record<string, unknown> | undefined>;
|
|
562
582
|
/** Mint a signed upload URL for a media blob (keyed under the tenant's `media/`
|
|
563
583
|
* prefix so the public /media route can serve it). The client PUTs the bytes to
|
|
564
584
|
* `url`, then calls `createMedia` with the returned `ref`. */
|
|
@@ -790,6 +810,28 @@ export declare const cmsHandlers: {
|
|
|
790
810
|
fieldsSchema?: FieldDefinition[];
|
|
791
811
|
defaultBlocks?: DefaultBlockDefinition[];
|
|
792
812
|
}, Record<string, unknown>>;
|
|
813
|
+
/** Update a block type (found by `id` or `slug`). `slug` is the stable key and is
|
|
814
|
+
* NOT mutable; only the metadata + `fieldsSchema` are patched. Editor-gated. This is
|
|
815
|
+
* what lets a schema evolve (e.g. a field text → date) without recreating the type. */
|
|
816
|
+
updateBlockType: import("@pramen/server").Handler<{
|
|
817
|
+
id?: string;
|
|
818
|
+
slug?: string;
|
|
819
|
+
name?: string;
|
|
820
|
+
fieldsSchema?: FieldDefinition[];
|
|
821
|
+
icon?: string | null;
|
|
822
|
+
category?: string | null;
|
|
823
|
+
description?: string | null;
|
|
824
|
+
}, Record<string, unknown> | undefined>;
|
|
825
|
+
/** Update a content type (found by `id` or `slug`). `slug` is the stable key and is
|
|
826
|
+
* NOT mutable; `name`/`regions`/`fieldsSchema`/`defaultBlocks` are patched. Editor-gated. */
|
|
827
|
+
updateContentType: import("@pramen/server").Handler<{
|
|
828
|
+
id?: string;
|
|
829
|
+
slug?: string;
|
|
830
|
+
name?: string;
|
|
831
|
+
regions?: RegionDefinition[];
|
|
832
|
+
fieldsSchema?: FieldDefinition[];
|
|
833
|
+
defaultBlocks?: DefaultBlockDefinition[];
|
|
834
|
+
}, Record<string, unknown> | undefined>;
|
|
793
835
|
/** Mint a signed upload URL for a media blob (keyed under the tenant's `media/`
|
|
794
836
|
* prefix so the public /media route can serve it). The client PUTs the bytes to
|
|
795
837
|
* `url`, then calls `createMedia` with the returned `ref`. */
|
package/dist/index.js
CHANGED
|
@@ -52,6 +52,8 @@ function tsTypeOf(f) {
|
|
|
52
52
|
case "textarea":
|
|
53
53
|
case "url":
|
|
54
54
|
case "select":
|
|
55
|
+
case "date":
|
|
56
|
+
case "datetime":
|
|
55
57
|
return "string";
|
|
56
58
|
case "richtext":
|
|
57
59
|
return "RichText";
|
|
@@ -203,6 +205,14 @@ export const cmsSchema = {
|
|
|
203
205
|
};
|
|
204
206
|
/** Validate a block/page's `fields` payload against a field schema, throwing a 400 on
|
|
205
207
|
* the first violation. Recursive (repeater/group). Lenient on unknown field types. */
|
|
208
|
+
/** A calendar date, `YYYY-MM-DD` (what an <input type="date"> emits) — must also parse. */
|
|
209
|
+
function isDateString(v) {
|
|
210
|
+
return /^\d{4}-\d{2}-\d{2}$/.test(v) && Number.isFinite(Date.parse(v));
|
|
211
|
+
}
|
|
212
|
+
/** A date-time: `YYYY-MM-DDTHH:MM[:SS[.sss]][Z|±HH:MM]` (ISO 8601 / datetime-local). */
|
|
213
|
+
function isDateTimeString(v) {
|
|
214
|
+
return /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(:\d{2})?(\.\d+)?(Z|[+-]\d{2}:\d{2})?$/.test(v) && Number.isFinite(Date.parse(v));
|
|
215
|
+
}
|
|
206
216
|
export function validateFields(schema, values, path = "", opts = {}) {
|
|
207
217
|
const requireRequired = opts.requireRequired !== false;
|
|
208
218
|
const defs = Array.isArray(schema) ? schema : [];
|
|
@@ -241,6 +251,14 @@ export function validateFields(schema, values, path = "", opts = {}) {
|
|
|
241
251
|
if (typeof v !== "boolean")
|
|
242
252
|
throw new BadRequest(`field '${at}' must be a boolean`);
|
|
243
253
|
break;
|
|
254
|
+
case "date":
|
|
255
|
+
if (typeof v !== "string" || !isDateString(v))
|
|
256
|
+
throw new BadRequest(`field '${at}' must be a date (YYYY-MM-DD)`);
|
|
257
|
+
break;
|
|
258
|
+
case "datetime":
|
|
259
|
+
if (typeof v !== "string" || !isDateTimeString(v))
|
|
260
|
+
throw new BadRequest(`field '${at}' must be a date-time (ISO 8601)`);
|
|
261
|
+
break;
|
|
244
262
|
case "media":
|
|
245
263
|
// Media ids are uuids (strings) — reject numbers so the value always resolves
|
|
246
264
|
// (collectMediaIds/resolveMediaFields only handle string ids).
|
|
@@ -553,6 +571,55 @@ export function createCmsHandlers(opts = {}) {
|
|
|
553
571
|
return o;
|
|
554
572
|
},
|
|
555
573
|
}),
|
|
574
|
+
/** Update a block type (found by `id` or `slug`). `slug` is the stable key and is
|
|
575
|
+
* NOT mutable; only the metadata + `fieldsSchema` are patched. Editor-gated. This is
|
|
576
|
+
* what lets a schema evolve (e.g. a field text → date) without recreating the type. */
|
|
577
|
+
updateBlockType: mutation(async (ctx, input) => {
|
|
578
|
+
const db = cdb(ctx);
|
|
579
|
+
const rows = await db.find({ from: "cms_block_types", where: input.id ? { id: input.id } : { slug: input.slug }, limit: 1 });
|
|
580
|
+
const row = rows[0];
|
|
581
|
+
if (!row)
|
|
582
|
+
throw notFound("block type");
|
|
583
|
+
const patch = {};
|
|
584
|
+
for (const k of ["name", "fieldsSchema", "icon", "category", "description"]) {
|
|
585
|
+
if (k in input)
|
|
586
|
+
patch[k] = input[k];
|
|
587
|
+
}
|
|
588
|
+
return db.update("cms_block_types", String(row.id), patch);
|
|
589
|
+
}, {
|
|
590
|
+
...editor,
|
|
591
|
+
input: (raw) => {
|
|
592
|
+
const o = asObj(raw);
|
|
593
|
+
if (typeof o.id !== "string" && typeof o.slug !== "string")
|
|
594
|
+
throw new BadRequest("id or slug is required");
|
|
595
|
+
return o;
|
|
596
|
+
},
|
|
597
|
+
}),
|
|
598
|
+
/** Update a content type (found by `id` or `slug`). `slug` is the stable key and is
|
|
599
|
+
* NOT mutable; `name`/`regions`/`fieldsSchema`/`defaultBlocks` are patched. Editor-gated. */
|
|
600
|
+
updateContentType: mutation(async (ctx, input) => {
|
|
601
|
+
const db = cdb(ctx);
|
|
602
|
+
if ("regions" in input && (!Array.isArray(input.regions) || input.regions.length === 0))
|
|
603
|
+
throw new BadRequest("at least one region is required");
|
|
604
|
+
const rows = await db.find({ from: "cms_content_types", where: input.id ? { id: input.id } : { slug: input.slug }, limit: 1 });
|
|
605
|
+
const row = rows[0];
|
|
606
|
+
if (!row)
|
|
607
|
+
throw notFound("content type");
|
|
608
|
+
const patch = {};
|
|
609
|
+
for (const k of ["name", "regions", "fieldsSchema", "defaultBlocks"]) {
|
|
610
|
+
if (k in input)
|
|
611
|
+
patch[k] = input[k];
|
|
612
|
+
}
|
|
613
|
+
return db.update("cms_content_types", String(row.id), patch);
|
|
614
|
+
}, {
|
|
615
|
+
...editor,
|
|
616
|
+
input: (raw) => {
|
|
617
|
+
const o = asObj(raw);
|
|
618
|
+
if (typeof o.id !== "string" && typeof o.slug !== "string")
|
|
619
|
+
throw new BadRequest("id or slug is required");
|
|
620
|
+
return o;
|
|
621
|
+
},
|
|
622
|
+
}),
|
|
556
623
|
// ---- media library ----
|
|
557
624
|
/** Mint a signed upload URL for a media blob (keyed under the tenant's `media/`
|
|
558
625
|
* prefix so the public /media route can serve it). The client PUTs the bytes to
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pramen/cms",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.17",
|
|
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.17"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"react": ">=18"
|
package/src/index.ts
CHANGED
|
@@ -58,6 +58,8 @@ export interface FieldDefinition {
|
|
|
58
58
|
| "url"
|
|
59
59
|
| "number"
|
|
60
60
|
| "boolean"
|
|
61
|
+
| "date"
|
|
62
|
+
| "datetime"
|
|
61
63
|
| "media"
|
|
62
64
|
| "select"
|
|
63
65
|
| "repeater"
|
|
@@ -101,7 +103,7 @@ export type RichText = string | { type: string; content?: unknown[] };
|
|
|
101
103
|
|
|
102
104
|
/** Map one FieldDefinition (as a const literal) to the TS type of its RENDERED value.
|
|
103
105
|
* Media resolves to `ResolvedMedia` (the assemble-time shape a component receives). */
|
|
104
|
-
export type FieldTsType<D extends FieldDefinition> = D["type"] extends "text" | "textarea" | "url" | "select"
|
|
106
|
+
export type FieldTsType<D extends FieldDefinition> = D["type"] extends "text" | "textarea" | "url" | "select" | "date" | "datetime"
|
|
105
107
|
? string
|
|
106
108
|
: D["type"] extends "richtext"
|
|
107
109
|
? RichText
|
|
@@ -171,6 +173,8 @@ function tsTypeOf(f: FieldDefinition): string {
|
|
|
171
173
|
case "textarea":
|
|
172
174
|
case "url":
|
|
173
175
|
case "select":
|
|
176
|
+
case "date":
|
|
177
|
+
case "datetime":
|
|
174
178
|
return "string";
|
|
175
179
|
case "richtext":
|
|
176
180
|
return "RichText";
|
|
@@ -356,6 +360,15 @@ export interface ValidateOpts {
|
|
|
356
360
|
|
|
357
361
|
/** Validate a block/page's `fields` payload against a field schema, throwing a 400 on
|
|
358
362
|
* the first violation. Recursive (repeater/group). Lenient on unknown field types. */
|
|
363
|
+
/** A calendar date, `YYYY-MM-DD` (what an <input type="date"> emits) — must also parse. */
|
|
364
|
+
function isDateString(v: string): boolean {
|
|
365
|
+
return /^\d{4}-\d{2}-\d{2}$/.test(v) && Number.isFinite(Date.parse(v));
|
|
366
|
+
}
|
|
367
|
+
/** A date-time: `YYYY-MM-DDTHH:MM[:SS[.sss]][Z|±HH:MM]` (ISO 8601 / datetime-local). */
|
|
368
|
+
function isDateTimeString(v: string): boolean {
|
|
369
|
+
return /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(:\d{2})?(\.\d+)?(Z|[+-]\d{2}:\d{2})?$/.test(v) && Number.isFinite(Date.parse(v));
|
|
370
|
+
}
|
|
371
|
+
|
|
359
372
|
export function validateFields(schema: FieldDefinition[] | undefined | null, values: unknown, path = "", opts: ValidateOpts = {}): void {
|
|
360
373
|
const requireRequired = opts.requireRequired !== false;
|
|
361
374
|
const defs = Array.isArray(schema) ? schema : [];
|
|
@@ -388,6 +401,12 @@ export function validateFields(schema: FieldDefinition[] | undefined | null, val
|
|
|
388
401
|
case "boolean":
|
|
389
402
|
if (typeof v !== "boolean") throw new BadRequest(`field '${at}' must be a boolean`);
|
|
390
403
|
break;
|
|
404
|
+
case "date":
|
|
405
|
+
if (typeof v !== "string" || !isDateString(v)) throw new BadRequest(`field '${at}' must be a date (YYYY-MM-DD)`);
|
|
406
|
+
break;
|
|
407
|
+
case "datetime":
|
|
408
|
+
if (typeof v !== "string" || !isDateTimeString(v)) throw new BadRequest(`field '${at}' must be a date-time (ISO 8601)`);
|
|
409
|
+
break;
|
|
391
410
|
case "media":
|
|
392
411
|
// Media ids are uuids (strings) — reject numbers so the value always resolves
|
|
393
412
|
// (collectMediaIds/resolveMediaFields only handle string ids).
|
|
@@ -808,6 +827,50 @@ export function createCmsHandlers(opts: CmsHandlerOpts = {}) {
|
|
|
808
827
|
},
|
|
809
828
|
}),
|
|
810
829
|
|
|
830
|
+
/** Update a block type (found by `id` or `slug`). `slug` is the stable key and is
|
|
831
|
+
* NOT mutable; only the metadata + `fieldsSchema` are patched. Editor-gated. This is
|
|
832
|
+
* what lets a schema evolve (e.g. a field text → date) without recreating the type. */
|
|
833
|
+
updateBlockType: mutation(async (ctx, input: { id?: string; slug?: string; name?: string; fieldsSchema?: FieldDefinition[]; icon?: string | null; category?: string | null; description?: string | null }) => {
|
|
834
|
+
const db = cdb(ctx);
|
|
835
|
+
const rows = await db.find({ from: "cms_block_types", where: input.id ? { id: input.id } : { slug: input.slug }, limit: 1 });
|
|
836
|
+
const row = rows[0];
|
|
837
|
+
if (!row) throw notFound("block type");
|
|
838
|
+
const patch: Record<string, unknown> = {};
|
|
839
|
+
for (const k of ["name", "fieldsSchema", "icon", "category", "description"] as const) {
|
|
840
|
+
if (k in input) patch[k] = (input as Record<string, unknown>)[k];
|
|
841
|
+
}
|
|
842
|
+
return db.update("cms_block_types", String(row.id), patch);
|
|
843
|
+
}, {
|
|
844
|
+
...editor,
|
|
845
|
+
input: (raw): { id?: string; slug?: string; name?: string; fieldsSchema?: FieldDefinition[]; icon?: string | null; category?: string | null; description?: string | null } => {
|
|
846
|
+
const o = asObj(raw);
|
|
847
|
+
if (typeof o.id !== "string" && typeof o.slug !== "string") throw new BadRequest("id or slug is required");
|
|
848
|
+
return o as never;
|
|
849
|
+
},
|
|
850
|
+
}),
|
|
851
|
+
|
|
852
|
+
/** Update a content type (found by `id` or `slug`). `slug` is the stable key and is
|
|
853
|
+
* NOT mutable; `name`/`regions`/`fieldsSchema`/`defaultBlocks` are patched. Editor-gated. */
|
|
854
|
+
updateContentType: mutation(async (ctx, input: { id?: string; slug?: string; name?: string; regions?: RegionDefinition[]; fieldsSchema?: FieldDefinition[]; defaultBlocks?: DefaultBlockDefinition[] }) => {
|
|
855
|
+
const db = cdb(ctx);
|
|
856
|
+
if ("regions" in input && (!Array.isArray(input.regions) || input.regions.length === 0)) throw new BadRequest("at least one region is required");
|
|
857
|
+
const rows = await db.find({ from: "cms_content_types", where: input.id ? { id: input.id } : { slug: input.slug }, limit: 1 });
|
|
858
|
+
const row = rows[0];
|
|
859
|
+
if (!row) throw notFound("content type");
|
|
860
|
+
const patch: Record<string, unknown> = {};
|
|
861
|
+
for (const k of ["name", "regions", "fieldsSchema", "defaultBlocks"] as const) {
|
|
862
|
+
if (k in input) patch[k] = (input as Record<string, unknown>)[k];
|
|
863
|
+
}
|
|
864
|
+
return db.update("cms_content_types", String(row.id), patch);
|
|
865
|
+
}, {
|
|
866
|
+
...editor,
|
|
867
|
+
input: (raw): { id?: string; slug?: string; name?: string; regions?: RegionDefinition[]; fieldsSchema?: FieldDefinition[]; defaultBlocks?: DefaultBlockDefinition[] } => {
|
|
868
|
+
const o = asObj(raw);
|
|
869
|
+
if (typeof o.id !== "string" && typeof o.slug !== "string") throw new BadRequest("id or slug is required");
|
|
870
|
+
return o as never;
|
|
871
|
+
},
|
|
872
|
+
}),
|
|
873
|
+
|
|
811
874
|
// ---- media library ----
|
|
812
875
|
/** Mint a signed upload URL for a media blob (keyed under the tenant's `media/`
|
|
813
876
|
* prefix so the public /media route can serve it). The client PUTs the bytes to
|