Skip to content
83 changes: 47 additions & 36 deletions supabase/functions/_backend/triggers/on_version_update.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import type { Context } from 'hono'
import type { MiddlewareKeyVariables } from '../utils/hono.ts'
import type { Database } from '../utils/supabase.types.ts'
import { eq } from 'drizzle-orm'
import { Hono } from 'hono/tiny'
import { BRES, middlewareAPISecret, triggerValidator } from '../utils/hono.ts'
import { cloudlog } from '../utils/loggin.ts'
import { closeClient, getDrizzleClient, getPgClient } from '../utils/pg.ts'
import { manifest } from '../utils/postgress_schema.ts'
import { getPath, s3 } from '../utils/s3.ts'
import { createStatsMeta } from '../utils/stats.ts'
import { supabaseAdmin } from '../utils/supabase.ts'
Expand Down Expand Up @@ -112,45 +115,53 @@ async function updateIt(c: Context, record: Database['public']['Tables']['app_ve

async function deleteManifest(c: Context, record: Database['public']['Tables']['app_versions']['Row']) {
// Delete manifest entries - first get them to delete from S3
const { data: manifestEntries, error: fetchError } = await supabaseAdmin(c)
.from('manifest')
.select()
.eq('app_version_id', record.id)
const pgClient = getPgClient(c)
const drizzleClient = getDrizzleClient(pgClient)

if (fetchError) {
cloudlog({ requestId: c.get('requestId'), message: 'error fetch manifest entries', error: fetchError })
}
else if (manifestEntries && manifestEntries.length > 0) {
// Delete each file from S3
const promisesDeleteS3 = []
for (const entry of manifestEntries) {
if (entry.s3_path) {
promisesDeleteS3.push(supabaseAdmin(c)
.from('manifest')
.select('*', { count: 'exact', head: true })
.eq('file_name', entry.file_name)
.eq('file_hash', entry.file_hash)
.neq('app_version_id', record.id)
.then((v) => {
const count = v.count ?? 0
if (!count) {
return supabaseAdmin(c)
.from('manifest')
.delete()
.eq('id', entry.id)
}
cloudlog({ requestId: c.get('requestId'), message: 'deleted manifest file from S3', s3_path: entry.s3_path })
return Promise.all([
s3.deleteObject(c, entry.s3_path),
supabaseAdmin(c)
.from('manifest')
.delete()
.eq('id', entry.id),
]) as any
}))
try {
const manifestEntries = await drizzleClient
.select()
.from(manifest)
.where(eq(manifest.app_version_id, record.id))

if (manifestEntries && manifestEntries.length > 0) {
// Delete each file from S3
const promisesDeleteS3 = []
for (const entry of manifestEntries) {
if (entry.s3_path) {
promisesDeleteS3.push(supabaseAdmin(c)
.from('manifest')
.select('*', { count: 'exact', head: true })
.eq('file_name', entry.file_name)
.eq('file_hash', entry.file_hash)
.neq('app_version_id', record.id)
.then((v) => {
const count = v.count ?? 0
if (!count) {
return supabaseAdmin(c)
.from('manifest')
.delete()
.eq('id', entry.id)
}
cloudlog({ requestId: c.get('requestId'), message: 'deleted manifest file from S3', s3_path: entry.s3_path })
return Promise.all([
s3.deleteObject(c, entry.s3_path),
supabaseAdmin(c)
.from('manifest')
.delete()
.eq('id', entry.id),
]) as any
}))
Comment on lines +139 to +154
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix inverted S3 deletion guard

count represents other manifest rows referencing the same asset. The current condition skips deleting the S3 object when count is zero (no other references) and does delete it when count is positive, which will wipe shared artifacts and strand orphans. Please flip the guard so S3 deletion only happens when no other rows point at the file.

-              const count = v.count ?? 0
-              if (!count) {
-                return supabaseAdmin(c)
-                  .from('manifest')
-                  .delete()
-                  .eq('id', entry.id)
-              }
-              cloudlog({ requestId: c.get('requestId'), message: 'deleted manifest file from S3', s3_path: entry.s3_path })
-              return Promise.all([
-                s3.deleteObject(c, entry.s3_path),
-                supabaseAdmin(c)
-                  .from('manifest')
-                  .delete()
-                  .eq('id', entry.id),
-              ]) as any
+              const count = v.count ?? 0
+              if (count && count > 0) {
+                return supabaseAdmin(c)
+                  .from('manifest')
+                  .delete()
+                  .eq('id', entry.id)
+              }
+              cloudlog({ requestId: c.get('requestId'), message: 'deleted manifest file from S3', s3_path: entry.s3_path })
+              return Promise.all([
+                s3.deleteObject(c, entry.s3_path),
+                supabaseAdmin(c)
+                  .from('manifest')
+                  .delete()
+                  .eq('id', entry.id),
+              ]) as any
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const count = v.count ?? 0
if (!count) {
return supabaseAdmin(c)
.from('manifest')
.delete()
.eq('id', entry.id)
}
cloudlog({ requestId: c.get('requestId'), message: 'deleted manifest file from S3', s3_path: entry.s3_path })
return Promise.all([
s3.deleteObject(c, entry.s3_path),
supabaseAdmin(c)
.from('manifest')
.delete()
.eq('id', entry.id),
]) as any
}))
const count = v.count ?? 0
if (count && count > 0) {
return supabaseAdmin(c)
.from('manifest')
.delete()
.eq('id', entry.id)
}
cloudlog({ requestId: c.get('requestId'), message: 'deleted manifest file from S3', s3_path: entry.s3_path })
return Promise.all([
s3.deleteObject(c, entry.s3_path),
supabaseAdmin(c)
.from('manifest')
.delete()
.eq('id', entry.id),
]) as any
🤖 Prompt for AI Agents
In supabase/functions/_backend/triggers/on_version_update.ts around lines 139 to
154 the S3-deletion guard is inverted: currently it skips S3 deletion when count
=== 0 and deletes S3 when count > 0; flip the logic so that when count === 0 (no
other manifest rows reference the asset) you delete the S3 object and the
manifest row, and when count > 0 you only delete the manifest row (no S3
deletion). Move or add the cloudlog to the branch where S3 is actually deleted,
and ensure the function returns the appropriate Promise (Promise.all for
S3+manifest deletion, single supabaseAdmin delete for manifest-only) with
correct typing.

}
}
await backgroundTask(c, Promise.all(promisesDeleteS3))
}
await backgroundTask(c, Promise.all(promisesDeleteS3))
}
catch (error) {
cloudlog({ requestId: c.get('requestId'), message: 'error fetch manifest entries', error })
}
finally {
await closeClient(c, pgClient)
}
}

Expand Down
18 changes: 12 additions & 6 deletions supabase/functions/_backend/utils/s3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,19 +66,25 @@ async function getUploadUrl(c: Context, fileId: string, expirySeconds = 1200) {

async function deleteObject(c: Context, fileId: string) {
const client = initS3(c)
await client.deleteObject(fileId)
return true
const url = await client.getPresignedUrl('DELETE', fileId)
const response = await fetch(url, {
method: 'DELETE',
})
return response.status >= 200 && response.status < 300
}

async function checkIfExist(c: Context, fileId: string | null) {
if (!fileId) {
return false
}
const client = initS3(c)

try {
const file = await client.statObject(fileId)
return file.size > 0
const client = initS3(c)
const url = await client.getPresignedUrl('HEAD', fileId)
const response = await fetch(url, {
method: 'HEAD',
})
const contentLength = Number.parseInt(response.headers.get('content-length') || '0')
return response.status === 200 && contentLength > 0
}
catch {
// cloudlog({ requestId: c.get('requestId'), message: 'checkIfExist', fileId, error })
Expand Down