-
Notifications
You must be signed in to change notification settings - Fork 71
Feature/migration imgs supabase #178
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| # πΈ Photos Module | ||
|
|
||
| This module handles uploading, fetching, and deleting images. | ||
| Unlike other modules in the application, its architecture is slightly different, since file handling introduces particular requirements (e.g., Supabase Storage, Multer, public URLs). | ||
|
|
||
| # π Key Architectural Differences | ||
|
|
||
| No use cases or repositories are used, only maintained Entity to get easy access on this information, but this Entity is generate for mi adapter, adapter interface required a return of PhotoEntity. | ||
| The flow is more direct: | ||
|
|
||
| Controller β calls the Adapter for Supabase Storage and Prisma for persistence. | ||
|
|
||
| Entity β ensures photo consistency (valid URL, userId, metadata). | ||
|
|
||
| Dedicated Adapters: | ||
|
|
||
| photo-service.adapter.interface.ts β defines the generic interface for a storage service. | ||
|
|
||
| supabase-service.adapter.ts β concrete implementation using Supabase Storage. | ||
|
|
||
| Simplified Controllers: | ||
| Controllers use asyncHandler, validators, and Multer for file processing. | ||
|
|
||
| # π Structure | ||
|
|
||
| ```text | ||
| modules/ | ||
| photo/ | ||
| domain/entities/interfaces/ #interfaces of entity | ||
| domain/entitites/photo.entity.ts #Entity | ||
| infrastructure/ | ||
| presentation/controllers/ | ||
| presentation/dto | ||
| routes.ts | ||
| README.md | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,49 @@ | ||||||||||
| export interface IStorageAdapter { | ||||||||||
| upload(file: Buffer, filename: string): Promise<{ url: string; key: string }>; | ||||||||||
| delete(key: string): Promise<void>; | ||||||||||
| } | ||||||||||
|
Comment on lines
+1
to
+4
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π οΈ Refactor suggestion Remove or relocate IStorageAdapter; it conflicts with the actual adapter contract. Domain-level -export interface IStorageAdapter {
- upload(file: Buffer, filename: string): Promise<{ url: string; key: string }>;
- delete(key: string): Promise<void>;
-}π Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||||||
|
|
||||||||||
| export interface IPhotoMetadata { | ||||||||||
| fileSize?: number; | ||||||||||
| mimeType?: string; | ||||||||||
| dimensions?: { | ||||||||||
| width: number; | ||||||||||
| height: number; | ||||||||||
| }; | ||||||||||
| camera?: { | ||||||||||
| make?: string; | ||||||||||
| model?: string; | ||||||||||
| settings?: { | ||||||||||
| iso?: number; | ||||||||||
| aperture?: string; | ||||||||||
| shutterSpeed?: string; | ||||||||||
| }; | ||||||||||
| }; | ||||||||||
| location?: { | ||||||||||
| latitude?: number; | ||||||||||
| longitude?: number; | ||||||||||
| address?: string; | ||||||||||
| }; | ||||||||||
| tags?: string[]; | ||||||||||
| description?: string; | ||||||||||
| [key: string]: unknown; // Allow additional properties | ||||||||||
| } | ||||||||||
|
|
||||||||||
| export interface IPhotoProps { | ||||||||||
| id?: string; | ||||||||||
| url: string; | ||||||||||
| userId: string; | ||||||||||
| uploadedAt?: Date; | ||||||||||
| metadata?: IPhotoMetadata; | ||||||||||
| } | ||||||||||
|
|
||||||||||
| export interface IPhoto { | ||||||||||
| id: string; | ||||||||||
| url: string; | ||||||||||
| userId: string; | ||||||||||
| uploadedAt: Date; | ||||||||||
| metadata?: IPhotoMetadata; | ||||||||||
| validate(): boolean; | ||||||||||
| updateMetadata(newMetadata: Partial<IPhotoMetadata>): void; | ||||||||||
| toObject(): IPhotoProps; | ||||||||||
| } | ||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import { DomainException } from "../../../shared/domain/exceptions/domain.exception"; | ||
|
|
||
| export class InvalidPhotoUrlException extends DomainException { | ||
| constructor(url: string) { | ||
| super(`Invalid photo URL: ${url}`); | ||
| } | ||
| } | ||
|
|
||
| export class MissingUserIdException extends DomainException { | ||
| constructor() { | ||
| super("User ID is required"); | ||
| } | ||
| } |
This file was deleted.
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,16 @@ | ||||||||||||||
| import { IPhotoMetadata } from "@/modules/photo/domain/entities/interfaces/photo.interface"; | ||||||||||||||
| import { PhotoEntity } from "@/modules/photo/domain/entities/photo.entity"; | ||||||||||||||
|
|
||||||||||||||
| export interface IPhotoServiceAdapter { | ||||||||||||||
| upload( | ||||||||||||||
| file: Express.Multer.File, | ||||||||||||||
| userId: string, | ||||||||||||||
| metadata?: IPhotoMetadata | ||||||||||||||
| ): Promise<PhotoEntity>; | ||||||||||||||
| getById(photoId: string): Promise<{ id: string; url: string } | null>; | ||||||||||||||
| delete(photoUrl: string): Promise<void>; | ||||||||||||||
| updateMetadata( | ||||||||||||||
|
Comment on lines
+10
to
+12
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π οΈ Refactor suggestion Align delete() contract to operate by photoId (not URL). Implementation and controller use an id. The interface should reflect that to avoid confusion at the boundary. Apply: - delete(photoUrl: string): Promise<void>;
+ delete(photoId: string): Promise<void>;π Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||||||||||
| photoId: string, | ||||||||||||||
| metadata: IPhotoMetadata | ||||||||||||||
| ): Promise<IPhotoMetadata>; | ||||||||||||||
| } | ||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,105 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { prisma } from "@/config/prisma"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { IPhotoMetadata } from "../../domain/entities/interfaces/photo.interface"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { IPhotoServiceAdapter } from "./interface/photo-service.adapter"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { supabase } from "@/config/supabase"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { PhotoEntity } from "../../domain/entities/photo.entity"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export class SupabasePhotoService implements IPhotoServiceAdapter { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| //Metadata options are enabled if needed later | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async upload( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| file: Express.Multer.File, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| userId: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| metadata?: IPhotoMetadata | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): Promise<PhotoEntity> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+10
to
+13
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π‘ Verification agent π§© Analysis chainMulter memory storage prerequisite. upload() relies on file.buffer; ensure Multer uses memoryStorage for the photo route. Run: π Script executed: #!/bin/bash
rg -nP 'multer\\s*\\(|memoryStorage\\s*\\(' -C2 srcLength of output: 126 π Script executed: #!/bin/bash
rg -nP 'import\s+multer' -C2 src
rg -nP 'multer\s*\(\s*{[^}]*storage' -C2 src
rg -nP 'memoryStorage\s*\(' -C2 srcLength of output: 493 Configure Multer with memoryStorage for photo uploads // src/modules/photo/presentation/routes.ts
-import multer from "multer";
-const upload = multer();
+import multer from "multer";
+const upload = multer({ storage: multer.memoryStorage() });and continue using π Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const fileName = `${Date.now()}-${file.originalname}`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Subir archivo a Supabase Storage | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { error: uploadError } = await supabase.storage | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .from("photos") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .upload(fileName, file.buffer, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| contentType: file.mimetype, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| upsert: false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+14
to
+22
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π οΈ Refactor suggestion Use robust, sanitized object keys; avoid timestamp+originalname. Risk of collisions and path injection via originalname. Use UUID and keep an extension whitelist; optionally namespace by userId. Apply: +import { randomUUID } from "crypto";
+import path from "path";
...
- const fileName = `${Date.now()}-${file.originalname}`;
+ const ext = (path.extname(file.originalname) || ".bin").toLowerCase();
+ const safeExt = [".jpg", ".jpeg", ".png", ".webp", ".gif", ".heic", ".bin"].includes(ext) ? ext : ".bin";
+ const fileName = `${userId}/${randomUUID()}${safeExt}`;
π€ Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (uploadError) throw uploadError; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Obtener URL pΓΊblica | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { data: publicUrl } = supabase.storage | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .from("photos") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .getPublicUrl(fileName); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!publicUrl) throw new Error("Could not get public URL"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Guardar en DB con Prisma | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+27
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π οΈ Refactor suggestion Harden public URL retrieval and error handling. Check the error object and the presence of data.publicUrl explicitly. Apply: - const { data: publicUrl } = supabase.storage
+ const { data: publicUrlData, error: publicUrlError } = supabase.storage
.from("photos")
.getPublicUrl(fileName);
-
- if (!publicUrl) throw new Error("Could not get public URL");
+ if (publicUrlError || !publicUrlData?.publicUrl) {
+ throw new Error("Could not get public URL");
+ }π Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const photo = await prisma.photo.create({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| userId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| url: publicUrl.publicUrl, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| metadata: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ...metadata, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fileName: file.originalname, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fileSize: file.size, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mimeType: file.mimetype, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| uploadedAt: new Date(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+34
to
+46
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π οΈ Refactor suggestion Persist enriched metadata and return it. You enrich metadata before persisting but return only the input metadata. Return DB metadata so callers see fileName/size/mimeType. Apply: - const photo = await prisma.photo.create({
+ const photo = await prisma.photo.create({
data: {
userId,
- url: publicUrl.publicUrl,
+ url: publicUrlData.publicUrl,
metadata: {
...metadata,
fileName: file.originalname,
fileSize: file.size,
mimeType: file.mimetype,
},
uploadedAt: new Date(),
},
});
π€ Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return PhotoEntity.create({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| id: photo.url, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| url: photo.url, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| userId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| metadata, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| uploadedAt: photo.uploadedAt ?? new Date(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+48
to
+54
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Incorrect id and metadata used when building the domain entity.
Apply: - return PhotoEntity.create({
- id: photo.url,
- url: photo.url,
- userId,
- metadata,
- uploadedAt: photo.uploadedAt ?? new Date(),
- });
+ return PhotoEntity.create({
+ id: photo.id,
+ url: photo.url,
+ userId,
+ metadata: (photo.metadata as IPhotoMetadata) ?? {},
+ uploadedAt: photo.uploadedAt ?? new Date(),
+ });π Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async delete(photoId: string): Promise<void> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Find in DB | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const photo = await prisma.photo.findUnique({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| where: { id: photoId }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!photo) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error("Photo not found"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Extract fileName from public URL (what follows after /photos/) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const urlParts = photo.url.split("/"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const fileName = urlParts[urlParts.length - 1]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Delete from Supabase Storage using supabaseStorage | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { error: deleteError } = await supabase.storage | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .from("photos") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .remove([fileName]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+67
to
+75
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π οΈ Refactor suggestion More reliable object path extraction for deletion. Parsing by split("/") is brittle. Use URL parsing and strip the bucket prefix. Apply: - const urlParts = photo.url.split("/");
- const fileName = urlParts[urlParts.length - 1];
+ const pathname = new URL(photo.url).pathname; // /storage/v1/object/public/photos/<key>
+ const idx = pathname.indexOf("/photos/");
+ const fileName = idx >= 0 ? decodeURIComponent(pathname.slice(idx + "/photos/".length)) : pathname.split("/").pop()!;π Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (deleteError) throw deleteError; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Delete from DB | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await prisma.photo.delete({ where: { id: photoId } }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async getById(photoId: string): Promise<{ id: string; url: string } | null> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const photo = await prisma.photo.findUnique({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| where: { id: photoId }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!photo) return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { id: photo.id, url: photo.url }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async updateMetadata( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| photoId: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| metadata: IPhotoMetadata | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): Promise<IPhotoMetadata> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const updatedPhoto = await prisma.photo.update({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| where: { id: photoId }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data: { metadata }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| select: { metadata: true }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Assuming metadata is not null after update | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return updatedPhoto.metadata as IPhotoMetadata; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+92
to
+104
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π οΈ Refactor suggestion Merge metadata instead of overwriting. Avoid losing existing keys; read, merge, then persist. Apply: - const updatedPhoto = await prisma.photo.update({
- where: { id: photoId },
- data: { metadata },
- select: { metadata: true },
- });
+ const existing = await prisma.photo.findUnique({
+ where: { id: photoId },
+ select: { metadata: true },
+ });
+ if (!existing) throw new Error("Photo not found");
+ const merged = { ...(existing.metadata as Record<string, unknown> ?? {}), ...metadata };
+ const updatedPhoto = await prisma.photo.update({
+ where: { id: photoId },
+ data: { metadata: merged },
+ select: { metadata: true },
+ });π Committable suggestion
Suggested change
π€ Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
π‘ Verification agent
π§© Analysis chain
Confirm Multer is a declared dependency.
Routes import Multer. Ensure
multer(and@types/multerif TS) are present in package.json to avoid runtime/TS errors.Run:
π Script executed:
Length of output: 101
Add Multer to package.json dependencies
Only
@types/multeris declared; add"multer": "β¨latest-versionβ©"underdependenciesinpackage.jsonto prevent runtime/TS errors.π§° Tools
πͺ LanguageTool
[grammar] ~21-~21: There might be a mistake here.
Context: ...pabase Storage. Simplified Controllers: Controllers use asyncHandler, validators...
(QB_NEW_EN)
π€ Prompt for AI Agents