@camox/api-contract 0.44.0 → 0.45.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
@@ -1,102 +1,70 @@
1
- // src/icons.ts
2
- import { getIconData, iconToSVG } from "@iconify/utils";
3
- var ICON_CATALOG_VERSION = "2.2.532";
4
- var collections = /* @__PURE__ */ new Map();
5
- async function loadIconCollection(prefix) {
6
- if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(prefix)) throw new Error("Invalid icon collection");
7
- const existing = collections.get(prefix);
8
- if (existing) return existing;
9
- const pending = (async () => {
10
- const response = await fetch(
11
- `https://unpkg.com/@iconify/json@${ICON_CATALOG_VERSION}/json/${prefix}.json`,
12
- { signal: AbortSignal.timeout(3e4) }
13
- );
14
- if (!response.ok) throw new Error(`Unable to load icon collection ${prefix}`);
15
- const data = await response.json();
16
- if (data.prefix !== prefix || !data.icons || !data.info?.license)
17
- throw new Error("Invalid icon catalog or missing license");
18
- return data;
19
- })();
20
- collections.set(prefix, pending);
21
- if (collections.size > 8) collections.delete(collections.keys().next().value);
22
- try {
23
- return await pending;
24
- } catch (error) {
25
- collections.delete(prefix);
26
- throw error;
27
- }
1
+ import { n as scopeIconSvgIds, r as validateIconValue, t as ICON_CATALOG_VERSION } from "./icons-Bk4igP2j.js";
2
+ import { PAGE_TEXT_LINK_PREFIX, generateUuidv7, projectIdSchema, uuidv7JsonSchema, uuidv7Schema } from "./ids.js";
3
+
4
+ //#region src/artifact.ts
5
+ /**
6
+ * Artifact titles, shared by the SDK (frame titles, studio sidebar) and the API (Markdown
7
+ * Representation, write warnings), so every surface names an artifact the same way.
8
+ */
9
+ const UNTITLED_ARTIFACT = "Untitled artifact";
10
+ const entities = {
11
+ amp: "&",
12
+ lt: "<",
13
+ gt: ">",
14
+ quot: "\"",
15
+ apos: "'",
16
+ nbsp: " "
17
+ };
18
+ function decodeEntities(text) {
19
+ return text.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (match, entity) => {
20
+ if (entity[0] !== "#") return entities[entity.toLowerCase()] ?? match;
21
+ const code = entity[1] === "x" || entity[1] === "X" ? Number.parseInt(entity.slice(2), 16) : Number(entity.slice(1));
22
+ return Number.isNaN(code) ? match : String.fromCodePoint(code);
23
+ });
28
24
  }
