@camox/api-contract 0.47.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.d.ts +541 -49
- package/dist/index.js +67 -1
- package/dist/projects.d.ts +5 -0
- package/dist/projects.js +14 -1
- package/package.json +1 -1
- package/src/index.ts +2 -0
- package/src/projects.ts +18 -0
- package/src/record-templates.ts +44 -0
- package/src/structured-data.ts +104 -0
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
|
-
|
|
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 };
|
package/dist/projects.d.ts
CHANGED
|
@@ -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
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
|
+
}
|