@camox/api-contract 0.46.0 → 0.48.0

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.js CHANGED
@@ -32,6 +32,38 @@ function artifactTitle(source) {
32
32
  return artifactSourceTitle(source) ?? "Untitled artifact";
33
33
  }
34
34
 
35
+ //#endregion
36
+ //#region src/record-templates.ts
37
+ /**
38
+ * A collection's `toTitle` and `toDescription` are record templates: text with `{{field}}` tokens
39
+ * naming the record's String fields, which the SDK writes and the API fills with each record's
40
+ * plain text. Both sides read them the same way through these helpers.
41
+ */
42
+ const TOKEN = /\{\{([^}]*)\}\}/g;
43
+ /** The fields a record template's tokens name, in order, such as `firstName` and `lastName`. */
44
+ function templateFields(template) {
45
+ return [...template.matchAll(TOKEN)].map(([, name]) => name);
46
+ }
47
+ /**
48
+ * The fields a record template names that aren't String fields of the content schema's
49
+ * properties: templates read fields as plain text, so they name no other field.
50
+ */
51
+ function nonStringTemplateFields(template, properties) {
52
+ const isString = (field) => field !== void 0 && "fieldType" in field && field.fieldType === "String";
53
+ return templateFields(template).filter((name) => !isString(properties[name]));
54
+ }
55
+ /**
56
+ * A record's title and description: its collection's templates filled with the plain text
57
+ * `textOf` reads from each field they name.
58
+ */
59
+ function recordText(templates, textOf) {
60
+ const fill = (template) => template.replace(TOKEN, (_token, name) => textOf(name)).trim();
61
+ return {
62
+ title: fill(templates.toTitle),
63
+ description: templates.toDescription && fill(templates.toDescription) || null
64
+ };
65
+ }
66
+
35
67
  //#endregion
36
68
  //#region src/reference-query.ts
37
69
  /**
@@ -67,4 +99,38 @@ function referenceQueryProblem(query, fieldTypeOf) {
67
99
  }
68
100
 
69
101
  //#endregion
70
- export { ICON_CATALOG_VERSION, PAGE_TEXT_LINK_PREFIX, QUERY_LIMIT_CAP, SYSTEM_ORDER_KEYS, UNTITLED_ARTIFACT, artifactSourceTitle, artifactTitle, generateUuidv7, isQueryBacked, isSystemOrderKey, projectIdSchema, referenceQueryProblem, scopeIconSvgIds, uuidv7JsonSchema, uuidv7Schema, validateIconValue };
102
+ //#region src/structured-data.ts
103
+ const RECORD_LINK_PREFIX = "camox:record:";
104
+ /** A link to the node of the record with id `id`. */
105
+ function recordLink(id) {
106
+ return { "@id": `${RECORD_LINK_PREFIX}${id}` };
107
+ }
108
+ /** The id of the record a value links, when it is a record link. */
109
+ function linkedRecordId(value) {
110
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
111
+ if (Object.keys(value).length !== 1) return void 0;
112
+ const id = value["@id"];
113
+ if (typeof id !== "string" || !id.startsWith(RECORD_LINK_PREFIX)) return void 0;
114
+ return id.slice(13);
115
+ }
116
+ /** The Article types, whose name is their `headline`. */
117
+ const ARTICLE_TYPES = [
118
+ "Article",
119
+ "BlogPosting",
120
+ "NewsArticle",
121
+ "TechArticle",
122
+ "ScholarlyArticle",
123
+ "Report",
124
+ "SocialMediaPosting"
125
+ ];
126
+ /** Whether a value is a marker for a page's URL. */
127
+ function isPageUrlMarker(value) {
128
+ return typeof value === "object" && value !== null && value["@camox"] === "pageUrl" && typeof value.path === "string";
129
+ }
130
+ /** Whether a template's value is shown only under a setting. */
131
+ function isSettingCondition(value) {
132
+ return typeof value === "object" && value !== null && value["@camox"] === "if" && typeof value.setting === "string";
133
+ }
134
+
135
+ //#endregion
136
+ export { ARTICLE_TYPES, ICON_CATALOG_VERSION, PAGE_TEXT_LINK_PREFIX, QUERY_LIMIT_CAP, SYSTEM_ORDER_KEYS, UNTITLED_ARTIFACT, artifactSourceTitle, artifactTitle, generateUuidv7, isPageUrlMarker, isQueryBacked, isSettingCondition, isSystemOrderKey, linkedRecordId, nonStringTemplateFields, projectIdSchema, recordLink, recordText, referenceQueryProblem, scopeIconSvgIds, templateFields, uuidv7JsonSchema, uuidv7Schema, validateIconValue };
@@ -25,4 +25,9 @@ export type ProjectStatus = "settingUp" | "live" | "setupFailed";
25
25
  */
26
26
  export type ProjectSetupStep = "copyTemplate" | "build" | "serve";
