Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,387 changes: 56 additions & 1,331 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 0 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
"s3:setup": "ts-node src/utils/setup-s3-bucket.ts"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.798.0",
"@aws-sdk/s3-request-presigner": "^3.798.0",
"@prisma/client": "^6.4.1",
"@stellar/stellar-sdk": "^13.3.0",
Expand All @@ -32,7 +31,6 @@
"@types/swagger-ui-express": "^4.1.7",
"@types/uuid": "^10.0.0",
"axios": "^1.7.9",
"backblaze-b2": "^1.7.0",
"bcryptjs": "^3.0.2",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
Expand All @@ -43,7 +41,6 @@
"express-rate-limit": "^7.5.0",
"express-validator": "^7.2.0",
"jsonwebtoken": "^9.0.2",
"multer": "^1.4.5-lts.2",
"node-cron": "^3.0.3",
"nodemailer": "^6.10.0",
"pdf-lib": "^1.17.1",
Expand Down
Empty file added src/config/index.ts
Empty file.
11 changes: 4 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,13 +186,10 @@ prisma
globalLogger.info("Cron jobs initialized successfully!");

app.listen(PORT, () => {
globalLogger.info(
`Server is running on http://localhost:${PORT}`,
{
port: PORT,
environment: ENV,
}
);
globalLogger.info(`Server is running on http://localhost:${PORT}`, {
port: PORT,
environment: ENV,
});

if (ENV === "development") {
globalLogger.info(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,7 @@ import { GetAllOrganizationsUseCase } from "../../application/use-cases/get-all-
import { CreateOrganizationDto } from "../dto/create-organization.dto";
import { UpdateOrganizationDto } from "../dto/update-organization.dto";
import { OrganizationNotFoundException } from "../../domain/exceptions/organization-not-found.exception";
import {
UuidParamsDto,
PaginationQueryDto,
} from "../../../shared/dto/base.dto";
import { PaginationQueryDto, UuidParamsDto } from "@/shared/dto/base.dto";

export class OrganizationController {
constructor(
Expand Down Expand Up @@ -40,7 +37,7 @@ export class OrganizationController {
);

getOrganizationById = asyncHandler(
async (req: Request<UuidParamsDto>, res: Response): Promise<void> => {
async (req: Request, res: Response): Promise<void> => {
const { id } = req.params;

try {
Expand Down
36 changes: 36 additions & 0 deletions src/modules/photo/README.md
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.

Comment on lines +21 to +23
Copy link

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/multer if TS) are present in package.json to avoid runtime/TS errors.

Run:


🏁 Script executed:

#!/bin/bash
jq -r '.dependencies, .devDependencies' package.json | rg -n 'multer'

Length of output: 101


Add Multer to package.json dependencies
Only @types/multer is declared; add "multer": "⟨latest-version⟩" under dependencies in package.json to 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
In src/modules/photo/README.md around lines 21 to 23, the note mentions Multer
but package.json only declares @types/multer; update package.json by adding a
runtime dependency for multer (e.g. add "multer": "<latest-version>" under
dependencies) so the library is installed at runtime and TypeScript typings
remain compatible; ensure package.json dependencies include the multer entry and
run npm/yarn install to update lockfiles.

# πŸ“‚ 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
```
49 changes: 49 additions & 0 deletions src/modules/photo/domain/entities/interfaces/photo.interface.ts
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
Copy link

Choose a reason for hiding this comment

The 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 IStorageAdapter couples to Node Buffer and returns {url,key}, while SupabasePhotoService.upload(file: Express.Multer.File, userId: string, metadata?) β†’ PhotoEntity uses a different shape. Keep the storage contract in infrastructure (e.g., IPhotoServiceAdapter) and delete this interface from domain to avoid duplication and drift. If a domain contract is required, make it framework-agnostic (no Buffer/Express types) and align signatures.

-export interface IStorageAdapter {
-  upload(file: Buffer, filename: string): Promise<{ url: string; key: string }>;
-  delete(key: string): Promise<void>;
-}
πŸ“ 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
export interface IStorageAdapter {
upload(file: Buffer, filename: string): Promise<{ url: string; key: string }>;
delete(key: string): Promise<void>;
}
πŸ€– Prompt for AI Agents
In src/modules/photo/domain/entities/interfaces/photo.interface.ts lines 1-4:
remove this domain-level IStorageAdapter (delete the file or its export) because
it couples to Node Buffer and a {url,key} shape that conflicts with
SupabasePhotoService; instead move or recreate the adapter contract under the
infrastructure layer (e.g., src/modules/photo/infrastructure/ with a name like
IPhotoServiceAdapter) and update all imports to point to the new infra
interface; if the domain truly needs an interface, replace it with a
framework-agnostic contract (no Buffer/Express types) whose signature accepts an
opaque binary type or stream and returns a domain PhotoEntity (or a
domain-friendly DTO), and ensure SupabasePhotoService implements the infra
adapter signature so signatures no longer drift.


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;
}
38 changes: 15 additions & 23 deletions src/modules/photo/domain/entities/photo.entity.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,45 @@
import { Entity, Column } from "typeorm";
import { BaseEntity } from "../../../shared/domain/entities/base.entity";
import { IPhotoMetadata, IPhotoProps } from "./interfaces/photo.interface";
import {
InvalidPhotoUrlException,
MissingUserIdException,
} from "../exceptions/domain.exception";

export interface PhotoProps {
id?: string;
url: string;
userId: string;
uploadedAt?: Date;
metadata?: Record<string, any>;
}

@Entity("photos")
export class Photo extends BaseEntity {
@Column({ type: "varchar", length: 500, nullable: false })
export class PhotoEntity extends BaseEntity {
url: string;

@Column({ type: "uuid", nullable: false })
userId: string;

@Column({ type: "jsonb", nullable: true })
metadata?: Record<string, any>;
metadata?: IPhotoMetadata;

// Domain logic and validation
public validate(): boolean {
protected validate(): boolean {
if (!this.url || this.url.trim() === "") {
throw new Error("Photo URL is required");
throw new InvalidPhotoUrlException(this.url);
}

if (!/^https?:\/\/.+$/.test(this.url)) {
throw new Error("Photo URL must be a valid HTTP/HTTPS URL");
throw new InvalidPhotoUrlException(this.url);
}

if (!this.userId || this.userId.trim() === "") {
throw new Error("User ID is required");
throw new MissingUserIdException();
}

return true;
}

// Update metadata
public updateMetadata(newMetadata: Record<string, any>): void {
public updateMetadata(newMetadata: IPhotoMetadata): void {
this.metadata = {
...this.metadata,
...newMetadata,
};
}

// Static factory method
public static create(props: PhotoProps): Photo {
const photo = new Photo();
public static create(props: IPhotoProps): PhotoEntity {
const photo = new PhotoEntity();
photo.url = props.url;
photo.userId = props.userId;
photo.metadata = props.metadata ?? {};
Expand All @@ -56,7 +48,7 @@ export class Photo extends BaseEntity {
}

// Convert to plain object for persistence
public toObject(): PhotoProps {
public toObject(): IPhotoProps {
return {
id: this.id,
url: this.url,
Expand Down
13 changes: 13 additions & 0 deletions src/modules/photo/domain/exceptions/domain.exception.ts
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");
}
}
2 changes: 0 additions & 2 deletions src/modules/photo/entities/photo.entity.ts

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
Copy link

Choose a reason for hiding this comment

The 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

‼️ 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
getById(photoId: string): Promise<{ id: string; url: string } | null>;
delete(photoUrl: string): Promise<void>;
updateMetadata(
getById(photoId: string): Promise<{ id: string; url: string } | null>;
delete(photoId: string): Promise<void>;
updateMetadata(
πŸ€– Prompt for AI Agents
In src/modules/photo/infrastructure/adapters/interface/photo-service.adapter.ts
around lines 10 to 12, the delete method signature currently accepts a photoUrl
but the implementation and controller operate with photoId; change the interface
to delete(photoId: string): Promise<void> and update any dependent types/usages
to expect an id (not a URL). Ensure method name and parameter name in
implementations, controllers, and unit tests are updated to match the new
signature and run tests/TS compile to catch call sites.

photoId: string,
metadata: IPhotoMetadata
): Promise<IPhotoMetadata>;
}
105 changes: 105 additions & 0 deletions src/modules/photo/infrastructure/adapters/supabase-service.adapter.ts
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
Copy link

Choose a reason for hiding this comment

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

πŸ’‘ Verification agent

🧩 Analysis chain

Multer 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 src

Length 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 src

Length of output: 493


Configure Multer with memoryStorage for photo uploads
upload() in supabase-service.adapter.ts reads file.buffer, which is only populated when Multer is using memoryStorage. In src/modules/photo/presentation/routes.ts, Multer is imported but not configuredβ€”update it to:

// 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 upload.single('file') (or the appropriate variant) for the photo route.

πŸ“ 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
file: Express.Multer.File,
userId: string,
metadata?: IPhotoMetadata
): Promise<PhotoEntity> {
// src/modules/photo/presentation/routes.ts
import multer from "multer";
const upload = multer({ storage: multer.memoryStorage() });
// then in your route:
router.post(
"/photos",
upload.single("file"),
photoController.upload.bind(photoController)
);
πŸ€– Prompt for AI Agents
In src/modules/photo/infrastructure/adapters/supabase-service.adapter.ts around
lines 10 to 13, the upload() implementation expects file.buffer (Multer
memoryStorage) but the photo route imports Multer without configuring it; update
src/modules/photo/presentation/routes.ts to import multer, create an uploader
using multer({ storage: multer.memoryStorage() }), and use that uploader (e.g.,
upload.single('file')) on the photo upload route so req.file.buffer is populated
for the adapter.

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
Copy link

Choose a reason for hiding this comment

The 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}`;

Committable suggestion skipped: line range outside the PR's diff.

πŸ€– Prompt for AI Agents
In src/modules/photo/infrastructure/adapters/supabase-service.adapter.ts around
lines 14 to 22, the current filename generation uses Date.now() + originalname
which risks collisions and path injection; replace this by generating a secure
UUID-based filename, derive and validate the file extension against a whitelist
(reject or map disallowed extensions), and construct the stored key as a safe
namespace (e.g., `${userId || 'anon'}/${uuid}.${ext}`) rather than using the raw
originalname; ensure you never include slashes or unsafe characters from
originalname, and keep upsert:false as before.


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
Copy link

Choose a reason for hiding this comment

The 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

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

Suggested change
const { data: publicUrl } = supabase.storage
.from("photos")
.getPublicUrl(fileName);
if (!publicUrl) throw new Error("Could not get public URL");
// Guardar en DB con Prisma
const { data: publicUrlData, error: publicUrlError } = supabase.storage
.from("photos")
.getPublicUrl(fileName);
if (publicUrlError || !publicUrlData?.publicUrl) {
throw new Error("Could not get public URL");
}
// Guardar en DB con Prisma
πŸ€– Prompt for AI Agents
In src/modules/photo/infrastructure/adapters/supabase-service.adapter.ts around
lines 27 to 33, the current code assumes the supabase response contains a valid
data.publicUrl and throws a generic error; update it to inspect the response
shape: check the returned error object from supabase and validate that data &&
data.publicUrl exist, and if not throw an Error containing the supabase error
message (or the response object serialized) and the fileName for context; ensure
subsequent code uses data.publicUrl (not the whole data) and avoid accessing
undefined properties.

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
Copy link

Choose a reason for hiding this comment

The 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(),
       },
     });

Committable suggestion skipped: line range outside the PR's diff.

πŸ€– Prompt for AI Agents
In src/modules/photo/infrastructure/adapters/supabase-service.adapter.ts around
lines 34 to 46, the code persists enriched metadata (fileName, fileSize,
mimeType) but returns only the original input metadata; update the function to
return the persisted photo's metadata from the database instead of the input
metadata. After creating the photo with prisma.photo.create, extract and return
photo.metadata (or map the created photo record to the response) so callers
receive the enriched values (fileName, fileSize, mimeType) saved in the DB.


return PhotoEntity.create({
id: photo.url,
url: photo.url,
userId,
metadata,
uploadedAt: photo.uploadedAt ?? new Date(),
});
Comment on lines +48 to +54
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Incorrect id and metadata used when building the domain entity.

  • id is set to photo.url instead of photo.id.
  • metadata should be the persisted one (photo.metadata).

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

‼️ 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
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(),
});
πŸ€– Prompt for AI Agents
In src/modules/photo/infrastructure/adapters/supabase-service.adapter.ts around
lines 48 to 54, the domain PhotoEntity is being constructed with the wrong
fields: the id is incorrectly set to photo.url and metadata is using the
in-memory variable instead of the persisted value. Fix by setting id to photo.id
and metadata to photo.metadata (keep uploadedAt fallback to new Date() as-is),
so the entity reflects the stored record.

}

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
Copy link

Choose a reason for hiding this comment

The 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

‼️ 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
// 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]);
// Extract fileName from public URL (what follows after /photos/)
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()!;
// Delete from Supabase Storage using supabaseStorage
const { error: deleteError } = await supabase.storage
.from("photos")
.remove([fileName]);
πŸ€– Prompt for AI Agents
In src/modules/photo/infrastructure/adapters/supabase-service.adapter.ts around
lines 67-75, extracting the object name by splitting the public URL is brittle;
instead parse the URL (new URL(photo.url)), use url.pathname and
decodeURIComponent, then locate and strip the bucket/prefix (find the '/photos/'
segment and take everything after it) to produce the exact object path used in
storage, and pass that path to
supabase.storage.from("photos").remove([objectPath]); also handle
leading/trailing slashes and query strings by relying on pathname so you delete
the correct object.

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
Copy link

Choose a reason for hiding this comment

The 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

‼️ 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 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;
}
async updateMetadata(
photoId: string,
metadata: IPhotoMetadata
): Promise<IPhotoMetadata> {
- 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 },
});
// Assuming metadata is not null after update
return updatedPhoto.metadata as IPhotoMetadata;
}
πŸ€– Prompt for AI Agents
In src/modules/photo/infrastructure/adapters/supabase-service.adapter.ts around
lines 92 to 104, the current updateMetadata overwrites the whole metadata
object; change it to read the existing metadata, merge it with the incoming
metadata (shallow or deep merge as appropriate for nested objects), and persist
the merged result. Specifically: fetch the current metadata for photoId, create
mergedMetadata = { ...existingMetadata, ...metadata } (or use a deep merge util
if nested), then call prisma.photo.update with data: { metadata: mergedMetadata
} and return mergedMetadata; handle existingMetadata being null by treating it
as an empty object and wrap read+update in a transaction if concurrent updates
are a concern.

}
Loading