snapshot of https://github.com/marmelab/atomic-crm.git (main, 2026-09-18; blobs > 131072B omitted)
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
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
This commit is contained in:
158
supabase/functions/delete_note_attachments/index.ts
Normal file
158
supabase/functions/delete_note_attachments/index.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
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" },
|
||||
});
|
||||
Reference in New Issue
Block a user