Prepare production and Coolify deployment with original exports and datetime pickers

This commit is contained in:
2026-09-09 17:59:20 -04:00
parent 574f29a68e
commit 64d7acc9c0
38 changed files with 1296 additions and 8 deletions
@@ -0,0 +1,42 @@
import {test,expect} from "bun:test";
import {unzipSync} from "fflate";
import {and,eq} from "drizzle-orm";
import {events,getDb,photos,photoExports,user,guests,submissions} from "@album/database";
import {getObjectBuffer,deleteObject,putObject,headObject} from "@album/storage";
import {processPhotoExport} from "./exports";
test.skipIf(process.env.EXPORT_INTEGRATION!=="1")("original ZIP bytes, approved filtering, duplicate requests and expiry",async()=>{
if (!["localhost","127.0.0.1"].includes(new URL(process.env.DATABASE_URL!).hostname) || !["localhost","127.0.0.1"].includes(new URL(process.env.S3_ENDPOINT!).hostname)) throw new Error("Local storage/database required");
const db=getDb();
const [demo]=await db.select().from(events).where(eq(events.slug,"demo"));
const [owner]=await db.select({id:user.id}).from(user).limit(1);
const [event]=await db.insert(events).values({groupId:demo!.groupId,title:"Export test",slug:`export-${crypto.randomUUID()}`}).returning();
const [guest]=await db.insert(guests).values({eventId:event!.id,tokenHash:crypto.randomUUID()}).returning();
const [submission]=await db.insert(submissions).values({eventId:event!.id,guestId:guest!.id}).returning();
const bytes=Buffer.from("original bytes preserved exactly");
const sourceKey=`events/${event!.id}/export-test/original`;
const jobIds:string[]=[];
try {
await putObject({key:sourceKey,body:bytes,contentType:"image/jpeg"});
const [photo]=await db.insert(photos).values({eventId:event!.id,submissionId:submission!.id,originalKey:sourceKey,contentType:"image/jpeg",byteSize:bytes.length,processingStatus:"ready",visibility:"public"}).returning();
await db.insert(photos).values({eventId:event!.id,submissionId:submission!.id,originalKey:sourceKey,contentType:"image/jpeg",byteSize:bytes.length,processingStatus:"ready",visibility:"private"});
for(const filter of ["approved","all"]) {
const [job]=await db.insert(photoExports).values({eventId:event!.id,requestedBy:owner!.id,filter,expiresAt:new Date(Date.now()+86400000)}).returning();
jobIds.push(job!.id);
expect(await db.insert(photoExports).values({eventId:event!.id,requestedBy:owner!.id,filter,expiresAt:new Date(Date.now()+86400000)}).onConflictDoNothing().returning()).toHaveLength(0);
await processPhotoExport();
const [finished]=await db.select().from(photoExports).where(and(eq(photoExports.id,job!.id),eq(photoExports.eventId,event!.id)));
expect(finished?.status).toBe("ready");
const zip=unzipSync(await getObjectBuffer(`exports/${event!.id}/${job!.id}.zip`));
expect(Object.keys(zip)).toHaveLength(filter==="approved"?1:2);
expect(Buffer.from(zip[`${photo!.id}.jpg`]!)).toEqual(bytes);
await db.update(photoExports).set({expiresAt:new Date(0)}).where(and(eq(photoExports.id,job!.id),eq(photoExports.eventId,event!.id)));
await processPhotoExport();
expect(await headObject(`exports/${event!.id}/${job!.id}.zip`)).toBeNull();
}
} finally {
for(const id of jobIds) await deleteObject(`exports/${event!.id}/${id}.zip`);
await deleteObject(sourceKey);
await db.delete(events).where(eq(events.id,event!.id));
}
},30000);