diff --git a/adapters/requests/dealCreate.adapter.js b/adapters/requests/dealCreate.adapter.js index 20fe308..6c6dbdd 100644 --- a/adapters/requests/dealCreate.adapter.js +++ b/adapters/requests/dealCreate.adapter.js @@ -6,16 +6,16 @@ function mapCreateDealRequestToDealCreateData( title: data.title, description: data.description ?? null, url: data.url ?? null, - price: data.price ?? null, + price: Number(data.price) ?? null, // 🔑 adapter burada seller’ı “custom” gibi yazar // service bunu düzeltecek - customCompany: data.sellerName, + customSeller: data.sellerName, user: { connect: { id: userId }, }, - +/* images: data.images?.length ? { create: data.images.map((imgUrl, index) => ({ @@ -24,9 +24,10 @@ function mapCreateDealRequestToDealCreateData( })), } : undefined, - } -} + */ +} +} module.exports = { mapCreateDealRequestToDealCreateData, } diff --git a/adapters/responses/comment.adapter.js b/adapters/responses/comment.adapter.js new file mode 100644 index 0000000..f1bb1ef --- /dev/null +++ b/adapters/responses/comment.adapter.js @@ -0,0 +1,30 @@ +function mapCommentToDealCommentResponse(comment) { + return { + id: comment.id, + text: comment.text, // eğer DB'de content ise burada text'e çevir + createdAt: comment.createdAt, + user: { + id: comment.user.id, + username: comment.user.username, + avatarUrl: comment.user.avatarUrl ?? null, + }, + } +} + +function mapCommentsToDealCommentResponse(comments) { + return comments.map(mapCommentToDealCommentResponse) +} + + +function mapCommentToUserCommentResponse(c) { + return { + ...mapCommentToDealCommentResponse(c), + deal: { id: c.deal.id, title: c.deal.title }, + } +} + +module.exports = { + mapCommentToDealCommentResponse, + mapCommentsToDealCommentResponse, + mapCommentToUserCommentResponse +} \ No newline at end of file diff --git a/adapters/responses/dealCard.adapter.js b/adapters/responses/dealCard.adapter.js index 7416cad..ffca02a 100644 --- a/adapters/responses/dealCard.adapter.js +++ b/adapters/responses/dealCard.adapter.js @@ -6,12 +6,14 @@ function mapDealToDealCardResponse(deal) { price: deal.price ?? null, score: deal.score, - commentsCount: deal._count?.comments ?? 0, + commentsCount: deal.commentCount, status: deal.status, saleType: deal.saletype, affiliateType: deal.affiliateType, + myVote:deal.myVote, + createdAt: deal.createdAt, updatedAt: deal.updatedAt, @@ -21,14 +23,21 @@ function mapDealToDealCardResponse(deal) { avatarUrl: deal.user.avatarUrl ?? null, }, - seller: deal.company - ? { name: deal.company.name, - url:deal.company.url + seller: deal.seller + ? { name: deal.seller.name, + url:deal.seller.url } - : { name: deal.customCompany || "" }, - + : { name: deal.customSeller || "" }, + imageUrl: deal.images?.[0]?.imageUrl || "", } } -module.exports = { mapDealToDealCardResponse } +function mapPaginatedDealsToDealCardResponse(paginated) { + return { + ...paginated, + results: paginated.results.map(mapDealToDealCardResponse), + } +} + +module.exports = { mapDealToDealCardResponse,mapPaginatedDealsToDealCardResponse } diff --git a/adapters/responses/dealDetail.adapter.js b/adapters/responses/dealDetail.adapter.js index fae0e6e..a78ea9d 100644 --- a/adapters/responses/dealDetail.adapter.js +++ b/adapters/responses/dealDetail.adapter.js @@ -22,9 +22,9 @@ function mapDealToDealDetailResponse(deal) { avatarUrl: deal.user.avatarUrl ?? null, }, - seller: deal.company - ? { id: deal.company.id, name: deal.company.name } - : { name: deal.customCompany || "Bilinmiyor" }, + seller: deal.seller + ? { id: deal.seller.id, name: deal.seller.name } + : { name: deal.customSeller || "Bilinmiyor" }, images: deal.images.map((img) => ({ id: img.id, diff --git a/adapters/responses/login.adapter.js b/adapters/responses/login.adapter.js new file mode 100644 index 0000000..16755b3 --- /dev/null +++ b/adapters/responses/login.adapter.js @@ -0,0 +1,26 @@ +// adapters/login.adapter.js + +function mapLoginRequestToLoginInput(body) { + return { + email: (body?.email || "").trim().toLowerCase(), + password: body?.password || "", + }; +} + +function mapLoginResultToResponse(result) { + // result: { token, user } + return { + token: result.token, + user: { + id: result.user.id, + username: result.user.username, + email: result.user.email, + avatarUrl: result.user.avatarUrl ?? null, + }, + }; +} + +module.exports = { + mapLoginRequestToLoginInput, + mapLoginResultToResponse, +}; diff --git a/adapters/responses/me.adapter.js b/adapters/responses/me.adapter.js new file mode 100644 index 0000000..f7361e9 --- /dev/null +++ b/adapters/responses/me.adapter.js @@ -0,0 +1,18 @@ +function mapMeRequestToUserId(req) { + // authMiddleware -> req.user.userId + return req.user.userId; +} + +function mapMeResultToResponse(user) { + return { + id: user.id, + username: user.username, + email: user.email, + avatarUrl: user.avatarUrl ?? null, + }; +} + +module.exports = { + mapMeRequestToUserId, + mapMeResultToResponse, +}; diff --git a/adapters/responses/publicUser.adapter.js b/adapters/responses/publicUser.adapter.js new file mode 100644 index 0000000..28bb341 --- /dev/null +++ b/adapters/responses/publicUser.adapter.js @@ -0,0 +1,22 @@ +// adapters/responses/publicUser.adapter.js +function mapUserToPublicUserSummaryResponse(user) { + return { + id: user.id, + username: user.username, + avatarUrl: user.avatarUrl ?? null, + } +} + +function mapUserToPublicUserDetailsResponse(user) { + return { + id: user.id, + username: user.username, + avatarUrl: user.avatarUrl ?? null, + createdAt: user.createdAt, // ISO string olmalı + } +} + +module.exports = { + mapUserToPublicUserSummaryResponse, + mapUserToPublicUserDetailsResponse, +} diff --git a/adapters/responses/register.adapter.js b/adapters/responses/register.adapter.js new file mode 100644 index 0000000..b04e1f6 --- /dev/null +++ b/adapters/responses/register.adapter.js @@ -0,0 +1,19 @@ +function mapRegisterRequestToRegisterInput(body) { + return { + username: (body?.username || "").trim(), + email: (body?.email || "").trim().toLowerCase(), + password: body?.password || "", + }; +} + +function mapRegisterResultToResponse(result) { + return { + token: result.token, + user: result.user, + }; +} + +module.exports = { + mapRegisterRequestToRegisterInput, + mapRegisterResultToResponse, +}; diff --git a/adapters/responses/userComment.adapter.js b/adapters/responses/userComment.adapter.js new file mode 100644 index 0000000..e69de29 diff --git a/adapters/responses/userProfile.adapter.js b/adapters/responses/userProfile.adapter.js new file mode 100644 index 0000000..a97fc35 --- /dev/null +++ b/adapters/responses/userProfile.adapter.js @@ -0,0 +1,16 @@ +// adapters/responses/userProfile.adapter.js +const dealCardAdapter = require("./dealCard.adapter") +const dealCommentAdapter = require("./comment.adapter") +const publicUserAdapter = require("./publicUser.adapter") // yoksa yaz +const userProfileStatsAdapter = require("./userProfileStats.adapter") + +function mapUserProfileToResponse({ user, deals, comments, stats }) { + return { + user: publicUserAdapter.mapUserToPublicUserDetailsResponse(user), + stats: userProfileStatsAdapter.mapUserProfileStatsToResponse(stats), + deals: deals.map(dealCardAdapter.mapDealToDealCardResponse), + comments: comments.map(dealCommentAdapter.mapCommentToUserCommentResponse), + } +} + +module.exports = { mapUserProfileToResponse } diff --git a/adapters/responses/userProfileStats.adapter.js b/adapters/responses/userProfileStats.adapter.js new file mode 100644 index 0000000..7d50a20 --- /dev/null +++ b/adapters/responses/userProfileStats.adapter.js @@ -0,0 +1,12 @@ +function mapUserProfileStatsToResponse(stats) { + return { + totalLikes: stats?.totalLikes ?? 0, + totalShares: stats?.totalShares ?? 0, + totalComments: stats?.totalComments ?? 0, + totalDeals: stats?.totalDeals ?? 0, + } +} + +module.exports = { + mapUserProfileStatsToResponse, +} \ No newline at end of file diff --git a/adapters/responses/vote.adapter.js b/adapters/responses/vote.adapter.js new file mode 100644 index 0000000..cde46f8 --- /dev/null +++ b/adapters/responses/vote.adapter.js @@ -0,0 +1,32 @@ +const { z } = require("zod"); + +const VoteBodySchema = z.object({ + dealId: z.coerce.number().int().positive(), + voteType: z.coerce + .number() + .int() + .refine((v) => v === 1 || v === 0 || v === -1, { + message: "voteType 1, 0 veya -1 olmalı", + }), +}); + +function mapVoteRequestToVoteInput(req) { + const parsed = VoteBodySchema.safeParse(req.body); + if (!parsed.success) { + const err = new Error(parsed.error.issues?.[0]?.message || "Geçersiz istek"); + err.statusCode = 400; + err.details = parsed.error.flatten(); + throw err; + } + + const userIdRaw = req.user?.userId; + const userId = Number(userIdRaw); + + return { + userId, + dealId: parsed.data.dealId, + voteType: parsed.data.voteType, // 1 | 0 | -1 + }; +} + +module.exports = { mapVoteRequestToVoteInput, VoteBodySchema }; diff --git a/db/auth.db.js b/db/auth.db.js new file mode 100644 index 0000000..cf175c0 --- /dev/null +++ b/db/auth.db.js @@ -0,0 +1,27 @@ + +const prisma = require("./client"); + +async function findUserByEmail(email, options = {}) { + return prisma.user.findUnique({ + where: { email }, + include: options.include || undefined, + select: options.select || undefined, + }); +} +async function createUser(data) { + return prisma.user.create({ data }); +} + +async function findUserById(id, options = {}) { + return prisma.user.findUnique({ + where: { id }, + select: options.select || undefined, + include: options.include || undefined, + }); +} + +module.exports = { + findUserByEmail, + createUser, + findUserById, +}; \ No newline at end of file diff --git a/db/comment.db.js b/db/comment.db.js index 2da0569..77e82f2 100644 --- a/db/comment.db.js +++ b/db/comment.db.js @@ -1,5 +1,9 @@ const prisma = require("./client") +function getDb(db) { + return db || prisma +} + async function findComments(where, options = {}) { return prisma.comment.findMany({ where, @@ -9,8 +13,9 @@ async function findComments(where, options = {}) { }) } -async function createComment(data, options = {}) { - return prisma.comment.create({ +async function createComment(data, options = {}, db) { + const p = getDb(db) + return p.comment.create({ data, include: options.include || undefined, select: options.select || undefined, @@ -20,9 +25,15 @@ async function createComment(data, options = {}) { async function deleteComment(where) { return prisma.comment.delete({ where }) } +async function countComments(where = {}, db) { + const p = getDb(db) + return p.comment.count({ where }) +} + module.exports = { findComments, + countComments, createComment, deleteComment, } diff --git a/db/deal.db.js b/db/deal.db.js index 39acf29..f04bb3f 100644 --- a/db/deal.db.js +++ b/db/deal.db.js @@ -1,5 +1,9 @@ const prisma = require("./client") +function getDb(db) { + return db || prisma +} + async function findDeals(where = {}, options = {}) { return prisma.deal.findMany({ where, @@ -11,14 +15,16 @@ async function findDeals(where = {}, options = {}) { }) } -async function findDeal(where, options = {}) { - return prisma.deal.findUnique({ +async function findDeal(where, options = {}, db) { + const p = getDb(db) + return p.deal.findUnique({ where, include: options.include || undefined, select: options.select || undefined, }) } + async function createDeal(data, options = {}) { return prisma.deal.create({ data, @@ -27,12 +33,13 @@ async function createDeal(data, options = {}) { }) } -async function updateDeal(where, data, options = {}) { - return prisma.deal.update({ +async function updateDeal(where, data, options = {}, db) { + const p = getDb(db) + return p.deal.update({ where, data, include: options.include || undefined, - select: options.select || undefined, + select: options.select || undefined, }) } @@ -68,9 +75,28 @@ async function updateVote(where, data, options = {}) { async function countVotes(where = {}) { return prisma.dealVote.count({ where }) } +async function getDealWithImages(dealId) { + return prisma.deal.findUnique({ + where: { id: dealId }, + include: { images: { orderBy: { order: "asc" } } }, + }); +} + +async function aggregateDeals(where = {}, db) { + const p = getDb(db) + return p.deal.aggregate({ + where, + _count: { _all: true }, + _sum: { score: true }, + }) +} + + module.exports = { findDeals, + aggregateDeals, + getDealWithImages, findDeal, createDeal, updateDeal, diff --git a/db/dealImage.db.js b/db/dealImage.db.js new file mode 100644 index 0000000..baf427d --- /dev/null +++ b/db/dealImage.db.js @@ -0,0 +1,14 @@ +const prisma = require("./client"); // sende prisma neredeyse oradan + +async function createManyDealImages(data) { + return prisma.dealImage.createMany({ data }); +} + +async function listDealImagesByDealId(dealId) { + return prisma.dealImage.findMany({ + where: { dealId }, + orderBy: { order: "asc" }, + }); +} + +module.exports = { createManyDealImages, listDealImagesByDealId }; diff --git a/db/seller.db.js b/db/seller.db.js index 9efb39f..2cb656e 100644 --- a/db/seller.db.js +++ b/db/seller.db.js @@ -1,16 +1,16 @@ const { PrismaClient } = require("@prisma/client") const prisma = new PrismaClient() -async function findCompany(where, options = {}) { - return prisma.company.findFirst({ +async function findSeller(where, options = {}) { + return prisma.seller.findFirst({ where, include: options.include || undefined, select: options.select || undefined, }) } -async function findCompanyByDomain(domain) { - return prisma.company.findFirst({ +async function findSellerByDomain(domain) { + return prisma.seller.findFirst({ where: { domains: { some: { @@ -23,6 +23,6 @@ async function findCompanyByDomain(domain) { module.exports = { - findCompany, - findCompanyByDomain, + findSeller, + findSellerByDomain, } diff --git a/db/vote.db.js b/db/vote.db.js new file mode 100644 index 0000000..f0056f3 --- /dev/null +++ b/db/vote.db.js @@ -0,0 +1,57 @@ +const prisma = require("./client"); + +async function voteDealTx({ dealId, userId, voteType }) { + return prisma.$transaction(async (db) => { + const current = await db.dealVote.findUnique({ + where: { dealId_userId: { dealId, userId } }, + select: { voteType: true }, + }); + + const oldValue = current ? current.voteType : 0; + const delta = voteType - oldValue; + + // history (append-only) + await db.dealVoteHistory.create({ + data: { dealId, userId, voteType }, + }); + + // current state + await db.dealVote.upsert({ + where: { dealId_userId: { dealId, userId } }, + create: { + dealId, + userId, + voteType, + lastVotedAt: new Date(), + }, + update: { + voteType, + lastVotedAt: new Date(), + }, + }); + + // score delta + if (delta !== 0) { + await db.deal.update({ + where: { id: dealId }, + data: { score: { increment: delta } }, + }); + } + + const deal = await db.deal.findUnique({ + where: { id: dealId }, + select: { score: true }, + }); + + return { + dealId, + voteType, + delta, + score: deal?.score ?? null, + }; + }); +} + +module.exports = { + voteDealTx, +}; diff --git a/middleware/authOptional.middleware.js b/middleware/authOptional.middleware.js new file mode 100644 index 0000000..2e98f95 --- /dev/null +++ b/middleware/authOptional.middleware.js @@ -0,0 +1,31 @@ +const jwt = require("jsonwebtoken"); + +module.exports = (req, res, next) => { + const authHeader = req.headers.authorization; + + // token yoksa normal devam + if (!authHeader) { + req.user = null; + return next(); + } + + const parts = authHeader.split(" "); + const token = parts.length === 2 ? parts[1] : null; + + if (!token) { + req.user = null; + return next(); + } + + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET); + req.user = { + ...decoded, + userId: Number(decoded.userId), + }; + return next(); + } catch (err) { + // token varsa ama bozuksa => 401 (tercih) + return res.status(401).json({ error: "Token geçersiz" }); + } +}; diff --git a/middleware/authMiddleware.js b/middleware/authRequired.middleware.js similarity index 100% rename from middleware/authMiddleware.js rename to middleware/authRequired.middleware.js diff --git a/middleware/upload.middleware.js b/middleware/upload.middleware.js new file mode 100644 index 0000000..91e8969 --- /dev/null +++ b/middleware/upload.middleware.js @@ -0,0 +1,11 @@ +const multer = require("multer"); + +const upload = multer({ + storage: multer.memoryStorage(), + limits: { + files: 5, + fileSize: 10 * 1024 * 1024, + }, +}); + +module.exports = { upload }; diff --git a/package-lock.json b/package-lock.json index 7bf8604..a834af5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,8 @@ "express": "^5.1.0", "jsonwebtoken": "^9.0.2", "multer": "^2.0.2", + "sharp": "^0.34.5", + "uuid": "^13.0.0", "zod": "^4.1.12" }, "devDependencies": { @@ -41,6 +43,481 @@ "node": ">=12" } }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -865,6 +1342,15 @@ "devOptional": true, "license": "MIT" }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -2118,6 +2604,50 @@ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "license": "ISC" }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -2428,6 +2958,19 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/uuid": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", diff --git a/package.json b/package.json index 13c4d71..ddb25b2 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, - "prisma": { + "prisma": { "seed": "node prisma/seed.js" }, "keywords": [], @@ -21,6 +21,8 @@ "express": "^5.1.0", "jsonwebtoken": "^9.0.2", "multer": "^2.0.2", + "sharp": "^0.34.5", + "uuid": "^13.0.0", "zod": "^4.1.12" }, "devDependencies": { diff --git a/prisma/migrations/20260118124917_add_company_and_affiliate_fields/migration.sql b/prisma/migrations/20260118124917_add_company_and_affiliate_fields/migration.sql index 00757d8..0c8f5a8 100644 --- a/prisma/migrations/20260118124917_add_company_and_affiliate_fields/migration.sql +++ b/prisma/migrations/20260118124917_add_company_and_affiliate_fields/migration.sql @@ -9,7 +9,7 @@ CREATE TYPE "AffiliateType" AS ENUM ('AFFILIATE', 'NON_AFFILIATE', 'USER_AFFILIA -- AlterTable ALTER TABLE "Deal" ADD COLUMN "affiliateType" "AffiliateType" NOT NULL DEFAULT 'NON_AFFILIATE', -ADD COLUMN "companyId" INTEGER, +ADD COLUMN "sellerId" INTEGER, ADD COLUMN "customCompany" TEXT, ADD COLUMN "saletype" "SaleType" NOT NULL DEFAULT 'ONLINE', ADD COLUMN "status" "DealStatus" NOT NULL DEFAULT 'PENDING'; @@ -18,7 +18,7 @@ ADD COLUMN "status" "DealStatus" NOT NULL DEFAULT 'PENDING'; CREATE TABLE "CompanyDomain" ( "id" SERIAL NOT NULL, "domain" TEXT NOT NULL, - "companyId" INTEGER NOT NULL, + "sellerId" INTEGER NOT NULL, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdById" INTEGER NOT NULL, @@ -44,7 +44,7 @@ CREATE UNIQUE INDEX "CompanyDomain_domain_key" ON "CompanyDomain"("domain"); CREATE UNIQUE INDEX "Company_name_key" ON "Company"("name"); -- AddForeignKey -ALTER TABLE "CompanyDomain" ADD CONSTRAINT "CompanyDomain_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE RESTRICT ON UPDATE CASCADE; +ALTER TABLE "CompanyDomain" ADD CONSTRAINT "CompanyDomain_sellerId_fkey" FOREIGN KEY ("sellerId") REFERENCES "Company"("id") ON DELETE RESTRICT ON UPDATE CASCADE; -- AddForeignKey ALTER TABLE "CompanyDomain" ADD CONSTRAINT "CompanyDomain_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; @@ -53,4 +53,4 @@ ALTER TABLE "CompanyDomain" ADD CONSTRAINT "CompanyDomain_createdById_fkey" FORE ALTER TABLE "Company" ADD CONSTRAINT "Company_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; -- AddForeignKey -ALTER TABLE "Deal" ADD CONSTRAINT "Deal_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "Deal" ADD CONSTRAINT "Deal_sellerId_fkey" FOREIGN KEY ("sellerId") REFERENCES "Company"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/20260120163044_seller_naming_change/migration.sql b/prisma/migrations/20260120163044_seller_naming_change/migration.sql new file mode 100644 index 0000000..50a27a7 --- /dev/null +++ b/prisma/migrations/20260120163044_seller_naming_change/migration.sql @@ -0,0 +1,72 @@ +/* + Warnings: + + - You are about to drop the column `sellerId` on the `Deal` table. All the data in the column will be lost. + - You are about to drop the column `customCompany` on the `Deal` table. All the data in the column will be lost. + - You are about to drop the `Company` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `CompanyDomain` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "public"."Company" DROP CONSTRAINT "Company_createdById_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."CompanyDomain" DROP CONSTRAINT "CompanyDomain_sellerId_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."CompanyDomain" DROP CONSTRAINT "CompanyDomain_createdById_fkey"; + +-- DropForeignKey +ALTER TABLE "public"."Deal" DROP CONSTRAINT "Deal_sellerId_fkey"; + +-- AlterTable +ALTER TABLE "Deal" DROP COLUMN "sellerId", +DROP COLUMN "customCompany", +ADD COLUMN "customSeller" TEXT, +ADD COLUMN "sellerId" INTEGER; + +-- DropTable +DROP TABLE "public"."Company"; + +-- DropTable +DROP TABLE "public"."CompanyDomain"; + +-- CreateTable +CREATE TABLE "SellerDomain" ( + "id" SERIAL NOT NULL, + "domain" TEXT NOT NULL, + "sellerId" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdById" INTEGER NOT NULL, + + CONSTRAINT "SellerDomain_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Seller" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdById" INTEGER NOT NULL, + + CONSTRAINT "Seller_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "SellerDomain_domain_key" ON "SellerDomain"("domain"); + +-- CreateIndex +CREATE UNIQUE INDEX "Seller_name_key" ON "Seller"("name"); + +-- AddForeignKey +ALTER TABLE "SellerDomain" ADD CONSTRAINT "SellerDomain_sellerId_fkey" FOREIGN KEY ("sellerId") REFERENCES "Seller"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "SellerDomain" ADD CONSTRAINT "SellerDomain_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Seller" ADD CONSTRAINT "Seller_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Deal" ADD CONSTRAINT "Deal_sellerId_fkey" FOREIGN KEY ("sellerId") REFERENCES "Seller"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/20260121111845_added_commentcount_into_deals/migration.sql b/prisma/migrations/20260121111845_added_commentcount_into_deals/migration.sql new file mode 100644 index 0000000..1b9794a --- /dev/null +++ b/prisma/migrations/20260121111845_added_commentcount_into_deals/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Deal" ADD COLUMN "commentCount" INTEGER NOT NULL DEFAULT 0; diff --git a/prisma/migrations/20260121134508_added_url_to_seller/migration.sql b/prisma/migrations/20260121134508_added_url_to_seller/migration.sql new file mode 100644 index 0000000..4b4efc1 --- /dev/null +++ b/prisma/migrations/20260121134508_added_url_to_seller/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Seller" ADD COLUMN "url" TEXT NOT NULL DEFAULT ''; diff --git a/prisma/migrations/20260121152845_vote_overhaul/migration.sql b/prisma/migrations/20260121152845_vote_overhaul/migration.sql new file mode 100644 index 0000000..5122149 --- /dev/null +++ b/prisma/migrations/20260121152845_vote_overhaul/migration.sql @@ -0,0 +1,39 @@ +/* + Warnings: + + - The `voteType` column on the `DealVote` table would be dropped and recreated. This will lead to data loss if there is data in the column. + +*/ +-- AlterTable +ALTER TABLE "DealVote" ADD COLUMN "lastVotedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +DROP COLUMN "voteType", +ADD COLUMN "voteType" INTEGER NOT NULL DEFAULT 0; + +-- CreateTable +CREATE TABLE "DealVoteHistory" ( + "id" SERIAL NOT NULL, + "dealId" INTEGER NOT NULL, + "userId" INTEGER NOT NULL, + "voteType" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "DealVoteHistory_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "DealVoteHistory_dealId_idx" ON "DealVoteHistory"("dealId"); + +-- CreateIndex +CREATE INDEX "DealVoteHistory_userId_idx" ON "DealVoteHistory"("userId"); + +-- CreateIndex +CREATE INDEX "DealVoteHistory_createdAt_idx" ON "DealVoteHistory"("createdAt"); + +-- CreateIndex +CREATE INDEX "DealVote_dealId_idx" ON "DealVote"("dealId"); + +-- AddForeignKey +ALTER TABLE "DealVoteHistory" ADD CONSTRAINT "DealVoteHistory_dealId_fkey" FOREIGN KEY ("dealId") REFERENCES "Deal"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "DealVoteHistory" ADD CONSTRAINT "DealVoteHistory_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4b60f9c..2c24115 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -24,8 +24,9 @@ model User { Deal Deal[] votes DealVote[] comments Comment[] - companies Company[] - domains CompanyDomain[] + companies Seller[] + domains SellerDomain[] + dealVoteHistory DealVoteHistory[] } enum DealStatus { @@ -47,28 +48,28 @@ enum AffiliateType{ USER_AFFILIATE } - model CompanyDomain { + model SellerDomain { id Int @id @default(autoincrement()) domain String @unique - companyId Int - company Company @relation(fields: [companyId], references: [id]) + sellerId Int + seller Seller @relation(fields: [sellerId], references: [id]) createdAt DateTime @default(now()) createdById Int createdBy User @relation(fields: [createdById], references: [id]) } - model Company { + model Seller { id Int @id @default(autoincrement()) name String @unique + url String @default("") isActive Boolean @default(true) - Links String? createdAt DateTime @default(now()) createdById Int deals Deal[] createdBy User @relation(fields: [createdById], references: [id]) - domains CompanyDomain[] + domains SellerDomain[] } model Deal { @@ -80,19 +81,21 @@ model Deal { userId Int score Int @default(0) + commentCount Int @default(0) status DealStatus @default(PENDING) saletype SaleType @default(ONLINE) affiliateType AffiliateType @default(NON_AFFILIATE) - companyId Int? - customCompany String? + sellerId Int? + customSeller String? - company Company? @relation(fields: [companyId], references: [id]) + seller Seller? @relation(fields: [sellerId], references: [id]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id]) votes DealVote[] + voteHistory DealVoteHistory[] comments Comment[] images DealImage[] // ← yeni ilişki } @@ -107,16 +110,33 @@ model DealImage { } model DealVote { + id Int @id @default(autoincrement()) + dealId Int + userId Int + voteType Int @default(0) // -1,0,1 + createdAt DateTime @default(now()) + lastVotedAt DateTime @default(now()) // her vote değişiminde set edeceğiz + + deal Deal @relation(fields: [dealId], references: [id]) + user User @relation(fields: [userId], references: [id]) + + @@unique([dealId, userId]) + @@index([dealId]) +} + +model DealVoteHistory { id Int @id @default(autoincrement()) dealId Int userId Int - voteType String + voteType Int createdAt DateTime @default(now()) deal Deal @relation(fields: [dealId], references: [id]) user User @relation(fields: [userId], references: [id]) - @@unique([dealId, userId]) // aynı kullanıcı aynı ilana bir kez oy verebilir + @@index([dealId]) + @@index([userId]) + @@index([createdAt]) } diff --git a/prisma/seed.js b/prisma/seed.js index ee82bd8..d6567a8 100644 --- a/prisma/seed.js +++ b/prisma/seed.js @@ -28,8 +28,8 @@ async function main() { }, }) - // ---------- COMPANY ---------- - const amazon = await prisma.company.upsert({ + // ---------- Seller ---------- + const amazon = await prisma.seller.upsert({ where: { name: 'Amazon' }, update: {}, create: { @@ -39,16 +39,16 @@ async function main() { }, }) - // ---------- COMPANY DOMAINS ---------- + // ---------- Seller DOMAINS ---------- const domains = ['amazon.com', 'amazon.com.tr'] for (const domain of domains) { - await prisma.companyDomain.upsert({ + await prisma.SellerDomain.upsert({ where: { domain }, update: {}, create: { domain, - companyId: amazon.id, + sellerId: amazon.id, createdById: admin.id, }, }) @@ -64,8 +64,9 @@ async function main() { status: DealStatus.ACTIVE, saletype: SaleType.ONLINE, affiliateType: AffiliateType.NON_AFFILIATE, + commentCount:1, userId: user.id, - companyId: amazon.id, + sellerId: amazon.id, }, }) @@ -97,7 +98,7 @@ async function main() { create: { dealId: deal.id, userId: admin.id, - voteType: 'UP', + voteType: 1, }, }) diff --git a/routes/account/accountSettings.routes.js b/routes/accountSettings.routes.js similarity index 58% rename from routes/account/accountSettings.routes.js rename to routes/accountSettings.routes.js index d88d789..62e0cd5 100644 --- a/routes/account/accountSettings.routes.js +++ b/routes/accountSettings.routes.js @@ -1,19 +1,21 @@ const express = require("express") const multer = require("multer") const fs = require("fs") -const { uploadProfileImage } = require("../../services/supabase/supabaseUpload.service") -const { validateImage } = require("../../utils/validateImage") -const authMiddleware = require("../../middleware/authMiddleware") -const { getUserProfile } = require("../../services/profile/profile.service") +const { uploadProfileImage } = require("../services/supabaseUpload.service") +const { validateImage } = require("../utils/validateImage") +const authRequiredMiddleware = require("../middleware/authRequired.middleware") +const authOptionalMiddleware = require("../middleware/authOptional.middleware") +const { getUserProfile } = require("../services/profile.service") const router = express.Router() const upload = multer({ dest: "uploads/" }) -const { updateUserAvatar } = require("../../services/account/avatar.service") +const { updateUserAvatar } = require("../services/avatar.service") router.post( "/avatar", - authMiddleware, + authRequiredMiddleware +, upload.single("file"), async (req, res) => { try { @@ -34,7 +36,8 @@ router.post( ) -router.get("/me", authMiddleware, async (req, res) => { +router.get("/me", authRequiredMiddleware +, async (req, res) => { try { const user = await getUserProfile(req.user.id) res.json(user) diff --git a/routes/auth.routes.js b/routes/auth.routes.js index 96672fe..dc5584b 100644 --- a/routes/auth.routes.js +++ b/routes/auth.routes.js @@ -1,79 +1,61 @@ const express = require("express"); -const bcrypt = require("bcryptjs"); -const jwt = require("jsonwebtoken"); -const { PrismaClient } = require("@prisma/client"); -const generateToken = require("../utils/generateToken"); -const authMiddleware = require("../middleware/authMiddleware"); - +const authRequiredMiddleware + = require("../middleware/authRequired.middleware"); +const authService=require("../services/auth.service") const router = express.Router(); -const prisma = new PrismaClient(); + +const { + mapLoginRequestToLoginInput, + mapLoginResultToResponse, +} = require("../adapters/responses/login.adapter"); +const { + mapRegisterRequestToRegisterInput, + mapRegisterResultToResponse, +} = require("../adapters/responses/register.adapter"); +const { + mapMeRequestToUserId, + mapMeResultToResponse, +} = require("../adapters/responses/me.adapter"); -// Kayıt ol router.post("/register", async (req, res) => { try { - const { username, email, password } = req.body; - - const existingUser = await prisma.user.findUnique({ where: { email } }); - if (existingUser) return res.status(400).json({ message: "Bu e-posta zaten kayıtlı." }); - - const hashedPassword = await bcrypt.hash(password, 10); - - const user = await prisma.user.create({ - data: { username, email, passwordHash: hashedPassword }, - }); - - const token = generateToken(user.id); - - res.json({ token, user: { id: user.id, username: user.username, email: user.email } }); + const input = mapRegisterRequestToRegisterInput(req.body); + const result = await authService.register(input); + res.json(mapRegisterResultToResponse(result)); } catch (err) { - res.status(500).json({ message: "Kayıt işlemi başarısız.", error: err.message }); + const status = err.statusCode || 500; + res.status(status).json({ + message: err.message || "Kayıt işlemi başarısız.", + }); } }); -// Giriş yap router.post("/login", async (req, res) => { try { - const { email, password } = req.body; - - const user = await prisma.user.findUnique({ where: { email } }); - if (!user) - return res.status(400).json({ message: "Kullanıcı bulunamadı." }); - - const isMatch = await bcrypt.compare(password, user.passwordHash); - if (!isMatch) - return res.status(401).json({ message: "Şifre hatalı." }); - - // userId olarak imzala - const token = generateToken(user.id); - - res.json({ - token, - user: { id: user.id, username: user.username, email: user.email,avatarUrl:user.avatarUrl }, - }); + const input = mapLoginRequestToLoginInput(req.body); + const result = await authService.login(input); + res.json(mapLoginResultToResponse(result)); } catch (err) { - console.error(err); - res - .status(500) - .json({ message: "Giriş işlemi başarısız.", error: err.message }); + const status = err.statusCode || 500; + res.status(status).json({ message: err.message || "Giriş işlemi başarısız." }); } }); -router.get("/me", authMiddleware, async (req, res) => { + +router.get("/me", authRequiredMiddleware +, async (req, res) => { try { - const user = await prisma.user.findUnique({ - where: { id: req.user.userId }, - select: { id: true, username: true, email: true,avatarUrl:true }, - }) - - if (!user) return res.status(404).json({ error: "Kullanıcı bulunamadı" }) - res.json(user) + const userId = mapMeRequestToUserId(req); + const user = await authService.getMe(userId); + res.json(mapMeResultToResponse(user)); } catch (err) { - console.error(err) - res.status(500).json({ error: "Sunucu hatası" }) + const status = err.statusCode || 500; + res.status(status).json({ + message: err.message || "Sunucu hatası", + }); } -}) - +}); module.exports = router; diff --git a/routes/deal/comment.routes.js b/routes/comment.routes.js similarity index 54% rename from routes/deal/comment.routes.js rename to routes/comment.routes.js index 6d93050..c2079df 100644 --- a/routes/deal/comment.routes.js +++ b/routes/comment.routes.js @@ -1,24 +1,31 @@ const express = require("express") -const authMiddleware = require("../../middleware/authMiddleware") +const authRequiredMiddleware = require("../middleware/authRequired.middleware") +const authOptionalMiddleware = require("../middleware/authOptional.middleware") const { getCommentsByDealId, createComment, deleteComment, -} = require("../../services/deal/comment.service") +} = require("../services/comment.service") + +const dealCommentAdapter=require("../adapters/responses/comment.adapter") +const commentService=require("../services/comment.service") const router = express.Router() router.get("/:dealId", async (req, res) => { try { - const comments = await getCommentsByDealId(req.params.dealId) - res.json(comments) + const dealId = Number(req.params.dealId) + const comments = await commentService.getCommentsByDealId(dealId) + res.json(dealCommentAdapter.mapCommentsToDealCommentResponse(comments)) + } catch (err) { - console.error(err) - res.status(500).json({ error: "Sunucu hatası" }) + console.log(err.message) + res.status(400).json({ error: err.message }) } }) -router.post("/", authMiddleware, async (req, res) => { +router.post("/", authRequiredMiddleware +, async (req, res) => { try { const { dealId, text } = req.body const userId = req.user.userId @@ -32,7 +39,8 @@ router.post("/", authMiddleware, async (req, res) => { } }) -router.delete("/:id", authMiddleware, async (req, res) => { +router.delete("/:id", authRequiredMiddleware +, async (req, res) => { try { const result = await deleteComment(req.params.id, req.user.userId) res.json(result) diff --git a/routes/deal.routes.js b/routes/deal.routes.js new file mode 100644 index 0000000..5e64ad9 --- /dev/null +++ b/routes/deal.routes.js @@ -0,0 +1,75 @@ +const express = require("express") +const router = express.Router() +const { getDeals, getDealById, createDeal,searchDeals } = require("../services/deal.service") +const authRequiredMiddleware = require("../middleware/authRequired.middleware") +const authOptionalMiddleware = require("../middleware/authOptional.middleware") +const { upload } = require("../middleware/upload.middleware"); + + +const {mapCreateDealRequestToDealCreateData} =require("../adapters/requests/dealCreate.adapter") +const { mapDealToDealDetailResponse } = require("../adapters/responses/dealDetail.adapter") +const { mapDealToDealCardResponse,mapPaginatedDealsToDealCardResponse } = require("../adapters/responses/dealCard.adapter") + + +router.get("/", authOptionalMiddleware, async (req, res) => { + try { + const q = (req.query.q ?? "").toString().trim() + const page = Number(req.query.page) || 1 + const limit = Number(req.query.limit) || 10 + const userId = req.user?.userId ?? null + const data = await getDeals({ q, page, limit, userId }) + + res.json(mapPaginatedDealsToDealCardResponse(data)) + } catch (e) { + console.error(e) + res.status(500).json({ error: "Sunucu hatası" }) + } +}) + + +router.get("/search", async (req, res) => { + try { + const query = req.query.q || "" + const page = Number(req.query.page) || 1 + const limit = 10 + + if (!query.trim()) { + return res.json({ results: [], total: 0, totalPages: 0, page }) + } + + const data = await searchDeals(query, page, limit) + res.json(mapPaginatedDealsToDealCardResponse(data)) + } catch (e) { + console.error(e) + res.status(500).json({ error: "Sunucu hatası" }) + } +}) + +router.get("/:id", async (req, res) => { //MAPPED + try { + const deal = await getDealById(req.params.id) + if (!deal) return res.status(404).json({ error: "Deal bulunamadı" }) + console.log(mapDealToDealDetailResponse(deal)) + res.json(mapDealToDealDetailResponse(deal)) + } catch (e) { + console.error(e) + res.status(500).json({ error: "Sunucu hatası" }) + } +}) + +router.post( "/", authRequiredMiddleware, upload.array("images", 5), async (req, res) => { + try { + const userId = req.user.userId; + const dealCreateData = mapCreateDealRequestToDealCreateData(req.body, userId); + const deal = await createDeal(dealCreateData, req.files || []); + res.json(deal); + } catch (err) { + console.error(err); + res.status(500).json({ error: "Sunucu hatası" }); + } + } +); + + + +module.exports = router diff --git a/routes/deal/deal.routes.js b/routes/deal/deal.routes.js deleted file mode 100644 index 1572b00..0000000 --- a/routes/deal/deal.routes.js +++ /dev/null @@ -1,61 +0,0 @@ -const express = require("express") -const router = express.Router() -const { getAllDeals, getDealById, createDeal,searchDeals } = require("../../services/deal/deal.service") -const authMiddleware = require("../../middleware/authMiddleware") - -router.get("/", async (req, res) => { - try { - const page = Number(req.query.page) || 1 - const limit = 10 - const data = await getAllDeals(page, limit) - res.json(data) - } catch (e) { - console.error(e) - res.status(500).json({ error: "Sunucu hatası" }) - } -}) - - -router.get("/search", async (req, res) => { - try { - const query = req.query.q || "" - const page = Number(req.query.page) || 1 - const limit = 10 - - if (!query.trim()) { - return res.json({ results: [], total: 0, totalPages: 0, page }) - } - - const data = await searchDeals(query, page, limit) - res.json(data) - } catch (e) { - console.error(e) - res.status(500).json({ error: "Sunucu hatası" }) - } -}) - -router.get("/:id", async (req, res) => { - try { - const deal = await getDealById(req.params.id) - if (!deal) return res.status(404).json({ error: "Deal bulunamadı" }) - res.json(deal) - } catch (e) { - console.error(e) - res.status(500).json({ error: "Sunucu hatası" }) - } -}) - -router.post("/", authMiddleware, async (req, res) => { - try { - const userId = req.user.userId - const deal = await createDeal(req.body, userId) - res.json(deal) - } catch (err) { - console.error(err) - res.status(500).json({ error: "Sunucu hatası" }) - } -}) - - - -module.exports = router diff --git a/routes/deal/vote.routes.js b/routes/deal/vote.routes.js deleted file mode 100644 index b9b22e3..0000000 --- a/routes/deal/vote.routes.js +++ /dev/null @@ -1,51 +0,0 @@ -const express = require("express") -const authMiddleware = require("../../middleware/authMiddleware") -const { voteDeal, getVotes } = require("../../services/deal/deal.service") -const { z } = require("zod") - -const router = express.Router() - -// Şema tanımı -const voteSchema = z.object({ - dealId: z.number().int().positive(), - voteType: z.enum(["UP", "DOWN"]), -}) - -// Oy verme -router.post("/", authMiddleware, async (req, res) => { - const parsed = voteSchema.safeParse(req.body) - if (!parsed.success) { - return res.status(400).json({ - error: "Geçersiz veri", - details: parsed.error.errors.map((e) => e.message), - }) - } - - const { dealId, voteType } = parsed.data - const userId = req.user.userId - - try { - const score = await voteDeal(dealId, userId, voteType) - res.json({ score }) - } catch (err) { - console.error(err) - res.status(500).json({ error: "Sunucu hatası" }) - } -}) - -// Belirli deal için oyları çek -router.get("/:dealId", async (req, res) => { - try { - const dealId = Number(req.params.dealId) - if (isNaN(dealId) || dealId <= 0) - return res.status(400).json({ error: "Geçersiz dealId" }) - - const data = await getVotes(dealId) - res.json(data) - } catch (err) { - console.error(err) - res.status(500).json({ error: "Sunucu hatası" }) - } -}) - -module.exports = router diff --git a/routes/seller.routes.js b/routes/seller.routes.js new file mode 100644 index 0000000..6fae30d --- /dev/null +++ b/routes/seller.routes.js @@ -0,0 +1,31 @@ +const express = require("express") +const router = express.Router() +const authRequiredMiddleware = require("../middleware/authRequired.middleware") +const authOptionalMiddleware = require("../middleware/authOptional.middleware") +const { findSellerFromLink } = require("../services/seller.service") + + +router.post("/from-link", authRequiredMiddleware +, async (req, res) => { + try { + const sellerUrl = req.body.url + const Seller = await findSellerFromLink(sellerUrl) + + if (!Seller) { + return res.json({ + sellerId: -1, + sellerName: null, + }) + } + return res.json({ + id: Seller.id, + name: Seller.name, + }) + + } catch (e) { + console.error(e) + res.status(500).json({ error: "Sunucu hatası" }) + } +}) + +module.exports = router diff --git a/routes/seller/seller.routes.js b/routes/seller/seller.routes.js deleted file mode 100644 index 3455387..0000000 --- a/routes/seller/seller.routes.js +++ /dev/null @@ -1,34 +0,0 @@ -const express = require("express") -const router = express.Router() -const authMiddleware = require("../../middleware/authMiddleware") -const { findCompanyFromLink } = require("../../services/seller/seller.service") - - -router.post("/from-link", authMiddleware, async (req, res) => { - try { - const seller = req.body.seller - - if (!seller) { - return res.status(400).json({ error: "URL gerekli" }) - } - - const company = await findCompanyFromLink(url) - - if (!company) { - return res.json({ - sellerId: -1, - sellerName: null, - }) - } - - return res.json({ - sellerId: company.id, - sellerName: company.name, - }) - } catch (e) { - console.error(e) - res.status(500).json({ error: "Sunucu hatası" }) - } -}) - -module.exports = router diff --git a/routes/user.routes.js b/routes/user.routes.js index a670e16..4b8b9ec 100644 --- a/routes/user.routes.js +++ b/routes/user.routes.js @@ -1,11 +1,19 @@ -const express = require("express"); -const { PrismaClient } = require("@prisma/client"); -const router = express.Router(); -const prisma = new PrismaClient(); +// routes/user.js +const express = require("express") +const router = express.Router() -router.get("/", async (req, res) => { - const users = await prisma.user.findMany(); - res.json(users); -}); +const userService = require("../services/user.service") +const userProfileAdapter = require("../adapters/responses/userProfile.adapter") -module.exports = router; +router.get("/:userName", async (req, res) => { + try { + const data = await userService.getUserProfileByUsername(req.params.userName) + res.json(userProfileAdapter.mapUserProfileToResponse(data)) + } catch (err) { + console.error(err) + const status = err.statusCode || 500 + res.status(status).json({ message: err.message || "Profil bilgileri alınamadı." }) + } +}) + +module.exports = router diff --git a/routes/user/user.routes.js b/routes/user/user.routes.js deleted file mode 100644 index fec874b..0000000 --- a/routes/user/user.routes.js +++ /dev/null @@ -1,63 +0,0 @@ -// routes/profileRoutes.js -const express = require("express") -const { PrismaClient } = require("@prisma/client") -const prisma = new PrismaClient() -const router = express.Router() - -// Belirli bir kullanıcının profil detayları -router.get("/:userName", async (req, res) => { - console.log("İstek geldi:", req.params.userName) - try { - const username = req.params.userName - const user = await prisma.user.findUnique({ - where: { username: username }, - select: { - id: true, - username: true, - avatarUrl: true, - createdAt: true, - }, - }) - - if (!user) return res.status(404).json({ message: "Kullanıcı bulunamadı." }) - - // Kullanıcının paylaştığı fırsatlar - const deals = await prisma.deal.findMany({ - where: { userId: user.id }, - orderBy: { createdAt: "desc" }, - select: { - id: true, - title: true, - price: true, - createdAt: true, - score: true, - images: { - orderBy: { order: "asc" }, // küçük order en önde - take: 1, // sadece ilk görsel - select: { imageUrl: true }, - }, - }, - }) - - - // Kullanıcının yaptığı yorumlar - const comments = await prisma.comment.findMany({ - where: { userId:user.id }, - orderBy: { createdAt: "desc" }, - select: { - id: true, - text: true, - dealId: true, - createdAt: true, - deal: { select: { title: true } }, - }, - }) - - res.json({ user, deals, comments }) - } catch (err) { - console.error(err) - res.status(500).json({ message: "Profil bilgileri alınamadı.", error: err.message }) - } -}) - -module.exports = router diff --git a/routes/vote.routes.js b/routes/vote.routes.js new file mode 100644 index 0000000..c3260f6 --- /dev/null +++ b/routes/vote.routes.js @@ -0,0 +1,35 @@ +const express = require("express") +const authRequiredMiddleware = require("../middleware/authRequired.middleware") +const authOptionalMiddleware = require("../middleware/authOptional.middleware") +const voteService = require("../services/vote.service") +const {mapVoteRequestToVoteInput,mapVoteResultToResponse}=require("../adapters/responses/vote.adapter") +const router = express.Router() + + +router.post("/", authRequiredMiddleware +, async (req, res) => { + try { + const input = mapVoteRequestToVoteInput(req); + const result = await voteService.voteDeal(input); + res.json(result); + } catch (err) { + const status = err.statusCode || 500; + res.status(status).json({ message: err.message || "Sunucu hatası" }); + } +}); +// Belirli deal için oyları çek +router.get("/:dealId", async (req, res) => { + try { + const dealId = Number(req.params.dealId) + if (isNaN(dealId) || dealId <= 0) + return res.status(400).json({ error: "Geçersiz dealId" }) + + const data = await voteService.getVotes(dealId) + res.json(data) + } catch (err) { + console.error(err) + res.status(500).json({ error: "Sunucu hatası" }) + } +}) + +module.exports = router diff --git a/server.js b/server.js index 4655858..ba9a4f4 100644 --- a/server.js +++ b/server.js @@ -3,15 +3,16 @@ const cors = require("cors") require("dotenv").config() const userRoutesneedRefactor = require("./routes/user.routes") -const dealRoutes = require("./routes/deal/deal.routes") +const dealRoutes = require("./routes/deal.routes") const authRoutes = require("./routes/auth.routes") -const dealVoteRoutes = require("./routes/deal/vote.routes") -const commentRoutes = require("./routes/deal/comment.routes") -const accountSettingsRoutes = require("./routes/account/accountSettings.routes") -const userRoutes = require("./routes/user/user.routes") -const sellerRoutes = require("./routes/seller/seller.routes") - +const dealVoteRoutes = require("./routes/vote.routes") +const commentRoutes = require("./routes/comment.routes") +const accountSettingsRoutes = require("./routes/accountSettings.routes") +const userRoutes = require("./routes/user.routes") +const sellerRoutes = require("./routes/seller.routes") +const voteRoutes=require("./routes/vote.routes") const app = express() + app.use(cors()) app.use(express.json()) app.use(express.urlencoded({ extended: true })) @@ -24,4 +25,6 @@ app.use("/api/comments", commentRoutes) app.use("/api/account", accountSettingsRoutes) app.use("/api/user", userRoutes) app.use("/api/seller", sellerRoutes) +app.use("/api/vote", voteRoutes) + app.listen(3000, () => console.log("Server running on http://localhost:3000")) diff --git a/services/auth.service.js b/services/auth.service.js new file mode 100644 index 0000000..7f39f00 --- /dev/null +++ b/services/auth.service.js @@ -0,0 +1,85 @@ +const bcrypt = require("bcryptjs"); +const generateToken = require("../utils/generateToken"); +const authDb = require("../db/auth.db"); + +async function login({ email, password }) { + const user = await authDb.findUserByEmail(email); + + if (!user) { + const err = new Error("Kullanıcı bulunamadı."); + err.statusCode = 400; + throw err; + } + + const isMatch = await bcrypt.compare(password, user.passwordHash); + if (!isMatch) { + const err = new Error("Şifre hatalı."); + err.statusCode = 401; + throw err; + } + + const token = generateToken(user.id); + + return { + token, + user: { + id: user.id, + username: user.username, + email: user.email, + avatarUrl: user.avatarUrl, + }, + }; +} + +async function register({ username, email, password }) { + const existingUser = await authDb.findUserByEmail(email); + if (existingUser) { + const err = new Error("Bu e-posta zaten kayıtlı."); + err.statusCode = 400; + throw err; + } + + const passwordHash = await bcrypt.hash(password, 10); + + const user = await authDb.createUser({ + username, + email, + passwordHash, + }); + + const token = generateToken(user.id); + + return { + token, + user: { + id: user.id, + username: user.username, + email: user.email, + avatarUrl: user.avatarUrl ?? null, + }, + }; +} + +async function getMe(userId) { + const user = await authDb.findUserById(userId, { + select: { + id: true, + username: true, + email: true, + avatarUrl: true, + }, + }); + + if (!user) { + const err = new Error("Kullanıcı bulunamadı"); + err.statusCode = 404; + throw err; + } + + return user; +} +module.exports = { + login, + register, + getMe, +}; diff --git a/services/account/avatar.service.js b/services/avatar.service.js similarity index 82% rename from services/account/avatar.service.js rename to services/avatar.service.js index 4fb1313..16a4b2a 100644 --- a/services/account/avatar.service.js +++ b/services/avatar.service.js @@ -1,8 +1,8 @@ const fs = require("fs") -const { uploadImage } = require("../uploadImage.service") -const { validateImage } = require("../../utils/validateImage") +const { uploadImage } = require("./uploadImage.service") +const { validateImage } = require("../utils/validateImage") -const userDB = require("../../db/user.db") +const userDB = require("../db/user.db") async function updateUserAvatar(userId, file) { if (!file) { diff --git a/services/deal/comment.service.js b/services/comment.service.js similarity index 61% rename from services/deal/comment.service.js rename to services/comment.service.js index 3b0f2b1..1d015aa 100644 --- a/services/deal/comment.service.js +++ b/services/comment.service.js @@ -1,5 +1,6 @@ -const dealDB = require("../../db/deal.db") -const commentDB = require("../../db/comment.db") +const dealDB = require("../db/deal.db") +const commentDB = require("../db/comment.db") +const prisma = require("../db/client") function assertPositiveInt(v, name = "id") { const n = Number(v) @@ -8,33 +9,41 @@ function assertPositiveInt(v, name = "id") { } async function getCommentsByDealId(dealId) { - const id = assertPositiveInt(dealId, "dealId") + const id = Number(dealId) const deal = await dealDB.findDeal({ id }) if (!deal) throw new Error("Deal bulunamadı.") - const include = { user: { select: { username: true, avatarUrl: true } } } + const include = { user: { select: { id:true,username: true, avatarUrl: true } } } return commentDB.findComments({ dealId: id }, { include }) } async function createComment({ dealId, userId, text }) { - const dId = assertPositiveInt(dealId, "dealId") - const uId = assertPositiveInt(userId, "userId") - if (!text || typeof text !== "string" || !text.trim()) throw new Error("Yorum boş olamaz.") - const deal = await dealDB.findDeal({ id: dId }) - if (!deal) throw new Error("Deal bulunamadı.") - + const trimmed = text.trim() const include = { user: { select: { username: true, avatarUrl: true } } } - const data = { - text: text.trim(), - userId: uId, - dealId: dId, - } - return commentDB.createComment(data, { include }) + return prisma.$transaction(async (tx) => { + const deal = await dealDB.findDeal({ id: dealId }, {}, tx) + if (!deal) throw new Error("Deal bulunamadı.") + + const comment = await commentDB.createComment( + { text: trimmed, userId, dealId }, + { include }, + tx + ) + + await dealDB.updateDeal( + { id: dealId }, + { commentCount: { increment: 1 } }, + {}, + tx + ) + + return comment + }) } async function deleteComment(commentId, userId) { @@ -51,6 +60,12 @@ async function deleteComment(commentId, userId) { await commentDB.deleteComment({ id: cId }) return { message: "Yorum silindi." } +} + +async function commentChange(length,dealId){ + + + } module.exports = { diff --git a/services/deal.service.js b/services/deal.service.js new file mode 100644 index 0000000..6697d11 --- /dev/null +++ b/services/deal.service.js @@ -0,0 +1,203 @@ +const dealDB = require("../db/deal.db") + +const { findSellerFromLink, } = require("./seller.service") +const { makeDetailWebp, makeThumbWebp } = require("../utils/processImage"); +const { v4: uuidv4 } = require("uuid"); +const {uploadImage}=require("./uploadImage.service") + +const dealImageDB = require("../db/dealImage.db"); + +async function getDeals({ q = "", page = 1, limit = 10, userId = null }) { + const skip = (page - 1) * limit + + const queryRaw = (q ?? "").toString().trim() + const query = + queryRaw === "undefined" || queryRaw === "null" ? "" : queryRaw + + const where = + query.length > 0 + ? { + OR: [ + { title: { contains: query, mode: "insensitive" } }, + { description: { contains: query, mode: "insensitive" } }, + ], + } + : {} + + const [deals, total] = await Promise.all([ + dealDB.findDeals(where, { + skip, + take: limit, + orderBy: { createdAt: "desc" }, + include: { + seller: { select: { name: true, url: true } }, + user: { select: { id: true, username: true, avatarUrl: true } }, + images: { + orderBy: { order: "asc" }, + take: 1, + select: { imageUrl: true }, + }, + }, + }), + dealDB.countDeals(where), + ]) + + // auth yoksa myVote=0 + if (!userId) { + return { + page, + total, + totalPages: Math.ceil(total / limit), + results: deals.map((d) => ({ ...d, myVote: 0 })), + } + } + + const dealIds = deals.map((d) => d.id) + + const votes = await dealDB.findVotes( + { userId, dealId: { in: dealIds } }, + { select: { dealId: true, voteType: true } } + ) + + const voteByDealId = new Map(votes.map((v) => [v.dealId, v.voteType])) + + return { + page, + total, + totalPages: Math.ceil(total / limit), + results: deals.map((d) => ({ + ...d, + myVote: voteByDealId.get(d.id) ?? 0, + })), + } +} + + + +async function getDealById(id) { + const deal=await dealDB.findDeal( + { id: Number(id) }, + { + include: { + seller:{ + select: { + name:true, + url:true + }, + }, + user: { + select: { + id: true, + username: true, + avatarUrl: true, + }, + }, + seller: { + select: { + id: true, + name: true, + }, + }, + images: { + orderBy: { order: "asc" }, + select: { + id: true, + imageUrl: true, + order: true, + }, + }, + comments: { + orderBy: { createdAt: "desc" }, + select: { + id: true, + text: true, + createdAt: true, + user: { + select: { + id: true, + username: true, + avatarUrl: true, + }, + }, + }, + }, + _count: { + select: { + comments: true, + }, + }, + }, + } + ) + + return deal +} + + +async function createDeal(dealCreateData, files = []) { + // seller bağlama + if (dealCreateData.url) { + const seller = await findSellerFromLink(dealCreateData.url); + if (seller) { + dealCreateData.seller = { connect: { id: seller.id } }; + dealCreateData.customSeller = null; + } + } + + // 1) Deal oluştur + const deal = await dealDB.createDeal(dealCreateData); + + // 2) Önce image işle + upload + const rows = []; + + for (let i = 0; i < files.length && i < 5; i++) { + const file = files[i]; + const order = i; + + const key = uuidv4(); + const basePath = `deals/${deal.id}/${key}`; + const detailPath = `${basePath}_detail.webp`; + const thumbPath = `${basePath}_thumb.webp`; + const BUCKET="deal"; + + const detailBuffer = await makeDetailWebp(file.buffer); + const detailUrl = await uploadImage({ + bucket: BUCKET, + path: detailPath, + fileBuffer: detailBuffer, + contentType: "image/webp", + }); + + if (order === 0) { + const thumbBuffer = await makeThumbWebp(file.buffer); + await uploadImage({ + bucket: BUCKET, + path: thumbPath, + fileBuffer: thumbBuffer, + contentType: "image/webp", + }); + } + + rows.push({ dealId: deal.id, order, imageUrl: detailUrl }); + } + + // 3) Uploadlar bitti -> DB’de tek seferde yaz + if (rows.length > 0) { + await dealImageDB.createManyDealImages(rows); + } + + // 4) Deal + images dön + return dealDB.getDealWithImages(deal.id); +} + + + + + + + +module.exports = { + getDeals, + getDealById, + createDeal, +} diff --git a/services/deal/deal.service.js b/services/deal/deal.service.js deleted file mode 100644 index a878225..0000000 --- a/services/deal/deal.service.js +++ /dev/null @@ -1,187 +0,0 @@ -const dealDB = require("../../db/deal.db") -const { mapDealToDealCardResponse } = require("../../adapters/responses/dealCard.adapter") -const { mapDealToDealDetailResponse } = require("../../adapters/responses/dealDetail.adapter") -const { - findCompanyFromLink, -} = require("../seller/seller.service") - - -async function searchDeals(query, page = 1, limit = 10) { - const skip = (page - 1) * limit - const where = { - OR: [ - { title: { contains: query, mode: "insensitive" } }, - { description: { contains: query, mode: "insensitive" } }, - ], - } - - const [deals, total] = await Promise.all([ - dealDB.findDeals(where, { - skip, - take: limit, - orderBy: { createdAt: "desc" }, - include: { - company:{select:{name:true}}, - user: { select: {id:true,username: true } }, - images: { - orderBy: { order: "asc" }, - take: 1, - select: { imageUrl: true }, - }, - - }, - }), - dealDB.countDeals(where), - ]) - - return { - page, - total, - totalPages: Math.ceil(total / limit), - results: deals.map(mapDealToDealCardResponse), - } -} - -async function getAllDeals(page = 1, limit = 10) { - const skip = (page - 1) * limit - - const [deals, total] = await Promise.all([ - dealDB.findDeals({}, { - skip, - take: limit, - orderBy: { createdAt: "desc" }, - include: { - company:{select:{name:true}}, - user: { select: { id:true,username: true } }, - images: { - orderBy: { order: "asc" }, - take: 1, - select: { imageUrl: true }, - }, - }, - }), - dealDB.countDeals(), - ]) - - return { - page, - total, - totalPages: Math.ceil(total / limit), - results: deals.map(mapDealToDealCardResponse), - } -} - -async function getDealById(id) { - const deal=await dealDB.findDeal( - { id: Number(id) }, - { - include: { - company:{ - select: { - name:true, - url:true - }, - }, - user: { - select: { - id: true, - username: true, - avatarUrl: true, - }, - }, - company: { - select: { - id: true, - name: true, - }, - }, - images: { - orderBy: { order: "asc" }, - select: { - id: true, - imageUrl: true, - order: true, - }, - }, - comments: { - orderBy: { createdAt: "desc" }, - select: { - id: true, - text: true, - createdAt: true, - user: { - select: { - id: true, - username: true, - avatarUrl: true, - }, - }, - }, - }, - _count: { - select: { - comments: true, - }, - }, - }, - } - ) - - return mapDealToDealDetailResponse(deal) -} - - -async function createDeal(dealCreateData) { - // 🔴 SADECE link varsa bak - if (dealCreateData.url) { - const company = await findCompanyFromLink( - dealCreateData.url - ) - - if (company) { - dealCreateData.company = { - connect: { id: company.id }, - } - dealCreateData.customCompany = null - } - } - - return dealDB.createDeal(dealCreateData, { - include: { images: true }, - }) -} - -async function voteDeal(dealId, userId, voteType) { - if (!dealId || !userId || !voteType) throw new Error("Eksik veri") - - const existingVote = await dealDB.findVotes({ dealId, userId }) - const vote = existingVote[0] - - if (vote) { - await dealDB.updateVote({ id: vote.id }, { voteType }) - } else { - await dealDB.createVote({ dealId, userId, voteType }) - } - - const upvotes = await dealDB.countVotes({ dealId, voteType: "UP" }) - const downvotes = await dealDB.countVotes({ dealId, voteType: "DOWN" }) - const score = upvotes - downvotes - - await dealDB.updateDeal({ id: dealId }, { score }) - return score -} - -async function getVotes(dealId) { - const upvotes = await dealDB.countVotes({ dealId: Number(dealId), voteType: "UP" }) - const downvotes = await dealDB.countVotes({ dealId: Number(dealId), voteType: "DOWN" }) - return { upvotes, downvotes, score: upvotes - downvotes } -} - -module.exports = { - getAllDeals, - getDealById, - createDeal, - voteDeal, - getVotes, - searchDeals, -} diff --git a/services/profile/profile.service.js b/services/profile.service.js similarity index 94% rename from services/profile/profile.service.js rename to services/profile.service.js index 7768936..29244d3 100644 --- a/services/profile/profile.service.js +++ b/services/profile.service.js @@ -1,4 +1,4 @@ -const userDb = require("../../db/user.db") +const userDb = require("../db/user.db") function assertPositiveInt(v, name = "id") { const n = Number(v) diff --git a/services/seller/seller.service.js b/services/seller.service.js similarity index 53% rename from services/seller/seller.service.js rename to services/seller.service.js index 929e314..1019e8e 100644 --- a/services/seller/seller.service.js +++ b/services/seller.service.js @@ -1,11 +1,11 @@ -// services/company/companyService.js -const { findCompanyByDomain } = require("../../db/seller.db") +// services/seller/sellerService.js +const { findSellerByDomain } = require("../db/seller.db") function normalizeDomain(hostname) { return hostname.replace(/^www\./, "") } -async function findCompanyFromLink(url) { +async function findSellerFromLink(url) { let hostname try { @@ -16,17 +16,17 @@ async function findCompanyFromLink(url) { const domain = normalizeDomain(hostname) - const company = await findCompanyByDomain(domain) - if (company) { - return company + const seller = await findSellerByDomain(domain) + if (seller) { + return seller } const domainParts = domain.split(".") for (let i = 1; i <= domainParts.length - 2; i += 1) { const parentDomain = domainParts.slice(i).join(".") - const parentCompany = await findCompanyByDomain(parentDomain) - if (parentCompany) { - return parentCompany + const parentSeller = await findSellerByDomain(parentDomain) + if (parentSeller) { + return parentSeller } } @@ -34,5 +34,5 @@ async function findCompanyFromLink(url) { } module.exports = { - findCompanyFromLink, + findSellerFromLink, } diff --git a/services/supabase/supabaseUpload.service.js b/services/supabaseUpload.service.js similarity index 100% rename from services/supabase/supabaseUpload.service.js rename to services/supabaseUpload.service.js diff --git a/services/user.service.js b/services/user.service.js new file mode 100644 index 0000000..4cf777d --- /dev/null +++ b/services/user.service.js @@ -0,0 +1,60 @@ +// services/user.service.js +const userDB = require("../db/user.db") +const dealDB = require("../db/deal.db") +const commentDB = require("../db/comment.db") + +async function getUserProfileByUsername(userName) { + const username = String(userName).trim() + if (!username) throw new Error("username zorunlu") + + const user = await userDB.findUser( + { username }, + { select: { id: true, username: true, avatarUrl: true, createdAt: true } } + ) + + if (!user) { + const err = new Error("Kullanıcı bulunamadı.") + err.statusCode = 404 + throw err + } + + const [dealAgg, totalComments, deals, comments] = await Promise.all([ + dealDB.aggregateDeals({ userId: user.id }), + commentDB.countComments({ userId: user.id }), + dealDB.findDeals( + { userId: user.id }, + { + orderBy: { createdAt: "desc" }, + take: 20, + include: { + user: { select: { id: true, username: true, avatarUrl: true } }, + seller: { select: { name: true, url: true } }, + images: { orderBy: { order: "asc" }, take: 1, select: { imageUrl: true } }, + }, + } + ), + commentDB.findComments( + { userId: user.id }, + { + orderBy: { createdAt: "desc" }, + take: 20, + include: { + user: { select: { id: true, username: true, avatarUrl: true } }, + deal: { select: { id: true, title: true } }, + }, + } + ), + ]) + + const stats = { + totalLikes: dealAgg?._sum?.score ?? 0, + totalComments: totalComments ?? 0, + totalShares: dealAgg?._count?._all ?? 0, + } + + + return { user, stats, deals, comments } + + } + +module.exports = { getUserProfileByUsername } diff --git a/services/vote.service.js b/services/vote.service.js new file mode 100644 index 0000000..15714aa --- /dev/null +++ b/services/vote.service.js @@ -0,0 +1,19 @@ +const voteDb = require("../db/vote.db"); + +async function voteDeal({ dealId, userId, voteType }) { + if (!dealId || !userId || voteType === undefined) { + const err = new Error("Eksik veri"); + err.statusCode = 400; + throw err; + } + + if (![ -1, 0, 1 ].includes(voteType)) { + const err = new Error("voteType -1, 0 veya 1 olmalı"); + err.statusCode = 400; + throw err; + } + + return voteDb.voteDealTx({ dealId, userId, voteType }); +} + +module.exports = { voteDeal }; diff --git a/utils/processImage.js b/utils/processImage.js new file mode 100644 index 0000000..49a0828 --- /dev/null +++ b/utils/processImage.js @@ -0,0 +1,19 @@ +const sharp = require("sharp"); + +async function makeDetailWebp(inputBuffer) { + return sharp(inputBuffer) + .rotate() + .resize({ width: 1200, withoutEnlargement: true }) + .webp({ quality: 80 }) + .toBuffer(); +} + +async function makeThumbWebp(inputBuffer) { + return sharp(inputBuffer) + .rotate() + .resize({ width: 400, withoutEnlargement: true }) + .webp({ quality: 75 }) + .toBuffer(); +} + +module.exports = { makeDetailWebp, makeThumbWebp };