Some checks failed
✅ Check / 🔬 ESLint (push) Failing after 21s
✅ Check / 🏷️ Typecheck (push) Failing after 11s
✅ Check / 🔎 Test (push) Failing after 11s
✅ Check / e2e-test (push) Failing after 11s
✅ Check / 🔨 Build (push) Failing after 11s
🚀 Deploy / 🚀 Deploy (doc) (push) Failing after 12s
🚀 Deploy / 🚀 Deploy (demo) (push) Failing after 12s
🚀 Deploy / 🚀 Deploy (supabase) (push) Failing after 18s
159 lines
3.8 KiB
TypeScript
159 lines
3.8 KiB
TypeScript
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
|
|
import { AuthMiddleware } from "../_shared/authentication.ts";
|
|
import { supabaseAdmin } from "../_shared/supabaseAdmin.ts";
|
|
|
|
const ATTACHMENTS_BUCKET =
|
|
Deno.env.get("VITE_ATTACHMENTS_BUCKET") || "attachments";
|
|
|
|
type NoteAttachment = {
|
|
path?: string | null;
|
|
src?: string | null;
|
|
};
|
|
|
|
type NoteRecord = {
|
|
id?: number | string | null;
|
|
attachments?: NoteAttachment[] | null;
|
|
};
|
|
|
|
type WebhookPayload = {
|
|
type?: string | null;
|
|
old_record?: NoteRecord | null;
|
|
record?: NoteRecord | null;
|
|
};
|
|
|
|
const deleteNoteAttachments = async (req: Request) => {
|
|
if (req.method !== "POST") {
|
|
return jsonResponse({ error: "Method Not Allowed" }, 405);
|
|
}
|
|
|
|
const payload = (await req.json()) as WebhookPayload;
|
|
const paths = getPathsToDelete(payload);
|
|
|
|
if (paths.length === 0) {
|
|
return jsonResponse({
|
|
status: "skipped",
|
|
reason: "no_paths_to_delete",
|
|
});
|
|
}
|
|
|
|
const { error } = await supabaseAdmin.storage
|
|
.from(ATTACHMENTS_BUCKET)
|
|
.remove(paths);
|
|
|
|
if (error) {
|
|
console.error("Failed to delete note attachments", {
|
|
type: payload.type ?? null,
|
|
paths,
|
|
error,
|
|
});
|
|
return jsonResponse({ error: "Failed to delete note attachments" }, 500);
|
|
}
|
|
|
|
return jsonResponse({
|
|
status: "ok",
|
|
});
|
|
};
|
|
|
|
Deno.serve(async (req: Request) =>
|
|
AuthMiddleware(req, async (req: Request) => deleteNoteAttachments(req)),
|
|
);
|
|
|
|
const getPathsToDelete = (payload: WebhookPayload): string[] => {
|
|
const oldPaths = extractAttachmentPaths(payload.old_record?.attachments);
|
|
const newPaths = extractAttachmentPaths(payload.record?.attachments);
|
|
|
|
if (payload.type === "UPDATE") {
|
|
const newPathsSet = new Set(newPaths);
|
|
return oldPaths.filter((path) => !newPathsSet.has(path));
|
|
}
|
|
|
|
if (payload.type === "DELETE") {
|
|
return oldPaths;
|
|
}
|
|
|
|
return [];
|
|
};
|
|
|
|
const extractAttachmentPaths = (
|
|
attachments?: NoteAttachment[] | null,
|
|
): string[] => {
|
|
const paths = attachments
|
|
?.map((attachment) => extractAttachmentPath(attachment))
|
|
.filter((path): path is string => path != null && path.length > 0);
|
|
|
|
return paths ? Array.from(new Set(paths)) : [];
|
|
};
|
|
|
|
const extractAttachmentPath = (attachment?: NoteAttachment | null) => {
|
|
if (!attachment) {
|
|
return null;
|
|
}
|
|
|
|
if (attachment.path) {
|
|
return normalizeStoragePath(attachment.path);
|
|
}
|
|
|
|
if (!attachment.src) {
|
|
return null;
|
|
}
|
|
|
|
const pathname = getPathname(attachment.src);
|
|
if (!pathname) {
|
|
return null;
|
|
}
|
|
|
|
const bucketSegment = `/${ATTACHMENTS_BUCKET}/`;
|
|
const bucketIndex = pathname.lastIndexOf(bucketSegment);
|
|
if (bucketIndex < 0) {
|
|
return null;
|
|
}
|
|
|
|
const path = pathname.slice(bucketIndex + bucketSegment.length);
|
|
return normalizeStoragePath(path);
|
|
};
|
|
|
|
const getPathname = (value: string) => {
|
|
try {
|
|
return new URL(value, "http://localhost").pathname;
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const safelyDecodePath = (path: string) => {
|
|
try {
|
|
return decodeURIComponent(path);
|
|
} catch {
|
|
return path;
|
|
}
|
|
};
|
|
|
|
const normalizeStoragePath = (path: string) => {
|
|
const trimmedPath = path.trim();
|
|
if (trimmedPath.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const parsedPath = getPathname(trimmedPath);
|
|
const candidatePath = parsedPath ?? trimmedPath;
|
|
|
|
const bucketSegment = `/${ATTACHMENTS_BUCKET}/`;
|
|
const bucketIndex = candidatePath.lastIndexOf(bucketSegment);
|
|
const withoutBucket =
|
|
bucketIndex >= 0
|
|
? candidatePath.slice(bucketIndex + bucketSegment.length)
|
|
: candidatePath.replace(/^\/+/, "").replace(/^attachments\//, "");
|
|
|
|
if (withoutBucket.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
return safelyDecodePath(withoutBucket);
|
|
};
|
|
|
|
const jsonResponse = (data: unknown, status = 200) =>
|
|
new Response(JSON.stringify(data), {
|
|
status,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|