Skip to content

fix: version_meta table#1150

Merged
riderx merged 3 commits into
mainfrom
fix_version_meta
Jun 9, 2025
Merged

fix: version_meta table#1150
riderx merged 3 commits into
mainfrom
fix_version_meta

Conversation

@WcaleNieWolny
Copy link
Copy Markdown
Contributor

@WcaleNieWolny WcaleNieWolny commented Jun 8, 2025

Summary by CodeRabbit

  • New Features

    • Introduced a command-line tool for managing and analyzing large files in S3-compatible storage, including reporting, exporting, and cleanup operations.
    • Added support for exporting and analyzing storage and app version data, with CSV and JSON outputs.
  • Bug Fixes

    • Improved error handling during version metadata updates to ensure issues are logged.
  • Chores

    • Added the AWS S3 SDK as a new dependency.
    • Updated ignore rules to exclude certain script output files.
  • Database & API Changes

    • Enhanced database schema with new fields, functions, and stricter constraints for version metadata.
    • Added a secure upsert function for version metadata to prevent duplicates and improve data integrity.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 8, 2025

Walkthrough

This 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 .gitignore rules for the scripts directory. It also modifies several Supabase backend utilities and types, implements a robust deduplication and upsert mechanism for the version_meta table via a migration and stored procedure, and updates related database type definitions.

Changes

File(s) Change Summary
.gitignore Added ignore patterns for .json, .txt, and .csv files in the scripts directory.
package.json Added @aws-sdk/client-s3 dependency (^3.821.0).
scripts/check_r2_big_files.ts New script for S3 (R2) storage analysis, file management, database correlation, and cleanup operations with multiple command-line subcommands.
supabase/functions/_backend/triggers/on_version_update.ts Enhanced error handling for createStatsMeta call by capturing and logging errors.
supabase/functions/_backend/utils/supabase.ts Changed trackMetaSB to use the upsert_version_meta stored procedure instead of a direct insert.
supabase/functions/_backend/utils/supabase.types.ts Extended Database type: added graphql_public schema, new fields to several tables, new functions (including upsert_version_meta), removed some enums/composite types, and updated constants to match schema changes.
supabase/migrations/20250608130257_fix_version_meta.sql Migration to deduplicate version_meta, add partial unique indexes, create the upsert_version_meta function for safe upserts, and restrict function execution to the service role.

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
Loading
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
Loading

Poem

In the land of buckets and bytes,
A rabbit hops through storage heights.
With scripts and types, and upserts anew,
It tidies up data, as all good bunnies do.
From deduped tables to S3 skies—
The code now leaps, and storage flies!
🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need 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)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@riderx
Copy link
Copy Markdown
Member

riderx commented Jun 8, 2025

⚠️ Snyk checks are incomplete.

⚠️ security/snyk check encountered an error. (View Details)

@socket-security
Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​@​aws-sdk/​client-s3@​3.826.09810010097100

View full report

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🔭 Outside diff range comments (1)
supabase/functions/_backend/utils/supabase.types.ts (1)

701-740: 🛠️ Refactor suggestion

Inconsistent devices table type definitions

The id field is removed from the Row type but still referenced in Insert and Update types with id?: 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 entries

The partial unique indexes for positive and negative sizes are well-designed. However, you might want to consider adding a similar constraint for size = 0 entries 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 timeout

The 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

📥 Commits

Reviewing files that changed from the base of the PR and between 3bb9fc4 and 5423ded.

⛔ Files ignored due to path filters (1)
  • bun.lock is 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-s3

Length 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 run npm audit to 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 createStatsMeta enhances 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_meta stored 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 sql

Length of output: 2189


Stored procedure verified: upsert_version_meta is defined

The upsert_version_meta function 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 getEnv in scripts/check_r2_big_files.ts reads from Node’s process.env, whereas the utilities in supabase/functions/_backend/utils/utils.ts operate on an Edge Function Context via env(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 function

The upsert_version_meta function type properly reflects the PL/pgSQL function created in the migration, ensuring type-safe RPC calls.

Comment on lines +86 to +135
-- 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;
$$;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
-- 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.

Comment on lines +316 to +408
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}`)
}
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
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.

Comment on lines +43 to +113
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)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ 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.

Suggested change
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'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 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.

Comment on lines +322 to +354
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
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

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.

Suggested change
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.

@riderx riderx merged commit b50fb70 into main Jun 9, 2025
10 of 11 checks passed
@riderx riderx deleted the fix_version_meta branch June 9, 2025 13:10
@coderabbitai coderabbitai Bot mentioned this pull request Oct 22, 2025
5 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants