@pramen/cms 0.0.47 → 0.0.48
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 +15 -2
- package/dist/index.js +14 -0
- package/package.json +2 -2
- package/src/index.ts +26 -1
package/dist/index.d.ts
CHANGED
|
@@ -31,7 +31,18 @@ export interface FieldDefinition {
|
|
|
31
31
|
* next week would be anonymously readable the moment it was saved — the scheduling
|
|
32
32
|
* affordance would be a UI label over no enforcement at all.
|
|
33
33
|
*/
|
|
34
|
-
| "publish"
|
|
34
|
+
| "publish"
|
|
35
|
+
/**
|
|
36
|
+
* A URL segment, derived from another field as you type.
|
|
37
|
+
*
|
|
38
|
+
* Set `from` to the field it follows (usually the title). The editor keeps them in
|
|
39
|
+
* sync only while the slug is untouched — once it has been edited, or on a row that
|
|
40
|
+
* already has one, it stops following, because silently rewriting a slug changes a
|
|
41
|
+
* live URL and breaks every link to it.
|
|
42
|
+
*
|
|
43
|
+
* Stored as text. Uniqueness is the schema's job (`unique(t.text())`).
|
|
44
|
+
*/
|
|
45
|
+
| "slug" | "media" | "select" | "repeater" | "group";
|
|
35
46
|
required?: boolean;
|
|
36
47
|
default?: unknown;
|
|
37
48
|
/** repeater/group only — the nested fields. */
|
|
@@ -44,6 +55,8 @@ export interface FieldDefinition {
|
|
|
44
55
|
/** select only — fetch options at edit time from a query handler of this name (returns
|
|
45
56
|
* `{ value, label }[]`), e.g. a live list of campaigns. Takes precedence over `options`. */
|
|
46
57
|
optionsFrom?: string;
|
|
58
|
+
/** slug only — the sibling field this one is derived from (e.g. `"title"`). */
|
|
59
|
+
from?: string;
|
|
47
60
|
}
|
|
48
61
|
/** A named region on a content type; `allowedTypes` (block-type slugs) restricts what
|
|
49
62
|
* may be placed there — `null`/omitted means any. */
|
|
@@ -65,7 +78,7 @@ export type RichText = string | {
|
|
|
65
78
|
};
|
|
66
79
|
/** Map one FieldDefinition (as a const literal) to the TS type of its RENDERED value.
|
|
67
80
|
* Media resolves to `ResolvedMedia` (the assemble-time shape a component receives). */
|
|
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;
|
|
81
|
+
export type FieldTsType<D extends FieldDefinition> = D["type"] extends "text" | "textarea" | "url" | "select" | "date" | "datetime" | "publish" | "slug" ? 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;
|
|
69
82
|
/** Infer the `fields` object type from a const `FieldDefinition[]`. Required fields are
|
|
70
83
|
* present; optional ones are `| undefined`. */
|
|
71
84
|
export type InferBlockFields<T extends readonly FieldDefinition[]> = {
|
package/dist/index.js
CHANGED
|
@@ -119,6 +119,7 @@ function tsTypeOf(f) {
|
|
|
119
119
|
case "date":
|
|
120
120
|
case "datetime":
|
|
121
121
|
case "publish":
|
|
122
|
+
case "slug":
|
|
122
123
|
return "string";
|
|
123
124
|
case "richtext":
|
|
124
125
|
return "RichText";
|
|
@@ -275,6 +276,12 @@ export const cmsSchema = {
|
|
|
275
276
|
function isDateString(v) {
|
|
276
277
|
return /^\d{4}-\d{2}-\d{2}$/.test(v) && Number.isFinite(Date.parse(v));
|
|
277
278
|
}
|
|
279
|
+
/** A URL segment: lowercase a-z/0-9 groups joined by single hyphens, capped like the editor
|
|
280
|
+
* control caps it. Normalization lives in the editor, but the editor is not the only writer —
|
|
281
|
+
* a script or another client posting "Hello World/../x" would otherwise land it in a route. */
|
|
282
|
+
function isSlugString(v) {
|
|
283
|
+
return v.length <= 80 && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(v);
|
|
284
|
+
}
|
|
278
285
|
/** A date-time: `YYYY-MM-DDTHH:MM[:SS[.sss]][Z|±HH:MM]` (ISO 8601 / datetime-local). */
|
|
279
286
|
function isDateTimeString(v) {
|
|
280
287
|
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));
|
|
@@ -305,6 +312,13 @@ export function validateFields(schema, values, path = "", opts = {}) {
|
|
|
305
312
|
throw new BadRequest(`field '${at}' must be one of: ${def.options.join(", ")}`);
|
|
306
313
|
}
|
|
307
314
|
break;
|
|
315
|
+
// A slug is text on the wire; only the editor control differs.
|
|
316
|
+
case "slug":
|
|
317
|
+
if (typeof v !== "string")
|
|
318
|
+
throw new BadRequest(`field '${at}' must be a string`);
|
|
319
|
+
if (!isSlugString(v))
|
|
320
|
+
throw new BadRequest(`field '${at}' must be a slug (lowercase letters, digits and single hyphens)`);
|
|
321
|
+
break;
|
|
308
322
|
case "richtext":
|
|
309
323
|
if (typeof v !== "string" && typeof v !== "object")
|
|
310
324
|
throw new BadRequest(`field '${at}' must be rich text`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pramen/cms",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.48",
|
|
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.48",
|
|
45
45
|
"xss": "^1.0.15"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
package/src/index.ts
CHANGED
|
@@ -88,6 +88,17 @@ export interface FieldDefinition {
|
|
|
88
88
|
* affordance would be a UI label over no enforcement at all.
|
|
89
89
|
*/
|
|
90
90
|
| "publish"
|
|
91
|
+
/**
|
|
92
|
+
* A URL segment, derived from another field as you type.
|
|
93
|
+
*
|
|
94
|
+
* Set `from` to the field it follows (usually the title). The editor keeps them in
|
|
95
|
+
* sync only while the slug is untouched — once it has been edited, or on a row that
|
|
96
|
+
* already has one, it stops following, because silently rewriting a slug changes a
|
|
97
|
+
* live URL and breaks every link to it.
|
|
98
|
+
*
|
|
99
|
+
* Stored as text. Uniqueness is the schema's job (`unique(t.text())`).
|
|
100
|
+
*/
|
|
101
|
+
| "slug"
|
|
91
102
|
| "media"
|
|
92
103
|
| "select"
|
|
93
104
|
| "repeater"
|
|
@@ -104,6 +115,8 @@ export interface FieldDefinition {
|
|
|
104
115
|
/** select only — fetch options at edit time from a query handler of this name (returns
|
|
105
116
|
* `{ value, label }[]`), e.g. a live list of campaigns. Takes precedence over `options`. */
|
|
106
117
|
optionsFrom?: string;
|
|
118
|
+
/** slug only — the sibling field this one is derived from (e.g. `"title"`). */
|
|
119
|
+
from?: string;
|
|
107
120
|
}
|
|
108
121
|
|
|
109
122
|
/** A named region on a content type; `allowedTypes` (block-type slugs) restricts what
|
|
@@ -134,7 +147,7 @@ export type RichText = string | { type: string; content?: unknown[] };
|
|
|
134
147
|
|
|
135
148
|
/** Map one FieldDefinition (as a const literal) to the TS type of its RENDERED value.
|
|
136
149
|
* Media resolves to `ResolvedMedia` (the assemble-time shape a component receives). */
|
|
137
|
-
export type FieldTsType<D extends FieldDefinition> = D["type"] extends "text" | "textarea" | "url" | "select" | "date" | "datetime" | "publish"
|
|
150
|
+
export type FieldTsType<D extends FieldDefinition> = D["type"] extends "text" | "textarea" | "url" | "select" | "date" | "datetime" | "publish" | "slug"
|
|
138
151
|
? string
|
|
139
152
|
: D["type"] extends "richtext"
|
|
140
153
|
? RichText
|
|
@@ -310,6 +323,7 @@ function tsTypeOf(f: FieldDefinition): string {
|
|
|
310
323
|
case "date":
|
|
311
324
|
case "datetime":
|
|
312
325
|
case "publish":
|
|
326
|
+
case "slug":
|
|
313
327
|
return "string";
|
|
314
328
|
case "richtext":
|
|
315
329
|
return "RichText";
|
|
@@ -501,6 +515,12 @@ export interface ValidateOpts {
|
|
|
501
515
|
function isDateString(v: string): boolean {
|
|
502
516
|
return /^\d{4}-\d{2}-\d{2}$/.test(v) && Number.isFinite(Date.parse(v));
|
|
503
517
|
}
|
|
518
|
+
/** A URL segment: lowercase a-z/0-9 groups joined by single hyphens, capped like the editor
|
|
519
|
+
* control caps it. Normalization lives in the editor, but the editor is not the only writer —
|
|
520
|
+
* a script or another client posting "Hello World/../x" would otherwise land it in a route. */
|
|
521
|
+
function isSlugString(v: string): boolean {
|
|
522
|
+
return v.length <= 80 && /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(v);
|
|
523
|
+
}
|
|
504
524
|
/** A date-time: `YYYY-MM-DDTHH:MM[:SS[.sss]][Z|±HH:MM]` (ISO 8601 / datetime-local). */
|
|
505
525
|
function isDateTimeString(v: string): boolean {
|
|
506
526
|
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));
|
|
@@ -529,6 +549,11 @@ export function validateFields(schema: FieldDefinition[] | undefined | null, val
|
|
|
529
549
|
throw new BadRequest(`field '${at}' must be one of: ${def.options.join(", ")}`);
|
|
530
550
|
}
|
|
531
551
|
break;
|
|
552
|
+
// A slug is text on the wire; only the editor control differs.
|
|
553
|
+
case "slug":
|
|
554
|
+
if (typeof v !== "string") throw new BadRequest(`field '${at}' must be a string`);
|
|
555
|
+
if (!isSlugString(v)) throw new BadRequest(`field '${at}' must be a slug (lowercase letters, digits and single hyphens)`);
|
|
556
|
+
break;
|
|
532
557
|
case "richtext":
|
|
533
558
|
if (typeof v !== "string" && typeof v !== "object") throw new BadRequest(`field '${at}' must be rich text`);
|
|
534
559
|
break;
|