Skip to content

Commit a4f2e0e

Browse files
committed
Set up Cloudinary integration for listing image uploads
- Add Cloudinary configuration to .env.example - Install Cloudinary package and add it to dependencies - Create CloudinaryService for image uploads - Implement ListingConsumer to handle image upload jobs - Update ListingService to use queue for image uploads - Add Cloudinary config to main configuration file
1 parent 2a9cfe3 commit a4f2e0e

File tree

10 files changed

+3268
-4087
lines changed

10 files changed

+3268
-4087
lines changed

.env.example

+5-1
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,8 @@ REDIS_HOST="localhost"
1212
REDIS_PORT="6379" # Needs to match the port in docker-compose.yml
1313
REDIS_USERNAME=""
1414
REDIS_PASSWORD=""
15-
REDIS_URL="redis://${REDIS_USERNAME}:${REDIS_PASSWORD}@${REDIS_HOST}:${REDIS_PORT}"
15+
REDIS_URL="redis://${REDIS_USERNAME}:${REDIS_PASSWORD}@${REDIS_HOST}:${REDIS_PORT}"
16+
17+
CLOUDINARY_CLOUD_NAME="your_cloud_name"
18+
CLOUDINARY_API_KEY="your_api_key"
19+
CLOUDINARY_API_SECRET="your_api_secret"

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"cache-manager-redis-store": "2",
4646
"class-transformer": "^0.5.1",
4747
"class-validator": "^0.14.1",
48+
"cloudinary": "^2.4.0",
4849
"helmet": "^7.1.0",
4950
"reflect-metadata": "^0.2.0",
5051
"rxjs": "^7.8.1",

pnpm-lock.yaml

+3,184-4,082
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/config/index.ts

+5
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,9 @@ export default () => ({
66
username: process.env.REDIS_USERNAME,
77
password: process.env.REDIS_PASSWORD,
88
},
9+
cloudinary: {
10+
cloudName: process.env.CLOUDINARY_CLOUD_NAME,
11+
apiKey: process.env.CLOUDINARY_API_KEY,
12+
apiSecret: process.env.CLOUDINARY_API_SECRET,
13+
},
914
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
-- CreateTable
2+
CREATE TABLE "ListingImage" (
3+
"id" SERIAL NOT NULL,
4+
"url" TEXT NOT NULL,
5+
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
6+
"updatedAt" TIMESTAMP(3) NOT NULL,
7+
"listingId" INTEGER NOT NULL,
8+
9+
CONSTRAINT "ListingImage_pkey" PRIMARY KEY ("id")
10+
);
11+
12+
-- AddForeignKey
13+
ALTER TABLE "ListingImage" ADD CONSTRAINT "ListingImage_listingId_fkey" FOREIGN KEY ("listingId") REFERENCES "Listing"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

src/database/schema.prisma

+10
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,16 @@ model Listing {
1919
bathrooms Int
2020
bedrooms Int
2121
squareMeters Int
22+
images ListingImage[]
2223
createdAt DateTime @default(now())
2324
updatedAt DateTime @updatedAt
2425
}
26+
27+
model ListingImage {
28+
id Int @id @default(autoincrement())
29+
url String
30+
createdAt DateTime @default(now())
31+
updatedAt DateTime @updatedAt
32+
listing Listing @relation(fields: [listingId], references: [id])
33+
listingId Int
34+
}

src/modules/listing/listing.service.ts

+2
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,15 @@ export class ListingService {
2222
const listing = await this.databaseService.listing.create({
2323
data,
2424
});
25+
2526
for (const image of images) {
2627
await this.listingQueue.uploadListingImage({
2728
base64File: this.fileService.bufferToBase64(image.buffer),
2829
mimeType: image.mimetype,
2930
listingId: listing.id,
3031
});
3132
}
33+
3234
return listing;
3335
}
3436
}

src/modules/listing/queue/listing.consumer.ts

+20-2
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,38 @@ import { LISTING_QUEUE } from '../../../core/queue/queue.constants';
55
import { BaseConsumer } from '../../../core/queue/base.consumer';
66
import { LoggerService } from '../../../core/logger/logger.service';
77
import { FileService } from '../../../utilities/file/file.service';
8+
import { CloudinaryService } from '../../../utilities/cloudinary/cloudinary.service';
9+
import { DatabaseService } from '../../../database/database.service';
810

911
@Processor(LISTING_QUEUE)
1012
export class ListingConsumer extends BaseConsumer {
1113
constructor(
1214
logger: LoggerService,
1315
private readonly fileService: FileService,
16+
private readonly cloudinaryService: CloudinaryService,
17+
private readonly databaseService: DatabaseService,
1418
) {
1519
super(logger);
1620
}
1721

1822
@Process(`createListingImage`)
1923
async createListingImage(job: Job<UploadListingImageDto>) {
2024
const buffer = this.fileService.base64ToBuffer(job.data.base64File);
21-
// TODO: upload file to Google Cloud Storage
22-
// TODO: store respective Google Cloud Storage URL in database
25+
const cloudinaryUrl = await this.cloudinaryService.uploadImage(
26+
buffer,
27+
'listings',
28+
);
29+
30+
// Create a new ListingImage record
31+
await this.databaseService.listingImage.create({
32+
data: {
33+
url: cloudinaryUrl,
34+
listing: {
35+
connect: { id: job.data.listingId },
36+
},
37+
},
38+
});
39+
40+
return cloudinaryUrl;
2341
}
2442
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { ConfigService } from '@nestjs/config';
3+
import { v2 as cloudinary } from 'cloudinary';
4+
5+
@Injectable()
6+
export class CloudinaryService {
7+
constructor(private configService: ConfigService) {
8+
cloudinary.config({
9+
cloud_name: this.configService.get('cloudinary.cloudName'),
10+
api_key: this.configService.get('cloudinary.apiKey'),
11+
api_secret: this.configService.get('cloudinary.apiSecret'),
12+
});
13+
}
14+
15+
async uploadImage(file: Buffer, folder: string): Promise<string> {
16+
return new Promise((resolve, reject) => {
17+
cloudinary.uploader
18+
.upload_stream({ folder }, (error, result) => {
19+
if (error) return reject(error);
20+
resolve(result?.secure_url || '');
21+
})
22+
.end(file);
23+
});
24+
}
25+
}

src/utilities/utilities.module.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { Module } from '@nestjs/common';
22
import { FileService } from './file/file.service';
3+
import { CloudinaryService } from './cloudinary/cloudinary.service';
34

45
@Module({
5-
providers: [FileService],
6-
exports: [FileService],
6+
providers: [FileService, CloudinaryService],
7+
exports: [FileService, CloudinaryService],
78
})
89
export class UtilitiesModule {}

0 commit comments

Comments
 (0)