Skip to content

Commit 2e41e38

Browse files
committed
adapt the jsonb image field
1 parent 705b6fd commit 2e41e38

5 files changed

Lines changed: 151 additions & 213 deletions

File tree

functions/delete-s3-object/__tests__/handler.e2e.test.ts

Lines changed: 59 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -82,18 +82,21 @@ describe('delete-s3-object handler e2e', () => {
8282
await cleanFilesStoreRows(pg);
8383
});
8484

85-
function callHandler(file_id: string, database_id: number, key: string) {
85+
const TEST_DB_ID = 'aaaaaaaa-0000-0000-0000-000000000099';
86+
87+
function callHandler(file_id: string, database_id: string, key: string, version_keys?: string[]) {
8688
const ctx = createMockContext({ env: ENV });
87-
return handler({ file_id, database_id, key }, ctx as any);
89+
return handler({ file_id, database_id, key, version_keys }, ctx as any);
8890
}
8991

9092
async function insertFile(opts: {
9193
s3Key: string;
9294
body: Buffer;
9395
status?: string;
94-
databaseId?: number;
95-
}): Promise<{ id: string; database_id: number }> {
96-
const databaseId = opts.databaseId ?? 1;
96+
databaseId?: string;
97+
versions?: object[];
98+
}): Promise<{ id: string; database_id: string }> {
99+
const databaseId = opts.databaseId ?? TEST_DB_ID;
97100
s3Keys.push(opts.s3Key);
98101

99102
await s3.send(new PutObjectCommand({
@@ -105,10 +108,10 @@ describe('delete-s3-object handler e2e', () => {
105108

106109
const res = await pg.query(
107110
`INSERT INTO ${SCHEMA}.files
108-
(database_id, key, bucket_key, status)
109-
VALUES ($1, $2, 'default', $3::${SCHEMA}.file_status)
111+
(database_id, key, bucket_key, status, versions)
112+
VALUES ($1, $2, 'default', $3::${SCHEMA}.file_status, $4::jsonb)
110113
RETURNING id, database_id`,
111-
[databaseId, opts.s3Key, opts.status ?? 'deleting']
114+
[databaseId, opts.s3Key, opts.status ?? 'deleting', opts.versions ? JSON.stringify(opts.versions) : null]
112115
);
113116
return res.rows[0];
114117
}
@@ -164,9 +167,9 @@ describe('delete-s3-object handler e2e', () => {
164167
const res = await pg.query(
165168
`INSERT INTO ${SCHEMA}.files
166169
(database_id, key, bucket_key, status)
167-
VALUES (1, $1, 'default', 'deleting')
170+
VALUES ($1, $2, 'default', 'deleting')
168171
RETURNING id, database_id`,
169-
[key]
172+
[TEST_DB_ID, key]
170173
);
171174
const { id, database_id } = res.rows[0];
172175

@@ -198,7 +201,7 @@ describe('delete-s3-object handler e2e', () => {
198201

199202
const result: any = await callHandler(
200203
'00000000-0000-0000-0000-000000000000',
201-
1,
204+
TEST_DB_ID,
202205
key
203206
);
204207

@@ -210,10 +213,54 @@ describe('delete-s3-object handler e2e', () => {
210213
// Test 4: Both already deleted — fully idempotent
211214
// -----------------------------------------------------------------------
212215

216+
// -----------------------------------------------------------------------
217+
// Test 4: Delete with version S3 objects
218+
// -----------------------------------------------------------------------
219+
220+
it('deletes origin + version S3 objects and DB row', async () => {
221+
const originKey = `e2e-del-ver-${Date.now()}-origin.bin`;
222+
const thumbKey = `e2e-del-ver-${Date.now()}-thumb.bin`;
223+
const mediumKey = `e2e-del-ver-${Date.now()}-medium.bin`;
224+
const body = Buffer.from('test');
225+
226+
// Upload origin + versions to S3
227+
for (const k of [originKey, thumbKey, mediumKey]) {
228+
s3Keys.push(k);
229+
await s3.send(new PutObjectCommand({ Bucket: BUCKET, Key: k, Body: body }));
230+
}
231+
232+
const { id, database_id } = await insertFile({
233+
s3Key: originKey,
234+
body,
235+
status: 'deleting',
236+
versions: [
237+
{ key: thumbKey, mime: 'image/jpeg', width: 150, height: 150 },
238+
{ key: mediumKey, mime: 'image/jpeg', width: 1200, height: 675 },
239+
],
240+
});
241+
242+
const result: any = await callHandler(id, database_id, originKey, [thumbKey, mediumKey]);
243+
244+
expect(result.success).toBe(true);
245+
expect(await s3ObjectExists(originKey)).toBe(false);
246+
expect(await s3ObjectExists(thumbKey)).toBe(false);
247+
expect(await s3ObjectExists(mediumKey)).toBe(false);
248+
249+
const dbRes = await pg.query(
250+
`SELECT * FROM ${SCHEMA}.files WHERE id = $1`,
251+
[id]
252+
);
253+
expect(dbRes.rows.length).toBe(0);
254+
});
255+
256+
// -----------------------------------------------------------------------
257+
// Test 5: Both already deleted — fully idempotent
258+
// -----------------------------------------------------------------------
259+
213260
it('succeeds when both S3 and DB are already gone', async () => {
214261
const result: any = await callHandler(
215262
'00000000-0000-0000-0000-000000000000',
216-
999,
263+
TEST_DB_ID,
217264
`nonexistent-key-${Date.now()}`
218265
);
219266

functions/delete-s3-object/handler.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ type DeleteParams = {
66
file_id: string;
77
database_id: string;
88
key: string;
9+
version_keys?: string[];
910
};
1011

1112
function createS3Client(env: Record<string, string | undefined>): S3Client {
@@ -48,20 +49,30 @@ const handler: FunctionHandler<DeleteParams> = async (
4849
const pool = createPgPool(env);
4950

5051
try {
51-
// Step 1: Delete from S3 (idempotent -- delete ignores missing keys)
52-
await s3.send(new DeleteObjectCommand({
53-
Bucket: env.BUCKET_NAME || 'test-bucket',
54-
Key: params.key,
55-
}));
52+
const bucket = env.BUCKET_NAME || 'test-bucket';
5653

57-
// Step 2: Delete the DB row
54+
// Step 1: Delete origin S3 object (idempotent)
55+
await s3.send(new DeleteObjectCommand({ Bucket: bucket, Key: params.key }));
56+
57+
// Step 2: Delete version S3 objects (from job payload)
58+
const versionKeys = params.version_keys || [];
59+
for (const vk of versionKeys) {
60+
try {
61+
await s3.send(new DeleteObjectCommand({ Bucket: bucket, Key: vk }));
62+
} catch (err) {
63+
log.error(`[delete-s3-object] failed to delete version ${vk}`, err);
64+
}
65+
}
66+
67+
// Step 3: Delete the DB row
5868
const result = await pool.query(
5969
'DELETE FROM files_store_public.files WHERE id = $1 AND database_id = $2',
6070
[params.file_id, params.database_id]
6171
);
6272

6373
log.info('[delete-s3-object] complete', {
6474
key: params.key,
75+
versionKeysDeleted: versionKeys.length,
6576
rowsDeleted: result.rowCount,
6677
});
6778

functions/process-image/__tests__/handler.file-mode.e2e.test.ts

Lines changed: 39 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const SOURCE_SCHEMA = 'public';
2424
const SOURCE_TABLE = 'test_process_file_uploads';
2525
const BUCKET = 'test-bucket';
2626
const USER_ID = 'aaaaaaaa-0000-0000-0000-000000000001';
27+
const TEST_DB_ID = 'aaaaaaaa-0000-0000-0000-000000000099';
2728

2829
const ENV: Record<string, string> = {
2930
PGHOST: 'localhost',
@@ -139,9 +140,10 @@ describe('process-image handler file mode e2e', () => {
139140
`INSERT INTO ${OBJECT_STORE_SCHEMA}.files
140141
(id, database_id, bucket_key, key, status, etag, created_by,
141142
source_table, source_column, source_id)
142-
VALUES ($1, 1, 'default', $2, 'pending', 'etag-origin', $3, $4, 'image', $5)`,
143+
VALUES ($1, $2, 'default', $3, 'pending', 'etag-origin', $4, $5, 'image', $6)`,
143144
[
144145
opts.fileId,
146+
TEST_DB_ID,
145147
opts.key,
146148
USER_ID,
147149
`${SOURCE_SCHEMA}.${SOURCE_TABLE}`,
@@ -154,23 +156,23 @@ describe('process-image handler file mode e2e', () => {
154156
await pg.query(
155157
`INSERT INTO ${OBJECT_STORE_SCHEMA}.files
156158
(id, database_id, bucket_key, key, status, etag, created_by)
157-
VALUES ($1, 1, 'default', $2, 'pending', 'etag-origin', $3)`,
158-
[opts.fileId, opts.key, USER_ID]
159+
VALUES ($1, $2, 'default', $3, 'pending', 'etag-origin', $4)`,
160+
[opts.fileId, TEST_DB_ID, opts.key, USER_ID]
159161
);
160162
}
161163

162164
async function callHandler(fileId: string) {
163165
const ctx = createMockContext({ env: ENV });
164-
return handler({ file_id: fileId, database_id: 1 }, ctx as any);
166+
return handler({ file_id: fileId, database_id: TEST_DB_ID }, ctx as any);
165167
}
166168

167-
it('processes an attached image into ready thumbnail and medium versions', async () => {
169+
it('processes an attached image into ready with versions JSONB', async () => {
168170
const fileId = randomUUID();
169171
const sourceId = randomUUID();
170172
const baseId = randomUUID();
171-
const originKey = `1/default/${baseId}_origin`;
172-
const thumbKey = `1/default/${baseId}_thumbnail`;
173-
const mediumKey = `1/default/${baseId}_medium`;
173+
const originKey = `${TEST_DB_ID}/default/${baseId}_origin`;
174+
const thumbKey = `${TEST_DB_ID}/default/${baseId}_thumbnail`;
175+
const mediumKey = `${TEST_DB_ID}/default/${baseId}_medium`;
174176
const imageBuffer = await generateTestImage(1600, 900);
175177

176178
await putOriginImage(originKey, imageBuffer);
@@ -197,44 +199,33 @@ describe('process-image handler file mode e2e', () => {
197199
s3Keys.add(thumbKey);
198200
s3Keys.add(mediumKey);
199201

202+
// Only 1 row — origin with versions JSONB
200203
const files = await pg.query(
201-
`SELECT key, status, source_table, source_column, source_id
204+
`SELECT key, status, versions, source_table, source_column, source_id
202205
FROM ${OBJECT_STORE_SCHEMA}.files
203-
WHERE key LIKE $1
204-
ORDER BY key`,
205-
[`1/default/${baseId}%`]
206+
WHERE id = $1`,
207+
[fileId]
206208
);
207209

208-
expect(files.rows).toEqual([
209-
{
210-
key: mediumKey,
211-
status: 'ready',
212-
source_table: `${SOURCE_SCHEMA}.${SOURCE_TABLE}`,
213-
source_column: 'image',
214-
source_id: sourceId,
215-
},
216-
{
217-
key: originKey,
218-
status: 'ready',
219-
source_table: `${SOURCE_SCHEMA}.${SOURCE_TABLE}`,
220-
source_column: 'image',
221-
source_id: sourceId,
222-
},
223-
{
224-
key: thumbKey,
225-
status: 'ready',
226-
source_table: `${SOURCE_SCHEMA}.${SOURCE_TABLE}`,
227-
source_column: 'image',
228-
source_id: sourceId,
229-
},
230-
]);
210+
expect(files.rows).toHaveLength(1);
211+
const row = files.rows[0];
212+
expect(row.status).toBe('ready');
213+
expect(row.key).toBe(originKey);
214+
expect(row.source_table).toBe(`${SOURCE_SCHEMA}.${SOURCE_TABLE}`);
215+
expect(row.versions).toHaveLength(2);
216+
expect(row.versions).toEqual(
217+
expect.arrayContaining([
218+
expect.objectContaining({ key: thumbKey, mime: 'image/jpeg', width: 150 }),
219+
expect.objectContaining({ key: mediumKey, mime: 'image/jpeg', width: 1200 }),
220+
])
221+
);
231222

223+
// Domain table also has versions
232224
const sourceRow = await pg.query(
233225
`SELECT image FROM ${SOURCE_SCHEMA}.${SOURCE_TABLE} WHERE id = $1`,
234226
[sourceId]
235227
);
236228
const versions = sourceRow.rows[0].image.versions;
237-
238229
expect(versions).toHaveLength(2);
239230
expect(versions).toEqual(
240231
expect.arrayContaining([
@@ -243,16 +234,17 @@ describe('process-image handler file mode e2e', () => {
243234
])
244235
);
245236

237+
// Idempotency
246238
const secondRun: any = await callHandler(fileId);
247239
expect(secondRun).toEqual({ skipped: true, reason: 'not_pending_or_locked' });
248240
});
249241

250242
it('processes an unattached image without writing domain metadata', async () => {
251243
const fileId = randomUUID();
252244
const baseId = randomUUID();
253-
const originKey = `1/default/${baseId}_origin`;
254-
const thumbKey = `1/default/${baseId}_thumbnail`;
255-
const mediumKey = `1/default/${baseId}_medium`;
245+
const originKey = `${TEST_DB_ID}/default/${baseId}_origin`;
246+
const thumbKey = `${TEST_DB_ID}/default/${baseId}_thumbnail`;
247+
const mediumKey = `${TEST_DB_ID}/default/${baseId}_medium`;
256248
const imageBuffer = await generateTestImage(1600, 900);
257249

258250
await putOriginImage(originKey, imageBuffer);
@@ -266,36 +258,17 @@ describe('process-image handler file mode e2e', () => {
266258
s3Keys.add(thumbKey);
267259
s3Keys.add(mediumKey);
268260

261+
// Only 1 row with versions JSONB
269262
const files = await pg.query(
270-
`SELECT key, status, source_table, source_column, source_id
263+
`SELECT key, status, versions, source_table
271264
FROM ${OBJECT_STORE_SCHEMA}.files
272-
WHERE key LIKE $1
273-
ORDER BY key`,
274-
[`1/default/${baseId}%`]
265+
WHERE id = $1`,
266+
[fileId]
275267
);
276268

277-
expect(files.rows).toEqual([
278-
{
279-
key: mediumKey,
280-
status: 'ready',
281-
source_table: null,
282-
source_column: null,
283-
source_id: null,
284-
},
285-
{
286-
key: originKey,
287-
status: 'ready',
288-
source_table: null,
289-
source_column: null,
290-
source_id: null,
291-
},
292-
{
293-
key: thumbKey,
294-
status: 'ready',
295-
source_table: null,
296-
source_column: null,
297-
source_id: null,
298-
},
299-
]);
269+
expect(files.rows).toHaveLength(1);
270+
expect(files.rows[0].status).toBe('ready');
271+
expect(files.rows[0].versions).toHaveLength(2);
272+
expect(files.rows[0].source_table).toBeNull();
300273
});
301274
});

0 commit comments

Comments
 (0)