summaryrefslogtreecommitdiffstats
path: root/src/deps
diff options
context:
space:
mode:
authorVolpeon <git@volpeon.ink>2026-07-26 11:57:13 +0200
committerVolpeon <git@volpeon.ink>2026-07-26 11:57:13 +0200
commit5c899b4a5c2c07398151960b2b86d238b49d5bf4 (patch)
treeccc743f27ba234a25396cb24b6124c139d13b027 /src/deps
parentUse Corvos 0.1.4 (diff)
downloadcorvos-website-master.tar.gz
corvos-website-master.tar.bz2
corvos-website-master.zip
Better Strapi integrationHEADmaster
Diffstat (limited to 'src/deps')
-rw-r--r--src/deps/strapi.ts110
-rw-r--r--src/deps/strapi.tsx416
-rw-r--r--src/deps/strapi/blocks.tsx125
-rw-r--r--src/deps/strapi/loader.ts116
-rw-r--r--src/deps/strapi/schema.ts143
5 files changed, 494 insertions, 416 deletions
diff --git a/src/deps/strapi.ts b/src/deps/strapi.ts
new file mode 100644
index 0000000..32e49b0
--- /dev/null
+++ b/src/deps/strapi.ts
@@ -0,0 +1,110 @@
1import type { Pipeline, PipelineStageError } from "corvos";
2import type { StandardSchemaV1, StandardTypedV1 } from "corvos/deps/standard_schema.ts";
3
4import { Page } from "corvos";
5import { replaceExtension } from "corvos/core/utils/path.ts";
6
7import type { Node } from "./strapi/schema.ts";
8
9import { blocksToTSX } from "./strapi/blocks.tsx";
10import { StrapiLoader } from "./strapi/loader.ts";
11
12export type { Node } from "./strapi/schema.ts";
13export { nodeSchema, fileAttributeSchema, imageAttributeSchema } from "./strapi/schema.ts";
14export { Blocks } from "./strapi/blocks.tsx";
15
16export interface StrapiContent<Type extends "single" | "collection" = "single" | "collection"> {
17 type: Type;
18 schema: StandardSchemaV1;
19 query?: Record<string, string>;
20}
21
22type ContentSingle<T extends Record<string, StrapiContent>> = {
23 [Key in keyof T]: T[Key]["type"] extends "single" ? Key : never;
24}[keyof T];
25
26type ContentCollection<T extends Record<string, StrapiContent>> = {
27 [Key in keyof T]: T[Key]["type"] extends "collection" ? Key : never;
28}[keyof T];
29
30export interface Options<Content extends Record<string, StrapiContent>> {
31 url: string;
32 token: string;
33 content: Content;
34}
35
36export function strapi<Content extends Record<string, StrapiContent>>(options: Options<Content>) {
37 const loader = new StrapiLoader(options.url, options.token);
38
39 function load<const Key extends ContentSingle<Content> & string>(
40 key: Key,
41 query?: Record<string, string>,
42 ): Promise<
43 {
44 documentId: string;
45 publishedAt: Date | null;
46 updatedAt: Date;
47 } & StandardTypedV1.InferOutput<Content[Key]["schema"]>
48 >;
49 function load<const Key extends ContentCollection<Content> & string>(
50 key: Key,
51 query?: Record<string, string>,
52 ): AsyncGenerator<
53 | PipelineStageError
54 | ({
55 documentId: string;
56 publishedAt: Date | null;
57 updatedAt: Date;
58 } & StandardTypedV1.InferOutput<Content[Key]["schema"]>),
59 void,
60 unknown
61 >;
62 function load<const Key extends Content & string>(
63 key: Key,
64 query?: Record<string, string>,
65 ):
66 | Promise<
67 {
68 documentId: string;
69 publishedAt: Date | null;
70 updatedAt: Date;
71 } & StandardTypedV1.InferOutput<Content[Key]["schema"]>
72 >
73 | AsyncGenerator<
74 | PipelineStageError
75 | ({
76 documentId: string;
77 publishedAt: Date | null;
78 updatedAt: Date;
79 } & StandardTypedV1.InferOutput<Content[Key]["schema"]>),
80 void,
81 unknown
82 > {
83 if (options.content[key]!.type === "single") {
84 return loader.loadSingle(key, options.content[key]!.schema, {
85 ...options.content[key]!.query,
86 ...query,
87 });
88 } else {
89 return loader.loadCollection(key, options.content[key]!.schema, {
90 ...options.content[key]!.query,
91 ...query,
92 });
93 }
94 }
95
96 return <Loaders extends string, Out extends { ".strapi": [Node[], {}] }, Helpers extends {}>(
97 pipeline: Pipeline<Loaders, Out, Helpers>,
98 ) =>
99 pipeline
100 .process("strapi", ".strapi", function* (page) {
101 yield new Page(
102 ".jsxelement",
103 blocksToTSX(page.content),
104 page.data as Out[".strapi"][1],
105 replaceExtension(page.url, ".jsx"),
106 page,
107 );
108 })
109 .helper("strapi", load);
110}
diff --git a/src/deps/strapi.tsx b/src/deps/strapi.tsx
deleted file mode 100644
index 127dffc..0000000
--- a/src/deps/strapi.tsx
+++ /dev/null
@@ -1,416 +0,0 @@
1import type { Pipeline } from "corvos";
2import type { StandardSchemaV1 } from "corvos/deps/standard_schema.ts";
3
4import { type } from "arktype";
5import { Page, PipelineStageError } from "corvos";
6import { Severity } from "corvos/core/error.ts";
7import { replaceExtension } from "corvos/core/utils/path.ts";
8
9import { ContentImage } from "$/ui/objects/ContentImage.tsx";
10
11export enum HeadingBlockLevel {
12 L1 = 1,
13 L2 = 2,
14 L3 = 3,
15 L4 = 4,
16 L5 = 5,
17 L6 = 6,
18}
19
20export const fileAttributeSchema = type({
21 "alternativeText?": "string | null",
22 "caption?": "string | null",
23 "createdAt": "string",
24 "documentId": "string",
25 "ext": "string",
26 "formats?": "Record<string, unknown> | null",
27 "hash": "string",
28 "height?": "number | null",
29 "mime": "string",
30 "name": "string",
31 "previewUrl?": "string | null",
32 "provider": "string",
33 "provider_metadata?": "unknown",
34 "size": "number",
35 "updatedAt": "string",
36 "url": "string",
37 "width?": "number | null",
38});
39
40export type FileAttribute = typeof fileAttributeSchema.infer;
41
42export const imageAttributeSchema = fileAttributeSchema.narrow(
43 (o): o is Omit<typeof o, "width" | "height"> & { width: number; height: number } =>
44 !!o.width && !!o.height,
45);
46
47export type ImageAttribute = typeof imageAttributeSchema.infer;
48
49const blocksAttributeScope = type
50 .scope({
51 codeBlockNode: {
52 "children": "defaultInlineNode[]",
53 "language?": "string",
54 "type": "'code'",
55 },
56
57 defaultInlineNode: "textInlineNode | linkInlineNode",
58
59 headingBlockNode: {
60 children: "defaultInlineNode[]",
61 level: "1 | 2 | 3 | 4 | 5 | 6",
62 type: "'heading'",
63 },
64
65 imageBlockNode: {
66 children: type({
67 text: "string",
68 type: "'text'",
69 }).array(),
70 image: fileAttributeSchema.omit("documentId"),
71 type: "'image'",
72 },
73
74 linkInlineNode: {
75 children: "textInlineNode[]",
76 type: "'link'",
77 url: "string",
78 },
79
80 listBlockNode: {
81 children: "(listItemInlineNode | listBlockNode)[]",
82 format: "'ordered' | 'unordered'",
83 type: "'list'",
84 },
85
86 listItemInlineNode: {
87 children: "defaultInlineNode[]",
88 type: "'list-item'",
89 },
90
91 node: "rootNode | nonTextInlineNode",
92
93 nonTextInlineNode: "linkInlineNode | listItemInlineNode",
94
95 paragraphBlockNode: {
96 children: "defaultInlineNode[]",
97 type: "'paragraph'",
98 },
99
100 quoteBlockNode: {
101 children: "defaultInlineNode[]",
102 type: "'quote'",
103 },
104
105 rootNode:
106 "paragraphBlockNode | quoteBlockNode | codeBlockNode | headingBlockNode | listBlockNode | imageBlockNode",
107
108 textInlineNode: {
109 "bold?": "boolean",
110 "code?": "boolean",
111 "italic?": "boolean",
112 "strikethrough?": "boolean",
113 "text": "string",
114 "type": "'text'",
115 "underline?": "boolean",
116 },
117 })
118 .export();
119
120export const defaultInlineNodeSchema = blocksAttributeScope.defaultInlineNode;
121
122export type DefaultInlineNode = typeof defaultInlineNodeSchema.infer;
123
124export const nodeSchema = blocksAttributeScope.node;
125
126export type Node = typeof nodeSchema.infer;
127
128export const strapiItemSchema = type({
129 "[string]": "unknown",
130 "documentId": "string",
131 "publishedAt": "string.date.parse | null",
132 "updatedAt": "string.date.parse",
133});
134
135export type StrapiItem = typeof strapiItemSchema.infer;
136
137export function walkBlocks(
138 content: (Node | DefaultInlineNode)[],
139 fn: (_: Node | DefaultInlineNode) => unknown,
140): void {
141 for (const node of content) {
142 fn(node);
143
144 switch (node.type) {
145 case "heading":
146 case "code":
147 case "quote":
148 case "paragraph":
149 case "list":
150 case "list-item":
151 case "link": {
152 return walkBlocks(node.children, fn);
153 }
154 }
155 }
156}
157
158function blocksNodeToTSX(node: Node | DefaultInlineNode) {
159 switch (true) {
160 case node.type === "heading": {
161 switch (node.level) {
162 case 1:
163 return <h1>{blocksToTSX(node.children)}</h1>;
164 case 2:
165 return <h2>{blocksToTSX(node.children)}</h2>;
166 case 3:
167 return <h3>{blocksToTSX(node.children)}</h3>;
168 case 4:
169 return <h4>{blocksToTSX(node.children)}</h4>;
170 case 5:
171 return <h5>{blocksToTSX(node.children)}</h5>;
172 default:
173 return <h6>{blocksToTSX(node.children)}</h6>;
174 }
175 }
176
177 case node.type === "image" && !!node.image.caption: {
178 return (
179 <figure>
180 <ContentImage src={node.image.url} alt={node.image.alternativeText ?? undefined} />
181 <figcaption>{node.image.caption}</figcaption>
182 </figure>
183 );
184 }
185
186 case node.type === "image": {
187 return <ContentImage src={node.image.url} alt={node.image.alternativeText ?? undefined} />;
188 }
189
190 case node.type === "code": {
191 return (
192 <pre>
193 <code>{blocksToTSX(node.children)}</code>
194 </pre>
195 );
196 }
197
198 case node.type === "quote": {
199 return <blockquote>{blocksToTSX(node.children)}</blockquote>;
200 }
201
202 case node.type === "paragraph" &&
203 node.children.length === 1 &&
204 node.children[0]?.type === "text" &&
205 node.children[0].text === "---": {
206 return <hr />;
207 }
208
209 case node.type === "paragraph": {
210 return <p>{blocksToTSX(node.children)}</p>;
211 }
212
213 case node.type === "list" && node.format === "ordered": {
214 return <ol>${blocksToTSX(node.children)}</ol>;
215 }
216
217 case node.type === "list": {
218 return <ul>${blocksToTSX(node.children)}</ul>;
219 }
220
221 case node.type === "list-item": {
222 return <li>${blocksToTSX(node.children)}</li>;
223 }
224
225 case node.type === "link": {
226 return <a href={node.url}>{blocksToTSX(node.children)}</a>;
227 }
228
229 case node.type === "text": {
230 return [
231 node.bold ? "strong" : undefined,
232 node.code ? "code" : undefined,
233 node.italic ? "em" : undefined,
234 node.strikethrough ? "del" : undefined,
235 node.underline ? "u" : undefined,
236 ].reduce((res: JSX.Element, Tag) => (Tag ? <Tag>{res}</Tag> : res), node.text);
237 }
238 }
239}
240
241export function blocksToTSX(
242 content: (Node | DefaultInlineNode)[],
243 filter: (node: Node | DefaultInlineNode) => boolean = () => true,
244): JSX.Element {
245 return content
246 .filter(filter)
247 .map(blocksNodeToTSX)
248 .filter((s) => s !== undefined);
249}
250
251export const strapiCollectionResponseSchema = type({
252 data: strapiItemSchema.array(),
253 meta: {
254 pagination: {
255 page: "number",
256 pageCount: "number",
257 pageSize: "number",
258 total: "number",
259 },
260 },
261});
262
263export const strapiItemResponseSchema = type({
264 data: strapiItemSchema,
265});
266
267async function* loadPaginated(url: URL | string, init?: RequestInit) {
268 let pageSize: number | undefined;
269 let pages = 1;
270
271 for (let i = 0; i < pages; ++i) {
272 const pageUrl = new URL(url);
273
274 if (pageSize) {
275 pageUrl.searchParams.set("pagination[page]", `${i + 1}`);
276 pageUrl.searchParams.set("pagination[pageSize]", `${pageSize}`);
277 }
278
279 const res = await fetch(pageUrl, init);
280 const json = await res.json();
281
282 const dec = strapiCollectionResponseSchema(json);
283
284 if (dec instanceof type.errors) {
285 throw new Error(dec.summary);
286 }
287
288 pages = dec.meta.pagination.pageCount;
289 pageSize = dec.meta.pagination.pageSize;
290
291 for (const item of dec.data) {
292 yield item;
293 }
294 }
295}
296
297export class StrapiLoader {
298 init: RequestInit;
299
300 constructor(
301 readonly host: string | URL,
302 token: string,
303 readonly publicHost = host,
304 ) {
305 this.init = {
306 headers: {
307 Authorization: `Bearer ${token}`,
308 },
309 keepalive: true,
310 };
311 }
312
313 async loadSingle<T>(
314 collection: string,
315 schema: StandardSchemaV1<unknown, T>,
316 query: Record<string, string> = {},
317 ) {
318 const url = new URL(`/api/${collection}`, this.host);
319 Object.entries(query).forEach(([key, value]) => {
320 url.searchParams.append(key, value);
321 });
322
323 const res = await fetch(url, this.init);
324 const json = await res.json();
325
326 const dec = strapiItemResponseSchema(json);
327
328 if (dec instanceof type.errors) {
329 throw new Error(`Schema mismatch in ${url}:\n${dec.summary}`);
330 }
331
332 const dec2 = await schema["~standard"].validate(dec.data);
333
334 if (dec2.issues) {
335 throw new Error(`Schema mismatch in ${url}:\n${dec2.issues.join("\n")}`);
336 }
337
338 return {
339 documentId: dec.data.documentId,
340 publishedAt: dec.data.publishedAt,
341 updatedAt: dec.data.updatedAt,
342 ...dec2.value,
343 };
344 }
345
346 async *loadCollection<T>(
347 collection: string,
348 schema: StandardSchemaV1<unknown, T>,
349 query: Record<string, string> = {},
350 ) {
351 const url = new URL(`/api/${collection}`, this.host);
352 Object.entries(query).forEach(([key, value]) => {
353 url.searchParams.append(key, value);
354 });
355
356 for await (const item of loadPaginated(url, this.init)) {
357 const dec = await schema["~standard"].validate(item);
358
359 if (dec.issues) {
360 yield new PipelineStageError(
361 Severity.ERROR,
362 `Schema mismatch in ${url}`,
363 dec.issues.join("\n"),
364 );
365 } else {
366 yield {
367 documentId: item.documentId,
368 publishedAt: item.publishedAt,
369 updatedAt: item.updatedAt,
370 ...dec.value,
371 };
372 }
373 }
374 }
375
376 readonly resolvedFileAttributeSchema = fileAttributeSchema.pipe(({ url, previewUrl, ...o }) => ({
377 ...o,
378 url: new URL(url, this.publicHost).href,
379 ...(previewUrl ? { previewUrl: new URL(previewUrl, this.publicHost).href } : {}),
380 }));
381
382 readonly resolvedImageAttributeSchema = imageAttributeSchema.pipe((o) => ({
383 ...o,
384 previewUrl: o.previewUrl ? new URL(o.previewUrl, this.publicHost).href : undefined,
385 url: new URL(o.url, this.publicHost).href,
386 }));
387}
388
389export function strapi() {
390 return <Loaders extends string, Out extends { ".strapi": [Node[], {}] }, Helpers extends {}>(
391 pipeline: Pipeline<Loaders, Out, Helpers>,
392 ) =>
393 pipeline.process("strapi", ".strapi", function* (page) {
394 yield new Page(
395 ".jsxelement",
396 blocksToTSX(page.content),
397 page.data as Out[".strapi"][1],
398 replaceExtension(page.url, ".jsxelement"),
399 page,
400 );
401 });
402}
403
404export const Blocks = ({
405 content,
406 filter = () => true,
407}: {
408 content: (Node | DefaultInlineNode)[];
409 filter?: (node: Node | DefaultInlineNode) => boolean;
410}) => blocksToTSX(content, filter);
411
412const strapiUrl = Deno.env.get("STRAPI_URL") ?? "";
413const strapiPublicUrl = Deno.env.get("STRAPI_PUBLIC_URL");
414const strapiToken = Deno.env.get("STRAPI_TOKEN") ?? "";
415
416export const strapiLoader = new StrapiLoader(strapiUrl, strapiToken, strapiPublicUrl);
diff --git a/src/deps/strapi/blocks.tsx b/src/deps/strapi/blocks.tsx
new file mode 100644
index 0000000..bb60f71
--- /dev/null
+++ b/src/deps/strapi/blocks.tsx
@@ -0,0 +1,125 @@
1import { ContentImage } from "$/ui/objects/ContentImage.tsx";
2
3import { type Node, type DefaultInlineNode } from "./schema.ts";
4
5export function walkBlocks(
6 content: (Node | DefaultInlineNode)[],
7 fn: (_: Node | DefaultInlineNode) => unknown,
8): void {
9 for (const node of content) {
10 fn(node);
11
12 switch (node.type) {
13 case "heading":
14 case "code":
15 case "quote":
16 case "paragraph":
17 case "list":
18 case "list-item":
19 case "link": {
20 return walkBlocks(node.children, fn);
21 }
22 }
23 }
24}
25
26function blocksNodeToTSX(node: Node | DefaultInlineNode) {
27 switch (true) {
28 case node.type === "heading": {
29 switch (node.level) {
30 case 1:
31 return <h1>{blocksToTSX(node.children)}</h1>;
32 case 2:
33 return <h2>{blocksToTSX(node.children)}</h2>;
34 case 3:
35 return <h3>{blocksToTSX(node.children)}</h3>;
36 case 4:
37 return <h4>{blocksToTSX(node.children)}</h4>;
38 case 5:
39 return <h5>{blocksToTSX(node.children)}</h5>;
40 default:
41 return <h6>{blocksToTSX(node.children)}</h6>;
42 }
43 }
44
45 case node.type === "image" && !!node.image.caption: {
46 return (
47 <figure>
48 <ContentImage src={node.image.url} alt={node.image.alternativeText ?? undefined} />
49 <figcaption>{node.image.caption}</figcaption>
50 </figure>
51 );
52 }
53
54 case node.type === "image": {
55 return <ContentImage src={node.image.url} alt={node.image.alternativeText ?? undefined} />;
56 }
57
58 case node.type === "code": {
59 return (
60 <pre>
61 <code>{blocksToTSX(node.children)}</code>
62 </pre>
63 );
64 }
65
66 case node.type === "quote": {
67 return <blockquote>{blocksToTSX(node.children)}</blockquote>;
68 }
69
70 case node.type === "paragraph" &&
71 node.children.length === 1 &&
72 node.children[0]?.type === "text" &&
73 node.children[0].text === "---": {
74 return <hr />;
75 }
76
77 case node.type === "paragraph": {
78 return <p>{blocksToTSX(node.children)}</p>;
79 }
80
81 case node.type === "list" && node.format === "ordered": {
82 return <ol>${blocksToTSX(node.children)}</ol>;
83 }
84
85 case node.type === "list": {
86 return <ul>${blocksToTSX(node.children)}</ul>;
87 }
88
89 case node.type === "list-item": {
90 return <li>${blocksToTSX(node.children)}</li>;
91 }
92
93 case node.type === "link": {
94 return <a href={node.url}>{blocksToTSX(node.children)}</a>;
95 }
96
97 case node.type === "text": {
98 return [
99 node.bold ? "strong" : undefined,
100 node.code ? "code" : undefined,
101 node.italic ? "em" : undefined,
102 node.strikethrough ? "del" : undefined,
103 node.underline ? "u" : undefined,
104 ].reduce((res: JSX.Element, Tag) => (Tag ? <Tag>{res}</Tag> : res), node.text);
105 }
106 }
107}
108
109export function blocksToTSX(
110 content: (Node | DefaultInlineNode)[],
111 filter: (node: Node | DefaultInlineNode) => boolean = () => true,
112): JSX.Element {
113 return content
114 .filter(filter)
115 .map(blocksNodeToTSX)
116 .filter((s) => s !== undefined);
117}
118
119export const Blocks = ({
120 content,
121 filter = () => true,
122}: {
123 content: (Node | DefaultInlineNode)[];
124 filter?: (node: Node | DefaultInlineNode) => boolean;
125}) => blocksToTSX(content, filter);
diff --git a/src/deps/strapi/loader.ts b/src/deps/strapi/loader.ts
new file mode 100644
index 0000000..c3b8e85
--- /dev/null
+++ b/src/deps/strapi/loader.ts
@@ -0,0 +1,116 @@
1import type { StandardSchemaV1 } from "corvos/deps/standard_schema.ts";
2
3import { type } from "arktype";
4import { PipelineStageError } from "corvos";
5import { Severity } from "corvos/core/error.ts";
6
7import { strapiCollectionResponseSchema, strapiItemResponseSchema } from "./schema.ts";
8
9async function* loadPaginated(url: URL | string, init?: RequestInit) {
10 let pageSize: number | undefined;
11 let pages = 1;
12
13 for (let i = 0; i < pages; ++i) {
14 const pageUrl = new URL(url);
15
16 if (pageSize) {
17 pageUrl.searchParams.set("pagination[page]", `${i + 1}`);
18 pageUrl.searchParams.set("pagination[pageSize]", `${pageSize}`);
19 }
20
21 const res = await fetch(pageUrl, init);
22 const json = await res.json();
23
24 const dec = strapiCollectionResponseSchema(json);
25
26 if (dec instanceof type.errors) {
27 throw new Error(dec.summary);
28 }
29
30 pages = dec.meta.pagination.pageCount;
31 pageSize = dec.meta.pagination.pageSize;
32
33 for (const item of dec.data) {
34 yield item;
35 }
36 }
37}
38
39export class StrapiLoader {
40 init: RequestInit;
41
42 constructor(
43 readonly host: string | URL,
44 token: string,
45 ) {
46 this.init = {
47 headers: {
48 Authorization: `Bearer ${token}`,
49 },
50 keepalive: true,
51 };
52 }
53
54 async loadSingle<T>(
55 collection: string,
56 schema: StandardSchemaV1<unknown, T>,
57 query: Record<string, string> = {},
58 ) {
59 const url = new URL(`/api/${collection}`, this.host);
60 Object.entries(query).forEach(([key, value]) => {
61 url.searchParams.append(key, value);
62 });
63
64 const res = await fetch(url, this.init);
65 const json = await res.json();
66
67 const dec = strapiItemResponseSchema(json);
68
69 if (dec instanceof type.errors) {
70 throw new Error(`Schema mismatch in ${url}:\n${dec.summary}`);
71 }
72
73 const dec2 = await schema["~standard"].validate(dec.data);
74
75 if (dec2.issues) {
76 throw new Error(`Schema mismatch in ${url}:\n${dec2.issues.join("\n")}`);
77 }
78
79 return {
80 documentId: dec.data.documentId,
81 publishedAt: dec.data.publishedAt,
82 updatedAt: dec.data.updatedAt,
83 ...dec2.value,
84 };
85 }
86
87 async *loadCollection<T>(
88 collection: string,
89 schema: StandardSchemaV1<unknown, T>,
90 query: Record<string, string> = {},
91 ) {
92 const url = new URL(`/api/${collection}`, this.host);
93 Object.entries(query).forEach(([key, value]) => {
94 url.searchParams.append(key, value);
95 });
96
97 for await (const item of loadPaginated(url, this.init)) {
98 const dec = await schema["~standard"].validate(item);
99
100 if (dec.issues) {
101 yield new PipelineStageError(
102 Severity.ERROR,
103 `Schema mismatch in ${url}`,
104 dec.issues.join("\n"),
105 );
106 } else {
107 yield {
108 documentId: item.documentId,
109 publishedAt: item.publishedAt,
110 updatedAt: item.updatedAt,
111 ...dec.value,
112 };
113 }
114 }
115 }
116}
diff --git a/src/deps/strapi/schema.ts b/src/deps/strapi/schema.ts
new file mode 100644
index 0000000..6161680
--- /dev/null
+++ b/src/deps/strapi/schema.ts
@@ -0,0 +1,143 @@
1import { type } from "arktype";
2
3export enum HeadingBlockLevel {
4 L1 = 1,
5 L2 = 2,
6 L3 = 3,
7 L4 = 4,
8 L5 = 5,
9 L6 = 6,
10}
11
12export const fileAttributeSchema = type({
13 "alternativeText?": "string | null",
14 "caption?": "string | null",
15 "createdAt": "string",
16 "documentId": "string",
17 "ext": "string",
18 "formats?": "Record<string, unknown> | null",
19 "hash": "string",
20 "height?": "number | null",
21 "mime": "string",
22 "name": "string",
23 "previewUrl?": "string | null",
24 "provider": "string",
25 "provider_metadata?": "unknown",
26 "size": "number",
27 "updatedAt": "string",
28 "url": "string",
29 "width?": "number | null",
30});
31
32export type FileAttribute = typeof fileAttributeSchema.infer;
33
34export const imageAttributeSchema = fileAttributeSchema.narrow(
35 (o): o is Omit<typeof o, "width" | "height"> & { width: number; height: number } =>
36 !!o.width && !!o.height,
37);
38
39export type ImageAttribute = typeof imageAttributeSchema.infer;
40
41const blocksAttributeScope = type
42 .scope({
43 codeBlockNode: {
44 "children": "defaultInlineNode[]",
45 "language?": "string",
46 "type": "'code'",
47 },
48
49 defaultInlineNode: "textInlineNode | linkInlineNode",
50
51 headingBlockNode: {
52 children: "defaultInlineNode[]",
53 level: "1 | 2 | 3 | 4 | 5 | 6",
54 type: "'heading'",
55 },
56
57 imageBlockNode: {
58 children: type({
59 text: "string",
60 type: "'text'",
61 }).array(),
62 image: fileAttributeSchema.omit("documentId"),
63 type: "'image'",
64 },
65
66 linkInlineNode: {
67 children: "textInlineNode[]",
68 type: "'link'",
69 url: "string",
70 },
71
72 listBlockNode: {
73 children: "(listItemInlineNode | listBlockNode)[]",
74 format: "'ordered' | 'unordered'",
75 type: "'list'",
76 },
77
78 listItemInlineNode: {
79 children: "defaultInlineNode[]",
80 type: "'list-item'",
81 },
82
83 node: "rootNode | nonTextInlineNode",
84
85 nonTextInlineNode: "linkInlineNode | listItemInlineNode",
86
87 paragraphBlockNode: {
88 children: "defaultInlineNode[]",
89 type: "'paragraph'",
90 },
91
92 quoteBlockNode: {
93 children: "defaultInlineNode[]",
94 type: "'quote'",
95 },
96
97 rootNode:
98 "paragraphBlockNode | quoteBlockNode | codeBlockNode | headingBlockNode | listBlockNode | imageBlockNode",
99
100 textInlineNode: {
101 "bold?": "boolean",
102 "code?": "boolean",
103 "italic?": "boolean",
104 "strikethrough?": "boolean",
105 "text": "string",
106 "type": "'text'",
107 "underline?": "boolean",
108 },
109 })
110 .export();
111
112export const defaultInlineNodeSchema = blocksAttributeScope.defaultInlineNode;
113
114export type DefaultInlineNode = typeof defaultInlineNodeSchema.infer;
115
116export const nodeSchema = blocksAttributeScope.node;
117
118export type Node = typeof nodeSchema.infer;
119
120export const strapiItemSchema = type({
121 "[string]": "unknown",
122 "documentId": "string",
123 "publishedAt": "string.date.parse | null",
124 "updatedAt": "string.date.parse",
125});
126
127export type StrapiItem = typeof strapiItemSchema.infer;
128
129export const strapiCollectionResponseSchema = type({
130 data: strapiItemSchema.array(),
131 meta: {
132 pagination: {
133 page: "number",
134 pageCount: "number",
135 pageSize: "number",
136 total: "number",
137 },
138 },
139});
140
141export const strapiItemResponseSchema = type({
142 data: strapiItemSchema,
143});