UNPKG

alinea

Version:
238 lines (236 loc) 8.65 kB
import { array, object, string } from "../chunks/chunk-WD7H5L2L.js"; import { pLimit } from "../chunks/chunk-C53YJRET.js"; import { PLazy } from "../chunks/chunk-IKINPSS5.js"; import "../chunks/chunk-NZLE2WMY.js"; // src/backend/Handler.ts import { JWTPreviews } from "alinea/backend/util/JWTPreviews"; import { CloudRemote } from "alinea/cloud/CloudRemote"; import { HttpError } from "alinea/core/HttpError"; import { getScope } from "alinea/core/Scope"; import { ShaMismatchError } from "alinea/core/source/ShaMismatchError"; import { base64 } from "alinea/core/util/Encoding"; import { InvalidCredentialsError, MissingCredentialsError } from "./Auth.js"; import { HandleAction } from "./HandleAction.js"; import { createPreviewParser } from "./resolver/ParsePreview.js"; var limit = pLimit(1); var PrepareBody = object({ filename: string }); var PreviewBody = object({ url: string }); function createHandler({ cms, remote = (context) => new CloudRemote(context, cms.config), db, ...hooks }) { let lastSync = 0; const previewParser = PLazy.from(async () => { const local = await db; return createPreviewParser(local); }); return async function handle(request, context) { const dev = process.env.ALINEA_DEV_SERVER; const local = await db; const simulateLatency = process.env.ALINEA_LATENCY; if (simulateLatency) await new Promise((resolve) => setTimeout(resolve, 2e3)); async function periodicSync(cnx, syncInterval = 60) { if (dev) return; return limit(async () => { if (syncInterval === Number.POSITIVE_INFINITY) return; const now = Date.now(); if (now - lastSync < syncInterval * 1e3) return; lastSync = now; await local.syncWith(cnx); }).catch((error) => { console.error(error); }); } try { const previews = new JWTPreviews(context.apiKey); const url = new URL(request.url); const params = url.searchParams; const auth = params.get("auth"); let cnx = remote(context); let userCtx; if (auth) return cnx.authenticate(request); const action = params.get("action"); const expectJson = () => { const acceptsJson = request.headers.get("accept")?.includes("application/json"); if (!acceptsJson) throw new Response("Expected JSON", { status: 400 }); }; if (action === HandleAction.Upload && request.method === "GET") { const entryId = url.searchParams.get("entryId"); if (entryId && cnx.previewUpload) return await cnx.previewUpload(entryId); } try { userCtx = await cnx.verify(request); cnx = remote(userCtx); } catch (cause) { if (cause instanceof MissingCredentialsError) { const authorization = request.headers.get("authorization"); const bearer = authorization?.slice("Bearer ".length); if (!context.apiKey) throw new MissingCredentialsError("Missing API key", { cause }); if (bearer !== context.apiKey) throw new InvalidCredentialsError("Expected matching api key", { cause }); } else { throw cause; } } if (action === HandleAction.User && request.method === "GET") { expectJson(); return Response.json(userCtx ? userCtx.user : null); } const expectUser = () => { if (!userCtx) throw new Response("Unauthorized", { status: 401 }); }; const body = PLazy.from(() => { const isJson = request.headers.get("content-type")?.includes("application/json"); if (!isJson) throw new Response("Expected JSON", { status: 400 }); return request.json(); }); if (action === HandleAction.PreviewToken && request.method === "POST") { expectUser(); expectJson(); return Response.json(await previews.sign(PreviewBody(await body))); } if (action === HandleAction.Resolve && request.method === "POST") { expectJson(); const raw = await request.text(); const scope = getScope(cms.config); const query = scope.parse(raw); if (!query.preview) { await periodicSync(cnx, query.syncInterval); } else { const { parse } = await previewParser; const preview = await parse(query.preview, () => local.syncWith(cnx)); query.preview = preview; } return Response.json(await local.resolve(query) ?? null); } if (action === HandleAction.Mutate && request.method === "POST") { expectUser(); expectJson(); const mutations = await body; const attempt = async (retry = 0) => { await local.syncWith(cnx); const request2 = await local.request(mutations); try { let { sha } = await cnx.write(request2); if (sha === request2.intoSha) { await local.write(request2); } else { sha = await local.syncWith(cnx); } return sha; } catch (error) { if (error instanceof ShaMismatchError && retry < 3) return attempt(retry + 1); throw error; } }; return Response.json({ sha: await attempt() }); } if (action === HandleAction.Commit && request.method === "POST") { throw new Error("Mutations expected"); } if (action === HandleAction.History && request.method === "GET") { expectUser(); expectJson(); const file = string(url.searchParams.get("file")); const revisionId = string.nullable(url.searchParams.get("revisionId")); const result = await (revisionId ? cnx.revisionData(file, revisionId) : cnx.revisions(file)); return Response.json(result ?? null); } if (action === HandleAction.Tree && request.method === "GET") { expectJson(); const sha = string(url.searchParams.get("sha")); await local.syncWith(cnx); const tree = await local.getTreeIfDifferent(sha); return Response.json(tree ?? null); } if (action === HandleAction.Blob && request.method === "POST") { const { shas } = object({ shas: array(string) })(await body); await periodicSync(cnx); const tree = await local.source.getTree(); const fromLocal = []; const fromRemote = []; for (const sha of shas) { if (tree.hasSha(sha)) fromLocal.push(sha); else fromRemote.push(sha); } const formData = new FormData(); if (fromLocal.length > 0) { const blobs = local.source.getBlobs(fromLocal); for await (const [sha, blob] of blobs) { formData.append(sha, new Blob([blob])); } } if (fromRemote.length > 0) { const blobs = cnx.getBlobs(fromRemote); for await (const [sha, blob] of blobs) { formData.append(sha, new Blob([blob])); } } return new Response(formData); } if (action === HandleAction.Upload) { expectUser(); const entryId = url.searchParams.get("entryId"); if (!entryId) { expectJson(); return Response.json( await cnx.prepareUpload(PrepareBody(await body).filename) ); } const isPost = request.method === "POST"; if (isPost && cnx.handleUpload) { await cnx.handleUpload(entryId, await request.blob()); return new Response("OK", { status: 200 }); } } if (action === HandleAction.Draft && request.method === "GET") { expectJson(); const key = string(url.searchParams.get("key")); const draft = await cnx.getDraft(key); return Response.json( draft ? { ...draft, draft: base64.stringify(draft.draft) } : null ); } if (action === HandleAction.Draft && request.method === "POST") { expectUser(); expectJson(); const data = await body; const draft = { ...data, draft: base64.parse(data.draft) }; return Response.json(await cnx.storeDraft(draft)); } return new Response("Bad Request", { status: 400 }); } catch (error) { if (error instanceof Response) return error; console.error(error); return Response.json( { success: false, error: error instanceof Error ? error.message : String(error) }, { status: error instanceof HttpError ? error.code : 500 } ); } }; } export { createHandler };