refactoring / image array / adapters

This commit is contained in:
cureb 2026-01-23 17:28:21 +00:00
parent c46871f99f
commit 4487709bf2
53 changed files with 1760 additions and 567 deletions

View File

@ -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,
}

View File

@ -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
}

View File

@ -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 }

View File

@ -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,

View File

@ -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,
};

View File

@ -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,
};

View File

@ -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,
}

View File

@ -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,
};

View File

@ -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 }

View File

@ -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,
}

View File

@ -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 };

27
db/auth.db.js Normal file
View File

@ -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,
};

View File

@ -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,
}

View File

@ -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,8 +33,9 @@ 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,
@ -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,

14
db/dealImage.db.js Normal file
View File

@ -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 };

View File

@ -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,
}

57
db/vote.db.js Normal file
View File

@ -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,
};

View File

@ -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" });
}
};

View File

@ -0,0 +1,11 @@
const multer = require("multer");
const upload = multer({
storage: multer.memoryStorage(),
limits: {
files: 5,
fileSize: 10 * 1024 * 1024,
},
});
module.exports = { upload };

543
package-lock.json generated
View File

@ -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",

View File

@ -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": {

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Deal" ADD COLUMN "commentCount" INTEGER NOT NULL DEFAULT 0;

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Seller" ADD COLUMN "url" TEXT NOT NULL DEFAULT '';

View File

@ -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;

View File

@ -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
}
@ -110,13 +113,30 @@ model DealVote {
id Int @id @default(autoincrement())
dealId Int
userId Int
voteType String
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 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])
}

View File

@ -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,
},
})

View File

@ -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)

View File

@ -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;

View File

@ -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)

75
routes/deal.routes.js Normal file
View File

@ -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

View File

@ -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

View File

@ -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

31
routes/seller.routes.js Normal file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

35
routes/vote.routes.js Normal file
View File

@ -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

View File

@ -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"))

85
services/auth.service.js Normal file
View File

@ -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,
};

View File

@ -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) {

View File

@ -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 })
const trimmed = text.trim()
const include = { user: { select: { username: true, avatarUrl: true } } }
return prisma.$transaction(async (tx) => {
const deal = await dealDB.findDeal({ id: dealId }, {}, tx)
if (!deal) throw new Error("Deal bulunamadı.")
const include = { user: { select: { username: true, avatarUrl: true } } }
const data = {
text: text.trim(),
userId: uId,
dealId: dId,
}
const comment = await commentDB.createComment(
{ text: trimmed, userId, dealId },
{ include },
tx
)
return commentDB.createComment(data, { include })
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 = {

203
services/deal.service.js Normal file
View File

@ -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 -> DBde 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,
}

View File

@ -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,
}

View File

@ -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)

View File

@ -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,
}

60
services/user.service.js Normal file
View File

@ -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 }

19
services/vote.service.js Normal file
View File

@ -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 };

19
utils/processImage.js Normal file
View File

@ -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 };