fix: production bugs + comprehensive production e2e tests

Fixes:
- RowList bug: postgres.js returns array directly, not {rows}. Fix in
  buckets.ts, files-ext.ts, multipart.ts (3 files, 8 functions)
- S3 ListBuckets routing: GET / was intercepted by handleHome route
- Presigned URL detection: isS3Request() only checked Authorization header
- FK constraint on bucket delete: cascade-delete files & multipart rows first
- docker-compose.yml: pass S3_ACCESS_KEY / S3_SECRET_KEY to container
- Dockerfile: copy home.html to runner stage for handleHome

Tests:
- test/production-e2e.test.ts: 29 tests (11 Web API + 18 S3 SigV4)
  All pass against https://upload.asepharyana.my.id
- Creates and cleans up real buckets/objects on production
This commit is contained in:
asepharyana
2026-07-07 00:49:45 +07:00
parent b731ddd7e1
commit 57f2740df9
7 changed files with 452 additions and 31 deletions
+18 -10
View File
@@ -8,16 +8,14 @@ export interface Bucket {
updatedAt: Date;
}
interface QueryResult {
rows: Record<string, unknown>[];
rowCount: number;
}
type QueryRow = Record<string, unknown>;
type QueryResult = QueryRow[];
export const createBucket = async (name: string): Promise<Bucket> => {
const result = (await db.execute(
sql`INSERT INTO buckets (name) VALUES (${name}) RETURNING id, name, created_at, updated_at`,
)) as unknown as QueryResult;
const row = result.rows[0];
const row = result[0]!;
return {
id: row.id as string,
name: row.name as string,
@@ -30,8 +28,8 @@ export const findBucketByName = async (name: string): Promise<Bucket | null> =>
const result = (await db.execute(
sql`SELECT id, name, created_at, updated_at FROM buckets WHERE name = ${name}`,
)) as unknown as QueryResult;
if (result.rows.length === 0) return null;
const row = result.rows[0];
if (result.length === 0) return null;
const row = result[0]!;
return {
id: row.id as string,
name: row.name as string,
@@ -44,7 +42,7 @@ export const listBuckets = async (): Promise<Bucket[]> => {
const result = (await db.execute(
sql`SELECT id, name, created_at, updated_at FROM buckets ORDER BY name`,
)) as unknown as QueryResult;
return result.rows.map((row) => ({
return result.map((row) => ({
id: row.id as string,
name: row.name as string,
createdAt: new Date(row.created_at as string),
@@ -53,15 +51,25 @@ export const listBuckets = async (): Promise<Bucket[]> => {
};
export const deleteBucket = async (name: string): Promise<boolean> => {
// Cascade-delete rows that hold FK references to the bucket
await db.execute(
sql`DELETE FROM multipart_parts WHERE upload_id IN (SELECT upload_id FROM multipart_uploads WHERE bucket_id IN (SELECT id FROM buckets WHERE name = ${name}))`,
).catch(() => {});
await db.execute(
sql`DELETE FROM multipart_uploads WHERE bucket_id IN (SELECT id FROM buckets WHERE name = ${name})`,
).catch(() => {});
await db.execute(
sql`DELETE FROM files WHERE bucket_id IN (SELECT id FROM buckets WHERE name = ${name})`,
).catch(() => {});
const result = (await db.execute(
sql`DELETE FROM buckets WHERE name = ${name}`,
)) as unknown as QueryResult;
return result.rowCount > 0;
return result.length > 0;
};
export const bucketExists = async (name: string): Promise<boolean> => {
const result = (await db.execute(
sql`SELECT 1 FROM buckets WHERE name = ${name}`,
)) as unknown as QueryResult;
return result.rows.length > 0;
return result.length > 0;
};