@camox/api-contract 0.47.0 → 0.51.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/drafts.js +1 -0
- package/dist/index.d.ts +75 -12327
- package/dist/index.js +67 -1
- package/dist/locales.d.ts +25 -0
- package/dist/locales.js +29 -0
- package/dist/projects.d.ts +7 -0
- package/dist/projects.js +16 -1
- package/dist/router.d.ts +13208 -0
- package/dist/router.js +0 -0
- package/package.json +9 -1
- package/src/drafts.ts +2 -1
- package/src/index.ts +2 -3
- package/src/locales.ts +34 -0
- package/src/projects.ts +21 -0
- package/src/record-templates.ts +44 -0
- package/src/router.ts +5 -0
- package/src/structured-data.ts +104 -0
package/dist/router.js
ADDED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camox/api-contract",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.51.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"files": [
|
|
6
6
|
"src",
|
|
@@ -39,6 +39,14 @@
|
|
|
39
39
|
"./projects": {
|
|
40
40
|
"types": "./dist/projects.d.ts",
|
|
41
41
|
"default": "./dist/projects.js"
|
|
42
|
+
},
|
|
43
|
+
"./locales": {
|
|
44
|
+
"types": "./dist/locales.d.ts",
|
|
45
|
+
"default": "./dist/locales.js"
|
|
46
|
+
},
|
|
47
|
+
"./router": {
|
|
48
|
+
"types": "./dist/router.d.ts",
|
|
49
|
+
"default": "./dist/router.js"
|
|
42
50
|
}
|
|
43
51
|
},
|
|
44
52
|
"dependencies": {
|
package/src/drafts.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Router } from "./
|
|
1
|
+
import type { Router } from "./router.ts";
|
|
2
2
|
|
|
3
3
|
/** Request header naming the draft a request reads and writes (ADR 0011). Absent means Live. */
|
|
4
4
|
export const DRAFT_HEADER = "x-camox-draft";
|
|
@@ -147,6 +147,7 @@ const FILE_WRITES: Record<keyof Router["files"], boolean> = {
|
|
|
147
147
|
const PAGE_WRITES: Record<keyof Router["pages"], boolean> = {
|
|
148
148
|
getByPath: false,
|
|
149
149
|
getStructure: false,
|
|
150
|
+
getMarkdown: false,
|
|
150
151
|
list: false,
|
|
151
152
|
listBySlug: false,
|
|
152
153
|
get: false,
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
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";
|
|
5
|
-
export
|
|
6
|
-
export type { PublishPreview } from "../../../apps/api/src/domains/drafts/publish";
|
|
7
|
-
export type { EntityKind } from "../../../apps/api/src/domains/drafts/overlay";
|
|
6
|
+
export * from "./structured-data.ts";
|
package/src/locales.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The URL names the locale (ADR 0015): a non-default locale's paths sit under its prefix
|
|
3
|
+
* (`/de/preise`), the default locale's have none. Pure, so the API, the runtime and the browser
|
|
4
|
+
* read a path the same way.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** A locale of a project, as `locales.list` returns it. */
|
|
8
|
+
export type LocaleView = { tag: string; isDefault: boolean };
|
|
9
|
+
|
|
10
|
+
/** A runtime path split into the locale it names and the locale-relative path after its prefix. */
|
|
11
|
+
export type LocalePath = { locale: LocaleView; path: string };
|
|
12
|
+
|
|
13
|
+
/** A locale's URL prefix: its tag in lowercase. The default locale has none. */
|
|
14
|
+
export function localePrefix(locale: LocaleView): string {
|
|
15
|
+
return locale.isDefault ? "" : `/${locale.tag.toLowerCase()}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The locale a runtime path names, and the path after its prefix, without search or hash. A first
|
|
20
|
+
* segment that isn't a non-default locale's prefix leaves the path the default locale's. A
|
|
21
|
+
* locale's home is `/de`; `/de/` names it too.
|
|
22
|
+
*/
|
|
23
|
+
export function parseLocalePath(pathname: string, locales: readonly LocaleView[]): LocalePath {
|
|
24
|
+
const path = pathname.replace(/[?#].*$/s, "") || "/";
|
|
25
|
+
const end = path.indexOf("/", 1);
|
|
26
|
+
const segment = end === -1 ? path.slice(1) : path.slice(1, end);
|
|
27
|
+
const named = locales.find(
|
|
28
|
+
(locale) => !locale.isDefault && localePrefix(locale) === `/${segment}`,
|
|
29
|
+
);
|
|
30
|
+
if (named) return { locale: named, path: end === -1 ? "/" : path.slice(end) };
|
|
31
|
+
const fallback = locales.find((locale) => locale.isDefault);
|
|
32
|
+
if (!fallback) throw new Error("A project's locales include its default locale.");
|
|
33
|
+
return { locale: fallback, path };
|
|
34
|
+
}
|
package/src/projects.ts
CHANGED
|
@@ -42,6 +42,9 @@ export const projectSlugSchema = /* @__PURE__ */ z
|
|
|
42
42
|
"Use lowercase letters, digits and hyphens, without a hyphen at either end",
|
|
43
43
|
);
|
|
44
44
|
|
|
45
|
+
/** The address a project's website is served at: its Project Slug's `camox.site` subdomain. */
|
|
46
|
+
export const projectWebsiteUrl = (slug: string) => `https://${slug}.camox.site`;
|
|
47
|
+
|
|
45
48
|
/** Why a slug can't be given to a new project. */
|
|
46
49
|
export type UnavailableSlugReason = "invalid" | "reserved" | "taken";
|
|
47
50
|
|
|
@@ -65,3 +68,21 @@ export type ProjectStatus = "settingUp" | "live" | "setupFailed";
|
|
|
65
68
|
export type ProjectSetupStep = "copyTemplate" | "build" | "serve";
|
|
66
69
|
|
|
67
70
|
export const PROJECT_SETUP_STEPS: readonly ProjectSetupStep[] = ["copyTemplate", "build", "serve"];
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A social profile's address: an absolute `https` URL. Shared so the studio checks a profile by
|
|
74
|
+
* the API's own rule before saving it.
|
|
75
|
+
*/
|
|
76
|
+
export const socialProfileUrlSchema = /* @__PURE__ */ z
|
|
77
|
+
.string()
|
|
78
|
+
.max(2048, "Use a shorter URL")
|
|
79
|
+
.refine(isHttpsUrl, "Enter a URL starting with https://");
|
|
80
|
+
|
|
81
|
+
function isHttpsUrl(value: string) {
|
|
82
|
+
if (!value.startsWith("https://")) return false;
|
|
83
|
+
try {
|
|
84
|
+
return new URL(value).hostname !== "";
|
|
85
|
+
} catch {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -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
|
+
}
|
package/src/router.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// The API's router and server-side types. Kept off the main entry so a site importing
|
|
2
|
+
// `camox` doesn't load the whole API's types (and packages it can't resolve).
|
|
3
|
+
export type { Router } from "../../../apps/api/src/router";
|
|
4
|
+
export type { PublishPreview } from "../../../apps/api/src/domains/drafts/publish";
|
|
5
|
+
export type { EntityKind } from "../../../apps/api/src/domains/drafts/overlay";
|
|
@@ -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
|
+
}
|