29
- function iconCollectionIds(data) {
30
- return [...Object.keys(data.icons), ...Object.keys(data.aliases ?? {})].filter((name) => getIconData(data, name) !== null).sort().map((name) => `${data.prefix}:${name}`);
25
+ /** The non-empty `<title>` of an artifact source, if it has one. */
26
+ function artifactSourceTitle(source) {
27
+ const match = /<title\b[^>]*>([\s\S]*?)<\/title\s*>/i.exec(source);
28
+ return (match ? decodeEntities(match[1]).replace(/\s+/g, " ").trim() : "") || void 0;
31
29
  }
32
- function resolveIconSvg(data, id) {
33
- const [prefix, name, extra] = id.split(":");
34
- if (prefix !== data.prefix || !name || extra !== void 0) throw new Error("Invalid icon ID");
35
- const icon = getIconData(data, name);
36
- if (!icon) throw new Error(`Unknown icon: ${id}`);
37
- const svg = iconToSVG(icon);
38
- return {
39
- body: svg.body,
40
- attributes: svg.attributes,
41
- attribution: data.info ? {
42
- collection: data.info.name,
43
- author: data.info.author.name,
44
- license: data.info.license.title,
45
- url: data.info.license.url
46
- } : void 0
47
- };
30
+ /** The artifact's name: its source's `<title>`, or "Untitled artifact". */
31
+ function artifactTitle(source) {
32
+ return artifactSourceTitle(source) ?? "Untitled artifact";
48
33
  }
49
- function scopeIconSvgIds(body, prefix) {
50
- const ids = [...body.matchAll(/\sid="([^\s"]+)"/g)].map((match) => match[1]);
51
- if (!ids.length) return body;
52
- const pattern = ids.map((id) => id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
53
- return body.replace(
54
- new RegExp(`([#;"])(${pattern})([")]|\\.[a-z])`, "g"),
55
- (_match, before, id, after) => `${before}${prefix}${id}${after}`
56
- );
34
+
35
+ //#endregion
36
+ //#region src/reference-query.ts
37
+ /**
38
+ * Rules for query-backed reference lists, shared by the SDK (builders) and the API (definition
39
+ * sync), so both accept and reject the same queries.
40
+ */
41
+ /** The most records a query-backed list resolves to, and its default `limit`. */
42
+ const QUERY_LIMIT_CAP = 100;
43
+ const SYSTEM_ORDER_KEYS = ["createdAt", "publishedAt"];
44
+ function isSystemOrderKey(key) {
45
+ return SYSTEM_ORDER_KEYS.includes(key);
57
46
  }
58
- function validateIconValue(value, schema) {
59
- if (schema.fieldType !== "Icon") return;
60
- if (typeof value !== "string" || !Array.isArray(schema.enum) || !schema.enum.includes(value)) {
61
- throw new Error(`Invalid icon ID: ${String(value)}`);
62
- }
47
+ /** Whether a reference list field is query-backed: code defines its records, nothing is stored. */
48
+ function isQueryBacked(field) {
49
+ return !!field && field.query !== void 0;
50
+ }
51
+ /**
52
+ * Why a query is invalid, if it is. `fieldTypeOf` gives the collection field type an ordering
53
+ * key names; keys must name a `String` field or system metadata.
54
+ */
55
+ function referenceQueryProblem(query, fieldTypeOf) {
56
+ if (!query || typeof query !== "object" || Array.isArray(query)) return "query must be an object";
57
+ const { orderBy, limit, ...rest } = query;
58
+ if (Object.keys(rest).length) return "query only supports orderBy and limit";
59
+ if (limit !== void 0 && !(typeof limit === "number" && Number.isInteger(limit) && limit >= 1 && limit <= 100)) return `limit must be an integer from 1 to ${100}`;
60
+ if (orderBy === void 0) return;
61
+ const entries = orderBy && typeof orderBy === "object" && !Array.isArray(orderBy) ? Object.entries(orderBy) : [];
62
+ if (entries.length !== 1) return "orderBy takes a single key";
63
+ const [[key, direction]] = entries;
64
+ if (direction !== "asc" && direction !== "desc") return "order must be \"asc\" or \"desc\"";
65
+ if (isSystemOrderKey(key)) return;
66
+ if (fieldTypeOf(key) !== "String") return `cannot order by "${key}"`;
63
67
  }
64
68
 
65
- // src/comments.ts
66
- import { z } from "zod";
67
- var COMMENT_MESSAGE_MAX_LENGTH = 1e4;
68
- var objectId = z.number().int().positive();
69
- var fieldName = z.string().min(1).max(200);
70
- var commentTargetSchema = z.discriminatedUnion("kind", [
71
- z.strictObject({ kind: z.literal("page") }),
72
- z.strictObject({ kind: z.literal("block"), blockId: objectId }),
73
- z.strictObject({ kind: z.literal("item"), blockId: objectId, itemId: objectId }),
74
- z.strictObject({ kind: z.literal("block-field"), blockId: objectId, fieldName }),
75
- z.strictObject({
76
- kind: z.literal("item-field"),
77
- blockId: objectId,
78
- itemId: objectId,
79
- fieldName
80
- })
81
- ]);
82
- var commentSchema = z.object({
83
- id: z.uuid(),
84
- pageId: objectId,
85
- environmentId: objectId,
86
- message: z.string(),
87
- resolved: z.boolean(),
88
- target: commentTargetSchema.nullable(),
89
- author: z.object({ name: z.string(), image: z.string().nullable() }),
90
- createdAt: z.number()
91
- });
92
- export {
93
- COMMENT_MESSAGE_MAX_LENGTH,
94
- ICON_CATALOG_VERSION,
95
- commentSchema,
96
- commentTargetSchema,
97
- iconCollectionIds,
98
- loadIconCollection,
99
- resolveIconSvg,
100
- scopeIconSvgIds,
101
- validateIconValue
102
- };
69
+ //#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 };
@@ -0,0 +1,28 @@
1
+ import { z } from "zod";
2
+ //#region src/projects.d.ts
3
+ /**
4
+ * Project Slugs Camox keeps for its own hosts and pages, so no project's `<slug>.camox.site`
5
+ * looks like one of them.
6
+ */
7
+ export declare const RESERVED_PROJECT_SLUGS: ReadonlySet<string>;
8
+ /**
9
+ * A Project Slug: one lowercase DNS label, the only depth Universal SSL covers on `camox.site`,
10
+ * made of letters, digits and inner hyphens.
11
+ */
12
+ export declare const projectSlugSchema: z.ZodString;
13
+ /** Why a slug can't be given to a new project. */
14
+ export type UnavailableSlugReason = "invalid" | "reserved" | "taken";
15
+ /** What a member is told when a slug can't be used, by reason. */
16
+ export declare const UNAVAILABLE_SLUG_MESSAGES: Record<UnavailableSlugReason, string>;
17
+ /**
18
+ * Where a project is in its life: `settingUp` from its creation until its first release
19
+ * succeeds, then `live`. `setupFailed` when its setup gave up: Camox is told and fixes it.
20
+ */
21
+ export type ProjectStatus = "settingUp" | "live" | "setupFailed";
22
+ /**
23
+ * The steps a project's setup goes through before it's live, as its page lists them: copying the
24
+ * Template into its repo, then its first release building it and serving it from its Worker.
25
+ */
26
+ export type ProjectSetupStep = "copyTemplate" | "build" | "serve";
27
+ export declare const PROJECT_SETUP_STEPS: readonly ProjectSetupStep[];
28
+ //#endregion
@@ -0,0 +1,50 @@
1
+ import { z } from "zod";
2
+
3
+ //#region src/projects.ts
4
+ /**
5
+ * Project Slugs Camox keeps for its own hosts and pages, so no project's `<slug>.camox.site`
6
+ * looks like one of them.
7
+ */
8
+ const RESERVED_PROJECT_SLUGS = /* @__PURE__ */ new Set([
9
+ "www",
10
+ "api",
11
+ "app",
12
+ "dashboard",
13
+ "studio",
14
+ "sites",
15
+ "site",
16
+ "docs",
17
+ "blog",
18
+ "help",
19
+ "support",
20
+ "status",
21
+ "admin",
22
+ "auth",
23
+ "login",
24
+ "mail",
25
+ "email",
26
+ "cdn",
27
+ "static",
28
+ "assets",
29
+ "files",
30
+ "camox"
31
+ ]);
32
+ /**
33
+ * A Project Slug: one lowercase DNS label, the only depth Universal SSL covers on `camox.site`,
34
+ * made of letters, digits and inner hyphens.
35
+ */
36
+ const projectSlugSchema = /* @__PURE__ */ z.string().min(1, "Enter a slug").max(63, "Use at most 63 characters").regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Use lowercase letters, digits and hyphens, without a hyphen at either end");
37
+ /** What a member is told when a slug can't be used, by reason. */
38
+ const UNAVAILABLE_SLUG_MESSAGES = {
39
+ invalid: "Use lowercase letters, digits and hyphens, without a hyphen at either end.",
40
+ reserved: "This slug is reserved by Camox. Choose another.",
41
+ taken: "This slug is already taken. Choose another."
42
+ };
43
+ const PROJECT_SETUP_STEPS = [
44
+ "copyTemplate",
45
+ "build",
46
+ "serve"
47
+ ];
48
+
49
+ //#endregion
50
+ export { PROJECT_SETUP_STEPS, RESERVED_PROJECT_SLUGS, UNAVAILABLE_SLUG_MESSAGES, projectSlugSchema };
@@ -1,3 +1,5 @@
1
+ import { EndedDraft } from "./drafts.js";
2
+ //#region src/query-keys.d.ts
1
3
  /**
2
4
  * Use TypeScript to enforce 'camox' as the first query key to ensure they're all namespaced.
3
5
  * This is because the Tanstack Query client on the frontend may be shared with the user's routes,
@@ -5,50 +7,64 @@
5
7
  */
6
8
  export type QueryKey = [first: "camox", ...rest: Array<string | number>];
7
9
  /**
8
- * Source axis on read query keys. `'live'` reads the page's live published
9
- * checkpoint snapshot; `'draft'` reads the live editor rows. The two render
10
- * through the same component — only the cache slot differs, so toggling the
11
- * source select is instant once both have been seeded.
12
- *
13
- * Block / page-read query keys include the source as the trailing key segment.
14
- * Omitting it on `invalidateQueries` is a prefix invalidation that hits every
15
- * source — used for navigation events and other source-agnostic invalidations.
16
- * Edit-time invalidations (which only touch draft data) pass `'draft'`
17
- * explicitly so the `'live'` cache stays untouched.
10
+ * The key prefixes the API's realtime invalidations name (ADR 0011). The studio caches each read
11
+ * under one of them, with the scope appended, so an invalidation reaches every read it should;
12
+ * the studio's own keys live with its reads, in the SDK.
18
13
  */
19
- export type ReadSource = "draft" | "live";
20
14
  export declare const queryKeys: {
21
- comments: {
22
- all: ["camox", string];
23
- list: (pageId: number) => ["camox", string, string, number];
24
- };
25
- pages: {
26
- list: ["camox", string, string];
27
- getByPath: (path: string, source?: ReadSource) => ["camox", string, string, string] | ["camox", string, string, string, ReadSource];
28
- getByPathAll: ["camox", string, string];
29
- getById: (id: number) => ["camox", string, string, number];
30
- };
31
- files: {
32
- list: ["camox", string, string];
33
- get: (id: number) => ["camox", string, string, number];
34
- };
35
- blocks: {
36
- get: (id: number, source?: ReadSource) => ["camox", string, string, number] | ["camox", string, string, number, ReadSource];
37
- getUsageCounts: ["camox", string, string];
38
- getPageMarkdown: (pageId: number) => ["camox", string, string, number];
39
- };
40
- repeatableItems: {
41
- get: (id: number) => ["camox", string, string, number];
42
- };
43
- layouts: {
44
- all: ["camox", string];
45
- };
46
- environments: {
47
- all: ["camox", string];
48
- checkCompatibility: ["camox", string, string];
49
- };
15
+ collections: {
16
+ record: (projectUuid: string, collectionId: string, id: string) => ["camox", string, string, string, string, string];
17
+ records: (projectUuid: string, collectionId: string) => ["camox", string, string, string, string];
18
+ };
19
+ pages: {
20
+ list: ["camox", string, string];
21
+ getByPath: (path: string) => ["camox", string, string, string];
22
+ getByPathAll: ["camox", string, string];
23
+ getById: (id: string) => ["camox", string, string, string];
24
+ };
25
+ files: {
26
+ list: ["camox", string, string];
27
+ get: (id: string) => ["camox", string, string, string];
28
+ };
29
+ blocks: {
30
+ get: (id: string) => ["camox", string, string, string];
31
+ getUsageCounts: ["camox", string, string];
32
+ getSyncedContent: (type: string) => ["camox", string, string, string, string];
33
+ getPageMarkdown: (pageId: string) => ["camox", string, string, string];
34
+ };
35
+ layouts: {
36
+ all: ["camox", string];
37
+ };
38
+ drafts: {
39
+ all: ["camox", string];
40
+ list: ["camox", string, string];
41
+ get: ["camox", string, string];
42
+ };
50
43
  };
44
+ /** The last segment of a query key: what the query reads, Live or a draft (ADR 0011). */
45
+ export declare const LIVE_KEY_SCOPE = "live";
46
+ /**
47
+ * A query key read from Live (`draftId` null) or from a draft. The keys above are prefixes naming
48
+ * what a query reads; the cache holds them with the scope appended, so each draft's queries are
49
+ * kept and invalidated apart.
50
+ */
51
+ export declare function inScope(key: QueryKey, draftId: string | null): QueryKey;
52
+ /** The scope a scoped query key ends in: `"live"` or a draft's id. */
53
+ export declare function scopeOfKey(key: readonly unknown[]): unknown;
54
+ /**
55
+ * Refetch the queries under these prefixes. `draftId` names the draft the change came from, so
56
+ * only that draft's queries refetch; a Live change (`null`) refetches every draft's too, since
57
+ * drafts read Live wherever they change nothing.
58
+ */
51
59
  export type InvalidationMessage = {
52
- type: "invalidate";
53
- targets: QueryKey[];
60
+ type: "invalidate";
61
+ draftId: string | null;
62
+ targets: QueryKey[];
54
63
  };
64
+ /** A draft was published or discarded: its viewers go back to Live. */
65
+ export type DraftEndedMessage = {
66
+ type: "draftEnded";
67
+ } & EndedDraft;
68
+ /** Everything the project room broadcasts. */
69
+ export type ProjectRoomMessage = InvalidationMessage | DraftEndedMessage;
70
+ //#endregion
@@ -1,33 +1,119 @@
1
- export const queryKeys = {
2
- comments: {
3
- all: ["camox", "comments"],
4
- list: (pageId) => ["camox", "comments", "list", pageId],
5
- },
6
- pages: {
7
- list: ["camox", "pages", "list"],
8
- getByPath: (path, source) => source
9
- ? ["camox", "pages", "getByPath", path, source]
10
- : ["camox", "pages", "getByPath", path],
11
- getByPathAll: ["camox", "pages", "getByPath"],
12
- getById: (id) => ["camox", "pages", "getById", id],
13
- },
14
- files: {
15
- list: ["camox", "files", "list"],
16
- get: (id) => ["camox", "files", "get", id],
17
- },
18
- blocks: {
19
- get: (id, source) => source ? ["camox", "blocks", "get", id, source] : ["camox", "blocks", "get", id],
20
- getUsageCounts: ["camox", "blocks", "getUsageCounts"],
21
- getPageMarkdown: (pageId) => ["camox", "blocks", "getPageMarkdown", pageId],
22
- },
23
- repeatableItems: {
24
- get: (id) => ["camox", "repeatableItems", "get", id],
25
- },
26
- layouts: {
27
- all: ["camox", "layouts"],
28
- },
29
- environments: {
30
- all: ["camox", "environments"],
31
- checkCompatibility: ["camox", "environments", "checkCompatibility"],
32
- },
1
+ //#region src/query-keys.ts
2
+ /**
3
+ * The key prefixes the API's realtime invalidations name (ADR 0011). The studio caches each read
4
+ * under one of them, with the scope appended, so an invalidation reaches every read it should;
5
+ * the studio's own keys live with its reads, in the SDK.
6
+ */
7
+ const queryKeys = {
8
+ collections: {
9
+ record: (projectUuid, collectionId, id) => [
10
+ "camox",
11
+ "collections",
12
+ "record",
13
+ projectUuid,
14
+ collectionId,
15
+ id
16
+ ],
17
+ records: (projectUuid, collectionId) => [
18
+ "camox",
19
+ "collections",
20
+ "records",
21
+ projectUuid,
22
+ collectionId
23
+ ]
24
+ },
25
+ pages: {
26
+ list: [
27
+ "camox",
28
+ "pages",
29
+ "list"
30
+ ],
31
+ getByPath: (path) => [
32
+ "camox",
33
+ "pages",
34
+ "getByPath",
35
+ path
36
+ ],
37
+ getByPathAll: [
38
+ "camox",
39
+ "pages",
40
+ "getByPath"
41
+ ],
42
+ getById: (id) => [
43
+ "camox",
44
+ "pages",
45
+ "getById",
46
+ id
47
+ ]
48
+ },
49
+ files: {
50
+ list: [
51
+ "camox",
52
+ "files",
53
+ "list"
54
+ ],
55
+ get: (id) => [
56
+ "camox",
57
+ "files",
58
+ "get",
59
+ id
60
+ ]
61
+ },
62
+ blocks: {
63
+ get: (id) => [
64
+ "camox",
65
+ "blocks",
66
+ "get",
67
+ id
68
+ ],
69
+ getUsageCounts: [
70
+ "camox",
71
+ "blocks",
72
+ "getUsageCounts"
73
+ ],
74
+ getSyncedContent: (type) => [
75
+ "camox",
76
+ "blocks",
77
+ "getUsageCounts",
78
+ "synced",
79
+ type
80
+ ],
81
+ getPageMarkdown: (pageId) => [
82
+ "camox",
83
+ "blocks",
84
+ "getPageMarkdown",
85
+ pageId
86
+ ]
87
+ },
88
+ layouts: { all: ["camox", "layouts"] },
89
+ drafts: {
90
+ all: ["camox", "drafts"],
91
+ list: [
92
+ "camox",
93
+ "drafts",
94
+ "list"
95
+ ],
96
+ get: [
97
+ "camox",
98
+ "drafts",
99
+ "get"
100
+ ]
101
+ }
33
102
  };
103
+ /** The last segment of a query key: what the query reads, Live or a draft (ADR 0011). */
104
+ const LIVE_KEY_SCOPE = "live";
105
+ /**
106
+ * A query key read from Live (`draftId` null) or from a draft. The keys above are prefixes naming
107
+ * what a query reads; the cache holds them with the scope appended, so each draft's queries are
108
+ * kept and invalidated apart.
109
+ */
110
+ function inScope(key, draftId) {
111
+ return [...key, draftId ?? "live"];
112
+ }
113
+ /** The scope a scoped query key ends in: `"live"` or a draft's id. */
114
+ function scopeOfKey(key) {
115
+ return key[key.length - 1];
116
+ }
117
+
118
+ //#endregion
119
+ export { LIVE_KEY_SCOPE, inScope, queryKeys, scopeOfKey };