@pramen/cms 0.0.46 → 0.0.47

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,34 @@ import type { HandlerContext, Policy, FileRef, BootstrapFn } from "@pramen/serve
4
4
  export interface FieldDefinition {
5
5
  name: string;
6
6
  label?: string;
7
- type: "text" | "textarea" | "richtext" | "url" | "number" | "boolean" | "date" | "datetime" | "media" | "select" | "repeater" | "group";
7
+ type: "text" | "textarea" | "richtext" | "url" | "number" | "boolean" | "date" | "datetime"
8
+ /**
9
+ * A publication timestamp with three states: unset (hidden), a past time
10
+ * (published), or a future time (scheduled). The editor renders it as
11
+ * publish-now / schedule / unpublish rather than a bare date picker, because that is
12
+ * the decision an editor is actually making.
13
+ *
14
+ * Validated as a `datetime` on the wire, but NOT interchangeable with one: the
15
+ * `publish` control always writes a UTC instant with a `Z` suffix
16
+ * (`2026-08-20T12:00:00.000Z`), where the `datetime` control writes the picker's
17
+ * naive local string (`2026-08-20T14:00`). Both land in the same TEXT column and
18
+ * both pass validation, yet they sort and range-compare lexicographically against
19
+ * each other as if hours apart — so converting an existing `datetime` field to
20
+ * `publish` needs a backfill of the stored values, not just a type change.
21
+ *
22
+ * A collection has no page-style publish workflow, so this is how a row goes live.
23
+ * Scope the anonymous read policy to it with `$now()` from `@pramen/server`:
24
+ *
25
+ * ```ts
26
+ * policy("cms_lectures", { read: { where: { publishedAt: { lte: $now() } } } })
27
+ * ```
28
+ *
29
+ * That, and not `{ publishedAt: { isNull: false } }`, is the real access boundary.
30
+ * `isNull: false` matches a FUTURE timestamp too, so a row the editor scheduled for
31
+ * next week would be anonymously readable the moment it was saved — the scheduling
32
+ * affordance would be a UI label over no enforcement at all.
33
+ */
34
+ | "publish" | "media" | "select" | "repeater" | "group";
8
35
  required?: boolean;
9
36
  default?: unknown;
10
37
  /** repeater/group only — the nested fields. */
@@ -38,7 +65,7 @@ export type RichText = string | {
38
65
  };
39
66
  /** Map one FieldDefinition (as a const literal) to the TS type of its RENDERED value.
40
67
  * Media resolves to `ResolvedMedia` (the assemble-time shape a component receives). */
41
- 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;
68
+ export type FieldTsType<D extends FieldDefinition> = D["type"] extends "text" | "textarea" | "url" | "select" | "date" | "datetime" | "publish" ? 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;
42
69
  /** Infer the `fields` object type from a const `FieldDefinition[]`. Required fields are
43
70
  * present; optional ones are `| undefined`. */
44
71
  export type InferBlockFields<T extends readonly FieldDefinition[]> = {
package/dist/index.js CHANGED
@@ -118,6 +118,7 @@ function tsTypeOf(f) {
118
118
  case "select":
119
119
  case "date":
120
120
  case "datetime":
121
+ case "publish":
121
122
  return "string";
122
123
  case "richtext":
123
124
  return "RichText";
@@ -321,6 +322,8 @@ export function validateFields(schema, values, path = "", opts = {}) {
321
322
  throw new BadRequest(`field '${at}' must be a date (YYYY-MM-DD)`);
322
323
  break;
323
324
  case "datetime":
325
+ // `publish` is a datetime on the wire; only the editor control differs.
326
+ case "publish":
324
327
  if (typeof v !== "string" || !isDateTimeString(v))
325
328
  throw new BadRequest(`field '${at}' must be a date-time (ISO 8601)`);
326
329
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pramen/cms",
3
- "version": "0.0.46",
3
+ "version": "0.0.47",
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.46",
44
+ "@pramen/server": "0.0.47",
45
45
  "xss": "^1.0.15"
46
46
  },
47
47
  "peerDependencies": {
package/src/index.ts CHANGED
@@ -61,6 +61,33 @@ export interface FieldDefinition {
61
61
  | "boolean"
62
62
  | "date"
63
63
  | "datetime"
64
+ /**
65
+ * A publication timestamp with three states: unset (hidden), a past time
66
+ * (published), or a future time (scheduled). The editor renders it as
67
+ * publish-now / schedule / unpublish rather than a bare date picker, because that is
68
+ * the decision an editor is actually making.
69
+ *
70
+ * Validated as a `datetime` on the wire, but NOT interchangeable with one: the
71
+ * `publish` control always writes a UTC instant with a `Z` suffix
72
+ * (`2026-08-20T12:00:00.000Z`), where the `datetime` control writes the picker's
73
+ * naive local string (`2026-08-20T14:00`). Both land in the same TEXT column and
74
+ * both pass validation, yet they sort and range-compare lexicographically against
75
+ * each other as if hours apart — so converting an existing `datetime` field to
76
+ * `publish` needs a backfill of the stored values, not just a type change.
77
+ *
78
+ * A collection has no page-style publish workflow, so this is how a row goes live.
79
+ * Scope the anonymous read policy to it with `$now()` from `@pramen/server`:
80
+ *
81
+ * ```ts
82
+ * policy("cms_lectures", { read: { where: { publishedAt: { lte: $now() } } } })
83
+ * ```
84
+ *
85
+ * That, and not `{ publishedAt: { isNull: false } }`, is the real access boundary.
86
+ * `isNull: false` matches a FUTURE timestamp too, so a row the editor scheduled for
87
+ * next week would be anonymously readable the moment it was saved — the scheduling
88
+ * affordance would be a UI label over no enforcement at all.
89
+ */
90
+ | "publish"
64
91
  | "media"
65
92
  | "select"
66
93
  | "repeater"
@@ -107,7 +134,7 @@ export type RichText = string | { type: string; content?: unknown[] };
107
134
 
108
135
  /** Map one FieldDefinition (as a const literal) to the TS type of its RENDERED value.
109
136
  * Media resolves to `ResolvedMedia` (the assemble-time shape a component receives). */
110
- export type FieldTsType<D extends FieldDefinition> = D["type"] extends "text" | "textarea" | "url" | "select" | "date" | "datetime"
137
+ export type FieldTsType<D extends FieldDefinition> = D["type"] extends "text" | "textarea" | "url" | "select" | "date" | "datetime" | "publish"
111
138
  ? string
112
139
  : D["type"] extends "richtext"
113
140
  ? RichText
@@ -282,6 +309,7 @@ function tsTypeOf(f: FieldDefinition): string {
282
309
  case "select":
283
310
  case "date":
284
311
  case "datetime":
312
+ case "publish":
285
313
  return "string";
286
314
  case "richtext":
287
315
  return "RichText";
@@ -514,6 +542,8 @@ export function validateFields(schema: FieldDefinition[] | undefined | null, val
514
542
  if (typeof v !== "string" || !isDateString(v)) throw new BadRequest(`field '${at}' must be a date (YYYY-MM-DD)`);
515
543
  break;
516
544
  case "datetime":
545
+ // `publish` is a datetime on the wire; only the editor control differs.
546
+ case "publish":
517
547
  if (typeof v !== "string" || !isDateTimeString(v)) throw new BadRequest(`field '${at}' must be a date-time (ISO 8601)`);
518
548
  break;
519
549
  case "media":