diff --git a/package.json b/package.json index 4f4a52db..d065a59c 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "@types/multer": "^2.0.0", "@types/node": "^22.10.5", "@types/passport-jwt": "^4.0.1", - "@types/sharp": "^0.31.1", + "@types/sharp": "^0.32.0", "@types/supertest": "^6.0.2", "@typescript-eslint/eslint-plugin": "^8.19.1", "@typescript-eslint/parser": "^8.19.1", diff --git a/src/modules/photo/photo.controller.ts b/src/modules/photo/photo.controller.ts index 5e287779..eed3ba39 100644 --- a/src/modules/photo/photo.controller.ts +++ b/src/modules/photo/photo.controller.ts @@ -229,6 +229,12 @@ export class PhotoController { createdAt: { type: "string", format: "date-time" }, uploadedBy: { type: "string" }, approvalStatus: { type: "string" }, + derivatives: { + type: "object", + additionalProperties: { type: "string" }, + description: + "Responsive image URLs (e.g., webp_480, webp_960, webp_1600)", + }, }, }, }, @@ -242,6 +248,7 @@ export class PhotoController { createdAt: Date; uploadedBy: string; approvalStatus: string; + derivatives: Record; }[] > { try { diff --git a/src/modules/photo/photo.service.ts b/src/modules/photo/photo.service.ts index d986d887..926717eb 100644 --- a/src/modules/photo/photo.service.ts +++ b/src/modules/photo/photo.service.ts @@ -76,15 +76,21 @@ export class PhotoService { format: "webp", ): Promise { const image = sharp(buffer); - const metadata = await image.metadata(); + + // Automatically rotate based on EXIF orientation + const rotatedImage = image.rotate(); + const metadata = await rotatedImage.metadata(); // Only resize if image is wider than target width if (metadata.width && metadata.width > width) { - return image.resize(width, null, { withoutEnlargement: true }).toFormat(format, { quality: 80 }).toBuffer(); + return rotatedImage + .resize(width, null, { withoutEnlargement: true }) + .toFormat(format, { quality: 80 }) + .toBuffer(); } // If image is smaller, just convert format without resizing - return image.toFormat(format, { quality: 80 }).toBuffer(); + return rotatedImage.toFormat(format, { quality: 80 }).toBuffer(); } private async uploadDerivatives( @@ -215,23 +221,33 @@ export class PhotoService { createdAt: Date; uploadedBy: string; approvalStatus: string; + derivatives: Record; }[] > { const [files] = await this.photoBucket.getFiles(); + // Filter out derivative files + const photoFiles = files.filter( + (file) => !file.name.startsWith("derivatives/"), + ); + // Get all photos with their approval status - return files.map((file) => ({ - name: file.name, - url: this.getPublicPhotoUrl(file.name), - createdAt: file.metadata.timeCreated - ? new Date(file.metadata.timeCreated) - : new Date(), - uploadedBy: String(file.metadata.metadata?.uploadedBy || "unknown"), - // If metadata is missing, treat as pending (backward compatibility) - approvalStatus: String( - file.metadata.metadata?.approvalStatus || "pending", - ), - })); + return photoFiles.map((file) => { + const photoId = this.extractPhotoIdFromFilename(file.name); + return { + name: file.name, + url: this.getPublicPhotoUrl(file.name), + createdAt: file.metadata.timeCreated + ? new Date(file.metadata.timeCreated) + : new Date(), + uploadedBy: String(file.metadata.metadata?.uploadedBy || "unknown"), + // If metadata is missing, treat as pending (backward compatibility) + approvalStatus: String( + file.metadata.metadata?.approvalStatus || "pending", + ), + derivatives: this.getDerivativeUrls(photoId), + }; + }); } async updatePhotoApprovalStatus( @@ -267,10 +283,15 @@ export class PhotoService { ): Promise { const [files] = await this.photoBucket.getFiles(); + // Filter out derivative files first + const photoFiles = files.filter( + (file) => !file.name.startsWith("derivatives/"), + ); + // Filter files based on status if provided - let filteredFiles = files; + let filteredFiles = photoFiles; if (status) { - filteredFiles = files.filter((file) => { + filteredFiles = photoFiles.filter((file) => { const fileStatus = file.metadata.metadata?.approvalStatus || "pending"; return fileStatus === status; }); @@ -291,15 +312,21 @@ export class PhotoService { const paginatedFiles = filteredFiles.slice(startIndex, endIndex); // Map files to response format - const photos = paginatedFiles.map((file) => ({ - name: file.name, - url: this.getPublicPhotoUrl(file.name), - createdAt: file.metadata.timeCreated - ? new Date(file.metadata.timeCreated) - : new Date(), - uploadedBy: String(file.metadata.metadata?.uploadedBy || ""), - approvalStatus: String(file.metadata.metadata?.approvalStatus || "pending"), - })); + const photos = paginatedFiles.map((file) => { + const photoId = this.extractPhotoIdFromFilename(file.name); + return { + name: file.name, + url: this.getPublicPhotoUrl(file.name), + createdAt: file.metadata.timeCreated + ? new Date(file.metadata.timeCreated) + : new Date(), + uploadedBy: String(file.metadata.metadata?.uploadedBy || ""), + approvalStatus: String( + file.metadata.metadata?.approvalStatus || "pending", + ), + derivatives: this.getDerivativeUrls(photoId), + }; + }); return { photos, diff --git a/src/modules/photo/photo.types.ts b/src/modules/photo/photo.types.ts index 7dd85b70..4bb26005 100644 --- a/src/modules/photo/photo.types.ts +++ b/src/modules/photo/photo.types.ts @@ -4,6 +4,7 @@ export interface PhotoItem { createdAt: Date; uploadedBy?: string; approvalStatus?: string; + derivatives?: Record; } export interface PaginationMeta { diff --git a/yarn.lock b/yarn.lock index a81269fe..61d7763d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2245,12 +2245,12 @@ "@types/node" "*" "@types/send" "<1" -"@types/sharp@^0.31.1": - version "0.31.1" - resolved "https://registry.npmjs.org/@types/sharp/-/sharp-0.31.1.tgz" - integrity sha512-5nWwamN9ZFHXaYEincMSuza8nNfOof8nmO+mcI+Agx1uMUk4/pQnNIcix+9rLPXzKrm1pS34+6WRDbDV0Jn7ag== +"@types/sharp@^0.32.0": + version "0.32.0" + resolved "https://registry.yarnpkg.com/@types/sharp/-/sharp-0.32.0.tgz#fc3ac6df6b456319bae807c3d24efdc6631cdd6f" + integrity sha512-OOi3kL+FZDnPhVzsfD37J88FNeZh6gQsGcLc95NbeURRGvmSjeXiDcyWzF2o3yh/gQAUn2uhh/e+CPCa5nwAxw== dependencies: - "@types/node" "*" + sharp "*" "@types/stack-utils@^2.0.3": version "2.0.3" @@ -2323,53 +2323,63 @@ "@typescript-eslint/visitor-keys" "8.46.1" debug "^4.3.4" -"@typescript-eslint/project-service@8.46.2": - version "8.46.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.46.2.tgz#ab2f02a0de4da6a7eeb885af5e059be57819d608" - integrity sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg== +"@typescript-eslint/project-service@8.46.1": + version "8.46.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.46.1.tgz#07be0e6f27fa90a17d8e5f6996ee02329c9a8c2e" + integrity sha512-FOIaFVMHzRskXr5J4Jp8lFVV0gz5ngv3RHmn+E4HYxSJ3DgDzU7fVI1/M7Ijh1zf6S7HIoaIOtln1H5y8V+9Zg== dependencies: - "@typescript-eslint/tsconfig-utils" "^8.46.2" - "@typescript-eslint/types" "^8.46.2" + "@typescript-eslint/tsconfig-utils" "^8.46.1" + "@typescript-eslint/types" "^8.46.1" debug "^4.3.4" -"@typescript-eslint/scope-manager@8.46.2": - version "8.46.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.46.2.tgz#7d37df2493c404450589acb3b5d0c69cc0670a88" - integrity sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA== +"@typescript-eslint/scope-manager@8.46.1": + version "8.46.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.46.1.tgz#590dd2e65e95af646bdaf50adeae9af39e25e8c1" + integrity sha512-weL9Gg3/5F0pVQKiF8eOXFZp8emqWzZsOJuWRUNtHT+UNV2xSJegmpCNQHy37aEQIbToTq7RHKhWvOsmbM680A== dependencies: - "@typescript-eslint/types" "8.46.2" - "@typescript-eslint/visitor-keys" "8.46.2" + "@typescript-eslint/types" "8.46.1" + "@typescript-eslint/visitor-keys" "8.46.1" -"@typescript-eslint/tsconfig-utils@8.46.2", "@typescript-eslint/tsconfig-utils@^8.46.2": +"@typescript-eslint/tsconfig-utils@8.46.1": + version "8.46.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.1.tgz#24405888560175c6c209c39df11ac06a2efef9d7" + integrity sha512-X88+J/CwFvlJB+mK09VFqx5FE4H5cXD+H/Bdza2aEWkSb8hnWIQorNcscRl4IEo1Cz9VI/+/r/jnGWkbWPx54g== + +"@typescript-eslint/tsconfig-utils@^8.46.1": version "8.46.2" resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.2.tgz#d110451cb93bbd189865206ea37ef677c196828c" integrity sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag== -"@typescript-eslint/type-utils@8.46.2": - version "8.46.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.46.2.tgz#802d027864e6fb752e65425ed09f3e089fb4d384" - integrity sha512-HbPM4LbaAAt/DjxXaG9yiS9brOOz6fabal4uvUmaUYe6l3K1phQDMQKBRUrr06BQkxkvIZVVHttqiybM9nJsLA== +"@typescript-eslint/type-utils@8.46.1": + version "8.46.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.46.1.tgz#14d4307dd6045f6b48a888cde1513d6ec305537f" + integrity sha512-+BlmiHIiqufBxkVnOtFwjah/vrkF4MtKKvpXrKSPLCkCtAp8H01/VV43sfqA98Od7nJpDcFnkwgyfQbOG0AMvw== dependencies: - "@typescript-eslint/types" "8.46.2" - "@typescript-eslint/typescript-estree" "8.46.2" - "@typescript-eslint/utils" "8.46.2" + "@typescript-eslint/types" "8.46.1" + "@typescript-eslint/typescript-estree" "8.46.1" + "@typescript-eslint/utils" "8.46.1" debug "^4.3.4" ts-api-utils "^2.1.0" -"@typescript-eslint/types@8.46.2", "@typescript-eslint/types@^8.46.2": +"@typescript-eslint/types@8.46.1": + version "8.46.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.46.1.tgz#4c5479538ec10b5508b8e982e172911c987446d8" + integrity sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ== + +"@typescript-eslint/types@^8.46.1": version "8.46.2" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.46.2.tgz#2bad7348511b31e6e42579820e62b73145635763" integrity sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ== -"@typescript-eslint/typescript-estree@8.46.2": - version "8.46.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.2.tgz#ab547a27e4222bb6a3281cb7e98705272e2c7d08" - integrity sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ== +"@typescript-eslint/typescript-estree@8.46.1": + version "8.46.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.1.tgz#1c146573b942ebe609c156c217ceafdc7a88e6ed" + integrity sha512-uIifjT4s8cQKFQ8ZBXXyoUODtRoAd7F7+G8MKmtzj17+1UbdzFl52AzRyZRyKqPHhgzvXunnSckVu36flGy8cg== dependencies: - "@typescript-eslint/project-service" "8.46.2" - "@typescript-eslint/tsconfig-utils" "8.46.2" - "@typescript-eslint/types" "8.46.2" - "@typescript-eslint/visitor-keys" "8.46.2" + "@typescript-eslint/project-service" "8.46.1" + "@typescript-eslint/tsconfig-utils" "8.46.1" + "@typescript-eslint/types" "8.46.1" + "@typescript-eslint/visitor-keys" "8.46.1" debug "^4.3.4" fast-glob "^3.3.2" is-glob "^4.0.3" @@ -2377,22 +2387,22 @@ semver "^7.6.0" ts-api-utils "^2.1.0" -"@typescript-eslint/utils@8.46.2": - version "8.46.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.46.2.tgz#b313d33d67f9918583af205bd7bcebf20f231732" - integrity sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg== +"@typescript-eslint/utils@8.46.1": + version "8.46.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.46.1.tgz#c572184d9227d66b10a954b90249a20c48b22452" + integrity sha512-vkYUy6LdZS7q1v/Gxb2Zs7zziuXN0wxqsetJdeZdRe/f5dwJFglmuvZBfTUivCtjH725C1jWCDfpadadD95EDQ== dependencies: "@eslint-community/eslint-utils" "^4.7.0" - "@typescript-eslint/scope-manager" "8.46.2" - "@typescript-eslint/types" "8.46.2" - "@typescript-eslint/typescript-estree" "8.46.2" + "@typescript-eslint/scope-manager" "8.46.1" + "@typescript-eslint/types" "8.46.1" + "@typescript-eslint/typescript-estree" "8.46.1" -"@typescript-eslint/visitor-keys@8.46.2": - version "8.46.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.2.tgz#803fa298948c39acf810af21bdce6f8babfa9738" - integrity sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w== +"@typescript-eslint/visitor-keys@8.46.1": + version "8.46.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.1.tgz#da35f1d58ec407419d68847cfd358b32746ac315" + integrity sha512-ptkmIf2iDkNUjdeu2bQqhFPV1m6qTnFFjg7PPDjxKWaMaP0Z6I9l30Jr3g5QqbZGdw8YdYvLp+XnqnWWZOg/NA== dependencies: - "@typescript-eslint/types" "8.46.2" + "@typescript-eslint/types" "8.46.1" eslint-visitor-keys "^4.2.1" "@ungap/structured-clone@^1.3.0": @@ -4468,10 +4478,10 @@ gcp-metadata@^6.1.0: google-logging-utils "^0.0.2" json-bigint "^1.0.0" -gcp-metadata@^8.0.0: - version "8.1.1" - resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-8.1.1.tgz#672f48cca050d543bfe185ec0b7978a14d775298" - integrity sha512-dTCcAe9fRQf06ELwel6lWWFrEbstwjUBYEhr5VRGoC+iPDZQucHppCowaIp8b8v92tU1G4X4H3b/Y6zXZxkMsQ== +gcp-metadata@^7.0.0: + version "7.0.1" + resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-7.0.1.tgz#43bb9cd482cf0590629b871ab9133af45b78382d" + integrity sha512-UcO3kefx6dCcZkgcTGgVOTFb7b1LlQ02hY1omMjjrrBzkajRMCFgYOjs7J71WqnuG1k2b+9ppGL7FsOfhZMQKQ== dependencies: gaxios "^7.0.0" google-logging-utils "^1.0.0" @@ -5681,10 +5691,10 @@ lines-and-columns@^1.1.6: resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== -load-esm@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/load-esm/-/load-esm-1.0.3.tgz#2073afe3da63902c323e80d9f135c301173ac92c" - integrity sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA== +load-esm@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/load-esm/-/load-esm-1.0.2.tgz#35dbac8a1a3abdb802cf236008048fcc8a9289a6" + integrity sha512-nVAvWk/jeyrWyXEAs84mpQCYccxRqgKY4OznLuJhJCa0XsPSfdOIr2zvBZEj3IHEHbX97jjscKRRV539bW0Gpw== loader-runner@^4.2.0: version "4.3.0" @@ -6725,16 +6735,16 @@ path-scurry@^2.0.0: lru-cache "^11.0.0" minipass "^7.1.2" +path-to-regexp@8.2.0, path-to-regexp@^8.0.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.2.0.tgz#73990cc29e57a3ff2a0d914095156df5db79e8b4" + integrity sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ== + path-to-regexp@8.3.0: version "8.3.0" resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz" integrity sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA== -path-to-regexp@^8.0.0: - version "8.2.0" - resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz" - integrity sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ== - path-type@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" @@ -7234,7 +7244,7 @@ setprototypeof@1.2.0: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== -sharp@^0.34.4: +sharp@*, sharp@^0.34.4: version "0.34.4" resolved "https://registry.npmjs.org/sharp/-/sharp-0.34.4.tgz" integrity sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==