Skip to content

Commit 52eabb5

Browse files
committed
Routes, with minimalist conversion
1 parent c8ae745 commit 52eabb5

7 files changed

Lines changed: 729 additions & 74 deletions

File tree

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import { NextResponse, NextRequest } from "next/server";
2+
import { createClient } from "~/utils/supabase/server";
3+
import { asPostgrestFailure } from "@repo/database/lib/contextFunctions";
4+
import {
5+
defaultOptionsHandler,
6+
createApiResponse,
7+
} from "~/utils/supabase/apiUtils";
8+
import { asJsonLD } from "~/utils/conversion/jsonld";
9+
import { Tables } from "@repo/database/dbTypes";
10+
import { convert, MIMETYPES, type DocType } from "~/utils/conversion/convert";
11+
12+
type Concept = Tables<"Concept">;
13+
type Content = Tables<"Content">;
14+
type Space = Tables<"Space">;
15+
type PlatformAccount = Tables<"PlatformAccount">;
16+
17+
export type SegmentDataType = { params: Promise<Record<string, string>> };
18+
19+
export const GET = async (
20+
request: NextRequest,
21+
segmentData: SegmentDataType,
22+
): Promise<NextResponse> => {
23+
const { space_id, resource_id } = await segmentData.params;
24+
let targetFormat: DocType = (request.nextUrl.searchParams.get("format") ??
25+
"html") as DocType;
26+
if (MIMETYPES[targetFormat] === undefined) {
27+
targetFormat = "html";
28+
}
29+
const targetMimetype = MIMETYPES[targetFormat];
30+
if (!targetMimetype) {
31+
return createApiResponse(
32+
request,
33+
asPostgrestFailure("Unsupported format", "404", 404),
34+
);
35+
}
36+
const includeData =
37+
targetFormat === "html" &&
38+
request.nextUrl.searchParams.get("data") !== "false";
39+
const spaceIdN = Number.parseInt(space_id || "NaN");
40+
if (isNaN(spaceIdN)) {
41+
return createApiResponse(
42+
request,
43+
asPostgrestFailure(`${space_id} is not a number`, "type"),
44+
);
45+
}
46+
const resourceIdN = Number.parseInt(resource_id || "NaN");
47+
if (isNaN(resourceIdN)) {
48+
return createApiResponse(
49+
request,
50+
asPostgrestFailure(`${resource_id} is not a number`, "type"),
51+
);
52+
}
53+
const supabase = await createClient();
54+
const spaceResponse = await supabase
55+
.from("Space")
56+
.select()
57+
.eq("id", spaceIdN)
58+
.maybeSingle();
59+
if (spaceResponse.error) {
60+
return createApiResponse(request, spaceResponse);
61+
}
62+
if (!spaceResponse.data) {
63+
// consideration: We may not see it because we don't have access,
64+
// so it would be worth re-fetching as superuser to see if I should redirect to login.
65+
return createApiResponse(
66+
request,
67+
asPostgrestFailure("Space not found", "401", 401),
68+
);
69+
}
70+
const space: Space = spaceResponse.data;
71+
const conceptResponse = await supabase
72+
.from("Concept")
73+
.select()
74+
.eq("id", resourceIdN)
75+
.maybeSingle();
76+
if (conceptResponse.error) {
77+
return createApiResponse(request, conceptResponse);
78+
}
79+
const concept = conceptResponse.data;
80+
if (!concept) {
81+
return createApiResponse(
82+
request,
83+
asPostgrestFailure("Resource not found", "401", 401),
84+
);
85+
}
86+
const contentResponse = await supabase
87+
.from("Content")
88+
.select()
89+
.eq("source_local_id", concept.source_local_id!);
90+
if (contentResponse.error) {
91+
return createApiResponse(request, conceptResponse);
92+
}
93+
const contents: Content[] = contentResponse.data;
94+
const requestUrlParts = request.url.split("/");
95+
const baseUrl = requestUrlParts
96+
.slice(0, requestUrlParts.length - 1)
97+
.join("/");
98+
const fullContentsArray = contents.filter((c) => c.variant === "full");
99+
const fullContents = fullContentsArray.length
100+
? fullContentsArray[0]
101+
: undefined;
102+
const titleArray = contents.filter((c) => c.variant === "direct");
103+
const title = titleArray.length ? titleArray[0] : undefined;
104+
105+
if (!fullContents) {
106+
return createApiResponse(
107+
request,
108+
asPostgrestFailure("Resource not found", "401", 401),
109+
);
110+
}
111+
112+
const rootUrl = baseUrl.split("/").slice(0, 3).join("/");
113+
// await initRT(rootUrl);
114+
const source: DocType | undefined =
115+
space.platform === "Obsidian"
116+
? "obsidian"
117+
: space.platform === "Roam"
118+
? "roam"
119+
: undefined;
120+
let text =
121+
source && source !== targetFormat
122+
? await convert(fullContents.text, source, targetFormat)
123+
: fullContents.text;
124+
if (includeData) {
125+
const isSchema = concept.is_schema;
126+
let schema: Concept | undefined = undefined;
127+
if (!isSchema && concept.schema_id) {
128+
const schemaResponse = await supabase
129+
.from("Concept")
130+
.select()
131+
.eq("id", concept.schema_id)
132+
.maybeSingle();
133+
if (schemaResponse.error) {
134+
return createApiResponse(request, schemaResponse);
135+
}
136+
if (!schemaResponse.data) {
137+
return createApiResponse(
138+
request,
139+
asPostgrestFailure("Resource schema not found", "401", 401),
140+
);
141+
}
142+
schema = schemaResponse.data;
143+
}
144+
145+
const authorId = concept.author_id ?? (contents ?? [{}])[0]?.author_id;
146+
let author: PlatformAccount | undefined = undefined;
147+
if (authorId) {
148+
const authorResponse = await supabase
149+
.from("PlatformAccount")
150+
.select()
151+
.eq("id", authorId)
152+
.maybeSingle();
153+
if (authorResponse.data) author = authorResponse.data;
154+
}
155+
156+
const jsonLdData = await asJsonLD({
157+
space,
158+
concept,
159+
baseUrl,
160+
title,
161+
schema,
162+
content: undefined,
163+
author,
164+
targetFormat,
165+
wrap: true,
166+
});
167+
text = `<div id="content">\n<script type="application/ld+json">${JSON.stringify(jsonLdData)}</script>\n${text}\n</div>`;
168+
}
169+
170+
return new NextResponse(text, {
171+
headers: { "Content-Type": targetMimetype },
172+
});
173+
};
174+
175+
export const OPTIONS = defaultOptionsHandler;
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import { NextResponse, NextRequest } from "next/server";
2+
import { createClient } from "~/utils/supabase/server";
3+
import { asPostgrestFailure } from "@repo/database/lib/contextFunctions";
4+
import {
5+
defaultOptionsHandler,
6+
createApiResponse,
7+
} from "~/utils/supabase/apiUtils";
8+
import { asJsonLD, wrapJsonLd } from "~/utils/conversion/jsonld";
9+
import { Tables } from "@repo/database/dbTypes";
10+
import { PostgrestMaybeSingleResponse } from "@supabase/supabase-js";
11+
import { MIMETYPES, type DocType } from "~/utils/conversion/convert";
12+
13+
type Concept = Tables<"Concept">;
14+
type Content = Tables<"Content">;
15+
type Space = Tables<"Space">;
16+
type PlatformAccount = Tables<"PlatformAccount">;
17+
18+
export type SegmentDataType = { params: Promise<Record<string, string>> };
19+
20+
export const GET = async (
21+
request: NextRequest,
22+
segmentData: SegmentDataType,
23+
): Promise<NextResponse> => {
24+
const { space_id, resource_id } = await segmentData.params;
25+
let targetFormat = (request.nextUrl.searchParams.get("format") ?? "html") as
26+
| DocType
27+
| "none";
28+
if (targetFormat !== "none" && MIMETYPES[targetFormat] === undefined) {
29+
targetFormat = "html";
30+
}
31+
const withContext = request.nextUrl.searchParams.get("context");
32+
const withSchema = request.nextUrl.searchParams.get("schema");
33+
const spaceIdN = Number.parseInt(space_id || "NaN");
34+
if (isNaN(spaceIdN)) {
35+
return createApiResponse(
36+
request,
37+
asPostgrestFailure(`${space_id} is not a number`, "type"),
38+
);
39+
}
40+
const resourceIdN = Number.parseInt(resource_id || "NaN");
41+
if (isNaN(resourceIdN)) {
42+
return createApiResponse(
43+
request,
44+
asPostgrestFailure(`${resource_id} is not a number`, "type"),
45+
);
46+
}
47+
const supabase = await createClient();
48+
const spaceResponse = await supabase
49+
.from("Space")
50+
.select()
51+
.eq("id", spaceIdN)
52+
.maybeSingle();
53+
if (spaceResponse.error) {
54+
return createApiResponse(request, spaceResponse);
55+
}
56+
if (!spaceResponse.data) {
57+
// consideration: We may not see it because we don't have access,
58+
// so it would be worth re-fetching as superuser to see if I should redirect to login.
59+
return createApiResponse(
60+
request,
61+
asPostgrestFailure("Space not found", "401", 401),
62+
);
63+
}
64+
const space: Space = spaceResponse.data;
65+
const conceptResponse = await supabase
66+
.from("Concept")
67+
.select()
68+
.eq("id", resourceIdN)
69+
.maybeSingle();
70+
if (conceptResponse.error) {
71+
return createApiResponse(request, conceptResponse);
72+
}
73+
const concept = conceptResponse.data;
74+
if (!concept) {
75+
return createApiResponse(
76+
request,
77+
asPostgrestFailure("Resource not found", "401", 401),
78+
);
79+
}
80+
const contentResponse = await supabase
81+
.from("Content")
82+
.select()
83+
.eq("source_local_id", concept.source_local_id!);
84+
if (contentResponse.error) {
85+
return createApiResponse(request, conceptResponse);
86+
}
87+
const contents: Content[] = contentResponse.data;
88+
const fullContentsArray = contents.filter((c) => c.variant === "full");
89+
const fullContents = fullContentsArray.length
90+
? fullContentsArray[0]
91+
: undefined;
92+
const titleArray = contents.filter((c) => c.variant === "direct");
93+
const title = titleArray.length ? titleArray[0] : undefined;
94+
95+
const requestUrlParts = request.url.split("/");
96+
const baseUrl = requestUrlParts
97+
.slice(0, requestUrlParts.length - 1)
98+
.join("/");
99+
const rootUrl = baseUrl.split("/").slice(0, 3).join("/");
100+
const pageUrl = `${rootUrl}/api/content/${baseUrl.split("/")[5]}/${concept.id}#`;
101+
let schemas: Record<number, Concept> = {};
102+
let authors: Record<number, PlatformAccount> = {};
103+
let relations: Concept[] = [];
104+
let schemaIds = new Set<number>();
105+
if (withContext) {
106+
const relationsResult = (await supabase
107+
.from("Concept")
108+
.select("relations:concept_in_relations!inner(*)")
109+
.eq("id", concept.id)
110+
.maybeSingle()) as PostgrestMaybeSingleResponse<{ relations: Concept[] }>;
111+
if (relationsResult.data?.relations?.length) {
112+
relations = relationsResult.data.relations;
113+
if (withSchema)
114+
schemaIds = new Set(
115+
relations.map((c) => c.schema_id).filter((id) => id !== null),
116+
);
117+
}
118+
}
119+
if (concept.schema_id) schemaIds.add(concept.schema_id);
120+
if (schemaIds.size > 0) {
121+
const schemaResponse = await supabase
122+
.from("Concept")
123+
.select()
124+
.in("id", [...schemaIds]);
125+
if (schemaResponse.error) {
126+
return createApiResponse(request, schemaResponse);
127+
}
128+
if (!schemaResponse.data) {
129+
return createApiResponse(
130+
request,
131+
asPostgrestFailure("Resource schema not found", "401", 401),
132+
);
133+
}
134+
schemas = Object.fromEntries(schemaResponse.data.map((s) => [s.id, s]));
135+
}
136+
const authorIds = new Set<number>([
137+
...relations.map((r) => r.author_id).filter((id) => id !== null),
138+
...Object.values(schemas)
139+
.map((s) => s.author_id)
140+
.filter((id) => id !== null),
141+
]);
142+
if (concept.author_id) authorIds.add(concept.author_id);
143+
if (authorIds.size > 0) {
144+
const authorsResponse = await supabase
145+
.from("PlatformAccount")
146+
.select()
147+
.in("id", [...authorIds]);
148+
if (authorsResponse.error) {
149+
return createApiResponse(request, authorsResponse);
150+
}
151+
if (!authorsResponse.data) {
152+
return createApiResponse(
153+
request,
154+
asPostgrestFailure("Resource schema not found", "401", 401),
155+
);
156+
}
157+
authors = Object.fromEntries(authorsResponse.data.map((a) => [a.id, a]));
158+
}
159+
160+
const relationsJLD = withContext
161+
? await Promise.all(
162+
relations.map((c) =>
163+
asJsonLD({
164+
space,
165+
concept: c,
166+
baseUrl,
167+
schema: c.schema_id ? schemas[c.schema_id] : undefined,
168+
author: c.author_id ? authors[c.author_id] : undefined,
169+
}),
170+
),
171+
)
172+
: [];
173+
const schemasJLD = withSchema
174+
? await Promise.all(
175+
Object.values(schemas).map((c) =>
176+
asJsonLD({
177+
space,
178+
concept: c,
179+
baseUrl,
180+
author: c.author_id ? authors[c.author_id] : undefined,
181+
}),
182+
),
183+
)
184+
: [];
185+
186+
const baseJLDData = await asJsonLD({
187+
space,
188+
concept,
189+
baseUrl,
190+
title,
191+
schema: concept.schema_id ? schemas[concept.schema_id] : undefined,
192+
content: targetFormat === "none" ? undefined : fullContents,
193+
author: concept.author_id ? authors[concept.author_id] : undefined,
194+
targetFormat: targetFormat === "none" ? undefined : targetFormat,
195+
});
196+
197+
const jsonLdData =
198+
relationsJLD.length > 0 || schemasJLD.length > 0
199+
? [baseJLDData, ...relationsJLD, ...schemasJLD]
200+
: baseJLDData;
201+
return NextResponse.json(wrapJsonLd(jsonLdData, baseUrl, pageUrl), {
202+
status: 200,
203+
headers: { "Content-Type": "application/ld+json" },
204+
});
205+
};
206+
207+
export const OPTIONS = defaultOptionsHandler;

0 commit comments

Comments
 (0)