Skip to content

refactor: standardize file storage and metadata management#446

Open
harbassan wants to merge 13 commits into
masterfrom
vps-137-standardize-file-storage-and-metadata-management
Open

refactor: standardize file storage and metadata management#446
harbassan wants to merge 13 commits into
masterfrom
vps-137-standardize-file-storage-and-metadata-management

Conversation

@harbassan

@harbassan harbassan commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Issue

The file upload system we use is inconsistent, which makes development unnecessarily difficult and increases our consumption of external resources (firestore, gridfs).

Essentially, we use both firebase storage and gridfs for file storage, and both firestore and mongodb for metadata storage. Even within firebase storage, the method we use to upload files and the paths they are stored with vary across our different upload endpoints. The ticket description has more info.

Solution

There is now a single upload endpoint to create files, which handles the storage on firebase alone and the metadata on mongodb alone. This metadata object contains a ref counter, which is in sync with all referencing documents (resource, scene component). This system means that we can minimise duplicate files in storage.

This is a soft delete system, so files are only marked for deletion, not actually deleted. The actual deletion will be handled later, as a service that occasionally checks for ready to delete files and prunes the metadata and file.

Again for more info read the ticket description.

Risk

THIS IS NOT BACKWARDS COMPATIBLE WITH EXISTING RESOURCES

This refactor required a major modification to the resources backend, given the previously tight coupling of the file concept and resource cocnept, and the use of gridfs.

Ive added many tests for the new and modified functionalities, coverage is close to 100%.

Checklist

  • Acceptance criteria met
  • Wiki documentation is written and up to date
  • Unit tests written and passing
  • Integration tests written and passing
  • Continuous integration build passing

Summary by CodeRabbit

  • New Features

    • Added scenario-scoped uploads for images, audio, and documents with centralized storage, plus scenario-scoped file retrieval (including image lists).
    • Introduced resource-based creation/deletion with state-conditional management, including automatic file reference tracking.
  • Bug Fixes

    • Improved authentication handling and error responses across backend and updated frontend flows for consistent uploads and previews.
    • Fixed image selection highlighting and broadened accepted audio types; improved preview rendering based on content type.
  • Tests

    • Expanded and refocused integration test coverage for uploads, file retrieval, resources/conditionals, and auth/error behavior.

@linear

linear Bot commented Jul 15, 2026

Copy link
Copy Markdown

VPS-137

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR migrates file handling from GridFS and client-side Firebase storage to Firebase Admin Storage with UploadedFile records. Resources reference uploaded files, APIs become scenario-scoped, reference counts are maintained, and frontend authoring and preview flows consume the new contracts.

Changes

File and resource migration

