refactoring / image array / adapters
This commit is contained in:
parent
c46871f99f
commit
4487709bf2
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
30
adapters/responses/comment.adapter.js
Normal file
30
adapters/responses/comment.adapter.js
Normal 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
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
26
adapters/responses/login.adapter.js
Normal file
26
adapters/responses/login.adapter.js
Normal 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,
|
||||
};
|
||||
18
adapters/responses/me.adapter.js
Normal file
18
adapters/responses/me.adapter.js
Normal 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,
|
||||
};
|
||||
22
adapters/responses/publicUser.adapter.js
Normal file
22
adapters/responses/publicUser.adapter.js
Normal 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,
|
||||
}
|
||||
19
adapters/responses/register.adapter.js
Normal file
19
adapters/responses/register.adapter.js
Normal 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,
|
||||
};
|
||||
0
adapters/responses/userComment.adapter.js
Normal file
0
adapters/responses/userComment.adapter.js
Normal file
16
adapters/responses/userProfile.adapter.js
Normal file
16
adapters/responses/userProfile.adapter.js
Normal 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 }
|
||||
12
adapters/responses/userProfileStats.adapter.js
Normal file
12
adapters/responses/userProfileStats.adapter.js
Normal 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,
|
||||
}
|
||||
32
adapters/responses/vote.adapter.js
Normal file
32
adapters/responses/vote.adapter.js
Normal 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
27
db/auth.db.js
Normal 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,
|
||||
};
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
const prisma = require("./client")
|
||||
|
||||
function getDb(db) {
|
||||
return db || prisma
|
||||
}
|
||||
|
||||
async function findDeals(where = {}, options = {}) {
|
||||
return prisma.deal.findMany({
|
||||
where,
|
||||
|
|
@ -11,14 +15,16 @@ async function findDeals(where = {}, options = {}) {
|
|||
})
|
||||
}
|
||||
|
||||
async function findDeal(where, options = {}) {
|
||||
return prisma.deal.findUnique({
|
||||
async function findDeal(where, options = {}, db) {
|
||||
const p = getDb(db)
|
||||
return p.deal.findUnique({
|
||||
where,
|
||||
include: options.include || undefined,
|
||||
select: options.select || undefined,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
async function createDeal(data, options = {}) {
|
||||
return prisma.deal.create({
|
||||
data,
|
||||
|
|
@ -27,12 +33,13 @@ async function createDeal(data, options = {}) {
|
|||
})
|
||||
}
|
||||
|
||||
async function updateDeal(where, data, options = {}) {
|
||||
return prisma.deal.update({
|
||||
async function updateDeal(where, data, options = {}, db) {
|
||||
const p = getDb(db)
|
||||
return p.deal.update({
|
||||
where,
|
||||
data,
|
||||
include: options.include || undefined,
|
||||
select: options.select || undefined,
|
||||
select: options.select || undefined,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -68,9 +75,28 @@ async function updateVote(where, data, options = {}) {
|
|||
async function countVotes(where = {}) {
|
||||
return prisma.dealVote.count({ where })
|
||||
}
|
||||
async function getDealWithImages(dealId) {
|
||||
return prisma.deal.findUnique({
|
||||
where: { id: dealId },
|
||||
include: { images: { orderBy: { order: "asc" } } },
|
||||
});
|
||||
}
|
||||
|
||||
async function aggregateDeals(where = {}, db) {
|
||||
const p = getDb(db)
|
||||
return p.deal.aggregate({
|
||||
where,
|
||||
_count: { _all: true },
|
||||
_sum: { score: true },
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
module.exports = {
|
||||
findDeals,
|
||||
aggregateDeals,
|
||||
getDealWithImages,
|
||||
findDeal,
|
||||
createDeal,
|
||||
updateDeal,
|
||||
|
|
|
|||
14
db/dealImage.db.js
Normal file
14
db/dealImage.db.js
Normal 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 };
|
||||
|
|
@ -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
57
db/vote.db.js
Normal 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,
|
||||
};
|
||||
31
middleware/authOptional.middleware.js
Normal file
31
middleware/authOptional.middleware.js
Normal 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" });
|
||||
}
|
||||
};
|
||||
11
middleware/upload.middleware.js
Normal file
11
middleware/upload.middleware.js
Normal 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
543
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"prisma": {
|
||||
"prisma": {
|
||||
"seed": "node prisma/seed.js"
|
||||
},
|
||||
"keywords": [],
|
||||
|
|
@ -21,6 +21,8 @@
|
|||
"express": "^5.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"multer": "^2.0.2",
|
||||
"sharp": "^0.34.5",
|
||||
"uuid": "^13.0.0",
|
||||
"zod": "^4.1.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "Deal" ADD COLUMN "commentCount" INTEGER NOT NULL DEFAULT 0;
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "Seller" ADD COLUMN "url" TEXT NOT NULL DEFAULT '';
|
||||
39
prisma/migrations/20260121152845_vote_overhaul/migration.sql
Normal file
39
prisma/migrations/20260121152845_vote_overhaul/migration.sql
Normal 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;
|
||||
|
|
@ -24,8 +24,9 @@ model User {
|
|||
Deal Deal[]
|
||||
votes DealVote[]
|
||||
comments Comment[]
|
||||
companies Company[]
|
||||
domains CompanyDomain[]
|
||||
companies Seller[]
|
||||
domains SellerDomain[]
|
||||
dealVoteHistory DealVoteHistory[]
|
||||
}
|
||||
|
||||
enum DealStatus {
|
||||
|
|
@ -47,28 +48,28 @@ enum AffiliateType{
|
|||
USER_AFFILIATE
|
||||
}
|
||||
|
||||
model CompanyDomain {
|
||||
model SellerDomain {
|
||||
id Int @id @default(autoincrement())
|
||||
domain String @unique
|
||||
companyId Int
|
||||
company Company @relation(fields: [companyId], references: [id])
|
||||
sellerId Int
|
||||
seller Seller @relation(fields: [sellerId], references: [id])
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
createdById Int
|
||||
createdBy User @relation(fields: [createdById], references: [id])
|
||||
}
|
||||
|
||||
model Company {
|
||||
model Seller {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @unique
|
||||
url String @default("")
|
||||
isActive Boolean @default(true)
|
||||
Links String?
|
||||
createdAt DateTime @default(now())
|
||||
createdById Int
|
||||
|
||||
deals Deal[]
|
||||
createdBy User @relation(fields: [createdById], references: [id])
|
||||
domains CompanyDomain[]
|
||||
domains SellerDomain[]
|
||||
}
|
||||
|
||||
model Deal {
|
||||
|
|
@ -80,19 +81,21 @@ model Deal {
|
|||
|
||||
userId Int
|
||||
score Int @default(0)
|
||||
commentCount Int @default(0)
|
||||
status DealStatus @default(PENDING)
|
||||
saletype SaleType @default(ONLINE)
|
||||
affiliateType AffiliateType @default(NON_AFFILIATE)
|
||||
|
||||
companyId Int?
|
||||
customCompany String?
|
||||
sellerId Int?
|
||||
customSeller String?
|
||||
|
||||
company Company? @relation(fields: [companyId], references: [id])
|
||||
seller Seller? @relation(fields: [sellerId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
votes DealVote[]
|
||||
voteHistory DealVoteHistory[]
|
||||
comments Comment[]
|
||||
images DealImage[] // ← yeni ilişki
|
||||
}
|
||||
|
|
@ -107,16 +110,33 @@ model DealImage {
|
|||
}
|
||||
|
||||
model DealVote {
|
||||
id Int @id @default(autoincrement())
|
||||
dealId Int
|
||||
userId Int
|
||||
voteType Int @default(0) // -1,0,1
|
||||
createdAt DateTime @default(now())
|
||||
lastVotedAt DateTime @default(now()) // her vote değişiminde set edeceğiz
|
||||
|
||||
deal Deal @relation(fields: [dealId], references: [id])
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
||||
@@unique([dealId, userId])
|
||||
@@index([dealId])
|
||||
}
|
||||
|
||||
model DealVoteHistory {
|
||||
id Int @id @default(autoincrement())
|
||||
dealId Int
|
||||
userId Int
|
||||
voteType String
|
||||
voteType Int
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
deal Deal @relation(fields: [dealId], references: [id])
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
||||
@@unique([dealId, userId]) // aynı kullanıcı aynı ilana bir kez oy verebilir
|
||||
@@index([dealId])
|
||||
@@index([userId])
|
||||
@@index([createdAt])
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
75
routes/deal.routes.js
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
31
routes/seller.routes.js
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
35
routes/vote.routes.js
Normal 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
|
||||
17
server.js
17
server.js
|
|
@ -3,15 +3,16 @@ const cors = require("cors")
|
|||
require("dotenv").config()
|
||||
|
||||
const userRoutesneedRefactor = require("./routes/user.routes")
|
||||
const dealRoutes = require("./routes/deal/deal.routes")
|
||||
const dealRoutes = require("./routes/deal.routes")
|
||||
const authRoutes = require("./routes/auth.routes")
|
||||
const dealVoteRoutes = require("./routes/deal/vote.routes")
|
||||
const commentRoutes = require("./routes/deal/comment.routes")
|
||||
const accountSettingsRoutes = require("./routes/account/accountSettings.routes")
|
||||
const userRoutes = require("./routes/user/user.routes")
|
||||
const sellerRoutes = require("./routes/seller/seller.routes")
|
||||
|
||||
const dealVoteRoutes = require("./routes/vote.routes")
|
||||
const commentRoutes = require("./routes/comment.routes")
|
||||
const accountSettingsRoutes = require("./routes/accountSettings.routes")
|
||||
const userRoutes = require("./routes/user.routes")
|
||||
const sellerRoutes = require("./routes/seller.routes")
|
||||
const voteRoutes=require("./routes/vote.routes")
|
||||
const app = express()
|
||||
|
||||
app.use(cors())
|
||||
app.use(express.json())
|
||||
app.use(express.urlencoded({ extended: true }))
|
||||
|
|
@ -24,4 +25,6 @@ app.use("/api/comments", commentRoutes)
|
|||
app.use("/api/account", accountSettingsRoutes)
|
||||
app.use("/api/user", userRoutes)
|
||||
app.use("/api/seller", sellerRoutes)
|
||||
app.use("/api/vote", voteRoutes)
|
||||
|
||||
app.listen(3000, () => console.log("Server running on http://localhost:3000"))
|
||||
|
|
|
|||
85
services/auth.service.js
Normal file
85
services/auth.service.js
Normal 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,
|
||||
};
|
||||
|
|
@ -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) {
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
const dealDB = require("../../db/deal.db")
|
||||
const commentDB = require("../../db/comment.db")
|
||||
const dealDB = require("../db/deal.db")
|
||||
const commentDB = require("../db/comment.db")
|
||||
const prisma = require("../db/client")
|
||||
|
||||
function assertPositiveInt(v, name = "id") {
|
||||
const n = Number(v)
|
||||
|
|
@ -8,33 +9,41 @@ function assertPositiveInt(v, name = "id") {
|
|||
}
|
||||
|
||||
async function getCommentsByDealId(dealId) {
|
||||
const id = assertPositiveInt(dealId, "dealId")
|
||||
const id = Number(dealId)
|
||||
|
||||
const deal = await dealDB.findDeal({ id })
|
||||
if (!deal) throw new Error("Deal bulunamadı.")
|
||||
|
||||
const include = { user: { select: { username: true, avatarUrl: true } } }
|
||||
const include = { user: { select: { id:true,username: true, avatarUrl: true } } }
|
||||
return commentDB.findComments({ dealId: id }, { include })
|
||||
}
|
||||
|
||||
async function createComment({ dealId, userId, text }) {
|
||||
const dId = assertPositiveInt(dealId, "dealId")
|
||||
const uId = assertPositiveInt(userId, "userId")
|
||||
|
||||
if (!text || typeof text !== "string" || !text.trim())
|
||||
throw new Error("Yorum boş olamaz.")
|
||||
|
||||
const deal = await dealDB.findDeal({ id: dId })
|
||||
if (!deal) throw new Error("Deal bulunamadı.")
|
||||
|
||||
const trimmed = text.trim()
|
||||
const include = { user: { select: { username: true, avatarUrl: true } } }
|
||||
const data = {
|
||||
text: text.trim(),
|
||||
userId: uId,
|
||||
dealId: dId,
|
||||
}
|
||||
|
||||
return commentDB.createComment(data, { include })
|
||||
return prisma.$transaction(async (tx) => {
|
||||
const deal = await dealDB.findDeal({ id: dealId }, {}, tx)
|
||||
if (!deal) throw new Error("Deal bulunamadı.")
|
||||
|
||||
const comment = await commentDB.createComment(
|
||||
{ text: trimmed, userId, dealId },
|
||||
{ include },
|
||||
tx
|
||||
)
|
||||
|
||||
await dealDB.updateDeal(
|
||||
{ id: dealId },
|
||||
{ commentCount: { increment: 1 } },
|
||||
{},
|
||||
tx
|
||||
)
|
||||
|
||||
return comment
|
||||
})
|
||||
}
|
||||
|
||||
async function deleteComment(commentId, userId) {
|
||||
|
|
@ -51,6 +60,12 @@ async function deleteComment(commentId, userId) {
|
|||
|
||||
await commentDB.deleteComment({ id: cId })
|
||||
return { message: "Yorum silindi." }
|
||||
}
|
||||
|
||||
async function commentChange(length,dealId){
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
203
services/deal.service.js
Normal file
203
services/deal.service.js
Normal 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 -> DB’de tek seferde yaz
|
||||
if (rows.length > 0) {
|
||||
await dealImageDB.createManyDealImages(rows);
|
||||
}
|
||||
|
||||
// 4) Deal + images dön
|
||||
return dealDB.getDealWithImages(deal.id);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
module.exports = {
|
||||
getDeals,
|
||||
getDealById,
|
||||
createDeal,
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
60
services/user.service.js
Normal 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
19
services/vote.service.js
Normal 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
19
utils/processImage.js
Normal 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 };
|
||||
Loading…
Reference in New Issue
Block a user