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, title: data.title,
description: data.description ?? null, description: data.description ?? null,
url: data.url ?? null, url: data.url ?? null,
price: data.price ?? null, price: Number(data.price) ?? null,
// 🔑 adapter burada sellerı “custom” gibi yazar // 🔑 adapter burada sellerı “custom” gibi yazar
// service bunu düzeltecek // service bunu düzeltecek
customCompany: data.sellerName, customSeller: data.sellerName,
user: { user: {
connect: { id: userId }, connect: { id: userId },
}, },
/*
images: data.images?.length images: data.images?.length
? { ? {
create: data.images.map((imgUrl, index) => ({ create: data.images.map((imgUrl, index) => ({
@ -24,9 +24,10 @@ function mapCreateDealRequestToDealCreateData(
})), })),
} }
: undefined, : undefined,
} */
}
}
}
module.exports = { module.exports = {
mapCreateDealRequestToDealCreateData, 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, price: deal.price ?? null,
score: deal.score, score: deal.score,
commentsCount: deal._count?.comments ?? 0, commentsCount: deal.commentCount,
status: deal.status, status: deal.status,
saleType: deal.saletype, saleType: deal.saletype,
affiliateType: deal.affiliateType, affiliateType: deal.affiliateType,
myVote:deal.myVote,
createdAt: deal.createdAt, createdAt: deal.createdAt,
updatedAt: deal.updatedAt, updatedAt: deal.updatedAt,
@ -21,14 +23,21 @@ function mapDealToDealCardResponse(deal) {
avatarUrl: deal.user.avatarUrl ?? null, avatarUrl: deal.user.avatarUrl ?? null,
}, },
seller: deal.company seller: deal.seller
? { name: deal.company.name, ? { name: deal.seller.name,
url:deal.company.url url:deal.seller.url
} }
: { name: deal.customCompany || "" }, : { name: deal.customSeller || "" },
imageUrl: deal.images?.[0]?.imageUrl || "", 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, avatarUrl: deal.user.avatarUrl ?? null,
}, },
seller: deal.company seller: deal.seller
? { id: deal.company.id, name: deal.company.name } ? { id: deal.seller.id, name: deal.seller.name }
: { name: deal.customCompany || "Bilinmiyor" }, : { name: deal.customSeller || "Bilinmiyor" },
images: deal.images.map((img) => ({ images: deal.images.map((img) => ({
id: img.id, 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") const prisma = require("./client")
function getDb(db) {
return db || prisma
}
async function findComments(where, options = {}) { async function findComments(where, options = {}) {
return prisma.comment.findMany({ return prisma.comment.findMany({
where, where,
@ -9,8 +13,9 @@ async function findComments(where, options = {}) {
}) })
} }
async function createComment(data, options = {}) { async function createComment(data, options = {}, db) {
return prisma.comment.create({ const p = getDb(db)
return p.comment.create({
data, data,
include: options.include || undefined, include: options.include || undefined,
select: options.select || undefined, select: options.select || undefined,
@ -20,9 +25,15 @@ async function createComment(data, options = {}) {
async function deleteComment(where) { async function deleteComment(where) {
return prisma.comment.delete({ where }) return prisma.comment.delete({ where })
} }
async function countComments(where = {}, db) {
const p = getDb(db)
return p.comment.count({ where })
}
module.exports = { module.exports = {
findComments, findComments,
countComments,
createComment, createComment,
deleteComment, deleteComment,
} }

View File

@ -1,5 +1,9 @@
const prisma = require("./client") const prisma = require("./client")
function getDb(db) {
return db || prisma
}
async function findDeals(where = {}, options = {}) { async function findDeals(where = {}, options = {}) {
return prisma.deal.findMany({ return prisma.deal.findMany({
where, where,
@ -11,14 +15,16 @@ async function findDeals(where = {}, options = {}) {
}) })
} }
async function findDeal(where, options = {}) { async function findDeal(where, options = {}, db) {
return prisma.deal.findUnique({ const p = getDb(db)
return p.deal.findUnique({
where, where,
include: options.include || undefined, include: options.include || undefined,
select: options.select || undefined, select: options.select || undefined,
}) })
} }
async function createDeal(data, options = {}) { async function createDeal(data, options = {}) {
return prisma.deal.create({ return prisma.deal.create({
data, data,
@ -27,8 +33,9 @@ async function createDeal(data, options = {}) {
}) })
} }
async function updateDeal(where, data, options = {}) { async function updateDeal(where, data, options = {}, db) {
return prisma.deal.update({ const p = getDb(db)
return p.deal.update({
where, where,
data, data,
include: options.include || undefined, include: options.include || undefined,
@ -68,9 +75,28 @@ async function updateVote(where, data, options = {}) {
async function countVotes(where = {}) { async function countVotes(where = {}) {
return prisma.dealVote.count({ 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 = { module.exports = {
findDeals, findDeals,
aggregateDeals,
getDealWithImages,
findDeal, findDeal,
createDeal, createDeal,
updateDeal, 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 { PrismaClient } = require("@prisma/client")
const prisma = new PrismaClient() const prisma = new PrismaClient()
async function findCompany(where, options = {}) { async function findSeller(where, options = {}) {
return prisma.company.findFirst({ return prisma.seller.findFirst({
where, where,
include: options.include || undefined, include: options.include || undefined,
select: options.select || undefined, select: options.select || undefined,
}) })
} }
async function findCompanyByDomain(domain) { async function findSellerByDomain(domain) {
return prisma.company.findFirst({ return prisma.seller.findFirst({
where: { where: {
domains: { domains: {
some: { some: {
@ -23,6 +23,6 @@ async function findCompanyByDomain(domain) {
module.exports = { module.exports = {
findCompany, findSeller,
findCompanyByDomain, 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", "express": "^5.1.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"multer": "^2.0.2", "multer": "^2.0.2",
"sharp": "^0.34.5",
"uuid": "^13.0.0",
"zod": "^4.1.12" "zod": "^4.1.12"
}, },
"devDependencies": { "devDependencies": {
@ -41,6 +43,481 @@
"node": ">=12" "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": { "node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
@ -865,6 +1342,15 @@
"devOptional": true, "devOptional": true,
"license": "MIT" "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": { "node_modules/diff": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
@ -2118,6 +2604,50 @@
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC" "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": { "node_modules/side-channel": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
@ -2428,6 +2958,19 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT" "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": { "node_modules/v8-compile-cache-lib": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "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", "express": "^5.1.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"multer": "^2.0.2", "multer": "^2.0.2",
"sharp": "^0.34.5",
"uuid": "^13.0.0",
"zod": "^4.1.12" "zod": "^4.1.12"
}, },
"devDependencies": { "devDependencies": {

View File

@ -9,7 +9,7 @@ CREATE TYPE "AffiliateType" AS ENUM ('AFFILIATE', 'NON_AFFILIATE', 'USER_AFFILIA
-- AlterTable -- AlterTable
ALTER TABLE "Deal" ADD COLUMN "affiliateType" "AffiliateType" NOT NULL DEFAULT 'NON_AFFILIATE', 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 "customCompany" TEXT,
ADD COLUMN "saletype" "SaleType" NOT NULL DEFAULT 'ONLINE', ADD COLUMN "saletype" "SaleType" NOT NULL DEFAULT 'ONLINE',
ADD COLUMN "status" "DealStatus" NOT NULL DEFAULT 'PENDING'; ADD COLUMN "status" "DealStatus" NOT NULL DEFAULT 'PENDING';
@ -18,7 +18,7 @@ ADD COLUMN "status" "DealStatus" NOT NULL DEFAULT 'PENDING';
CREATE TABLE "CompanyDomain" ( CREATE TABLE "CompanyDomain" (
"id" SERIAL NOT NULL, "id" SERIAL NOT NULL,
"domain" TEXT NOT NULL, "domain" TEXT NOT NULL,
"companyId" INTEGER NOT NULL, "sellerId" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"createdById" INTEGER NOT NULL, "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"); CREATE UNIQUE INDEX "Company_name_key" ON "Company"("name");
-- AddForeignKey -- 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 -- AddForeignKey
ALTER TABLE "CompanyDomain" ADD CONSTRAINT "CompanyDomain_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 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; ALTER TABLE "Company" ADD CONSTRAINT "Company_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey -- 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[] Deal Deal[]
votes DealVote[] votes DealVote[]
comments Comment[] comments Comment[]
companies Company[] companies Seller[]
domains CompanyDomain[] domains SellerDomain[]
dealVoteHistory DealVoteHistory[]
} }
enum DealStatus { enum DealStatus {
@ -47,28 +48,28 @@ enum AffiliateType{
USER_AFFILIATE USER_AFFILIATE
} }
model CompanyDomain { model SellerDomain {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
domain String @unique domain String @unique
companyId Int sellerId Int
company Company @relation(fields: [companyId], references: [id]) seller Seller @relation(fields: [sellerId], references: [id])
createdAt DateTime @default(now()) createdAt DateTime @default(now())
createdById Int createdById Int
createdBy User @relation(fields: [createdById], references: [id]) createdBy User @relation(fields: [createdById], references: [id])
} }
model Company { model Seller {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
name String @unique name String @unique
url String @default("")
isActive Boolean @default(true) isActive Boolean @default(true)
Links String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
createdById Int createdById Int
deals Deal[] deals Deal[]
createdBy User @relation(fields: [createdById], references: [id]) createdBy User @relation(fields: [createdById], references: [id])
domains CompanyDomain[] domains SellerDomain[]
} }
model Deal { model Deal {
@ -80,19 +81,21 @@ model Deal {
userId Int userId Int
score Int @default(0) score Int @default(0)
commentCount Int @default(0)
status DealStatus @default(PENDING) status DealStatus @default(PENDING)
saletype SaleType @default(ONLINE) saletype SaleType @default(ONLINE)
affiliateType AffiliateType @default(NON_AFFILIATE) affiliateType AffiliateType @default(NON_AFFILIATE)
companyId Int? sellerId Int?
customCompany String? customSeller String?
company Company? @relation(fields: [companyId], references: [id]) seller Seller? @relation(fields: [sellerId], references: [id])
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id]) user User @relation(fields: [userId], references: [id])
votes DealVote[] votes DealVote[]
voteHistory DealVoteHistory[]
comments Comment[] comments Comment[]
images DealImage[] // ← yeni ilişki images DealImage[] // ← yeni ilişki
} }
@ -110,13 +113,30 @@ model DealVote {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
dealId Int dealId Int
userId 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()) createdAt DateTime @default(now())
deal Deal @relation(fields: [dealId], references: [id]) deal Deal @relation(fields: [dealId], references: [id])
user User @relation(fields: [userId], 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 ---------- // ---------- Seller ----------
const amazon = await prisma.company.upsert({ const amazon = await prisma.seller.upsert({
where: { name: 'Amazon' }, where: { name: 'Amazon' },
update: {}, update: {},
create: { create: {
@ -39,16 +39,16 @@ async function main() {
}, },
}) })
// ---------- COMPANY DOMAINS ---------- // ---------- Seller DOMAINS ----------
const domains = ['amazon.com', 'amazon.com.tr'] const domains = ['amazon.com', 'amazon.com.tr']
for (const domain of domains) { for (const domain of domains) {
await prisma.companyDomain.upsert({ await prisma.SellerDomain.upsert({
where: { domain }, where: { domain },
update: {}, update: {},
create: { create: {
domain, domain,
companyId: amazon.id, sellerId: amazon.id,
createdById: admin.id, createdById: admin.id,
}, },
}) })
@ -64,8 +64,9 @@ async function main() {
status: DealStatus.ACTIVE, status: DealStatus.ACTIVE,
saletype: SaleType.ONLINE, saletype: SaleType.ONLINE,
affiliateType: AffiliateType.NON_AFFILIATE, affiliateType: AffiliateType.NON_AFFILIATE,
commentCount:1,
userId: user.id, userId: user.id,
companyId: amazon.id, sellerId: amazon.id,
}, },
}) })
@ -97,7 +98,7 @@ async function main() {
create: { create: {
dealId: deal.id, dealId: deal.id,
userId: admin.id, userId: admin.id,
voteType: 'UP', voteType: 1,
}, },
}) })

View File

@ -1,19 +1,21 @@
const express = require("express") const express = require("express")
const multer = require("multer") const multer = require("multer")
const fs = require("fs") const fs = require("fs")
const { uploadProfileImage } = require("../../services/supabase/supabaseUpload.service") const { uploadProfileImage } = require("../services/supabaseUpload.service")
const { validateImage } = require("../../utils/validateImage") const { validateImage } = require("../utils/validateImage")
const authMiddleware = require("../../middleware/authMiddleware") const authRequiredMiddleware = require("../middleware/authRequired.middleware")
const { getUserProfile } = require("../../services/profile/profile.service") const authOptionalMiddleware = require("../middleware/authOptional.middleware")
const { getUserProfile } = require("../services/profile.service")
const router = express.Router() const router = express.Router()
const upload = multer({ dest: "uploads/" }) const upload = multer({ dest: "uploads/" })
const { updateUserAvatar } = require("../../services/account/avatar.service") const { updateUserAvatar } = require("../services/avatar.service")
router.post( router.post(
"/avatar", "/avatar",
authMiddleware, authRequiredMiddleware
,
upload.single("file"), upload.single("file"),
async (req, res) => { async (req, res) => {
try { try {
@ -34,7 +36,8 @@ router.post(
) )
router.get("/me", authMiddleware, async (req, res) => { router.get("/me", authRequiredMiddleware
, async (req, res) => {
try { try {
const user = await getUserProfile(req.user.id) const user = await getUserProfile(req.user.id)
res.json(user) res.json(user)

View File

@ -1,79 +1,61 @@
const express = require("express"); const express = require("express");
const bcrypt = require("bcryptjs"); const authRequiredMiddleware
const jwt = require("jsonwebtoken"); = require("../middleware/authRequired.middleware");
const { PrismaClient } = require("@prisma/client"); const authService=require("../services/auth.service")
const generateToken = require("../utils/generateToken");
const authMiddleware = require("../middleware/authMiddleware");
const router = express.Router(); 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) => { router.post("/register", async (req, res) => {
try { try {
const { username, email, password } = req.body; const input = mapRegisterRequestToRegisterInput(req.body);
const result = await authService.register(input);
const existingUser = await prisma.user.findUnique({ where: { email } }); res.json(mapRegisterResultToResponse(result));
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 } });
} catch (err) { } 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) => { router.post("/login", async (req, res) => {
try { try {
const { email, password } = req.body; const input = mapLoginRequestToLoginInput(req.body);
const result = await authService.login(input);
const user = await prisma.user.findUnique({ where: { email } }); res.json(mapLoginResultToResponse(result));
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 },
});
} catch (err) { } catch (err) {
console.error(err); const status = err.statusCode || 500;
res res.status(status).json({ message: err.message || "Giriş işlemi başarısız." });
.status(500)
.json({ message: "Giriş işlemi başarısız.", error: err.message });
} }
}); });
router.get("/me", authMiddleware, async (req, res) => {
router.get("/me", authRequiredMiddleware
, async (req, res) => {
try { try {
const user = await prisma.user.findUnique({ const userId = mapMeRequestToUserId(req);
where: { id: req.user.userId }, const user = await authService.getMe(userId);
select: { id: true, username: true, email: true,avatarUrl:true }, res.json(mapMeResultToResponse(user));
})
if (!user) return res.status(404).json({ error: "Kullanıcı bulunamadı" })
res.json(user)
} catch (err) { } catch (err) {
console.error(err) const status = err.statusCode || 500;
res.status(500).json({ error: "Sunucu hatası" }) res.status(status).json({
message: err.message || "Sunucu hatası",
});
} }
}) });
module.exports = router; module.exports = router;

View File

@ -1,24 +1,31 @@
const express = require("express") const express = require("express")
const authMiddleware = require("../../middleware/authMiddleware") const authRequiredMiddleware = require("../middleware/authRequired.middleware")
const authOptionalMiddleware = require("../middleware/authOptional.middleware")
const { const {
getCommentsByDealId, getCommentsByDealId,
createComment, createComment,
deleteComment, 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() const router = express.Router()
router.get("/:dealId", async (req, res) => { router.get("/:dealId", async (req, res) => {
try { try {
const comments = await getCommentsByDealId(req.params.dealId) const dealId = Number(req.params.dealId)
res.json(comments) const comments = await commentService.getCommentsByDealId(dealId)
res.json(dealCommentAdapter.mapCommentsToDealCommentResponse(comments))
} catch (err) { } catch (err) {
console.error(err) console.log(err.message)
res.status(500).json({ error: "Sunucu hatası" }) res.status(400).json({ error: err.message })
} }
}) })
router.post("/", authMiddleware, async (req, res) => { router.post("/", authRequiredMiddleware
, async (req, res) => {
try { try {
const { dealId, text } = req.body const { dealId, text } = req.body
const userId = req.user.userId 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 { try {
const result = await deleteComment(req.params.id, req.user.userId) const result = await deleteComment(req.params.id, req.user.userId)
res.json(result) 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"); // routes/user.js
const { PrismaClient } = require("@prisma/client"); const express = require("express")
const router = express.Router(); const router = express.Router()
const prisma = new PrismaClient();
router.get("/", async (req, res) => { const userService = require("../services/user.service")
const users = await prisma.user.findMany(); const userProfileAdapter = require("../adapters/responses/userProfile.adapter")
res.json(users);
});
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() require("dotenv").config()
const userRoutesneedRefactor = require("./routes/user.routes") 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 authRoutes = require("./routes/auth.routes")
const dealVoteRoutes = require("./routes/deal/vote.routes") const dealVoteRoutes = require("./routes/vote.routes")
const commentRoutes = require("./routes/deal/comment.routes") const commentRoutes = require("./routes/comment.routes")
const accountSettingsRoutes = require("./routes/account/accountSettings.routes") const accountSettingsRoutes = require("./routes/accountSettings.routes")
const userRoutes = require("./routes/user/user.routes") const userRoutes = require("./routes/user.routes")
const sellerRoutes = require("./routes/seller/seller.routes") const sellerRoutes = require("./routes/seller.routes")
const voteRoutes=require("./routes/vote.routes")
const app = express() const app = express()
app.use(cors()) app.use(cors())
app.use(express.json()) app.use(express.json())
app.use(express.urlencoded({ extended: true })) app.use(express.urlencoded({ extended: true }))
@ -24,4 +25,6 @@ app.use("/api/comments", commentRoutes)
app.use("/api/account", accountSettingsRoutes) app.use("/api/account", accountSettingsRoutes)
app.use("/api/user", userRoutes) app.use("/api/user", userRoutes)
app.use("/api/seller", sellerRoutes) app.use("/api/seller", sellerRoutes)
app.use("/api/vote", voteRoutes)
app.listen(3000, () => console.log("Server running on http://localhost:3000")) 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 fs = require("fs")
const { uploadImage } = require("../uploadImage.service") const { uploadImage } = require("./uploadImage.service")
const { validateImage } = require("../../utils/validateImage") const { validateImage } = require("../utils/validateImage")
const userDB = require("../../db/user.db") const userDB = require("../db/user.db")
async function updateUserAvatar(userId, file) { async function updateUserAvatar(userId, file) {
if (!file) { if (!file) {

View File

@ -1,5 +1,6 @@
const dealDB = require("../../db/deal.db") const dealDB = require("../db/deal.db")
const commentDB = require("../../db/comment.db") const commentDB = require("../db/comment.db")
const prisma = require("../db/client")
function assertPositiveInt(v, name = "id") { function assertPositiveInt(v, name = "id") {
const n = Number(v) const n = Number(v)
@ -8,33 +9,41 @@ function assertPositiveInt(v, name = "id") {
} }
async function getCommentsByDealId(dealId) { async function getCommentsByDealId(dealId) {
const id = assertPositiveInt(dealId, "dealId") const id = Number(dealId)
const deal = await dealDB.findDeal({ id }) const deal = await dealDB.findDeal({ id })
if (!deal) throw new Error("Deal bulunamadı.") 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 }) return commentDB.findComments({ dealId: id }, { include })
} }
async function createComment({ dealId, userId, text }) { async function createComment({ dealId, userId, text }) {
const dId = assertPositiveInt(dealId, "dealId")
const uId = assertPositiveInt(userId, "userId")
if (!text || typeof text !== "string" || !text.trim()) if (!text || typeof text !== "string" || !text.trim())
throw new Error("Yorum boş olamaz.") 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ı.") if (!deal) throw new Error("Deal bulunamadı.")
const include = { user: { select: { username: true, avatarUrl: true } } } const comment = await commentDB.createComment(
const data = { { text: trimmed, userId, dealId },
text: text.trim(), { include },
userId: uId, tx
dealId: dId, )
}
return commentDB.createComment(data, { include }) await dealDB.updateDeal(
{ id: dealId },
{ commentCount: { increment: 1 } },
{},
tx
)
return comment
})
} }
async function deleteComment(commentId, userId) { async function deleteComment(commentId, userId) {
@ -51,6 +60,12 @@ async function deleteComment(commentId, userId) {
await commentDB.deleteComment({ id: cId }) await commentDB.deleteComment({ id: cId })
return { message: "Yorum silindi." } return { message: "Yorum silindi." }
}
async function commentChange(length,dealId){
} }
module.exports = { 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") { function assertPositiveInt(v, name = "id") {
const n = Number(v) const n = Number(v)

View File

@ -1,11 +1,11 @@
// services/company/companyService.js // services/seller/sellerService.js
const { findCompanyByDomain } = require("../../db/seller.db") const { findSellerByDomain } = require("../db/seller.db")
function normalizeDomain(hostname) { function normalizeDomain(hostname) {
return hostname.replace(/^www\./, "") return hostname.replace(/^www\./, "")
} }
async function findCompanyFromLink(url) { async function findSellerFromLink(url) {
let hostname let hostname
try { try {
@ -16,17 +16,17 @@ async function findCompanyFromLink(url) {
const domain = normalizeDomain(hostname) const domain = normalizeDomain(hostname)
const company = await findCompanyByDomain(domain) const seller = await findSellerByDomain(domain)
if (company) { if (seller) {
return company return seller
} }
const domainParts = domain.split(".") const domainParts = domain.split(".")
for (let i = 1; i <= domainParts.length - 2; i += 1) { for (let i = 1; i <= domainParts.length - 2; i += 1) {
const parentDomain = domainParts.slice(i).join(".") const parentDomain = domainParts.slice(i).join(".")
const parentCompany = await findCompanyByDomain(parentDomain) const parentSeller = await findSellerByDomain(parentDomain)
if (parentCompany) { if (parentSeller) {
return parentCompany return parentSeller
} }
} }
@ -34,5 +34,5 @@ async function findCompanyFromLink(url) {
} }
module.exports = { 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 };