Layer / File(s) Summary
Firebase storage and upload contracts
backend/src/firebase/*, backend/src/db/models/uploadedFile.js, backend/jest.*
Firebase Admin uploads and deletes files, while UploadedFile stores scenario-scoped metadata and lifecycle fields.
Resource schema and reference counting
backend/src/db/models/resource.js, backend/src/db/daos/fileDao.js, backend/src/db/daos/sceneDao.js
Resources reference scenarios, groups, and uploaded files; reference deltas update refCount and deletedAt during resource and scene changes.
Scenario-scoped backend APIs
backend/src/routes/api/*, backend/src/middleware/*, backend/src/index.js
File, resource, collection, and authentication routes use the new storage model, while legacy GridFS and image/group-resource endpoints are removed.
Authoring and resource management
frontend/src/features/authoring/*, frontend/src/features/resources/*, frontend/src/components/*
Image and audio authoring uploads through the API, resource management creates and deletes resource records, and conditional endpoints use scenario-scoped resource paths.
Resource previews
frontend/src/features/playScenario/*
Resource previews use normalized file metadata, direct URLs, and React Query for text loading and preview state.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AuthoringUI
  participant FilesAPI
  participant FirebaseStorage
  participant ResourcesAPI
  participant MongoDB
  AuthoringUI->>FilesAPI: Upload file for scenario
  FilesAPI->>FirebaseStorage: Save buffer and metadata
  FirebaseStorage-->>FilesAPI: Return path and URL
  FilesAPI->>MongoDB: Create UploadedFile
  FilesAPI-->>AuthoringUI: Return uploaded file
  AuthoringUI->>ResourcesAPI: Create resource with fileId
  ResourcesAPI->>MongoDB: Increment file refCount
  ResourcesAPI-->>AuthoringUI: Return resource
Loading

Possibly related PRs

  • UoaWDCC/VPS#393: Both PRs modify state-conditional endpoint wiring between the frontend and resource APIs.

Suggested labels: backend

Suggested reviewers: del-ereno

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.35% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is concise and accurately reflects the main change: standardizing file storage and metadata management.
Description check ✅ Passed The description follows the required Issue/Solution/Risk/Checklist template and covers the main refactor, with only noncritical checklist items left unchecked.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch vps-137-standardize-file-storage-and-metadata-management

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

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 15

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
frontend/src/features/resources/ManageResourcesPage.jsx (1)

92-114: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add user to the load effect. api.get calls user.getIdToken(), so this runs too early when auth is still resolving, the first fetch fails, and it never reruns when user becomes available. Guard on user and depend on [scenarioId, user].

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/features/resources/ManageResourcesPage.jsx` around lines 92 -
114, Update the groups/files-loading useEffect to return early when user is
unavailable, add user to its dependency array alongside scenarioId, and only
call api.get after authentication resolves so the request reruns with the
available user.
backend/src/routes/api/collections.js (1)

85-104: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Decrement file refs before deleting a group. Resource.deleteMany({ groupId }) bypasses the applyReferenceDelta path used in backend/src/routes/api/resources.js, so the deleted group's files keep their refCount and never reach deletedAt/cleanup. Build a fileId delta map from files and apply it before removing the resources.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/routes/api/collections.js` around lines 85 - 104, Update the
DELETE /groups/:groupId handler to build a fileId delta map from the queried
files and apply those decrements through the existing applyReferenceDelta path
before Resource.deleteMany and group.deleteOne. Preserve the current response
counts and not-found behavior.
🧹 Nitpick comments (9)
frontend/src/features/authoring/images.tsx (1)

39-64: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Invalidate the images query cache after a successful upload.

addNewImage uploads and adds the image directly to the scene, but doesn't invalidate the ["images", scenarioId] query. Since imagesQuery lives in the parent ImageCreateMenu (it doesn't remount when the "Select Existing Image" modal opens), a newly uploaded image won't show up in that picker later in the same session without a manual refetch.

♻️ Proposed fix
+import { useQuery, useQueryClient } from "`@tanstack/react-query`";
...
 function ImageCreateMenu() {
   const { scenarioId } = useParams<{ scenarioId: string }>();
   const [selectedImage, setSelectedImage] = useState<UploadedFile | null>(null);
+  const queryClient = useQueryClient();
...
   function handleFileChange(event: React.ChangeEvent<HTMLInputElement>) {
     const file = event.target.files?.[0];
-    if (file) addNewImage(file, scenarioId, user).catch(handleGeneric);
+    if (file)
+      addNewImage(file, scenarioId, user)
+        .then(() => queryClient.invalidateQueries({ queryKey: ["images", scenarioId] }))
+        .catch(handleGeneric);
   }

Also applies to: 77-104

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/features/authoring/images.tsx` around lines 39 - 64, Update
addNewImage so that after a successful upload and scene insertion, it
invalidates the ["images", scenarioId] query cache. Reuse the existing
query-client/cache mechanism used by ImageCreateMenu, and keep invalidation out
of the failure path.
backend/src/firebase/storage.js (2)

21-23: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Wrapping swallows the original error's stack/type.

Rethrowing as new Error(upload failed: ${err.message}) discards err's stack trace and any structured fields (e.g. GCS err.code), making production diagnosis harder.

🐛 Suggested fix
   } catch (err) {
-    throw new Error(`upload failed: ${err.message}`);
+    throw new Error(`upload failed: ${err.message}`, { cause: err });
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/firebase/storage.js` around lines 21 - 23, Update the catch
handling around the upload operation to preserve the original error object,
including its stack trace and structured fields such as code, while retaining
the “upload failed” context. Avoid creating a plain Error from only err.message;
use the existing upload failure path and nearest relevant symbol.

12-20: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Disable resumable uploads for these small buffers.

All uploads here are capped by MAX_FILE_SIZE_MB (backend/src/routes/api/files.js, default 10MB) yet file.save() defaults to a resumable upload, which adds an extra initiate round-trip per request. The Node Cloud Storage client recommends resumable: false for files under 10MB.

⚡ Suggested fix
     await file.save(buffer, {
       metadata: {
         contentType,
         metadata: {
           firebaseStorageDownloadTokens: downloadToken,
         },
       },
+      resumable: false,
     });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/firebase/storage.js` around lines 12 - 20, Update the options
passed to file.save in the upload flow to set resumable to false, while
preserving the existing buffer and metadata handling for files capped by
MAX_FILE_SIZE_MB.
backend/src/firebase/firebase.js (1)

1-14: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Consider guarding against double initializeApp() calls.

admin.initializeApp() runs unconditionally at import time; calling it twice without a name throws The default Firebase app already exists. A one-line guard is the standard mitigation:

🛡️ Suggested guard
-admin.initializeApp({
-  credential: admin.credential.cert({
-    projectId: process.env.FIREBASE_PROJECT_ID,
-    clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
-    privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, "\n"),
-  }),
-  storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
-});
+if (!admin.apps.length) {
+  admin.initializeApp({
+    credential: admin.credential.cert({
+      projectId: process.env.FIREBASE_PROJECT_ID,
+      clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
+      privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, "\n"),
+    }),
+    storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
+  });
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/firebase/firebase.js` around lines 1 - 14, Guard the
unconditionally executed admin.initializeApp call in the Firebase setup so
initialization occurs only when the default app does not already exist. Preserve
the existing credential and storageBucket configuration, and leave getBucket and
getAuth unchanged.
backend/src/db/models/uploadedFile.js (1)

16-21: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Redundant single-field index on scenarioId.

scenarioId has its own index (line 20) and also leads the compound index { scenarioId: 1, createdAt: -1 } (line 28). The compound index already serves scenarioId-only queries as a prefix, so the standalone index adds write overhead without additional query benefit.

♻️ Proposed fix
     scenarioId: {
       type: Schema.Types.ObjectId,
       ref: "Scenario",
       required: true,
-      index: true,
     },

Also applies to: 28-28

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/db/models/uploadedFile.js` around lines 16 - 21, Remove the
standalone index declaration from the scenarioId field in the uploaded file
schema, while preserving the compound { scenarioId: 1, createdAt: -1 } index for
scenarioId-based queries.
backend/src/db/daos/fileDao.js (1)

8-50: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate pipeline logic between applyReferenceDeltas and applyReferenceDelta.

Both functions build an identical two-stage refCount/deletedAt pipeline. Consider having the single-item version delegate to the bulk version (e.g. applyReferenceDeltas(new Map([[fileId, delta]]))), or extracting the pipeline array into a shared helper, so any future change to this logic (e.g. tweaking the deletedAt condition) doesn't need to be kept in sync in two places.

♻️ Proposed fix
+function buildRefCountPipeline(delta) {
+  return [
+    { $set: { refCount: { $add: ["$refCount", delta] } } },
+    {
+      $set: {
+        deletedAt: { $cond: [{ $lte: ["$refCount", 0] }, "$$NOW", "$$REMOVE"] },
+      },
+    },
+  ];
+}
+
 export async function applyReferenceDeltas(fileRefDeltas) {
   if (fileRefDeltas.size === 0) return;

   const fileOperations = Array.from(fileRefDeltas.entries())
     .filter(([, delta]) => delta !== 0)
     .map(([fileId, delta]) => ({
       updateOne: {
         filter: { _id: fileId },
-        update: [
-          { $set: { refCount: { $add: ["$refCount", delta] } } },
-          {
-            $set: {
-              deletedAt: {
-                $cond: [{ $lte: ["$refCount", 0] }, "$$NOW", "$$REMOVE"],
-              },
-            },
-          },
-        ],
+        update: buildRefCountPipeline(delta),
       },
     }));
   ...
 export async function applyReferenceDelta(fileId, delta) {
   if (delta === 0) return;
-  await UploadedFile.updateOne({ _id: fileId }, [
-    { $set: { refCount: { $add: ["$refCount", delta] } } },
-    {
-      $set: {
-        deletedAt: { $cond: [{ $lte: ["$refCount", 0] }, "$$NOW", "$$REMOVE"] },
-      },
-    },
-  ]);
+  await UploadedFile.updateOne({ _id: fileId }, buildRefCountPipeline(delta));
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/db/daos/fileDao.js` around lines 8 - 50, Remove the duplicated
update pipeline from applyReferenceDelta by delegating its nonzero single-file
operation to applyReferenceDeltas, using a Map containing fileId and delta.
Preserve the existing zero-delta early return and ensure the bulk helper remains
the single source of truth for refCount and deletedAt updates.
backend/src/db/models/resource.js (1)

5-10: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Redundant single-field index on scenarioId.

Same pattern as uploadedFile.js: scenarioId has its own index (line 9) and is also the leading field of the compound index (lines 41-45), making the standalone index redundant.

Also applies to: 41-45

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/db/models/resource.js` around lines 5 - 10, Remove the standalone
index declaration from the scenarioId field in the Resource schema, while
preserving scenarioId as the leading field of the existing compound index.
backend/src/db/models/scene.js (1)

1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Commented-out pre("remove") hook was already dead code under Mongoose 8.

Mongoose 7+ removed Document.prototype.remove() entirely (doc.remove() no longer exists; migration path is doc.deleteOne() / schema.pre('deleteOne', { document: true, query: false }, ...)). Since this project is on mongoose 8.7.0, pre("remove") could never have fired here regardless of this diff, so commenting it out is not a functional regression. That said, leaving a large commented block (with the removed tryDeleteFile import gone) is dead weight — prefer deleting it and tracking the "scheduled remove of unused files" follow-up in an issue/ticket rather than an inline comment.

♻️ Suggested cleanup
-// NOTE: this will be replaced by a scheduled remove of unused files
-//
-// before removal of scene from the database, first attempt to delete all user-uploaded images from firebase
-// sceneSchema.pre("remove", function () {
-//   this.components.forEach((c) => {
-//     if (c.type === "image" || c.type === "audio") {
-//       tryDeleteFile(c.href ?? c.url);
-//     }
-//   });
-// });
+// File cleanup on scene deletion is handled by a scheduled sweep of
+// unreferenced UploadedFile records (see <tracking-issue-link>).

Also applies to: 39-48

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/db/models/scene.js` at line 1, Delete the commented-out
pre("remove") hook block and any associated dead-code comments or remnants in
the scene model; leave the active Mongoose schema behavior unchanged and track
the unused-file removal follow-up separately.
backend/src/middleware/firebaseAuth.js (1)

8-24: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Harden auth against unhandled rejections; the promise chain is fire-and-forget.

The .then()/.catch() chain is neither returned nor awaited, so auth()'s own promise resolves independently of token verification. If getAuth() throws synchronously (e.g. Firebase Admin not yet initialized), that exception is never caught anywhere — Express doesn't await middleware return values, so it becomes an unhandled rejection that can crash the process. Using await + try/catch fixes this and also removes the reliance on manual microtask flushing in tests.

🔒️ Suggested fix
 export default async function auth(req, res, next) {
   if (!req.headers.authorization) {
     res.sendStatus(HttpStatusCode.Unauthorized);
     return;
   }

   const token = req.headers.authorization.split(" ")[1];
-  getAuth()
-    .verifyIdToken(token)
-    .then((decoded) => {
-      const { uid } = decoded;
-      req.body.uid = uid;
-      req.uid = uid; // to ensure other middleware (multer) doesn't overwrite
-      next();
-    })
-    .catch(() => res.sendStatus(HttpStatusCode.Unauthorized));
+  try {
+    const { uid } = await getAuth().verifyIdToken(token);
+    req.body.uid = uid;
+    req.uid = uid; // to ensure other middleware (multer) doesn't overwrite
+    next();
+  } catch {
+    res.sendStatus(HttpStatusCode.Unauthorized);
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/middleware/firebaseAuth.js` around lines 8 - 24, Update the auth
middleware function to await getAuth().verifyIdToken(token) inside a try/catch,
handling both synchronous initialization errors and rejected verification
promises with an Unauthorized response. Preserve the existing uid assignments
and next() behavior on successful verification, and remove the fire-and-forget
then/catch chain.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/src/db/daos/fileDao.js`:
- Around line 52-59: Update retrieveImageList to exclude soft-deleted files by
filtering for documents whose deletedAt field is unset or null, while preserving
the existing scenarioId and image-type filters.

In `@backend/src/db/daos/sceneDao.js`:
- Around line 248-278: Update computeFileRefDeltas so modified existing
components compare their previous and current fileId values: decrement the old
file reference and increment the new one when they differ, while preserving
new-component handling and avoiding changes when the fileId is unchanged.
- Around line 296-302: Guard the result of Scene.findById in the scene update
flow before accessing existingScene.components. When no scene is found, return
the existing clean not-found response, and preserve computeFileRefDeltas
behavior for valid scenes.

In `@backend/src/routes/api/__tests__/collectionsApi.test.js`:
- Around line 59-78: Update the DELETE /api/collections/groups/:groupId handling
to decrement linked UploadedFile reference counts when removing Resource
documents, allowing the existing refCount/deletedAt lifecycle to update
correctly. Extend the related collections API test around applyReferenceDelta
and the group deletion request to assert the attached file’s resulting state.

In `@backend/src/routes/api/files.js`:
- Around line 17-18: Validate the parsed value used by MAX_FILE_SIZE_MB before
calculating MAX_FILE_SIZE_BYTES, and fall back to the existing 10 MB default
when parsing produces NaN or another invalid value. Keep the byte conversion
unchanged for valid positive configuration values so multer always receives a
usable size limit.
- Around line 104-118: Validate fileId in the GET /:scenarioId/:fileId handler
before calling retrieveFile, using the existing ObjectId validation or HttpError
conventions, and reject malformed values with a client-facing 400 BadRequest.
Keep the not-found response for valid IDs that retrieveFile does not find, and
remove or replace the unreachable !fileId check.
- Around line 70-90: Wrap the upload and UploadedFile.create flow in the
relevant handler with error handling that tracks firebaseInfo.path and deletes
the uploaded blob if database creation fails. Preserve the existing successful
response, and ensure cleanup errors do not mask the original failure.

In `@backend/src/routes/api/resources.js`:
- Around line 128-145: Update the DELETE conditional handler to include
`"stateConditionals._id": conditionalId` in the `Resource.findOneAndUpdate`
filter, matching the PUT handler’s validation. Preserve the existing 404
response when either the resource or conditional does not exist, and continue
returning the updated resource for successful deletions.
- Around line 74-93: Enable schema validation on both conditional update routes
by adding runValidators: true to the options object passed to each
findOneAndUpdate call, including the POST handler shown and its corresponding
PUT handler. Preserve the existing new: true behavior and other update options.
- Around line 19-46: Validate fileId before Resource.create by reusing
retrieveFile(scenarioId, fileId) in the POST handler, ensuring the file exists
and belongs to the requested scenarioId. Reject missing or mismatched files with
the established 4xx error handling, and only create the resource and call
applyReferenceDelta after validation succeeds.

In `@frontend/src/features/authoring/images.tsx`:
- Around line 31-37: Restore error handling for addExistingImage by catching
failures from getImageDimensions and routing them through the existing
handleGeneric feedback handler. Ensure the invocation that currently uses void
addExistingImage(selectedImage) does not leave the promise rejection unhandled,
while preserving the existing image creation flow on success.

In `@frontend/src/features/playScenario/components/ResourcePreview.jsx`:
- Around line 75-78: Update the isText error-rendering branch in ResourcePreview
to display a safe string derived from text.error rather than rendering the raw
Error object; preserve the existing warning alert and fallback behavior while
ensuring rejected loadText fetches cannot crash rendering.
- Around line 1-14: Update ResourcePreview’s loadText and useQuery configuration
to match the established Preview behavior in ManageResourcesPage: guard
contentType before calling startsWith, treat application/json as previewable
alongside text types, and check res.ok in loadText before returning the response
text. Preserve the existing file?.url gating and query key.

In `@frontend/src/features/resources/ManageResourcesPage.jsx`:
- Around line 408-417: Update Preview’s text-preview eligibility to safely
handle missing file.contentType and include application/json alongside text,
CSV, and Markdown so JSON queries actually run; keep the existing isText
behavior aligned with the same supported-content classification. Update loadText
to validate the fetch response and reject or throw when res.ok is false before
returning the response body.
- Around line 463-466: Update the isText && text.isError rendering branch in
ManageResourcesPage to display a string derived from text.error, such as its
message or a safe fallback, instead of rendering the error object directly.
Preserve the existing warning alert and ensure fetch failures cannot produce an
invalid React child.

---

Outside diff comments:
In `@backend/src/routes/api/collections.js`:
- Around line 85-104: Update the DELETE /groups/:groupId handler to build a
fileId delta map from the queried files and apply those decrements through the
existing applyReferenceDelta path before Resource.deleteMany and
group.deleteOne. Preserve the current response counts and not-found behavior.

In `@frontend/src/features/resources/ManageResourcesPage.jsx`:
- Around line 92-114: Update the groups/files-loading useEffect to return early
when user is unavailable, add user to its dependency array alongside scenarioId,
and only call api.get after authentication resolves so the request reruns with
the available user.

---

Nitpick comments:
In `@backend/src/db/daos/fileDao.js`:
- Around line 8-50: Remove the duplicated update pipeline from
applyReferenceDelta by delegating its nonzero single-file operation to
applyReferenceDeltas, using a Map containing fileId and delta. Preserve the
existing zero-delta early return and ensure the bulk helper remains the single
source of truth for refCount and deletedAt updates.

In `@backend/src/db/models/resource.js`:
- Around line 5-10: Remove the standalone index declaration from the scenarioId
field in the Resource schema, while preserving scenarioId as the leading field
of the existing compound index.

In `@backend/src/db/models/scene.js`:
- Line 1: Delete the commented-out pre("remove") hook block and any associated
dead-code comments or remnants in the scene model; leave the active Mongoose
schema behavior unchanged and track the unused-file removal follow-up
separately.

In `@backend/src/db/models/uploadedFile.js`:
- Around line 16-21: Remove the standalone index declaration from the scenarioId
field in the uploaded file schema, while preserving the compound { scenarioId:
1, createdAt: -1 } index for scenarioId-based queries.

In `@backend/src/firebase/firebase.js`:
- Around line 1-14: Guard the unconditionally executed admin.initializeApp call
in the Firebase setup so initialization occurs only when the default app does
not already exist. Preserve the existing credential and storageBucket
configuration, and leave getBucket and getAuth unchanged.

In `@backend/src/firebase/storage.js`:
- Around line 21-23: Update the catch handling around the upload operation to
preserve the original error object, including its stack trace and structured
fields such as code, while retaining the “upload failed” context. Avoid creating
a plain Error from only err.message; use the existing upload failure path and
nearest relevant symbol.
- Around line 12-20: Update the options passed to file.save in the upload flow
to set resumable to false, while preserving the existing buffer and metadata
handling for files capped by MAX_FILE_SIZE_MB.

In `@backend/src/middleware/firebaseAuth.js`:
- Around line 8-24: Update the auth middleware function to await
getAuth().verifyIdToken(token) inside a try/catch, handling both synchronous
initialization errors and rejected verification promises with an Unauthorized
response. Preserve the existing uid assignments and next() behavior on
successful verification, and remove the fire-and-forget then/catch chain.

In `@frontend/src/features/authoring/images.tsx`:
- Around line 39-64: Update addNewImage so that after a successful upload and
scene insertion, it invalidates the ["images", scenarioId] query cache. Reuse
the existing query-client/cache mechanism used by ImageCreateMenu, and keep
invalidation out of the failure path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 03e25548-aea7-47d3-9787-906b024ac10a

📥 Commits

Reviewing files that changed from the base of the PR and between 5da0f70 and e13adb2.

📒 Files selected for processing (41)
  • backend/jest.config.js
  • backend/jest.setup.env.js
  • backend/src/db/daos/fileDao.js
  • backend/src/db/daos/imageDao.js
  • backend/src/db/daos/resourcesDao.js
  • backend/src/db/daos/sceneDao.js
  • backend/src/db/models/StoredFile.js
  • backend/src/db/models/resource.js
  • backend/src/db/models/scene.js
  • backend/src/db/models/uploadedFile.js
  • backend/src/firebase/__tests__/storage.test.js
  • backend/src/firebase/firebase.js
  • backend/src/firebase/storage.js
  • backend/src/index.js
  • backend/src/middleware/__tests__/firebaseAuth.test.js
  • backend/src/middleware/firebaseAuth.js
  • backend/src/routes/api/__tests__/collectionsApi.test.js
  • backend/src/routes/api/__tests__/filesApi.test.js
  • backend/src/routes/api/__tests__/imageApi.test.js
  • backend/src/routes/api/__tests__/navigateGroupApi.test.js
  • backend/src/routes/api/__tests__/resourcesApi.test.js
  • backend/src/routes/api/collections.js
  • backend/src/routes/api/files.js
  • backend/src/routes/api/image.js
  • backend/src/routes/api/index.js
  • backend/src/routes/api/navigate/group.js
  • backend/src/routes/api/navigate/index.js
  • backend/src/routes/api/resources.js
  • backend/src/util/gridfs.js
  • frontend/src/components/ListContainer/ImageListContainer.jsx
  • frontend/src/components/StateVariables/StateConditionalMenu.jsx
  • frontend/src/features/authoring/CanvasSideBar/AudioManager.jsx
  • frontend/src/features/authoring/audio/EditAudioComponent.tsx
  • frontend/src/features/authoring/images.tsx
  • frontend/src/features/authoring/types.ts
  • frontend/src/features/playScenario/components/MDTextViewer.jsx
  • frontend/src/features/playScenario/components/ResourcePreview.jsx
  • frontend/src/features/playScenario/components/ResourcesPanel.jsx
  • frontend/src/features/playScenario/hooks/useDownloadUrl.js
  • frontend/src/features/resources/ManageResourcesPage.jsx
  • frontend/src/features/resources/util.js
💤 Files with no reviewable changes (10)
  • backend/src/db/models/StoredFile.js
  • backend/src/db/daos/imageDao.js
  • frontend/src/features/playScenario/hooks/useDownloadUrl.js
  • backend/src/db/daos/resourcesDao.js
  • backend/src/routes/api/tests/imageApi.test.js
  • backend/src/routes/api/image.js
  • backend/src/util/gridfs.js
  • frontend/src/features/authoring/audio/EditAudioComponent.tsx
  • backend/src/routes/api/tests/navigateGroupApi.test.js
  • backend/src/routes/api/navigate/group.js

Comment thread backend/src/db/daos/fileDao.js
Comment thread backend/src/db/daos/sceneDao.js Outdated
Comment thread backend/src/db/daos/sceneDao.js
Comment thread backend/src/routes/api/__tests__/collectionsApi.test.js
Comment thread backend/src/routes/api/files.js Outdated
Comment thread frontend/src/features/authoring/images.tsx
Comment thread frontend/src/features/playScenario/components/ResourcePreview.jsx
Comment thread frontend/src/features/playScenario/components/ResourcePreview.jsx
Comment thread frontend/src/features/resources/ManageResourcesPage.jsx
Comment thread frontend/src/features/resources/ManageResourcesPage.jsx

@coderabbitai coderabbitai Bot left a comment

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.

🧹 Nitpick comments (2)
backend/src/util/validation.js (1)

1-3: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer Mongoose's built-in isValidObjectId.

The current regex validation will incorrectly return true for an array containing a single valid ObjectId string (e.g., ["5f4d8e8b2b5d4c2b9c8b4567"]), because RegExp.test() coerces arrays to strings. Relying on Mongoose's built-in validator prevents edge cases like this and leverages the existing ecosystem.

♻️ Proposed refactor
+import mongoose from "mongoose";
+
 export function isValidObjectId(id) {
-  return /^[0-9a-fA-F]{24}$/.test(id);
+  return mongoose.isValidObjectId(id);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/util/validation.js` around lines 1 - 3, Replace the regex-based
validation in isValidObjectId with Mongoose’s built-in isValidObjectId
validator, passing the input through unchanged so arrays and other non-scalar
values are handled correctly.
backend/src/firebase/storage.js (1)

31-38: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Make file deletion idempotent.

Firebase Storage's delete() method throws an error if the file does not exist. It's highly recommended to make this operation idempotent by passing { ignoreNotFound: true }. This prevents errors if a deletion flow is retried or if the file was already removed out-of-band.

♻️ Proposed refactor
 export async function deleteFile(path) {
   if (!path) throw new Error("path is required");
   try {
-    await getBucket().file(path).delete();
+    await getBucket().file(path).delete({ ignoreNotFound: true });
   } catch (err) {
     throw new Error(`delete failed: ${err.message}`);
   }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/firebase/storage.js` around lines 31 - 38, Update deleteFile to
pass the Firebase Storage delete option ignoreNotFound: true when invoking
getBucket().file(path).delete(), while preserving the existing path validation
and error-wrapping behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@backend/src/firebase/storage.js`:
- Around line 31-38: Update deleteFile to pass the Firebase Storage delete
option ignoreNotFound: true when invoking getBucket().file(path).delete(), while
preserving the existing path validation and error-wrapping behavior.

In `@backend/src/util/validation.js`:
- Around line 1-3: Replace the regex-based validation in isValidObjectId with
Mongoose’s built-in isValidObjectId validator, passing the input through
unchanged so arrays and other non-scalar values are handled correctly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 56f28291-bcdf-4e75-8c7d-484eede6d462

📥 Commits

Reviewing files that changed from the base of the PR and between e13adb2 and 19d1e8b.

📒 Files selected for processing (11)
  • backend/src/db/daos/sceneDao.js
  • backend/src/firebase/__tests__/storage.test.js
  • backend/src/firebase/storage.js
  • backend/src/routes/api/__tests__/filesApi.test.js
  • backend/src/routes/api/collections.js
  • backend/src/routes/api/files.js
  • backend/src/routes/api/resources.js
  • backend/src/util/validation.js
  • frontend/src/features/authoring/images.tsx
  • frontend/src/features/playScenario/components/ResourcePreview.jsx
  • frontend/src/features/resources/ManageResourcesPage.jsx
🚧 Files skipped from review as they are similar to previous changes (7)
  • backend/src/routes/api/collections.js
  • frontend/src/features/playScenario/components/ResourcePreview.jsx
  • backend/src/routes/api/files.js
  • backend/src/routes/api/tests/filesApi.test.js
  • frontend/src/features/authoring/images.tsx
  • backend/src/routes/api/resources.js
  • frontend/src/features/resources/ManageResourcesPage.jsx

@harbassan
harbassan force-pushed the vps-137-standardize-file-storage-and-metadata-management branch from 19d1e8b to 6f00e5f Compare July 17, 2026 03:51

@coderabbitai coderabbitai Bot left a comment

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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
frontend/src/features/authoring/images.tsx (2)

100-103: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reset the file input to support same-file retries.

After a failed upload, selecting the same file again may not trigger onChange because the input value is unchanged. Clear event.currentTarget.value after capturing the file.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/features/authoring/images.tsx` around lines 100 - 103, Update
handleFileChange to capture the selected file, then clear
event.currentTarget.value before starting the upload so selecting the same file
again triggers onChange after failures. Preserve the existing addNewImage and
handleGeneric flow.

94-103: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Refresh the images query after upload

frontend/src/features/authoring/images.tsx: addNewImage only adds the new image to the canvas; it never invalidates or updates ["images", scenarioId], so the existing-image modal can keep showing stale data until a later refetch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/features/authoring/images.tsx` around lines 94 - 103, Update
handleFileChange to refresh the imagesQuery after addNewImage completes by
invalidating or updating the ["images", scenarioId] query through the existing
query client mechanism. Preserve handleGeneric error handling and ensure the
refresh occurs only after a successful upload.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@frontend/src/features/authoring/images.tsx`:
- Around line 100-103: Update handleFileChange to capture the selected file,
then clear event.currentTarget.value before starting the upload so selecting the
same file again triggers onChange after failures. Preserve the existing
addNewImage and handleGeneric flow.
- Around line 94-103: Update handleFileChange to refresh the imagesQuery after
addNewImage completes by invalidating or updating the ["images", scenarioId]
query through the existing query client mechanism. Preserve handleGeneric error
handling and ensure the refresh occurs only after a successful upload.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3ec1e252-4bf9-4eef-bd91-24cf9a0c9370

📥 Commits

Reviewing files that changed from the base of the PR and between 19d1e8b and 6f00e5f.

📒 Files selected for processing (11)
  • backend/src/db/daos/sceneDao.js
  • backend/src/firebase/__tests__/storage.test.js
  • backend/src/firebase/storage.js
  • backend/src/routes/api/__tests__/filesApi.test.js
  • backend/src/routes/api/collections.js
  • backend/src/routes/api/files.js
  • backend/src/routes/api/resources.js
  • backend/src/util/validation.js
  • frontend/src/features/authoring/images.tsx
  • frontend/src/features/playScenario/components/ResourcePreview.jsx
  • frontend/src/features/resources/ManageResourcesPage.jsx
🚧 Files skipped from review as they are similar to previous changes (10)
  • backend/src/firebase/tests/storage.test.js
  • backend/src/firebase/storage.js
  • backend/src/routes/api/collections.js
  • backend/src/routes/api/resources.js
  • backend/src/util/validation.js
  • backend/src/db/daos/sceneDao.js
  • backend/src/routes/api/files.js
  • backend/src/routes/api/tests/filesApi.test.js
  • frontend/src/features/playScenario/components/ResourcePreview.jsx
  • frontend/src/features/resources/ManageResourcesPage.jsx

@coderabbitai coderabbitai Bot mentioned this pull request Jul 17, 2026
5 tasks
@hazikchaudhry

Copy link
Copy Markdown
Contributor

alot of changes, I like the approach reminds me of hard inode pointers from OS, with the ref counters. will review fully soon.

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