27
27
  export declare const PROJECT_SETUP_STEPS: readonly ProjectSetupStep[];
28
+ /**
29
+ * A social profile's address: an absolute `https` URL. Shared so the studio checks a profile by
30
+ * the API's own rule before saving it.
31
+ */
32
+ export declare const socialProfileUrlSchema: z.ZodString;
28
33
  //#endregion
package/dist/projects.js CHANGED
@@ -45,6 +45,19 @@ const PROJECT_SETUP_STEPS = [
45
45
  "build",
46
46
  "serve"
47
47
  ];
48
+ /**
49
+ * A social profile's address: an absolute `https` URL. Shared so the studio checks a profile by
50
+ * the API's own rule before saving it.
51
+ */
52
+ const socialProfileUrlSchema = /* @__PURE__ */ z.string().max(2048, "Use a shorter URL").refine(isHttpsUrl, "Enter a URL starting with https://");
53
+ function isHttpsUrl(value) {
54
+ if (!value.startsWith("https://")) return false;
55
+ try {
56
+ return new URL(value).hostname !== "";
57
+ } catch {
58
+ return false;
59
+ }
60
+ }
48
61
 
49
62
  //#endregion
50
- export { PROJECT_SETUP_STEPS, RESERVED_PROJECT_SLUGS, UNAVAILABLE_SLUG_MESSAGES, projectSlugSchema };
63
+ export { PROJECT_SETUP_STEPS, RESERVED_PROJECT_SLUGS, UNAVAILABLE_SLUG_MESSAGES, projectSlugSchema, socialProfileUrlSchema };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camox/api-contract",
3
- "version": "0.46.0",
3
+ "version": "0.48.0",
4
4
  "license": "MIT",
5
5
  "files": [
6
6
  "src",
package/src/index.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  export * from "./artifact.ts";
2
2
  export * from "./icons.ts";
3
3
  export * from "./ids.ts";
4
+ export * from "./record-templates.ts";
4
5
  export * from "./reference-query.ts";
6
+ export * from "./structured-data.ts";
5
7
  export type { Router } from "../../../apps/api/src/router";
6
8
  export type { PublishPreview } from "../../../apps/api/src/domains/drafts/publish";
7
9
  export type { EntityKind } from "../../../apps/api/src/domains/drafts/overlay";
package/src/projects.ts CHANGED
@@ -65,3 +65,21 @@ export type ProjectStatus = "settingUp" | "live" | "setupFailed";
65
65
  export type ProjectSetupStep = "copyTemplate" | "build" | "serve";
66
66
 
67
67
  export const PROJECT_SETUP_STEPS: readonly ProjectSetupStep[] = ["copyTemplate", "build", "serve"];
68
+
69
+ /**
70
+ * A social profile's address: an absolute `https` URL. Shared so the studio checks a profile by
71
+ * the API's own rule before saving it.
72
+ */
73
+ export const socialProfileUrlSchema = /* @__PURE__ */ z
74
+ .string()
75
+ .max(2048, "Use a shorter URL")
76
+ .refine(isHttpsUrl, "Enter a URL starting with https://");
77
+
78
+ function isHttpsUrl(value: string) {
79
+ if (!value.startsWith("https://")) return false;
80
+ try {
81
+ return new URL(value).hostname !== "";
82
+ } catch {
83
+ return false;
84
+ }
85
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * A collection's `toTitle` and `toDescription` are record templates: text with `{{field}}` tokens
3
+ * naming the record's String fields, which the SDK writes and the API fills with each record's
4
+ * plain text. Both sides read them the same way through these helpers.
5
+ */
6
+
7
+ const TOKEN = /\{\{([^}]*)\}\}/g;
8
+
9
+ /** The fields a record template's tokens name, in order, such as `firstName` and `lastName`. */
10
+ export function templateFields(template: string): string[] {
11
+ return [...template.matchAll(TOKEN)].map(([, name]) => name!);
12
+ }
13
+
14
+ /**
15
+ * The fields a record template names that aren't String fields of the content schema's
16
+ * properties: templates read fields as plain text, so they name no other field.
17
+ */
18
+ export function nonStringTemplateFields(
19
+ template: string,
20
+ properties: Readonly<Record<string, object | undefined>>,
21
+ ): string[] {
22
+ const isString = (field: object | undefined) =>
23
+ field !== undefined && "fieldType" in field && field.fieldType === "String";
24
+ return templateFields(template).filter((name) => !isString(properties[name]));
25
+ }
26
+
27
+ /** How a record names itself: its title, and its description, or null when it has none. */
28
+ export type RecordText = { title: string; description: string | null };
29
+
30
+ /**
31
+ * A record's title and description: its collection's templates filled with the plain text
32
+ * `textOf` reads from each field they name.
33
+ */
34
+ export function recordText(
35
+ templates: { toTitle: string; toDescription?: string | null },
36
+ textOf: (field: string) => string,
37
+ ): RecordText {
38
+ const fill = (template: string) =>
39
+ template.replace(TOKEN, (_token, name: string) => textOf(name)).trim();
40
+ return {
41
+ title: fill(templates.toTitle),
42
+ description: (templates.toDescription && fill(templates.toDescription)) || null,
43
+ };
44
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Structured Data the API evaluates from block and repeatable item templates, for the runtime to
3
+ * put in a page's graph. Only the runtime knows the website's address, so where a node needs an
4
+ * absolute URL to a page of the website, the API leaves a marker the runtime resolves.
5
+ */
6
+
7
+ /** A JSON value of a node the API evaluated, which may hold markers. */
8
+ export type StructuredDataValue =
9
+ | string
10
+ | number
11
+ | boolean
12
+ | null
13
+ | StructuredDataValue[]
14
+ | { [key: string]: StructuredDataValue };
15
+
16
+ /** A node the API evaluated from a template, before the runtime resolves its markers. */
17
+ export type EvaluatedNode = { [key: string]: StructuredDataValue };
18
+
19
+ /**
20
+ * A page of the website, by its path in a locale, whose absolute URL the runtime fills in. The
21
+ * path is locale-relative, as pages store it; without `locale`, it's the default locale's.
22
+ */
23
+ export type PageUrlMarker = { "@camox": "pageUrl"; path: string; locale?: string };
24
+
25
+ /**
26
+ * A link to a collection record's node, as templates and record pages' layouts leave it. Only the
27
+ * runtime knows the website's address, which a record's `@id` is built from, so the link names
28
+ * the record by its id, and the runtime gives it the record's `@id` in the page's locale.
29
+ */
30
+ export type RecordLink = { "@id": `camox:record:${string}` };
31
+
32
+ const RECORD_LINK_PREFIX = "camox:record:";
33
+
34
+ /** A link to the node of the record with id `id`. */
35
+ export function recordLink(id: string): RecordLink {
36
+ return { "@id": `${RECORD_LINK_PREFIX}${id}` };
37
+ }
38
+
39
+ /** The id of the record a value links, when it is a record link. */
40
+ export function linkedRecordId(value: unknown): string | undefined {
41
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
42
+ // A link holds nothing else: a node with a record's `@id` is the record's node.
43
+ if (Object.keys(value).length !== 1) return undefined;
44
+ const id = (value as { "@id"?: unknown })["@id"];
45
+ if (typeof id !== "string" || !id.startsWith(RECORD_LINK_PREFIX)) return undefined;
46
+ return id.slice(RECORD_LINK_PREFIX.length);
47
+ }
48
+
49
+ /**
50
+ * The nodes of the records a read's content references, from their collections' templates, and
51
+ * of the records those link in turn. Their `@id`s and links are record links.
52
+ */
53
+ export type RecordStructuredData = {
54
+ /** Each record's node, by record id. */
55
+ nodes: Record<string, EvaluatedNode>;
56
+ /**
57
+ * The records with nodes that the content references itself, in the order found: the page's
58
+ * `mentions`, unless a node places them.
59
+ */
60
+ referenced: string[];
61
+ };
62
+
63
+ /** The Article types, whose name is their `headline`. */
64
+ export const ARTICLE_TYPES = [
65
+ "Article",
66
+ "BlogPosting",
67
+ "NewsArticle",
68
+ "TechArticle",
69
+ "ScholarlyArticle",
70
+ "Report",
71
+ "SocialMediaPosting",
72
+ ] as const;
73
+
74
+ /** Whether a value is a marker for a page's URL. */
75
+ export function isPageUrlMarker(value: unknown): value is PageUrlMarker {
76
+ return (
77
+ typeof value === "object" &&
78
+ value !== null &&
79
+ (value as { "@camox"?: unknown })["@camox"] === "pageUrl" &&
80
+ typeof (value as { path?: unknown }).path === "string"
81
+ );
82
+ }
83
+
84
+ /**
85
+ * A part of a block's or repeatable item's template shown only while one of its settings holds:
86
+ * a Boolean setting is on, or an Enum setting `equals` a value. The API evaluates `show` when it
87
+ * does and leaves the part out otherwise; a list in a list adds its entries.
88
+ */
89
+ export type SettingCondition = {
90
+ "@camox": "if";
91
+ setting: `settings.${string}` | `itemSettings.${string}`;
92
+ equals?: string;
93
+ show: unknown;
94
+ };
95
+
96
+ /** Whether a template's value is shown only under a setting. */
97
+ export function isSettingCondition(value: unknown): value is SettingCondition {
98
+ return (
99
+ typeof value === "object" &&
100
+ value !== null &&
101
+ (value as { "@camox"?: unknown })["@camox"] === "if" &&
102
+ typeof (value as { setting?: unknown }).setting === "string"
103
+ );
104
+ }