fix: version_meta table#1150
Conversation
WalkthroughThis update introduces a new TypeScript tool for analyzing and managing large files in an S3-compatible bucket, adds the AWS S3 SDK as a dependency, and enhances Changes
Sequence Diagram(s)sequenceDiagram
participant Script as check_r2_big_files.ts
participant S3 as S3 (R2 Bucket)
participant DB as PostgreSQL
Script->>S3: List objects under orgs/
Script->>Script: Save object metadata to JSON
Script->>DB: Fetch app versions (if required)
Script->>Script: Analyze, correlate, or export data
Script->>S3: (Optional) Copy or delete orphaned files
Script->>DB: (Optional) Save reports or fetch org plan info
sequenceDiagram
participant Backend as Supabase Backend
participant DB as PostgreSQL
Backend->>DB: Call upsert_version_meta(app_id, version_id, size)
alt No existing row with same sign
DB-->>Backend: Insert new row, return true
else Existing row with same sign
DB-->>Backend: Do not insert, return false
end
Poem
✨ Finishing Touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
|
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
There was a problem hiding this comment.
Actionable comments posted: 5
🔭 Outside diff range comments (1)
supabase/functions/_backend/utils/supabase.types.ts (1)
701-740: 🛠️ Refactor suggestionInconsistent devices table type definitions
The
idfield is removed from theRowtype but still referenced inInsertandUpdatetypes withid?: never. If the field was removed from the table, it should be completely removed from all type definitions.Insert: { app_id: string custom_id?: string device_id: string - id?: never is_emulator?: boolean | null is_prod?: boolean | null os_version?: string | null platform: Database["public"]["Enums"]["platform_os"] plugin_version?: string updated_at: string version: number version_build?: string | null } Update: { app_id?: string custom_id?: string device_id?: string - id?: never is_emulator?: boolean | null is_prod?: boolean | null os_version?: string | null platform?: Database["public"]["Enums"]["platform_os"] plugin_version?: string updated_at?: string version?: number version_build?: string | null }
🧹 Nitpick comments (4)
supabase/migrations/20250608130257_fix_version_meta.sql (1)
75-85: Consider adding an index for zero-size entriesThe partial unique indexes for positive and negative sizes are well-designed. However, you might want to consider adding a similar constraint for
size = 0entries to prevent duplicates in that case as well.CREATE UNIQUE INDEX unique_app_version_negative ON version_meta (app_id, version_id) WHERE size < 0; + +-- Prevent duplicate zero-size entries +CREATE UNIQUE INDEX unique_app_version_zero + ON version_meta (app_id, version_id) + WHERE size = 0;scripts/check_r2_big_files.ts (3)
200-204: Use optional chaining for cleaner code// Filter objects in the specified folder const folderObjects = objects.filter(obj => - obj.Key && obj.Key.startsWith(normalizedPath) + obj.Key?.startsWith(normalizedPath) )🧰 Tools
🪛 Biome (1.9.4)
[error] 201-204: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
538-540: Use optional chaining for cleaner code// Filter objects in the specified folder const folderObjects = objects.filter(obj => - obj.Key && obj.Key.startsWith(normalizedPath) && obj.Key.endsWith('.zip') + obj.Key?.startsWith(normalizedPath) && obj.Key?.endsWith('.zip') )
649-653: Consider increasing database connection idle timeoutThe 2-second idle timeout is very aggressive and might cause connection drops during longer operations.
export function getPgClient(c: Context) { const dbUrl = getDatabaseURL() console.log({ message: 'getPgClient', dbUrl }) - return postgres(dbUrl, { prepare: false, idle_timeout: 2 }) + return postgres(dbUrl, { prepare: false, idle_timeout: 20 }) }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (7)
.gitignore(1 hunks)package.json(2 hunks)scripts/check_r2_big_files.ts(1 hunks)supabase/functions/_backend/triggers/on_version_update.ts(1 hunks)supabase/functions/_backend/utils/supabase.ts(1 hunks)supabase/functions/_backend/utils/supabase.types.ts(14 hunks)supabase/migrations/20250608130257_fix_version_meta.sql(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (3)
supabase/functions/_backend/triggers/on_version_update.ts (2)
supabase/functions/_backend/utils/stats.ts (1)
createStatsMeta(61-66)supabase/functions/_backend/utils/loggin.ts (1)
cloudlog(3-17)
scripts/check_r2_big_files.ts (2)
supabase/functions/_backend/utils/s3.ts (1)
s3(103-109)supabase/functions/_backend/utils/utils.ts (2)
getEnv(160-164)existInEnv(156-158)
supabase/functions/_backend/utils/supabase.types.ts (1)
src/types/supabase.types.ts (1)
Json(1-7)
🪛 Biome (1.9.4)
scripts/check_r2_big_files.ts
[error] 51-52: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.
The declaration is defined in this switch clause:
Unsafe fix: Wrap the declaration in a block.
(lint/correctness/noSwitchDeclarations)
[error] 63-64: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.
The declaration is defined in this switch clause:
Unsafe fix: Wrap the declaration in a block.
(lint/correctness/noSwitchDeclarations)
[error] 73-74: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.
The declaration is defined in this switch clause:
Unsafe fix: Wrap the declaration in a block.
(lint/correctness/noSwitchDeclarations)
[error] 89-90: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.
The declaration is defined in this switch clause:
Unsafe fix: Wrap the declaration in a block.
(lint/correctness/noSwitchDeclarations)
[error] 201-204: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 360-403: This code is unreachable
(lint/correctness/noUnreachable)
[error] 542-543: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
⏰ Context from checks skipped due to timeout of 90000ms (3)
- GitHub Check: Run tests
- GitHub Check: Analyze (javascript-typescript)
- GitHub Check: Analyze (java-kotlin)
🔇 Additional comments (7)
package.json (1)
84-84:Details
❓ Verification inconclusive
Verify AWS SDK version for security and validity.
The AWS SDK for S3 v3.821.0 should be checked to ensure it's a valid version and free from known security vulnerabilities.
🏁 Script executed:
#!/bin/bash # Description: Verify AWS SDK version and check for security advisories # Check if the specific version exists on npm echo "Checking if @aws-sdk/client-s3@3.821.0 exists..." npm view @aws-sdk/client-s3@3.821.0 version # Get latest version for comparison echo "Latest version:" npm view @aws-sdk/client-s3 version # Check for security advisories echo "Checking for security advisories..." npm audit --audit-level=moderate --package-lock-only @aws-sdk/client-s3Length of output: 751
Verify AWS SDK client-S3 Version and Security
File: package.json
Line: 84"@aws-sdk/client-s3": "^3.821.0",
- ✔️ @aws-sdk/client-s3@3.821.0 is a valid published version.
- ℹ️ Latest patch is 3.826.0; the caret range (^3.821.0) will automatically pick up these patch releases.
- 🔒 Security audit can’t run without a lockfile. Please generate/update
package-lock.json(e.g.,npm install --package-lock-only) and then runnpm auditto confirm there are no known vulnerabilities..gitignore (1)
87-89: LGTM! Appropriate gitignore patterns for generated files.The additions correctly exclude generated files from the new S3 analysis script while maintaining specificity to the scripts directory.
supabase/functions/_backend/triggers/on_version_update.ts (1)
135-137: Excellent improvement in error handling.Capturing and logging the error from
createStatsMetaenhances observability and follows the established error handling pattern used throughout the file.supabase/functions/_backend/utils/supabase.ts (1)
680-684:Details
✅ Verification successful
Good refactor to use stored procedure for version metadata management.
Replacing the direct insert with the
upsert_version_metastored procedure improves data integrity and handles deduplication logic at the database level, which aligns with the PR's objective to fix version_meta table issues.
🏁 Script executed:
#!/bin/bash # Description: Verify the upsert_version_meta stored procedure exists in the migration files # Search for the stored procedure definition echo "Searching for upsert_version_meta stored procedure..." fd -e sql -x grep -l "upsert_version_meta" {} \; # Show the procedure definition if found echo "Procedure definition:" rg -A 20 "CREATE.*FUNCTION.*upsert_version_meta" --type sqlLength of output: 2189
Stored procedure verified: upsert_version_meta is defined
The
upsert_version_metafunction exists and matches the RPC call parameters:
- Location: supabase/migrations/20250608130257_fix_version_meta.sql
- Signature:
- p_app_id VARCHAR(255)
- p_version_id BIGINT
- p_size BIGINT
- Includes deduplication logic at the database layer
All set—approving this change.
supabase/migrations/20250608130257_fix_version_meta.sql (1)
16-74: Well-designed deduplication strategy!The approach of using different timestamp ordering for positive vs negative sizes is clever, correctly preserving the earliest addition and latest removal for each (app_id, version_id) pair. The use of a temporary table ensures data safety during the migration.
scripts/check_r2_big_files.ts (1)
473-476:Details
❌ Incorrect review comment
Remove duplicate environment variable utilities
These functions duplicate the utilities already available in
supabase/functions/_backend/utils/utils.ts.Consider importing and using the existing utilities instead:
-function getEnv(c: any, s: string) { - return process.env[s] ?? '' -} +import { getEnv, existInEnv } from '../supabase/functions/_backend/utils/utils.ts'
🏁 Script executed:
#!/bin/bash # Verify the existing utilities cat supabase/functions/_backend/utils/utils.ts | grep -A 10 "getEnv\|existInEnv"Length of output: 978
Ignore duplicate environment utilities suggestion
The
getEnvinscripts/check_r2_big_files.tsreads from Node’sprocess.env, whereas the utilities insupabase/functions/_backend/utils/utils.tsoperate on an Edge FunctionContextviaenv(c). They serve different runtimes and aren’t interchangeable. The import suggestion can be discarded.Likely an incorrect or invalid review comment.
supabase/functions/_backend/utils/supabase.types.ts (1)
1911-1914: Type definition correctly matches the migration functionThe
upsert_version_metafunction type properly reflects the PL/pgSQL function created in the migration, ensuring type-safe RPC calls.
| -- Create a secure function to handle version_meta upserts | ||
| -- Only available to supabase service role, not public users | ||
| CREATE OR REPLACE FUNCTION upsert_version_meta( | ||
| p_app_id VARCHAR(255), | ||
| p_version_id BIGINT, | ||
| p_size BIGINT | ||
| ) | ||
| RETURNS BOOLEAN | ||
| LANGUAGE plpgsql | ||
| SECURITY DEFINER -- Run with definer's privileges (postgres/service role) | ||
| SET search_path = '' -- Security: fix search path | ||
| AS $$ | ||
| DECLARE | ||
| existing_count INTEGER; | ||
| BEGIN | ||
| -- Check if a row already exists for this app_id, version_id with same sign | ||
| IF p_size > 0 THEN | ||
| -- Check for existing positive size | ||
| SELECT COUNT(*) INTO existing_count | ||
| FROM public.version_meta | ||
| WHERE public.version_meta.app_id = p_app_id | ||
| AND public.version_meta.version_id = p_version_id | ||
| AND public.version_meta.size > 0; | ||
| ELSIF p_size < 0 THEN | ||
| -- Check for existing negative size | ||
| SELECT COUNT(*) INTO existing_count | ||
| FROM public.version_meta | ||
| WHERE public.version_meta.app_id = p_app_id | ||
| AND public.version_meta.version_id = p_version_id | ||
| AND public.version_meta.size < 0; | ||
| END IF; | ||
|
|
||
| -- If row already exists, do nothing and return false | ||
| IF existing_count > 0 THEN | ||
| RETURN FALSE; | ||
| END IF; | ||
|
|
||
| -- Insert the new row | ||
| INSERT INTO version_meta (app_id, version_id, size) | ||
| VALUES (p_app_id, p_version_id, p_size); | ||
|
|
||
| -- Return true to indicate insertion happened | ||
| RETURN TRUE; | ||
|
|
||
| EXCEPTION | ||
| WHEN unique_violation THEN | ||
| -- If there's a race condition and constraint is violated, just return false | ||
| RETURN FALSE; | ||
| END; | ||
| $$; |
There was a problem hiding this comment.
Missing logic for zero-size entries in upsert function
The function handles positive and negative sizes but doesn't explicitly check for existing zero-size entries. This could lead to duplicate zero-size entries being inserted.
-- Check if a row already exists for this app_id, version_id with same sign
IF p_size > 0 THEN
-- Check for existing positive size
SELECT COUNT(*) INTO existing_count
FROM public.version_meta
WHERE public.version_meta.app_id = p_app_id
AND public.version_meta.version_id = p_version_id
AND public.version_meta.size > 0;
ELSIF p_size < 0 THEN
-- Check for existing negative size
SELECT COUNT(*) INTO existing_count
FROM public.version_meta
WHERE public.version_meta.app_id = p_app_id
AND public.version_meta.version_id = p_version_id
AND public.version_meta.size < 0;
+ ELSE
+ -- Check for existing zero size
+ SELECT COUNT(*) INTO existing_count
+ FROM public.version_meta
+ WHERE public.version_meta.app_id = p_app_id
+ AND public.version_meta.version_id = p_version_id
+ AND public.version_meta.size = 0;
END IF;📝 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.
| -- Create a secure function to handle version_meta upserts | |
| -- Only available to supabase service role, not public users | |
| CREATE OR REPLACE FUNCTION upsert_version_meta( | |
| p_app_id VARCHAR(255), | |
| p_version_id BIGINT, | |
| p_size BIGINT | |
| ) | |
| RETURNS BOOLEAN | |
| LANGUAGE plpgsql | |
| SECURITY DEFINER -- Run with definer's privileges (postgres/service role) | |
| SET search_path = '' -- Security: fix search path | |
| AS $$ | |
| DECLARE | |
| existing_count INTEGER; | |
| BEGIN | |
| -- Check if a row already exists for this app_id, version_id with same sign | |
| IF p_size > 0 THEN | |
| -- Check for existing positive size | |
| SELECT COUNT(*) INTO existing_count | |
| FROM public.version_meta | |
| WHERE public.version_meta.app_id = p_app_id | |
| AND public.version_meta.version_id = p_version_id | |
| AND public.version_meta.size > 0; | |
| ELSIF p_size < 0 THEN | |
| -- Check for existing negative size | |
| SELECT COUNT(*) INTO existing_count | |
| FROM public.version_meta | |
| WHERE public.version_meta.app_id = p_app_id | |
| AND public.version_meta.version_id = p_version_id | |
| AND public.version_meta.size < 0; | |
| END IF; | |
| -- If row already exists, do nothing and return false | |
| IF existing_count > 0 THEN | |
| RETURN FALSE; | |
| END IF; | |
| -- Insert the new row | |
| INSERT INTO version_meta (app_id, version_id, size) | |
| VALUES (p_app_id, p_version_id, p_size); | |
| -- Return true to indicate insertion happened | |
| RETURN TRUE; | |
| EXCEPTION | |
| WHEN unique_violation THEN | |
| -- If there's a race condition and constraint is violated, just return false | |
| RETURN FALSE; | |
| END; | |
| $$; | |
| -- Check if a row already exists for this app_id, version_id with same sign | |
| IF p_size > 0 THEN | |
| -- Check for existing positive size | |
| SELECT COUNT(*) INTO existing_count | |
| FROM public.version_meta | |
| WHERE public.version_meta.app_id = p_app_id | |
| AND public.version_meta.version_id = p_version_id | |
| AND public.version_meta.size > 0; | |
| ELSIF p_size < 0 THEN | |
| -- Check for existing negative size | |
| SELECT COUNT(*) INTO existing_count | |
| FROM public.version_meta | |
| WHERE public.version_meta.app_id = p_app_id | |
| AND public.version_meta.version_id = p_version_id | |
| AND public.version_meta.size < 0; | |
| ELSE | |
| -- Check for existing zero size | |
| SELECT COUNT(*) INTO existing_count | |
| FROM public.version_meta | |
| WHERE public.version_meta.app_id = p_app_id | |
| AND public.version_meta.version_id = p_version_id | |
| AND public.version_meta.size = 0; | |
| END IF; |
🤖 Prompt for AI Agents
In supabase/migrations/20250608130257_fix_version_meta.sql around lines 86 to
135, the upsert_version_meta function lacks handling for zero-size entries,
which may cause duplicate zero-size rows. Update the function to include a
condition that checks for existing entries where p_size equals zero, similar to
the positive and negative size checks, and prevent insertion if such an entry
exists.
| async function object_size(objectPath: string) { | ||
| console.log('🔄 Checking if object exists in R2...') | ||
| const s3 = await initS3() | ||
|
|
||
| try { | ||
| // First check if object exists (similar to checkIfExist in s3.ts) | ||
| let objectExists = false | ||
| let headResponse | ||
|
|
||
| try { | ||
|
|
||
| const response = await s3.send(new HeadObjectCommand({ | ||
| Bucket: S3_BUCKET, | ||
| Key: objectPath | ||
| })) | ||
|
|
||
| const s3Lite = await initS3Lite() | ||
|
|
||
| const file = await s3Lite.statObject(convertToValidS3Path(objectPath)) | ||
| console.log({ message: 'object_size', file: JSON.stringify(file, null, 2) }) | ||
|
|
||
|
|
||
| } catch (error: any) { | ||
| if (error.name === 'NoSuchKey' || error.$metadata?.httpStatusCode === 404) { | ||
| objectExists = false | ||
| } else { | ||
| throw error // Re-throw if it's not a "not found" error | ||
| } | ||
| } | ||
|
|
||
| if (!objectExists) { | ||
| console.error(`❌ Object not exist in R2: ${objectPath}`) | ||
| console.error('\n💡 Tips:') | ||
| console.error(' - Make sure the path is exact (case-sensitive)') | ||
| console.error(' - Check if the object exists in R2') | ||
| console.error(' - Example: orgs/74eea063-512c-4763-beae-1d4ba1c303c5/apps/com.math99.mobile/3.0.384+b.114134.master.1c98e3dfe.zip') | ||
| return | ||
| } | ||
|
|
||
| console.log('✅ Object exists, fetching details...') | ||
| return | ||
|
|
||
| // Calculate size in different units | ||
| const size = headResponse!.ContentLength ?? 0 | ||
| const sizeKB = size / 1024 | ||
| const sizeMB = sizeKB / 1024 | ||
| const sizeGB = sizeMB / 1024 | ||
|
|
||
| console.log('\n📄 Object Details (Live from R2)') | ||
| console.log('===================================') | ||
| console.log(`🔑 Key: ${objectPath}`) | ||
| console.log(`💾 Size:`) | ||
| console.log(` - ${size.toLocaleString()} bytes`) | ||
| console.log(` - ${sizeKB.toFixed(2)} KB`) | ||
| console.log(` - ${sizeMB.toFixed(2)} MB`) | ||
| if (sizeGB >= 0.01) { | ||
| console.log(` - ${sizeGB.toFixed(2)} GB`) | ||
| } | ||
|
|
||
| if (headResponse!.LastModified) { | ||
| console.log(`📅 Last Modified: ${headResponse!.LastModified.toISOString()}`) | ||
| } | ||
|
|
||
| if (headResponse!.ETag) { | ||
| console.log(`🏷️ ETag: ${headResponse!.ETag}`) | ||
| } | ||
|
|
||
| if (headResponse!.ContentType) { | ||
| console.log(`📝 Content Type: ${headResponse!.ContentType}`) | ||
| } | ||
|
|
||
| if (headResponse!.StorageClass) { | ||
| console.log(`💼 Storage Class: ${headResponse!.StorageClass}`) | ||
| } | ||
|
|
||
| // Show file extension info | ||
| const extension = objectPath.split('.').pop() | ||
| if (extension) { | ||
| console.log(`📝 File Extension: .${extension}`) | ||
| } | ||
|
|
||
| // Show folder path | ||
| const folderPath = objectPath.substring(0, objectPath.lastIndexOf('/')) | ||
| if (folderPath) { | ||
| console.log(`📁 Folder: ${folderPath}/`) | ||
| } | ||
| } catch (error: any) { | ||
| console.error('❌ Error checking object:', error.message) | ||
| if (error.$metadata?.httpStatusCode) { | ||
| console.error(` HTTP Status: ${error.$metadata.httpStatusCode}`) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Remove early return that makes size calculation code unreachable
The function returns on line 356 after confirming the object exists, making all the subsequent size calculation and display code unreachable.
console.log('✅ Object exists, fetching details...')
- return
// Calculate size in different units
const size = headResponse!.ContentLength ?? 0📝 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.
| async function object_size(objectPath: string) { | |
| console.log('🔄 Checking if object exists in R2...') | |
| const s3 = await initS3() | |
| try { | |
| // First check if object exists (similar to checkIfExist in s3.ts) | |
| let objectExists = false | |
| let headResponse | |
| try { | |
| const response = await s3.send(new HeadObjectCommand({ | |
| Bucket: S3_BUCKET, | |
| Key: objectPath | |
| })) | |
| const s3Lite = await initS3Lite() | |
| const file = await s3Lite.statObject(convertToValidS3Path(objectPath)) | |
| console.log({ message: 'object_size', file: JSON.stringify(file, null, 2) }) | |
| } catch (error: any) { | |
| if (error.name === 'NoSuchKey' || error.$metadata?.httpStatusCode === 404) { | |
| objectExists = false | |
| } else { | |
| throw error // Re-throw if it's not a "not found" error | |
| } | |
| } | |
| if (!objectExists) { | |
| console.error(`❌ Object not exist in R2: ${objectPath}`) | |
| console.error('\n💡 Tips:') | |
| console.error(' - Make sure the path is exact (case-sensitive)') | |
| console.error(' - Check if the object exists in R2') | |
| console.error(' - Example: orgs/74eea063-512c-4763-beae-1d4ba1c303c5/apps/com.math99.mobile/3.0.384+b.114134.master.1c98e3dfe.zip') | |
| return | |
| } | |
| console.log('✅ Object exists, fetching details...') | |
| return | |
| // Calculate size in different units | |
| const size = headResponse!.ContentLength ?? 0 | |
| const sizeKB = size / 1024 | |
| const sizeMB = sizeKB / 1024 | |
| const sizeGB = sizeMB / 1024 | |
| console.log('\n📄 Object Details (Live from R2)') | |
| console.log('===================================') | |
| console.log(`🔑 Key: ${objectPath}`) | |
| console.log(`💾 Size:`) | |
| console.log(` - ${size.toLocaleString()} bytes`) | |
| console.log(` - ${sizeKB.toFixed(2)} KB`) | |
| console.log(` - ${sizeMB.toFixed(2)} MB`) | |
| if (sizeGB >= 0.01) { | |
| console.log(` - ${sizeGB.toFixed(2)} GB`) | |
| } | |
| if (headResponse!.LastModified) { | |
| console.log(`📅 Last Modified: ${headResponse!.LastModified.toISOString()}`) | |
| } | |
| if (headResponse!.ETag) { | |
| console.log(`🏷️ ETag: ${headResponse!.ETag}`) | |
| } | |
| if (headResponse!.ContentType) { | |
| console.log(`📝 Content Type: ${headResponse!.ContentType}`) | |
| } | |
| if (headResponse!.StorageClass) { | |
| console.log(`💼 Storage Class: ${headResponse!.StorageClass}`) | |
| } | |
| // Show file extension info | |
| const extension = objectPath.split('.').pop() | |
| if (extension) { | |
| console.log(`📝 File Extension: .${extension}`) | |
| } | |
| // Show folder path | |
| const folderPath = objectPath.substring(0, objectPath.lastIndexOf('/')) | |
| if (folderPath) { | |
| console.log(`📁 Folder: ${folderPath}/`) | |
| } | |
| } catch (error: any) { | |
| console.error('❌ Error checking object:', error.message) | |
| if (error.$metadata?.httpStatusCode) { | |
| console.error(` HTTP Status: ${error.$metadata.httpStatusCode}`) | |
| } | |
| } | |
| } | |
| console.log('✅ Object exists, fetching details...') | |
| - return | |
| // Calculate size in different units | |
| const size = headResponse!.ContentLength ?? 0 |
🧰 Tools
🪛 Biome (1.9.4)
[error] 360-403: This code is unreachable
(lint/correctness/noUnreachable)
🤖 Prompt for AI Agents
In scripts/check_r2_big_files.ts between lines 316 and 408, remove the early
return statement on line 356 that occurs right after confirming the object
exists. This return causes the size calculation and display code that follows to
be unreachable. Instead, allow the function to continue executing so it can
calculate and log the object's size and metadata details properly.
| switch (command) { | ||
| case 'fetch_objects': | ||
| await fetch_objects() | ||
| break | ||
| case 'json_big_files': | ||
| await json_big_files() | ||
| break | ||
| case 'folder_size': | ||
| const folderPath = args[1] | ||
| if (!folderPath) { | ||
| console.error('❌ No folder path specified') | ||
| console.error('Usage: ts-node check_r2_big_files.ts folder_size <folder_path>') | ||
| process.exit(1) | ||
| } | ||
| await folder_size(folderPath) | ||
| break | ||
| case 'total_size': | ||
| await total_size() | ||
| break | ||
| case 'object_size': | ||
| const objectPath = args[1] | ||
| if (!objectPath) { | ||
| console.error('❌ No object path specified') | ||
| console.error('Usage: ts-node check_r2_big_files.ts object_size <object_path>') | ||
| console.error('Example: ts-node check_r2_big_files.ts object_size orgs/74eea063-512c-4763-beae-1d4ba1c303c5/apps/com.math99.mobile/3.0.384+b.114134.master.1c98e3dfe.zip') | ||
| process.exit(1) | ||
| } | ||
| await object_size(objectPath) | ||
| break | ||
| case 'export_files_folder_to_csv': | ||
| const csvFolderPath = args[1] | ||
| if (!csvFolderPath) { | ||
| console.error('❌ No folder path specified') | ||
| console.error('Usage: ts-node check_r2_big_files.ts export_files_folder_to_csv <folder_path>') | ||
| console.error('Example: ts-node check_r2_big_files.ts export_files_folder_to_csv orgs/my-org/apps/') | ||
| process.exit(1) | ||
| } | ||
| await export_files_folder_to_csv(csvFolderPath) | ||
| break | ||
| case 'get_app_versions': | ||
| await get_app_versions() | ||
| break | ||
| case 'get_big_orgs': | ||
| await get_big_orgs() | ||
| break | ||
| case 'export_supabase_csv': | ||
| const orgId = args[1] | ||
| if (!orgId) { | ||
| console.error('❌ No organization ID specified') | ||
| console.error('Usage: ts-node check_r2_big_files.ts export_supabase_csv <org_id>') | ||
| process.exit(1) | ||
| } | ||
| await export_supabase_csv(orgId) | ||
| break | ||
| case 'prepare_cleanup_zip': | ||
| await prepare_cleanup_zip() | ||
| break | ||
| case 'copy_cleanup_candidates_to_backup_bucket': | ||
| await copy_cleanup_candidates_to_backup_bucket() | ||
| break | ||
| case 'copy_cleanup_candidates_direct': | ||
| await copy_cleanup_candidates_direct() | ||
| break | ||
| case 'delete_cleanup_candidates': | ||
| await delete_cleanup_candidates() | ||
| break | ||
| default: | ||
| console.error(`❌ Unknown command: ${command}`) | ||
| console.error('Available commands: fetch_objects, json_big_files, folder_size, total_size, object_size, export_files_folder_to_csv, get_app_versions, get_big_orgs, export_supabase_csv, prepare_cleanup_zip, copy_cleanup_candidates_to_backup_bucket, copy_cleanup_candidates_direct, delete_cleanup_candidates') | ||
| process.exit(1) | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Wrap switch case declarations in blocks to prevent variable leakage
Variables declared in switch cases without blocks can be accessed by other cases, which can lead to unexpected behavior.
switch (command) {
case 'fetch_objects':
await fetch_objects()
break
case 'json_big_files':
await json_big_files()
break
- case 'folder_size':
+ case 'folder_size': {
const folderPath = args[1]
if (!folderPath) {
console.error('❌ No folder path specified')
console.error('Usage: ts-node check_r2_big_files.ts folder_size <folder_path>')
process.exit(1)
}
await folder_size(folderPath)
break
+ }
case 'total_size':
await total_size()
break
- case 'object_size':
+ case 'object_size': {
const objectPath = args[1]
if (!objectPath) {
console.error('❌ No object path specified')
console.error('Usage: ts-node check_r2_big_files.ts object_size <object_path>')
console.error('Example: ts-node check_r2_big_files.ts object_size orgs/74eea063-512c-4763-beae-1d4ba1c303c5/apps/com.math99.mobile/3.0.384+b.114134.master.1c98e3dfe.zip')
process.exit(1)
}
await object_size(objectPath)
break
+ }
- case 'export_files_folder_to_csv':
+ case 'export_files_folder_to_csv': {
const csvFolderPath = args[1]
if (!csvFolderPath) {
console.error('❌ No folder path specified')
console.error('Usage: ts-node check_r2_big_files.ts export_files_folder_to_csv <folder_path>')
console.error('Example: ts-node check_r2_big_files.ts export_files_folder_to_csv orgs/my-org/apps/')
process.exit(1)
}
await export_files_folder_to_csv(csvFolderPath)
break
+ }
case 'get_app_versions':
await get_app_versions()
break
case 'get_big_orgs':
await get_big_orgs()
break
- case 'export_supabase_csv':
+ case 'export_supabase_csv': {
const orgId = args[1]
if (!orgId) {
console.error('❌ No organization ID specified')
console.error('Usage: ts-node check_r2_big_files.ts export_supabase_csv <org_id>')
process.exit(1)
}
await export_supabase_csv(orgId)
break
+ }📝 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.
| switch (command) { | |
| case 'fetch_objects': | |
| await fetch_objects() | |
| break | |
| case 'json_big_files': | |
| await json_big_files() | |
| break | |
| case 'folder_size': | |
| const folderPath = args[1] | |
| if (!folderPath) { | |
| console.error('❌ No folder path specified') | |
| console.error('Usage: ts-node check_r2_big_files.ts folder_size <folder_path>') | |
| process.exit(1) | |
| } | |
| await folder_size(folderPath) | |
| break | |
| case 'total_size': | |
| await total_size() | |
| break | |
| case 'object_size': | |
| const objectPath = args[1] | |
| if (!objectPath) { | |
| console.error('❌ No object path specified') | |
| console.error('Usage: ts-node check_r2_big_files.ts object_size <object_path>') | |
| console.error('Example: ts-node check_r2_big_files.ts object_size orgs/74eea063-512c-4763-beae-1d4ba1c303c5/apps/com.math99.mobile/3.0.384+b.114134.master.1c98e3dfe.zip') | |
| process.exit(1) | |
| } | |
| await object_size(objectPath) | |
| break | |
| case 'export_files_folder_to_csv': | |
| const csvFolderPath = args[1] | |
| if (!csvFolderPath) { | |
| console.error('❌ No folder path specified') | |
| console.error('Usage: ts-node check_r2_big_files.ts export_files_folder_to_csv <folder_path>') | |
| console.error('Example: ts-node check_r2_big_files.ts export_files_folder_to_csv orgs/my-org/apps/') | |
| process.exit(1) | |
| } | |
| await export_files_folder_to_csv(csvFolderPath) | |
| break | |
| case 'get_app_versions': | |
| await get_app_versions() | |
| break | |
| case 'get_big_orgs': | |
| await get_big_orgs() | |
| break | |
| case 'export_supabase_csv': | |
| const orgId = args[1] | |
| if (!orgId) { | |
| console.error('❌ No organization ID specified') | |
| console.error('Usage: ts-node check_r2_big_files.ts export_supabase_csv <org_id>') | |
| process.exit(1) | |
| } | |
| await export_supabase_csv(orgId) | |
| break | |
| case 'prepare_cleanup_zip': | |
| await prepare_cleanup_zip() | |
| break | |
| case 'copy_cleanup_candidates_to_backup_bucket': | |
| await copy_cleanup_candidates_to_backup_bucket() | |
| break | |
| case 'copy_cleanup_candidates_direct': | |
| await copy_cleanup_candidates_direct() | |
| break | |
| case 'delete_cleanup_candidates': | |
| await delete_cleanup_candidates() | |
| break | |
| default: | |
| console.error(`❌ Unknown command: ${command}`) | |
| console.error('Available commands: fetch_objects, json_big_files, folder_size, total_size, object_size, export_files_folder_to_csv, get_app_versions, get_big_orgs, export_supabase_csv, prepare_cleanup_zip, copy_cleanup_candidates_to_backup_bucket, copy_cleanup_candidates_direct, delete_cleanup_candidates') | |
| process.exit(1) | |
| } | |
| switch (command) { | |
| case 'fetch_objects': | |
| await fetch_objects() | |
| break | |
| case 'json_big_files': | |
| await json_big_files() | |
| break | |
| case 'folder_size': { | |
| const folderPath = args[1] | |
| if (!folderPath) { | |
| console.error('❌ No folder path specified') | |
| console.error('Usage: ts-node check_r2_big_files.ts folder_size <folder_path>') | |
| process.exit(1) | |
| } | |
| await folder_size(folderPath) | |
| break | |
| } | |
| case 'total_size': | |
| await total_size() | |
| break | |
| case 'object_size': { | |
| const objectPath = args[1] | |
| if (!objectPath) { | |
| console.error('❌ No object path specified') | |
| console.error('Usage: ts-node check_r2_big_files.ts object_size <object_path>') | |
| console.error('Example: ts-node check_r2_big_files.ts object_size orgs/74eea063-512c-4763-beae-1d4ba1c303c5/apps/com.math99.mobile/3.0.384+b.114134.master.1c98e3dfe.zip') | |
| process.exit(1) | |
| } | |
| await object_size(objectPath) | |
| break | |
| } | |
| case 'export_files_folder_to_csv': { | |
| const csvFolderPath = args[1] | |
| if (!csvFolderPath) { | |
| console.error('❌ No folder path specified') | |
| console.error('Usage: ts-node check_r2_big_files.ts export_files_folder_to_csv <folder_path>') | |
| console.error('Example: ts-node check_r2_big_files.ts export_files_folder_to_csv orgs/my-org/apps/') | |
| process.exit(1) | |
| } | |
| await export_files_folder_to_csv(csvFolderPath) | |
| break | |
| } | |
| case 'get_app_versions': | |
| await get_app_versions() | |
| break | |
| case 'get_big_orgs': | |
| await get_big_orgs() | |
| break | |
| case 'export_supabase_csv': { | |
| const orgId = args[1] | |
| if (!orgId) { | |
| console.error('❌ No organization ID specified') | |
| console.error('Usage: ts-node check_r2_big_files.ts export_supabase_csv <org_id>') | |
| process.exit(1) | |
| } | |
| await export_supabase_csv(orgId) | |
| break | |
| } | |
| case 'prepare_cleanup_zip': | |
| await prepare_cleanup_zip() | |
| break | |
| case 'copy_cleanup_candidates_to_backup_bucket': | |
| await copy_cleanup_candidates_to_backup_bucket() | |
| break | |
| case 'copy_cleanup_candidates_direct': | |
| await copy_cleanup_candidates_direct() | |
| break | |
| case 'delete_cleanup_candidates': | |
| await delete_cleanup_candidates() | |
| break | |
| default: | |
| console.error(`❌ Unknown command: ${command}`) | |
| console.error('Available commands: fetch_objects, json_big_files, folder_size, total_size, object_size, export_files_folder_to_csv, get_app_versions, get_big_orgs, export_supabase_csv, prepare_cleanup_zip, copy_cleanup_candidates_to_backup_bucket, copy_cleanup_candidates_direct, delete_cleanup_candidates') | |
| process.exit(1) | |
| } |
🧰 Tools
🪛 Biome (1.9.4)
[error] 51-52: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.
The declaration is defined in this switch clause:
Unsafe fix: Wrap the declaration in a block.
(lint/correctness/noSwitchDeclarations)
[error] 63-64: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.
The declaration is defined in this switch clause:
Unsafe fix: Wrap the declaration in a block.
(lint/correctness/noSwitchDeclarations)
[error] 73-74: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.
The declaration is defined in this switch clause:
Unsafe fix: Wrap the declaration in a block.
(lint/correctness/noSwitchDeclarations)
[error] 89-90: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.
The declaration is defined in this switch clause:
Unsafe fix: Wrap the declaration in a block.
(lint/correctness/noSwitchDeclarations)
🤖 Prompt for AI Agents
In scripts/check_r2_big_files.ts between lines 43 and 113, each switch case
declares variables without enclosing them in blocks, causing variable leakage
across cases. To fix this, wrap the code inside each case that declares
variables in curly braces {} to create a block scope, ensuring variables are
scoped only within their respective cases and preventing unintended access or
conflicts.
| import { writeFileSync, existsSync, readFileSync } from 'fs' | ||
| import { S3Client as S3ClientLite } from '@bradenmacdonald/s3-lite-client/mod.ts' | ||
| import postgres from 'postgres' | ||
| import { Context } from 'vm' |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Incorrect Context import from 'vm' module
The Context type imported from Node's vm module is for script execution contexts, not web framework contexts. Based on the usage pattern in getPgClient, this should likely be imported from your web framework.
🏁 Script executed:
#!/bin/bash
# Check for proper Context type definition in backend utilities
rg -A 5 "export.*Context|interface.*Context|type.*Context" --glob "**/*.ts" --glob "!node_modules/**"
# Check how Context is used in other backend files
ast-grep --pattern 'import { $$$, Context, $$$ } from $_'Length of output: 86368
Replace Node ‘vm’ Context import with Hono’s Context type
The script is currently pulling in Context from Node’s VM API, but getPgClient and related utilities all expect the Hono Context (imported throughout the backend as import type { Context } from '@hono/hono'). Update your import and ensure your mock contexts satisfy the minimal Hono interface (.get(), .env, etc.).
Locations to update:
- scripts/check_r2_big_files.ts, line 5
Suggested diff:
--- a/scripts/check_r2_big_files.ts
+++ b/scripts/check_r2_big_files.ts
@@ -5,1 +5,1 @@
-import { Context } from 'vm'
+import type { Context } from '@hono/hono'Make sure your mock context objects (lines 659, 759, 1093) implement at least the methods and properties getPgClient uses (e.g. c.get, c.env).
🤖 Prompt for AI Agents
In scripts/check_r2_big_files.ts at line 5, replace the import of Context from
Node's 'vm' module with the Context type from '@hono/hono' to match the expected
web framework context used in getPgClient. Additionally, update any mock context
objects around lines 659, 759, and 1093 to implement the minimal Hono Context
interface, including methods like .get() and properties like .env, ensuring
compatibility with getPgClient usage.
| let objectExists = false | ||
| let headResponse | ||
|
|
||
| try { | ||
|
|
||
| const response = await s3.send(new HeadObjectCommand({ | ||
| Bucket: S3_BUCKET, | ||
| Key: objectPath | ||
| })) | ||
|
|
||
| const s3Lite = await initS3Lite() | ||
|
|
||
| const file = await s3Lite.statObject(convertToValidS3Path(objectPath)) | ||
| console.log({ message: 'object_size', file: JSON.stringify(file, null, 2) }) | ||
|
|
||
|
|
||
| } catch (error: any) { | ||
| if (error.name === 'NoSuchKey' || error.$metadata?.httpStatusCode === 404) { | ||
| objectExists = false | ||
| } else { | ||
| throw error // Re-throw if it's not a "not found" error | ||
| } | ||
| } | ||
|
|
||
| if (!objectExists) { | ||
| console.error(`❌ Object not exist in R2: ${objectPath}`) | ||
| console.error('\n💡 Tips:') | ||
| console.error(' - Make sure the path is exact (case-sensitive)') | ||
| console.error(' - Check if the object exists in R2') | ||
| console.error(' - Example: orgs/74eea063-512c-4763-beae-1d4ba1c303c5/apps/com.math99.mobile/3.0.384+b.114134.master.1c98e3dfe.zip') | ||
| return | ||
| } | ||
|
|
There was a problem hiding this comment.
Fix object existence check logic
The objectExists flag is never set to true when the object is found, and headResponse may be undefined when used later.
let objectExists = false
let headResponse
try {
-
- const response = await s3.send(new HeadObjectCommand({
+ headResponse = await s3.send(new HeadObjectCommand({
Bucket: S3_BUCKET,
Key: objectPath
}))
+ objectExists = true
const s3Lite = await initS3Lite()
const file = await s3Lite.statObject(convertToValidS3Path(objectPath))
console.log({ message: 'object_size', file: JSON.stringify(file, null, 2) })
-
-
} catch (error: any) {
if (error.name === 'NoSuchKey' || error.$metadata?.httpStatusCode === 404) {
objectExists = false
} else {
throw error // Re-throw if it's not a "not found" error
}
}📝 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.
| let objectExists = false | |
| let headResponse | |
| try { | |
| const response = await s3.send(new HeadObjectCommand({ | |
| Bucket: S3_BUCKET, | |
| Key: objectPath | |
| })) | |
| const s3Lite = await initS3Lite() | |
| const file = await s3Lite.statObject(convertToValidS3Path(objectPath)) | |
| console.log({ message: 'object_size', file: JSON.stringify(file, null, 2) }) | |
| } catch (error: any) { | |
| if (error.name === 'NoSuchKey' || error.$metadata?.httpStatusCode === 404) { | |
| objectExists = false | |
| } else { | |
| throw error // Re-throw if it's not a "not found" error | |
| } | |
| } | |
| if (!objectExists) { | |
| console.error(`❌ Object not exist in R2: ${objectPath}`) | |
| console.error('\n💡 Tips:') | |
| console.error(' - Make sure the path is exact (case-sensitive)') | |
| console.error(' - Check if the object exists in R2') | |
| console.error(' - Example: orgs/74eea063-512c-4763-beae-1d4ba1c303c5/apps/com.math99.mobile/3.0.384+b.114134.master.1c98e3dfe.zip') | |
| return | |
| } | |
| let objectExists = false | |
| let headResponse | |
| try { | |
| headResponse = await s3.send(new HeadObjectCommand({ | |
| Bucket: S3_BUCKET, | |
| Key: objectPath | |
| })) | |
| objectExists = true | |
| const s3Lite = await initS3Lite() | |
| const file = await s3Lite.statObject(convertToValidS3Path(objectPath)) | |
| console.log({ message: 'object_size', file: JSON.stringify(file, null, 2) }) | |
| } catch (error: any) { | |
| if (error.name === 'NoSuchKey' || error.$metadata?.httpStatusCode === 404) { | |
| objectExists = false | |
| } else { | |
| throw error // Re-throw if it's not a "not found" error | |
| } | |
| } | |
| if (!objectExists) { | |
| console.error(`❌ Object not exist in R2: ${objectPath}`) | |
| console.error('\n💡 Tips:') | |
| console.error(' - Make sure the path is exact (case-sensitive)') | |
| console.error(' - Check if the object exists in R2') | |
| console.error(' - Example: orgs/74eea063-512c-4763-beae-1d4ba1c303c5/apps/com.math99.mobile/3.0.384+b.114134.master.1c98e3dfe.zip') | |
| return | |
| } |
🤖 Prompt for AI Agents
In scripts/check_r2_big_files.ts around lines 322 to 354, the objectExists flag
is never set to true when the object is found, causing incorrect existence
checks. Fix this by setting objectExists to true immediately after successfully
receiving the HeadObjectCommand response. Also, assign the response to
headResponse to avoid it being undefined later. This ensures the logic correctly
reflects whether the object exists or not.
Summary by CodeRabbit
New Features
Bug Fixes
Chores
Database & API Changes