@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 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.15",
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.15"
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