Format api
This commit is contained in:
parent
9f255c87db
commit
370cd11dad
10 changed files with 513 additions and 619 deletions
|
|
@ -734,10 +734,8 @@ describe("generateTimeSlots", () => {
|
|||
|
||||
expect(slot09_00?.available, "09:00 should be available").to.be.true;
|
||||
expect(slot09_30?.available, "09:30 should not be available").to.be.false;
|
||||
expect(slot10_00?.available, "10:00 should not be unavailable").to.be
|
||||
.false; // Within buffered time
|
||||
expect(slot10_30?.available, "10:30 should not be unavailable").to.be
|
||||
.false; // Within buffered time
|
||||
expect(slot10_00?.available, "10:00 should not be unavailable").to.be.false; // Within buffered time
|
||||
expect(slot10_30?.available, "10:30 should not be unavailable").to.be.false; // Within buffered time
|
||||
expect(slot11_00?.available, "11:00 should be available").to.be.true; // After buffered time
|
||||
});
|
||||
|
||||
|
|
@ -1145,12 +1143,8 @@ describe("generateTimeSlots", () => {
|
|||
});
|
||||
|
||||
it("should format date strings correctly", () => {
|
||||
expect(getDateStringCET(new Date("2024-01-15T10:30:00Z"))).to.equal(
|
||||
"2024-01-15"
|
||||
);
|
||||
expect(getDateStringCET(new Date("2024-12-31T23:59:59Z"))).to.equal(
|
||||
"2025-01-01"
|
||||
);
|
||||
expect(getDateStringCET(new Date("2024-01-15T10:30:00Z"))).to.equal("2024-01-15");
|
||||
expect(getDateStringCET(new Date("2024-12-31T23:59:59Z"))).to.equal("2025-01-01");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -47,38 +47,20 @@ function createConfig(): AppConfig {
|
|||
process.env.SUPABASE_SERVICE_ROLE_KEY
|
||||
),
|
||||
SUPABASE_CONNECTION_STRING: process.env.SUPABASE_CONNECTION_STRING || "",
|
||||
STREAM_CHAT_API_KEY: validateEnvVar(
|
||||
"STREAM_CHAT_API_KEY",
|
||||
process.env.STREAM_CHAT_API_KEY
|
||||
),
|
||||
STREAM_CHAT_API_KEY: validateEnvVar("STREAM_CHAT_API_KEY", process.env.STREAM_CHAT_API_KEY),
|
||||
STREAM_CHAT_API_SECRET: validateEnvVar(
|
||||
"STREAM_CHAT_API_SECRET",
|
||||
process.env.STREAM_CHAT_API_SECRET
|
||||
),
|
||||
EMAIL_USER: validateEnvVar("EMAIL_USER", process.env.EMAIL_USER),
|
||||
EMAIL_CLIENT_ID: validateEnvVar(
|
||||
"EMAIL_CLIENT_ID",
|
||||
process.env.EMAIL_CLIENT_ID
|
||||
),
|
||||
EMAIL_CLIENT_SECRET: validateEnvVar(
|
||||
"EMAIL_CLIENT_SECRET",
|
||||
process.env.EMAIL_CLIENT_SECRET
|
||||
),
|
||||
EMAIL_REFRESH_TOKEN: validateEnvVar(
|
||||
"EMAIL_REFRESH_TOKEN",
|
||||
process.env.EMAIL_REFRESH_TOKEN
|
||||
),
|
||||
EMAIL_CLIENT_ID: validateEnvVar("EMAIL_CLIENT_ID", process.env.EMAIL_CLIENT_ID),
|
||||
EMAIL_CLIENT_SECRET: validateEnvVar("EMAIL_CLIENT_SECRET", process.env.EMAIL_CLIENT_SECRET),
|
||||
EMAIL_REFRESH_TOKEN: validateEnvVar("EMAIL_REFRESH_TOKEN", process.env.EMAIL_REFRESH_TOKEN),
|
||||
CORS_ORIGIN: process.env.CORS_ORIGIN || "https://app.xtablo.com",
|
||||
XTABLO_URL: process.env.XTABLO_URL || "https://app.xtablo.com",
|
||||
R2_ACCOUNT_ID: validateEnvVar("R2_ACCOUNT_ID", process.env.R2_ACCOUNT_ID),
|
||||
R2_ACCESS_KEY_ID: validateEnvVar(
|
||||
"R2_ACCESS_KEY_ID",
|
||||
process.env.R2_ACCESS_KEY_ID
|
||||
),
|
||||
R2_SECRET_ACCESS_KEY: validateEnvVar(
|
||||
"R2_SECRET_ACCESS_KEY",
|
||||
process.env.R2_SECRET_ACCESS_KEY
|
||||
),
|
||||
R2_ACCESS_KEY_ID: validateEnvVar("R2_ACCESS_KEY_ID", process.env.R2_ACCESS_KEY_ID),
|
||||
R2_SECRET_ACCESS_KEY: validateEnvVar("R2_SECRET_ACCESS_KEY", process.env.R2_SECRET_ACCESS_KEY),
|
||||
SYNC_CALS_SECRET: process.env.SYNC_CALS_SECRET || "",
|
||||
LOG_LEVEL: "info",
|
||||
};
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,8 +1,4 @@
|
|||
import {
|
||||
ListObjectsV2Command,
|
||||
PutObjectCommand,
|
||||
S3Client,
|
||||
} from "@aws-sdk/client-s3";
|
||||
import { ListObjectsV2Command, PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
||||
import type { SupabaseClient } from "@supabase/supabase-js";
|
||||
import type { EventAndTablo } from "./types.ts";
|
||||
|
||||
|
|
@ -54,14 +50,10 @@ export const generateICSFromEvents = (
|
|||
`DTSTART:${startDateTime}`,
|
||||
`DTEND:${endDateTime}`,
|
||||
`SUMMARY:${escapeICSText(event.title)}`,
|
||||
`DESCRIPTION:${escapeICSText(
|
||||
`Tablo: ${event.tablo_name}\n${event.description || ""}`
|
||||
)}`,
|
||||
`DESCRIPTION:${escapeICSText(`Tablo: ${event.tablo_name}\n${event.description || ""}`)}`,
|
||||
event.tablo_name ? `CATEGORIES:${escapeICSText(event.tablo_name)}` : "",
|
||||
`CREATED:${new Date().toISOString().replace(/[-:]/g, "").split(".")[0]}Z`,
|
||||
`LAST-MODIFIED:${
|
||||
new Date().toISOString().replace(/[-:]/g, "").split(".")[0]
|
||||
}Z`,
|
||||
`LAST-MODIFIED:${new Date().toISOString().replace(/[-:]/g, "").split(".")[0]}Z`,
|
||||
"STATUS:CONFIRMED",
|
||||
"TRANSP:OPAQUE",
|
||||
"END:VEVENT",
|
||||
|
|
@ -112,10 +104,7 @@ export const writeCalendarFileToR2 = async (
|
|||
);
|
||||
};
|
||||
|
||||
export const getTabloFileNames = async (
|
||||
s3_client: S3Client,
|
||||
tabloId: string
|
||||
) => {
|
||||
export const getTabloFileNames = async (s3_client: S3Client, tabloId: string) => {
|
||||
const bucketName = "tablo-data";
|
||||
|
||||
const { Contents } = await s3_client.send(
|
||||
|
|
@ -130,11 +119,7 @@ export const getTabloFileNames = async (
|
|||
);
|
||||
};
|
||||
|
||||
export const isTabloMember = async (
|
||||
supabase: SupabaseClient,
|
||||
tabloId: string,
|
||||
userId: string
|
||||
) => {
|
||||
export const isTabloMember = async (supabase: SupabaseClient, tabloId: string, userId: string) => {
|
||||
const { data: tabloAccess, error: isMemberError } = await supabase
|
||||
.from("tablo_access")
|
||||
.select("*")
|
||||
|
|
@ -149,11 +134,7 @@ export const isTabloMember = async (
|
|||
return tabloAccess?.length > 0;
|
||||
};
|
||||
|
||||
export const isTabloAdmin = async (
|
||||
supabase: SupabaseClient,
|
||||
tabloId: string,
|
||||
userId: string
|
||||
) => {
|
||||
export const isTabloAdmin = async (supabase: SupabaseClient, tabloId: string, userId: string) => {
|
||||
const { data: tabloAccess, error: isAdminError } = await supabase
|
||||
.from("tablo_access")
|
||||
.select("*")
|
||||
|
|
|
|||
|
|
@ -51,25 +51,22 @@ publicRouter.get("/slots/:shortUserId/:standardName", async (c) => {
|
|||
return c.json({ error: "Event type not found" }, 404);
|
||||
}
|
||||
|
||||
const eventType =
|
||||
eventTypeData as Database["public"]["Tables"]["event_types"]["Row"];
|
||||
const eventType = eventTypeData as Database["public"]["Tables"]["event_types"]["Row"];
|
||||
const eventTypeConfig = eventType.config as EventTypeConfig;
|
||||
|
||||
// Get user's availabilities
|
||||
const { data: availabilitiesData, error: availabilitiesError } =
|
||||
await supabase
|
||||
.from("availabilities")
|
||||
.select("*")
|
||||
.eq("user_id", user.id)
|
||||
.single();
|
||||
const { data: availabilitiesData, error: availabilitiesError } = await supabase
|
||||
.from("availabilities")
|
||||
.select("*")
|
||||
.eq("user_id", user.id)
|
||||
.single();
|
||||
|
||||
if (availabilitiesError) {
|
||||
return c.json({ error: "Availabilities not found" }, 404);
|
||||
}
|
||||
|
||||
const availabilities = availabilitiesData as Tables<"availabilities">;
|
||||
const weeklyAvailability =
|
||||
availabilities.availability_data as WeeklyAvailability;
|
||||
const weeklyAvailability = availabilities.availability_data as WeeklyAvailability;
|
||||
const exceptions = (availabilities.exceptions as Exception[]) || [];
|
||||
|
||||
// Get existing events for the next month
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import type { Tables } from "./database.types.js";
|
||||
|
||||
import { DateTime } from "luxon";
|
||||
import type { Tables } from "./database.types.js";
|
||||
|
||||
// Types for availability calculation
|
||||
type TimeRange = {
|
||||
|
|
@ -57,9 +56,7 @@ function parseTime(timeStr: string): { hours: number; minutes: number } {
|
|||
}
|
||||
|
||||
function formatTime(hours: number, minutes: number): string {
|
||||
return `${hours.toString().padStart(2, "0")}:${minutes
|
||||
.toString()
|
||||
.padStart(2, "0")}`;
|
||||
return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
function addMinutes(timeStr: string, minutesToAdd: number): string {
|
||||
|
|
@ -82,9 +79,7 @@ function mergeOverlappingTimeRanges(ranges: TimeRange[]): TimeRange[] {
|
|||
if (ranges.length <= 1) return ranges;
|
||||
|
||||
// Sort ranges by start time
|
||||
const sortedRanges = [...ranges].sort((a, b) =>
|
||||
a.start.localeCompare(b.start)
|
||||
);
|
||||
const sortedRanges = [...ranges].sort((a, b) => a.start.localeCompare(b.start));
|
||||
const merged: TimeRange[] = [sortedRanges[0]];
|
||||
|
||||
for (let i = 1; i < sortedRanges.length; i++) {
|
||||
|
|
@ -94,8 +89,7 @@ function mergeOverlappingTimeRanges(ranges: TimeRange[]): TimeRange[] {
|
|||
// Check if current range overlaps with the last merged range
|
||||
if (current.start <= lastMerged.end) {
|
||||
// Merge by extending the end time if current range extends further
|
||||
lastMerged.end =
|
||||
current.end > lastMerged.end ? current.end : lastMerged.end;
|
||||
lastMerged.end = current.end > lastMerged.end ? current.end : lastMerged.end;
|
||||
} else {
|
||||
// No overlap, add current range to merged array
|
||||
merged.push(current);
|
||||
|
|
@ -144,9 +138,7 @@ function getMinAdvanceBookingDate(
|
|||
}
|
||||
|
||||
export function getDateStringCET(date: Date): string {
|
||||
return DateTime.fromJSDate(date)
|
||||
.setZone("Europe/Paris")
|
||||
.toFormat("yyyy-MM-dd");
|
||||
return DateTime.fromJSDate(date).setZone("Europe/Paris").toFormat("yyyy-MM-dd");
|
||||
}
|
||||
|
||||
export function generateTimeSlots(
|
||||
|
|
@ -183,10 +175,7 @@ export function generateTimeSlots(
|
|||
}
|
||||
|
||||
// Check minimum advance booking
|
||||
const minAdvanceBooking = getMinAdvanceBookingDate(
|
||||
eventTypeConfig,
|
||||
currentTime
|
||||
);
|
||||
const minAdvanceBooking = getMinAdvanceBookingDate(eventTypeConfig, currentTime);
|
||||
|
||||
// Generate slots for each time range
|
||||
for (const range of timeRanges) {
|
||||
|
|
@ -197,17 +186,13 @@ export function generateTimeSlots(
|
|||
const endMinutes = endTime.hours * 60 + endTime.minutes;
|
||||
|
||||
while (currentMinutes + eventTypeConfig.duration <= endMinutes) {
|
||||
const slotTime = formatTime(
|
||||
Math.floor(currentMinutes / 60),
|
||||
currentMinutes % 60
|
||||
);
|
||||
const slotTime = formatTime(Math.floor(currentMinutes / 60), currentMinutes % 60);
|
||||
|
||||
// Check if slot is in the future (considering minimum advance booking)
|
||||
// Compare dates first, then times if on the same date
|
||||
const isInFuture =
|
||||
dateStr > minAdvanceBooking.date ||
|
||||
(dateStr === minAdvanceBooking.date &&
|
||||
slotTime >= minAdvanceBooking.time);
|
||||
(dateStr === minAdvanceBooking.date && slotTime >= minAdvanceBooking.time);
|
||||
|
||||
slots.push({
|
||||
date: dateStr,
|
||||
|
|
@ -233,8 +218,7 @@ export function generateTimeSlots(
|
|||
if (event.start_date !== dateStr || event.deleted_at) return false;
|
||||
|
||||
const eventStart = event.start_time;
|
||||
const eventEnd =
|
||||
event.end_time || addMinutes(eventStart, eventTypeConfig.duration);
|
||||
const eventEnd = event.end_time || addMinutes(eventStart, eventTypeConfig.duration);
|
||||
|
||||
// Apply buffer time around the existing event
|
||||
const bufferedEventStart = addMinutes(eventStart, -bufferTime);
|
||||
|
|
|
|||
|
|
@ -6,11 +6,7 @@ import type { StreamChat } from "stream-chat";
|
|||
import { config } from "./config.js";
|
||||
import type { Tables } from "./database.types.ts";
|
||||
import { writeCalendarFileToR2 } from "./helpers.js";
|
||||
import {
|
||||
authMiddleware,
|
||||
r2Middleware,
|
||||
streamChatMiddleware,
|
||||
} from "./middleware.js";
|
||||
import { authMiddleware, r2Middleware, streamChatMiddleware } from "./middleware.js";
|
||||
import { generateToken } from "./token.js";
|
||||
import { transporter } from "./transporter.js";
|
||||
import type { EventInsertInTablo, TabloInsert } from "./types.ts";
|
||||
|
|
@ -170,9 +166,7 @@ tabloRouter.post("/create-and-invite", async (c) => {
|
|||
const { data: insertedTablo, error } = await supabase
|
||||
.from("tablos")
|
||||
.insert({
|
||||
name: `${invitedUserDataTyped.name || "Invité"} / ${
|
||||
ownerDataTyped.name || "Propriétaire"
|
||||
}`,
|
||||
name: `${invitedUserDataTyped.name || "Invité"} / ${ownerDataTyped.name || "Propriétaire"}`,
|
||||
color: "bg-blue-500",
|
||||
status: "todo",
|
||||
owner_id: ownerId,
|
||||
|
|
@ -190,22 +184,20 @@ tabloRouter.post("/create-and-invite", async (c) => {
|
|||
}
|
||||
|
||||
// Grant access to the current user (invited user) as a non-admin member
|
||||
const { error: tabloAccessError } = await supabase
|
||||
.from("tablo_access")
|
||||
.insert(
|
||||
{
|
||||
tablo_id: tabloData.id,
|
||||
user_id: user.id,
|
||||
// ** IMPORTANT **
|
||||
is_admin: false,
|
||||
// -------------
|
||||
is_active: true,
|
||||
granted_by: ownerId,
|
||||
}
|
||||
// {
|
||||
// onConflict: "tablo_id, user_id",
|
||||
// }
|
||||
);
|
||||
const { error: tabloAccessError } = await supabase.from("tablo_access").insert(
|
||||
{
|
||||
tablo_id: tabloData.id,
|
||||
user_id: user.id,
|
||||
// ** IMPORTANT **
|
||||
is_admin: false,
|
||||
// -------------
|
||||
is_active: true,
|
||||
granted_by: ownerId,
|
||||
}
|
||||
// {
|
||||
// onConflict: "tablo_id, user_id",
|
||||
// }
|
||||
);
|
||||
|
||||
if (tabloAccessError) {
|
||||
console.error("tabloAccessError", tabloAccessError);
|
||||
|
|
@ -298,8 +290,7 @@ tabloRouter.patch("/update", async (c) => {
|
|||
|
||||
const updatedTablo = update as Tables<"tablos">;
|
||||
|
||||
const isUpdatingName =
|
||||
tablo.name !== undefined && tablo.name !== updatedTablo.name;
|
||||
const isUpdatingName = tablo.name !== undefined && tablo.name !== updatedTablo.name;
|
||||
|
||||
if (error) {
|
||||
return c.json({ error: error.message }, 500);
|
||||
|
|
@ -344,10 +335,7 @@ tabloRouter.delete("/delete", async (c) => {
|
|||
.single();
|
||||
|
||||
if (accessError || !tabloAccess || !tabloAccess.is_admin) {
|
||||
return c.json(
|
||||
{ error: "You are not authorized to delete this tablo" },
|
||||
403
|
||||
);
|
||||
return c.json({ error: "You are not authorized to delete this tablo" }, 403);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
|
|
@ -388,10 +376,7 @@ tabloRouter.post("/invite", async (c) => {
|
|||
}
|
||||
|
||||
if (tablo.owner_id !== sender.id) {
|
||||
return c.json(
|
||||
{ error: "You are not allowed to invite users to this tablo" },
|
||||
400
|
||||
);
|
||||
return c.json({ error: "You are not allowed to invite users to this tablo" }, 400);
|
||||
}
|
||||
|
||||
const { data: introConfigData, error: introError } = await supabase
|
||||
|
|
@ -425,8 +410,8 @@ tabloRouter.post("/invite", async (c) => {
|
|||
<p>Cliquez sur <a href="${
|
||||
config.XTABLO_URL
|
||||
}/join/${encodeURIComponent(tablo.name)}?token=${encodeURIComponent(
|
||||
token
|
||||
)}">ce lien</a> pour accepter l'invitation.</p>
|
||||
token
|
||||
)}">ce lien</a> pour accepter l'invitation.</p>
|
||||
<br>
|
||||
<p>Cordialement.</p>
|
||||
`,
|
||||
|
|
@ -462,17 +447,15 @@ tabloRouter.post("/join", async (c) => {
|
|||
|
||||
const { id: invite_id, tablo_id, invited_by } = inviteData;
|
||||
|
||||
const { error: tabloAccessError } = await supabase
|
||||
.from("tablo_access")
|
||||
.insert({
|
||||
tablo_id,
|
||||
user_id: joiner.id,
|
||||
// ** IMPORTANT **
|
||||
is_admin: false,
|
||||
// -------------
|
||||
is_active: true,
|
||||
granted_by: invited_by,
|
||||
});
|
||||
const { error: tabloAccessError } = await supabase.from("tablo_access").insert({
|
||||
tablo_id,
|
||||
user_id: joiner.id,
|
||||
// ** IMPORTANT **
|
||||
is_admin: false,
|
||||
// -------------
|
||||
is_active: true,
|
||||
granted_by: invited_by,
|
||||
});
|
||||
|
||||
if (tabloAccessError) {
|
||||
console.error("tabloAccessError", tabloAccessError);
|
||||
|
|
|
|||
|
|
@ -2,11 +2,7 @@ import { PutObjectCommand, type S3Client } from "@aws-sdk/client-s3";
|
|||
import type { SupabaseClient, User } from "@supabase/supabase-js";
|
||||
import { type Context, Hono, type Next } from "hono";
|
||||
import { getTabloFileNames, isTabloAdmin, isTabloMember } from "./helpers.js";
|
||||
import {
|
||||
authMiddleware,
|
||||
r2Middleware,
|
||||
streamChatMiddleware,
|
||||
} from "./middleware.js";
|
||||
import { authMiddleware, r2Middleware, streamChatMiddleware } from "./middleware.js";
|
||||
|
||||
export const tabloDataRouter = new Hono<{
|
||||
Variables: {
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ taskRouter.post("/sync-calendars", async (c) => {
|
|||
token: string;
|
||||
tablo_id: string;
|
||||
tablos: { name: string };
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
calendarSubscriptionsData.forEach(async (subscription) => {
|
||||
|
|
|
|||
|
|
@ -1,20 +1,16 @@
|
|||
import type { SupabaseClient, User } from "@supabase/supabase-js";
|
||||
import { Hono } from "hono";
|
||||
import type { Transporter } from "nodemailer";
|
||||
import { StreamChat } from "stream-chat";
|
||||
import type { Tables } from "./database.types.ts";
|
||||
import {
|
||||
authMiddleware,
|
||||
r2Middleware,
|
||||
streamChatMiddleware,
|
||||
} from "./middleware.js";
|
||||
import { transporter } from "./transporter.js";
|
||||
import {
|
||||
DeleteObjectsCommand,
|
||||
ListObjectsV2Command,
|
||||
PutObjectCommand,
|
||||
type S3Client,
|
||||
} from "@aws-sdk/client-s3";
|
||||
import type { SupabaseClient, User } from "@supabase/supabase-js";
|
||||
import { Hono } from "hono";
|
||||
import type { Transporter } from "nodemailer";
|
||||
import { StreamChat } from "stream-chat";
|
||||
import type { Tables } from "./database.types.ts";
|
||||
import { authMiddleware, r2Middleware, streamChatMiddleware } from "./middleware.js";
|
||||
import { transporter } from "./transporter.js";
|
||||
|
||||
export const userRouter = new Hono<{
|
||||
Variables: {
|
||||
|
|
@ -34,11 +30,7 @@ userRouter.post("/sign-up-to-stream", async (c) => {
|
|||
const { id } = c.get("user");
|
||||
const supabase = c.get("supabase");
|
||||
|
||||
const { data } = await supabase
|
||||
.from("profiles")
|
||||
.select("*")
|
||||
.eq("id", id)
|
||||
.single();
|
||||
const { data } = await supabase.from("profiles").select("*").eq("id", id).single();
|
||||
|
||||
const user = data as Tables<"profiles">;
|
||||
|
||||
|
|
@ -59,11 +51,7 @@ userRouter.get("/me", async (c) => {
|
|||
const supabase = c.get("supabase");
|
||||
const streamServerClient = c.get("streamServerClient");
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from("profiles")
|
||||
.select("*")
|
||||
.eq("id", user.id)
|
||||
.single();
|
||||
const { data, error } = await supabase.from("profiles").select("*").eq("id", user.id).single();
|
||||
|
||||
const userData = data as Tables<"profiles">;
|
||||
|
||||
|
|
@ -212,9 +200,7 @@ userRouter.post("/profile/avatar", async (c) => {
|
|||
|
||||
const randomString = Math.random().toString(36).substring(2, 15);
|
||||
const base64Content = Buffer.from(content, "base64");
|
||||
const key = `${user.id}/public_avatar_${randomString}.${
|
||||
contentType.split("/")[1]
|
||||
}`;
|
||||
const key = `${user.id}/public_avatar_${randomString}.${contentType.split("/")[1]}`;
|
||||
|
||||
try {
|
||||
await s3Client.send(
|
||||
|
|
@ -263,8 +249,7 @@ userRouter.delete("/profile/avatar", async (c) => {
|
|||
})
|
||||
);
|
||||
|
||||
if (listedObjects.Contents.length === 0)
|
||||
return c.json({ error: "No objects found" }, 404);
|
||||
if (listedObjects.Contents.length === 0) return c.json({ error: "No objects found" }, 404);
|
||||
|
||||
await s3Client.send(
|
||||
new DeleteObjectsCommand({
|
||||
|
|
|
|||
Loading…
Reference in a new issue