@camox/api-contract 0.42.0 → 0.44.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/icons.d.ts +20 -0
- package/dist/icons.js +72 -0
- package/dist/index.d.ts +551 -114
- package/dist/index.js +102 -0
- package/dist/query-keys.d.ts +4 -0
- package/dist/query-keys.js +4 -0
- package/package.json +6 -1
- package/src/comments.ts +33 -0
- package/src/icons.ts +84 -0
- package/src/index.ts +2 -0
- package/src/query-keys.ts +4 -0
package/dist/index.js
CHANGED
|
@@ -0,0 +1,102 @@
|
|
|
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
|
+
}
|
|
28
|
+
}
|
|
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}`);
|
|
31
|
+
}
|
|
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
|
+
};
|
|
48
|
+
}
|
|
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
|
+
);
|
|
57
|
+
}
|
|
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
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
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
|
+
};
|
package/dist/query-keys.d.ts
CHANGED
|
@@ -18,6 +18,10 @@ export type QueryKey = [first: "camox", ...rest: Array<string | number>];
|
|
|
18
18
|
*/
|
|
19
19
|
export type ReadSource = "draft" | "live";
|
|
20
20
|
export declare const queryKeys: {
|
|
21
|
+
comments: {
|
|
22
|
+
all: ["camox", string];
|
|
23
|
+
list: (pageId: number) => ["camox", string, string, number];
|
|
24
|
+
};
|
|
21
25
|
pages: {
|
|
22
26
|
list: ["camox", string, string];
|
|
23
27
|
getByPath: (path: string, source?: ReadSource) => ["camox", string, string, string] | ["camox", string, string, string, ReadSource];
|
package/dist/query-keys.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camox/api-contract",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"files": [
|
|
6
6
|
"src",
|
|
@@ -17,6 +17,11 @@
|
|
|
17
17
|
"default": "./dist/query-keys.js"
|
|
18
18
|
}
|
|
19
19
|
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@iconify/types": "^2.0.0",
|
|
22
|
+
"@iconify/utils": "^3.1.7",
|
|
23
|
+
"zod": "^4.4.3"
|
|
24
|
+
},
|
|
20
25
|
"devDependencies": {
|
|
21
26
|
"dts-bundle-generator": "^9.5.1",
|
|
22
27
|
"esbuild": "^0.25.12"
|
package/src/comments.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const COMMENT_MESSAGE_MAX_LENGTH = 10_000;
|
|
4
|
+
|
|
5
|
+
const objectId = z.number().int().positive();
|
|
6
|
+
const fieldName = z.string().min(1).max(200);
|
|
7
|
+
|
|
8
|
+
export const commentTargetSchema = z.discriminatedUnion("kind", [
|
|
9
|
+
z.strictObject({ kind: z.literal("page") }),
|
|
10
|
+
z.strictObject({ kind: z.literal("block"), blockId: objectId }),
|
|
11
|
+
z.strictObject({ kind: z.literal("item"), blockId: objectId, itemId: objectId }),
|
|
12
|
+
z.strictObject({ kind: z.literal("block-field"), blockId: objectId, fieldName }),
|
|
13
|
+
z.strictObject({
|
|
14
|
+
kind: z.literal("item-field"),
|
|
15
|
+
blockId: objectId,
|
|
16
|
+
itemId: objectId,
|
|
17
|
+
fieldName,
|
|
18
|
+
}),
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
export const commentSchema = z.object({
|
|
22
|
+
id: z.uuid(),
|
|
23
|
+
pageId: objectId,
|
|
24
|
+
environmentId: objectId,
|
|
25
|
+
message: z.string(),
|
|
26
|
+
resolved: z.boolean(),
|
|
27
|
+
target: commentTargetSchema.nullable(),
|
|
28
|
+
author: z.object({ name: z.string(), image: z.string().nullable() }),
|
|
29
|
+
createdAt: z.number(),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export type CommentTarget = z.infer<typeof commentTargetSchema>;
|
|
33
|
+
export type Comment = z.infer<typeof commentSchema>;
|
package/src/icons.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { IconifyJSON } from "@iconify/types";
|
|
2
|
+
import { getIconData, iconToSVG } from "@iconify/utils";
|
|
3
|
+
|
|
4
|
+
// One immutable snapshot for declarations, schemas, picker and SVG resolution.
|
|
5
|
+
export const ICON_CATALOG_VERSION = "2.2.532";
|
|
6
|
+
export type IconSvgData = {
|
|
7
|
+
body: string;
|
|
8
|
+
attributes: Record<string, string>;
|
|
9
|
+
attribution?: { collection: string; author: string; license: string; url?: string };
|
|
10
|
+
};
|
|
11
|
+
const collections = new Map<string, Promise<IconifyJSON>>();
|
|
12
|
+
|
|
13
|
+
export async function loadIconCollection(prefix: string): Promise<IconifyJSON> {
|
|
14
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(prefix)) throw new Error("Invalid icon collection");
|
|
15
|
+
const existing = collections.get(prefix);
|
|
16
|
+
if (existing) return existing;
|
|
17
|
+
const pending = (async () => {
|
|
18
|
+
const response = await fetch(
|
|
19
|
+
`https://unpkg.com/@iconify/json@${ICON_CATALOG_VERSION}/json/${prefix}.json`,
|
|
20
|
+
{ signal: AbortSignal.timeout(30_000) },
|
|
21
|
+
);
|
|
22
|
+
if (!response.ok) throw new Error(`Unable to load icon collection ${prefix}`);
|
|
23
|
+
const data = (await response.json()) as IconifyJSON;
|
|
24
|
+
if (data.prefix !== prefix || !data.icons || !data.info?.license)
|
|
25
|
+
throw new Error("Invalid icon catalog or missing license");
|
|
26
|
+
return data;
|
|
27
|
+
})();
|
|
28
|
+
collections.set(prefix, pending);
|
|
29
|
+
// Bound worker memory; failed fetches must be retryable.
|
|
30
|
+
if (collections.size > 8) collections.delete(collections.keys().next().value!);
|
|
31
|
+
try {
|
|
32
|
+
return await pending;
|
|
33
|
+
} catch (error) {
|
|
34
|
+
collections.delete(prefix);
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function iconCollectionIds(data: IconifyJSON): string[] {
|
|
40
|
+
return [...Object.keys(data.icons), ...Object.keys(data.aliases ?? {})]
|
|
41
|
+
.filter((name) => getIconData(data, name) !== null)
|
|
42
|
+
.sort()
|
|
43
|
+
.map((name) => `${data.prefix}:${name}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function resolveIconSvg(data: IconifyJSON, id: string): IconSvgData {
|
|
47
|
+
const [prefix, name, extra] = id.split(":");
|
|
48
|
+
if (prefix !== data.prefix || !name || extra !== undefined) throw new Error("Invalid icon ID");
|
|
49
|
+
const icon = getIconData(data, name);
|
|
50
|
+
if (!icon) throw new Error(`Unknown icon: ${id}`);
|
|
51
|
+
const svg = iconToSVG(icon);
|
|
52
|
+
return {
|
|
53
|
+
body: svg.body,
|
|
54
|
+
attributes: svg.attributes,
|
|
55
|
+
attribution: data.info
|
|
56
|
+
? {
|
|
57
|
+
collection: data.info.name,
|
|
58
|
+
author: data.info.author.name,
|
|
59
|
+
license: data.info.license.title,
|
|
60
|
+
url: data.info.license.url,
|
|
61
|
+
}
|
|
62
|
+
: undefined,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function scopeIconSvgIds(body: string, prefix: string): string {
|
|
67
|
+
const ids = [...body.matchAll(/\sid="([^\s"]+)"/g)].map((match) => match[1]!);
|
|
68
|
+
if (!ids.length) return body;
|
|
69
|
+
const pattern = ids.map((id) => id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
|
|
70
|
+
return body.replace(
|
|
71
|
+
new RegExp(`([#;"])(${pattern})([")]|\\.[a-z])`, "g"),
|
|
72
|
+
(_match, before: string, id: string, after: string) => `${before}${prefix}${id}${after}`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function validateIconValue(
|
|
77
|
+
value: unknown,
|
|
78
|
+
schema: { fieldType?: string; enum?: unknown },
|
|
79
|
+
): void {
|
|
80
|
+
if (schema.fieldType !== "Icon") return;
|
|
81
|
+
if (typeof value !== "string" || !Array.isArray(schema.enum) || !schema.enum.includes(value)) {
|
|
82
|
+
throw new Error(`Invalid icon ID: ${String(value)}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
package/src/index.ts
CHANGED
package/src/query-keys.ts
CHANGED
|
@@ -21,6 +21,10 @@ type QueryKeyGroup = Record<string, QueryKey | ((...args: any[]) => QueryKey)>;
|
|
|
21
21
|
export type ReadSource = "draft" | "live";
|
|
22
22
|
|
|
23
23
|
export const queryKeys = {
|
|
24
|
+
comments: {
|
|
25
|
+
all: ["camox", "comments"],
|
|
26
|
+
list: (pageId: number) => ["camox", "comments", "list", pageId],
|
|
27
|
+
},
|
|
24
28
|
pages: {
|
|
25
29
|
list: ["camox", "pages", "list"],
|
|
26
30
|
getByPath: (path: string, source?: ReadSource) =>
|