alinea
Version:
Headless git-based CMS
238 lines (236 loc) • 8.65 kB
JavaScript
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
};