This commit is contained in:
dimitar 2025-05-06 10:01:03 +02:00
commit 0fa3c7ac48
140 changed files with 24289 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
app/node_modules
price-compare-api/node_modules
node_modules

8
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

12
.idea/dataSources.xml Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="postgres@localhost" uuid="ca26bcae-7956-4de9-8f8f-def9dd1e7878">
<driver-ref>postgresql</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
<jdbc-url>jdbc:postgresql://localhost:5432/postgres</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/sporedi1.iml" filepath="$PROJECT_DIR$/.idea/sporedi1.iml" />
</modules>
</component>
</project>

6
.idea/prettier.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PrettierConfiguration">
<option name="myConfigurationMode" value="AUTOMATIC" />
</component>
</project>

12
.idea/sporedi1.iml Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

11
.idea/vcs.xml Normal file
View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
<mapping directory="$PROJECT_DIR$/../rooTest/news-app" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
<component name="VcsProjectSettings">
<option name="detectVcsMappingsAutomatically" value="false" />
</component>
</project>

123
README.md Normal file
View File

@ -0,0 +1,123 @@
# Price Comparison PWA Implementation
This repository contains the implementation of a price comparison PWA with NestJS backend and PostgreSQL database, as outlined in the improvement plan.
## Implementation Details
The implementation follows the structure outlined in the improvement plan, with the following components:
### Backend (NestJS)
The backend is implemented using NestJS and includes the following modules:
1. **Product Module**
- API endpoints for products
- Search and filtering functionality
- CRUD operations
2. **Price Module**
- Price history tracking
- Price comparison functionality
- Discount calculations
3. **Source Module**
- Source metadata management
- Product listing by source
4. **Scraper Module**
- HTML parsing using cheerio
- Data transformation
- Scheduled scraping jobs
5. **User Module**
- Watchlist functionality
- Price drop notifications
### Database (PostgreSQL)
The database schema includes the following models:
1. **Product**
- Basic product information
- Category and description
- Availability status
2. **Price**
- Regular and discounted prices
- Promotion information
- Historical price tracking
3. **Source**
- Data source information
- Last scraped timestamp
4. **User**
- User information
- Watchlist and notifications
5. **WatchlistItem**
- User-product relationship for watchlist
6. **Notification**
- Price drop notification configuration
### API Endpoints
The API provides the following endpoints:
1. **Products**
- `GET /products` - List all products with pagination
- `GET /products/:id` - Get product details
- `GET /products/search` - Search products by name/category
2. **Prices**
- `GET /prices/product/:id` - Get all prices for a product
- `GET /prices/compare/:ids` - Compare prices for multiple products
- `GET /prices/history/:id` - Get price history for a product
3. **Sources**
- `GET /sources` - List all data sources
- `GET /sources/:id` - Get source details
- `GET /sources/:id/products` - Get products from a specific source
4. **User Features**
- `GET /users/:userId/watchlist` - Get user's watchlist
- `POST /users/:userId/watchlist` - Add product to watchlist
- `DELETE /users/:userId/watchlist/:productId` - Remove product from watchlist
- `GET /users/:userId/notifications` - Get user's notifications
- `POST /users/:userId/notifications` - Configure price drop notification
- `DELETE /users/:userId/notifications/:productId` - Remove notification
## Getting Started
1. Clone the repository
2. Install dependencies:
```
cd price-compare-api
npm install
cd ../app
npm install
```
3. Set up the database:
```
cd price-compare-api
npx prisma migrate dev
```
4. Start the backend:
```
cd price-compare-api
npm run start:dev
```
5. Start the frontend:
```
cd app
npm run dev
```
## Next Steps
1. Implement authentication and authorization
2. Add more data sources
3. Enhance the frontend with more features
4. Implement push notifications for price drops
5. Add analytics and reporting

41
app/.gitignore vendored Normal file
View File

@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

36
app/README.md Normal file
View File

@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.

16
app/eslint.config.mjs Normal file
View File

@ -0,0 +1,16 @@
import { dirname } from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
];
export default eslintConfig;

7
app/next.config.ts Normal file
View File

@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;

5927
app/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

28
app/package.json Normal file
View File

@ -0,0 +1,28 @@
{
"name": "app",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"axios": "^1.8.4",
"next": "15.3.1",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.3.1",
"tailwindcss": "^4",
"typescript": "^5"
}
}

5
app/postcss.config.mjs Normal file
View File

@ -0,0 +1,5 @@
const config = {
plugins: ["@tailwindcss/postcss"],
};
export default config;

1
app/public/file.svg Normal file
View File

@ -0,0 +1 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 391 B

1
app/public/globe.svg Normal file
View File

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

1
app/public/next.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

1
app/public/vercel.svg Normal file
View File

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 128 B

1
app/public/window.svg Normal file
View File

@ -0,0 +1 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

After

Width:  |  Height:  |  Size: 385 B

BIN
app/src/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

26
app/src/app/globals.css Normal file
View File

@ -0,0 +1,26 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}

41
app/src/app/layout.tsx Normal file
View File

@ -0,0 +1,41 @@
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: 'Price Comparison',
description: 'Compare prices across different stores',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>
<header className="bg-white shadow">
<div className="container mx-auto px-4 py-6">
<nav className="flex items-center justify-between">
<h1 className="text-xl font-bold text-blue-600">PriceCompare</h1>
<div className="flex gap-4">
<a href="/" className="text-gray-600 hover:text-blue-600">Home</a>
<a href="#categories" className="text-gray-600 hover:text-blue-600">Categories</a>
<a href="#about" className="text-gray-600 hover:text-blue-600">About</a>
</div>
</nav>
</div>
</header>
{children}
<footer className="bg-gray-100 mt-12">
<div className="container mx-auto px-4 py-6 text-center text-gray-600">
<p>© {new Date().getFullYear()} PriceCompare. All rights reserved.</p>
</div>
</footer>
</body>
</html>
)
}

92
app/src/app/page.tsx Normal file
View File

@ -0,0 +1,92 @@
'use client';
import { useState, useEffect } from 'react';
import { getProducts, Product } from '@/lib/api';
import { ProductCard } from '@/components/ProductCard';
export default function Home() {
const [products, setProducts] = useState<Product[]>([]);
const [search, setSearch] = useState('');
const [maxPrice, setMaxPrice] = useState<string>('');
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
loadProducts();
}, []);
const loadProducts = async (params?: { search?: string; maxPrice?: number }) => {
try {
setLoading(true);
setError(null);
const data = await getProducts(params);
setProducts(data);
} catch (err) {
console.error('Failed to load products:', err);
setError('Failed to load products. Please check if the backend server is running on port 3001.');
} finally {
setLoading(false);
}
};
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
loadProducts({
search: search || undefined,
maxPrice: maxPrice ? Number(maxPrice) : undefined
});
};
return (
<main className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-8">Price Comparison</h1>
<form onSubmit={handleSearch} className="mb-8 flex flex-wrap gap-4">
<div className="flex-1 min-w-[200px]">
<input
type="text"
placeholder="Search products..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full p-2 border rounded"
/>
</div>
<div className="w-[150px]">
<input
type="number"
placeholder="Max price"
value={maxPrice}
onChange={(e) => setMaxPrice(e.target.value)}
className="w-full p-2 border rounded"
min="0"
step="1"
/>
</div>
<button
type="submit"
className="bg-blue-500 text-white px-6 py-2 rounded hover:bg-blue-600"
>
Search
</button>
</form>
{error && (
<div className="text-red-500 text-center p-4 mb-4 bg-red-50 rounded">
{error}
</div>
)}
{loading ? (
<div className="text-center">Loading...</div>
) : products.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
) : (
<div className="text-center text-gray-500">No products found</div>
)}
</main>
);
}

View File

@ -0,0 +1,49 @@
import { Product } from '@/lib/api';
interface ProductCardProps {
product: Product;
}
export function ProductCard({ product }: ProductCardProps) {
const latestPrice = product.prices[0];
return (
<div className="border rounded-lg p-4 shadow hover:shadow-md transition-shadow">
<h3 className="text-lg font-semibold">{product.name}</h3>
<p className="text-sm text-gray-600">{product.category}</p>
{product.description && (
<p className="text-sm text-gray-500 mt-2">{product.description}</p>
)}
<div className="mt-4">
{latestPrice && (
<div className="flex items-baseline gap-2">
<span className="text-xl font-bold">
{latestPrice.discountedPrice ?? latestPrice.regularPrice} ден
</span>
{latestPrice.discountedPrice && (
<span className="text-sm text-gray-500 line-through">
{latestPrice.regularPrice} ден
</span>
)}
{latestPrice.discountPercentage && (
<span className="text-sm text-red-500">
-{latestPrice.discountPercentage}%
</span>
)}
</div>
)}
{latestPrice?.unitPrice && (
<p className="text-sm text-gray-500">{latestPrice.unitPrice}</p>
)}
<p className="text-sm mt-2">
From: {latestPrice?.source.name}
</p>
</div>
<div className="mt-2">
<span className={`text-sm ${product.availability ? 'text-green-500' : 'text-red-500'}`}>
{product.availability ? 'In Stock' : 'Out of Stock'}
</span>
</div>
</div>
);
}

41
app/src/lib/api.ts Normal file
View File

@ -0,0 +1,41 @@
import axios from 'axios';
const API_BASE_URL = 'http://localhost:3001';
const api = axios.create({
baseURL: API_BASE_URL,
});
export interface Product {
id: number;
name: string;
description: string | null;
category: string;
availability: boolean;
prices: {
regularPrice: number;
discountedPrice: number | null;
discountPercentage: number | null;
unitPrice: string | null;
promotionType: string | null;
source: {
name: string;
logo: string | null;
};
}[];
}
export const getProducts = async (params?: {
skip?: number;
take?: number;
search?: string;
maxPrice?: number;
}) => {
const response = await api.get<Product[]>('/products', { params });
return response.data;
};
export const getProduct = async (id: number) => {
const response = await api.get<Product>(`/products/${id}`);
return response.data;
};

27
app/tsconfig.json Normal file
View File

@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

293
docs/improvment_plan.md Normal file
View File

@ -0,0 +1,293 @@
# Price Comparison PWA Solution Structure
This document outlines the comprehensive solution structure for a price comparison PWA with NestJS backend and PostgreSQL database.
## System Architecture
```mermaid
graph TD
A[Web Scrapers] -->|Extract Data| B[NestJS Backend]
B -->|Store Data| C[PostgreSQL Database]
B -->|Serve API| D[PWA Frontend]
E[Users] -->|Use| D
```
## Backend Structure
### Database Schema
```mermaid
erDiagram
PRODUCT {
int id PK
string name
string description
string category
boolean availability
}
PRICE {
int id PK
int product_id FK
float regular_price
float discounted_price
float discount_percentage
string unit_price
string promotion_type
date promotion_start
date promotion_end
date last_updated
int source_id FK
}
SOURCE {
int id PK
string name
string url
string logo
datetime last_scraped
}
PRODUCT ||--o{ PRICE : has
SOURCE ||--o{ PRICE : provides
```
### Additional Database Fields
We'll add these fields to handle the specific data format:
- **Product**: Add `sourceProductId` to track original product IDs
- **Price**: Add `vatIncluded` boolean flag since prices include VAT
- **Source**: Add `lastUpdateTime` to track the "Последно ажурирање" timestamp
### Data Transformation Rules
1. **Text Processing**
- Handle Cyrillic text encoding (UTF-8)
- Parse product names and descriptions
- Extract category from description field
2. **Price Processing**
- Convert prices from string to float
- Handle "ден/кг" unit price format
- Store both VAT-included and VAT-excluded prices
3. **Date Processing**
- Parse dates from "DD/MM/YYYY" format
- Handle time in "HH:mm" format for last update
- Store timestamps in UTC
### Scraper Implementation
The scraper will process the HTML table structure:
```typescript
interface RawProductData {
productName: string; // "Назив на стока"
regularPrice: string; // "Продажна цена (со ДДВ)"
unitPrice: string; // "Единечна цена"
availability: string; // "Достапност во продажен објект"
description: string; // "Опис на стока"
discountPrice: string; // "Цена со попуст"
discountPercent: string; // "Попуст (%)"
promotionType: string; // "Вид на продажно потикнување"
promotionPeriod: string; // "Времетраење на промоција или попуст"
}
interface ProcessedProduct {
name: string;
description: string;
category: string; // Extracted from description
availability: boolean;
prices: {
regular: number;
discounted: number | null;
unit: {
price: number;
measurement: string; // "ден/кг", etc.
};
};
promotion: {
type: string;
discountPercentage: number;
startDate: Date;
endDate: Date;
} | null;
}
```
### HTML Parsing Strategy
1. **Table Structure**
```typescript
const parseTable = async (html: string): Promise<RawProductData[]> => {
// Use cheerio or similar for HTML parsing
// Target structure: table > tr > td
// Skip header row (first row)
// Handle Cyrillic encoding
}
```
2. **Data Extraction**
```typescript
const extractProduct = (row: CheerioElement): RawProductData => {
// Extract td contents
// Clean and normalize text
// Handle special characters
}
```
3. **Data Transformation**
```typescript
const transformProduct = (raw: RawProductData): ProcessedProduct => {
// Convert prices to numbers
// Parse dates
// Extract category
// Convert availability to boolean
}
```
### NestJS Modules
1. **Scraper Module**
- Service for each data source
- HTML parsing utilities
- Scheduling for regular updates
- Error handling and retry logic
2. **Product Module**
- Product entity and repository
- CRUD operations
- Search and filtering
3. **Price Module**
- Price entity and repository
- Price history tracking
- Discount calculations
4. **Source Module**
- Source entity and repository
- Source metadata management
5. **API Module**
- RESTful endpoints
- GraphQL API (optional)
- Authentication and rate limiting
## Frontend Structure (PWA)
1. **Core Components**
- Product listing
- Product details
- Price comparison
- Search and filters
- Favorites/Watchlist
2. **PWA Features**
- Offline support
- Push notifications for price drops
- App installation
- Responsive design
## Implementation Plan
### Phase 1: Backend Setup
1. Initialize NestJS project
2. Set up PostgreSQL connection
3. Define database entities
4. Create basic API endpoints
### Phase 2: Scraper Implementation
1. Create scraper services for each source
2. Implement HTML parsing based on the provided structure
3. Set up scheduled scraping jobs
4. Implement data normalization and storage
### Phase 3: Frontend Development
1. Set up PWA framework
2. Implement core UI components
3. Connect to backend API
4. Implement offline functionality
### Phase 4: Testing & Deployment
1. Unit and integration testing
2. Performance optimization
3. Deployment setup
4. Monitoring and analytics
## Scraper Implementation Details
Based on the HTML structure provided, here's how we'll parse the data:
```typescript
interface ProductData {
name: string;
regularPrice: number;
unitPrice: string;
availability: boolean;
description: string;
discountedPrice: number | null;
discountPercentage: number | null;
promotionType: string | null;
promotionPeriod: {
start: Date | null;
end: Date | null;
};
lastUpdated: Date;
source: string;
}
```
The scraper will:
1. Fetch the HTML content
2. Parse the table structure
3. Extract data from each row
4. Transform dates and numeric values
5. Store normalized data in the database
## Data Extraction Process
The HTML structure contains product information in a table format. Each row represents a product with the following columns:
- Product name
- Regular price (with VAT)
- Unit price
- Availability
- Product description
- Regular price (repeated)
- Discounted price
- Discount percentage
- Type of promotion
- Promotion duration
The scraper will need to handle:
- Text encoding (appears to be in Cyrillic)
- Date parsing (format: DD/MM/YYYY)
- Price conversion to numeric values
- Availability conversion to boolean
- Extracting promotion date ranges
## API Endpoints
The backend will provide the following key API endpoints:
1. **Products**
- `GET /products` - List all products with pagination
- `GET /products/:id` - Get product details
- `GET /products/search` - Search products by name/category
2. **Prices**
- `GET /prices/product/:id` - Get all prices for a product
- `GET /prices/compare/:ids` - Compare prices for multiple products
- `GET /prices/history/:id` - Get price history for a product
3. **Sources**
- `GET /sources` - List all data sources
- `GET /sources/:id/products` - Get products from a specific source
4. **User Features**
- `POST /watchlist` - Add product to watchlist
- `GET /watchlist` - Get user's watchlist
- `POST /notifications` - Configure price drop notifications

293
docs/requirements.md Normal file
View File

@ -0,0 +1,293 @@
# Price Comparison PWA Solution Structure
This document outlines the comprehensive solution structure for a price comparison PWA with NestJS backend and PostgreSQL database.
## System Architecture
```mermaid
graph TD
A[Web Scrapers] -->|Extract Data| B[NestJS Backend]
B -->|Store Data| C[PostgreSQL Database]
B -->|Serve API| D[PWA Frontend]
E[Users] -->|Use| D
```
## Backend Structure
### Database Schema
```mermaid
erDiagram
PRODUCT {
int id PK
string name
string description
string category
boolean availability
}
PRICE {
int id PK
int product_id FK
float regular_price
float discounted_price
float discount_percentage
string unit_price
string promotion_type
date promotion_start
date promotion_end
date last_updated
int source_id FK
}
SOURCE {
int id PK
string name
string url
string logo
datetime last_scraped
}
PRODUCT ||--o{ PRICE : has
SOURCE ||--o{ PRICE : provides
```
### Additional Database Fields
We'll add these fields to handle the specific data format:
- **Product**: Add `sourceProductId` to track original product IDs
- **Price**: Add `vatIncluded` boolean flag since prices include VAT
- **Source**: Add `lastUpdateTime` to track the "Последно ажурирање" timestamp
### Data Transformation Rules
1. **Text Processing**
- Handle Cyrillic text encoding (UTF-8)
- Parse product names and descriptions
- Extract category from description field
2. **Price Processing**
- Convert prices from string to float
- Handle "ден/кг" unit price format
- Store both VAT-included and VAT-excluded prices
3. **Date Processing**
- Parse dates from "DD/MM/YYYY" format
- Handle time in "HH:mm" format for last update
- Store timestamps in UTC
### Scraper Implementation
The scraper will process the HTML table structure:
```typescript
interface RawProductData {
productName: string; // "Назив на стока"
regularPrice: string; // "Продажна цена (со ДДВ)"
unitPrice: string; // "Единечна цена"
availability: string; // "Достапност во продажен објект"
description: string; // "Опис на стока"
discountPrice: string; // "Цена со попуст"
discountPercent: string; // "Попуст (%)"
promotionType: string; // "Вид на продажно потикнување"
promotionPeriod: string; // "Времетраење на промоција или попуст"
}
interface ProcessedProduct {
name: string;
description: string;
category: string; // Extracted from description
availability: boolean;
prices: {
regular: number;
discounted: number | null;
unit: {
price: number;
measurement: string; // "ден/кг", etc.
};
};
promotion: {
type: string;
discountPercentage: number;
startDate: Date;
endDate: Date;
} | null;
}
```
### HTML Parsing Strategy
1. **Table Structure**
```typescript
const parseTable = async (html: string): Promise<RawProductData[]> => {
// Use cheerio or similar for HTML parsing
// Target structure: table > tr > td
// Skip header row (first row)
// Handle Cyrillic encoding
}
```
2. **Data Extraction**
```typescript
const extractProduct = (row: CheerioElement): RawProductData => {
// Extract td contents
// Clean and normalize text
// Handle special characters
}
```
3. **Data Transformation**
```typescript
const transformProduct = (raw: RawProductData): ProcessedProduct => {
// Convert prices to numbers
// Parse dates
// Extract category
// Convert availability to boolean
}
```
### NestJS Modules
1. **Scraper Module**
- Service for each data source
- HTML parsing utilities
- Scheduling for regular updates
- Error handling and retry logic
2. **Product Module**
- Product entity and repository
- CRUD operations
- Search and filtering
3. **Price Module**
- Price entity and repository
- Price history tracking
- Discount calculations
4. **Source Module**
- Source entity and repository
- Source metadata management
5. **API Module**
- RESTful endpoints
- GraphQL API (optional)
- Authentication and rate limiting
## Frontend Structure (PWA)
1. **Core Components**
- Product listing
- Product details
- Price comparison
- Search and filters
- Favorites/Watchlist
2. **PWA Features**
- Offline support
- Push notifications for price drops
- App installation
- Responsive design
## Implementation Plan
### Phase 1: Backend Setup
1. Initialize NestJS project
2. Set up PostgreSQL connection
3. Define database entities
4. Create basic API endpoints
### Phase 2: Scraper Implementation
1. Create scraper services for each source
2. Implement HTML parsing based on the provided structure
3. Set up scheduled scraping jobs
4. Implement data normalization and storage
### Phase 3: Frontend Development
1. Set up PWA framework
2. Implement core UI components
3. Connect to backend API
4. Implement offline functionality
### Phase 4: Testing & Deployment
1. Unit and integration testing
2. Performance optimization
3. Deployment setup
4. Monitoring and analytics
## Scraper Implementation Details
Based on the HTML structure provided, here's how we'll parse the data:
```typescript
interface ProductData {
name: string;
regularPrice: number;
unitPrice: string;
availability: boolean;
description: string;
discountedPrice: number | null;
discountPercentage: number | null;
promotionType: string | null;
promotionPeriod: {
start: Date | null;
end: Date | null;
};
lastUpdated: Date;
source: string;
}
```
The scraper will:
1. Fetch the HTML content
2. Parse the table structure
3. Extract data from each row
4. Transform dates and numeric values
5. Store normalized data in the database
## Data Extraction Process
The HTML structure contains product information in a table format. Each row represents a product with the following columns:
- Product name
- Regular price (with VAT)
- Unit price
- Availability
- Product description
- Regular price (repeated)
- Discounted price
- Discount percentage
- Type of promotion
- Promotion duration
The scraper will need to handle:
- Text encoding (appears to be in Cyrillic)
- Date parsing (format: DD/MM/YYYY)
- Price conversion to numeric values
- Availability conversion to boolean
- Extracting promotion date ranges
## API Endpoints
The backend will provide the following key API endpoints:
1. **Products**
- `GET /products` - List all products with pagination
- `GET /products/:id` - Get product details
- `GET /products/search` - Search products by name/category
2. **Prices**
- `GET /prices/product/:id` - Get all prices for a product
- `GET /prices/compare/:ids` - Compare prices for multiple products
- `GET /prices/history/:id` - Get price history for a product
3. **Sources**
- `GET /sources` - List all data sources
- `GET /sources/:id/products` - Get products from a specific source
4. **User Features**
- `POST /watchlist` - Add product to watchlist
- `GET /watchlist` - Get user's watchlist
- `POST /notifications` - Configure price drop notifications

2
price-compare-api/.env Normal file
View File

@ -0,0 +1,2 @@
DATABASE_URL="postgresql://root:irina76@localhost:5432/price_compare_db?schema=public"
DIRECT_URL="postgresql://root:irina76@localhost:5432/price_compare_db?schema=public"

View File

@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}

View File

@ -0,0 +1,98 @@
<p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
</p>
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[circleci-url]: https://circleci.com/gh/nestjs/nest
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
</p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Project setup
```bash
$ npm install
```
## Compile and run the project
```bash
# development
$ npm run start
# watch mode
$ npm run start:dev
# production mode
$ npm run start:prod
```
## Run tests
```bash
# unit tests
$ npm run test
# e2e tests
$ npm run test:e2e
# test coverage
$ npm run test:cov
```
## Deployment
When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information.
If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
```bash
$ npm install -g @nestjs/mau
$ mau deploy
```
With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
## Resources
Check out a few resources that may come in handy when working with NestJS:
- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
## Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## Stay in touch
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).

View File

@ -0,0 +1,9 @@
import { AppService } from './app.service';
import { ScraperService } from './scraper/scraper.service';
export declare class AppController {
private readonly appService;
private readonly scraperService;
constructor(appService: AppService, scraperService: ScraperService);
getHello(): string;
triggerScrape(sourceId: number): Promise<void>;
}

View File

@ -0,0 +1,52 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AppController = void 0;
const common_1 = require("@nestjs/common");
const app_service_1 = require("./app.service");
const scraper_service_1 = require("./scraper/scraper.service");
let AppController = class AppController {
appService;
scraperService;
constructor(appService, scraperService) {
this.appService = appService;
this.scraperService = scraperService;
}
getHello() {
return this.appService.getHello();
}
async triggerScrape(sourceId) {
return this.scraperService.manualScrape(sourceId);
}
};
exports.AppController = AppController;
__decorate([
(0, common_1.Get)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", String)
], AppController.prototype, "getHello", null);
__decorate([
(0, common_1.Get)('scrape/:sourceId'),
__param(0, (0, common_1.Param)('sourceId', common_1.ParseIntPipe)),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Number]),
__metadata("design:returntype", Promise)
], AppController.prototype, "triggerScrape", null);
exports.AppController = AppController = __decorate([
(0, common_1.Controller)(),
__metadata("design:paramtypes", [app_service_1.AppService,
scraper_service_1.ScraperService])
], AppController);
//# sourceMappingURL=app.controller.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"app.controller.js","sourceRoot":"","sources":["../src/app.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAAsE;AACtE,+CAA2C;AAC3C,+DAA2D;AAGpD,IAAM,aAAa,GAAnB,MAAM,aAAa;IAEL;IACA;IAFnB,YACmB,UAAsB,EACtB,cAA8B;QAD9B,eAAU,GAAV,UAAU,CAAY;QACtB,mBAAc,GAAd,cAAc,CAAgB;IAC9C,CAAC;IAGJ,QAAQ;QACN,OAAO,IAAI,CAAC,UAAU,CAAC,QAAQ,EAAE,CAAC;IACpC,CAAC;IAGK,AAAN,KAAK,CAAC,aAAa,CAAkC,QAAgB;QACnE,OAAO,IAAI,CAAC,cAAc,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;IACpD,CAAC;CACF,CAAA;AAfY,sCAAa;AAOxB;IADC,IAAA,YAAG,GAAE;;;;6CAGL;AAGK;IADL,IAAA,YAAG,EAAC,kBAAkB,CAAC;IACH,WAAA,IAAA,cAAK,EAAC,UAAU,EAAE,qBAAY,CAAC,CAAA;;;;kDAEnD;wBAdU,aAAa;IADzB,IAAA,mBAAU,GAAE;qCAGoB,wBAAU;QACN,gCAAc;GAHtC,aAAa,CAezB"}

View File

@ -0,0 +1,2 @@
export declare class AppModule {
}

42
price-compare-api/dist/app.module.js vendored Normal file
View File

@ -0,0 +1,42 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AppModule = void 0;
const common_1 = require("@nestjs/common");
const schedule_1 = require("@nestjs/schedule");
const config_1 = require("@nestjs/config");
const app_controller_1 = require("./app.controller");
const app_service_1 = require("./app.service");
const prisma_module_1 = require("./prisma/prisma.module");
const product_module_1 = require("./product/product.module");
const scraper_module_1 = require("./scraper/scraper.module");
const price_module_1 = require("./price/price.module");
const source_module_1 = require("./source/source.module");
const user_module_1 = require("./user/user.module");
let AppModule = class AppModule {
};
exports.AppModule = AppModule;
exports.AppModule = AppModule = __decorate([
(0, common_1.Module)({
imports: [
config_1.ConfigModule.forRoot({
isGlobal: true,
}),
schedule_1.ScheduleModule.forRoot(),
prisma_module_1.PrismaModule,
product_module_1.ProductModule,
scraper_module_1.ScraperModule,
price_module_1.PriceModule,
source_module_1.SourceModule,
user_module_1.UserModule,
],
controllers: [app_controller_1.AppController],
providers: [app_service_1.AppService],
})
], AppModule);
//# sourceMappingURL=app.module.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"app.module.js","sourceRoot":"","sources":["../src/app.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,+CAAkD;AAClD,2CAA8C;AAC9C,qDAAiD;AACjD,+CAA2C;AAC3C,0DAAsD;AACtD,6DAAyD;AACzD,6DAAyD;AACzD,uDAAmD;AACnD,0DAAsD;AACtD,oDAAgD;AAkBzC,IAAM,SAAS,GAAf,MAAM,SAAS;CAAG,CAAA;AAAZ,8BAAS;oBAAT,SAAS;IAhBrB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE;YACP,qBAAY,CAAC,OAAO,CAAC;gBACnB,QAAQ,EAAE,IAAI;aACf,CAAC;YACF,yBAAc,CAAC,OAAO,EAAE;YACxB,4BAAY;YACZ,8BAAa;YACb,8BAAa;YACb,0BAAW;YACX,4BAAY;YACZ,wBAAU;SACX;QACD,WAAW,EAAE,CAAC,8BAAa,CAAC;QAC5B,SAAS,EAAE,CAAC,wBAAU,CAAC;KACxB,CAAC;GACW,SAAS,CAAG"}

View File

@ -0,0 +1,3 @@
export declare class AppService {
getHello(): string;
}

20
price-compare-api/dist/app.service.js vendored Normal file
View File

@ -0,0 +1,20 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AppService = void 0;
const common_1 = require("@nestjs/common");
let AppService = class AppService {
getHello() {
return 'Hello World!';
}
};
exports.AppService = AppService;
exports.AppService = AppService = __decorate([
(0, common_1.Injectable)()
], AppService);
//# sourceMappingURL=app.service.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"app.service.js","sourceRoot":"","sources":["../src/app.service.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAA4C;AAGrC,IAAM,UAAU,GAAhB,MAAM,UAAU;IACrB,QAAQ;QACN,OAAO,cAAc,CAAC;IACxB,CAAC;CACF,CAAA;AAJY,gCAAU;qBAAV,UAAU;IADtB,IAAA,mBAAU,GAAE;GACA,UAAU,CAItB"}

View File

@ -0,0 +1 @@
export {};

37
price-compare-api/dist/create-source.js vendored Normal file
View File

@ -0,0 +1,37 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const client_1 = require("@prisma/client");
const prisma = new client_1.PrismaClient();
async function main() {
try {
const sourcesToUpsert = [
{
name: 'Vero',
url: 'https://pricelist.vero.com.mk/89_2.html',
logo: 'vero-logo.png',
},
{
name: 'dim',
url: 'https://dim.marketceni.mk/',
logo: 'tinex-logo.png',
},
];
const upsertedSources = await Promise.all(sourcesToUpsert.map((source) => prisma.source.upsert({
where: { name: source.name },
update: {
url: source.url,
logo: source.logo,
},
create: source,
})));
console.log('Upserted sources:', upsertedSources);
}
catch (error) {
console.error('Error:', error);
}
finally {
await prisma.$disconnect();
}
}
main();
//# sourceMappingURL=create-source.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"create-source.js","sourceRoot":"","sources":["../src/create-source.ts"],"names":[],"mappings":";;AAAA,2CAA8C;AAE9C,MAAM,MAAM,GAAG,IAAI,qBAAY,EAAE,CAAC;AAElC,KAAK,UAAU,IAAI;IACjB,IAAI,CAAC;QAEH,MAAM,eAAe,GAAG;YACtB;gBACE,IAAI,EAAE,MAAM;gBACZ,GAAG,EAAE,yCAAyC;gBAC9C,IAAI,EAAE,eAAe;aACtB;YACD;gBACE,IAAI,EAAE,KAAK;gBACX,GAAG,EAAE,4BAA4B;gBACjC,IAAI,EAAE,gBAAgB;aACvB;SAGF,CAAC;QAGF,MAAM,eAAe,GAAG,MAAM,OAAO,CAAC,GAAG,CACvC,eAAe,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAC7B,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC;YACnB,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE;YAC5B,MAAM,EAAE;gBACN,GAAG,EAAE,MAAM,CAAC,GAAG;gBACf,IAAI,EAAE,MAAM,CAAC,IAAI;aAClB;YACD,MAAM,EAAE,MAAM;SACf,CAAC,CACH,CACF,CAAC;QAEF,OAAO,CAAC,GAAG,CAAC,mBAAmB,EAAE,eAAe,CAAC,CAAC;IACpD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;IACjC,CAAC;YAAS,CAAC;QACT,MAAM,MAAM,CAAC,WAAW,EAAE,CAAC;IAC7B,CAAC;AACH,CAAC;AAED,IAAI,EAAE,CAAC"}

1
price-compare-api/dist/main.d.ts vendored Normal file
View File

@ -0,0 +1 @@
export {};

15
price-compare-api/dist/main.js vendored Normal file
View File

@ -0,0 +1,15 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const core_1 = require("@nestjs/core");
const app_module_1 = require("./app.module");
async function bootstrap() {
const app = await core_1.NestFactory.create(app_module_1.AppModule);
app.enableCors({
origin: true,
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
credentials: true,
});
await app.listen(3001);
}
bootstrap();
//# sourceMappingURL=main.js.map

1
price-compare-api/dist/main.js.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";;AAAA,uCAA2C;AAC3C,6CAAyC;AAEzC,KAAK,UAAU,SAAS;IACtB,MAAM,GAAG,GAAG,MAAM,kBAAW,CAAC,MAAM,CAAC,sBAAS,CAAC,CAAC;IAGhD,GAAG,CAAC,UAAU,CAAC;QACb,MAAM,EAAE,IAAI;QACZ,OAAO,EAAE,gCAAgC;QACzC,WAAW,EAAE,IAAI;KAClB,CAAC,CAAC;IAEH,MAAM,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;AACzB,CAAC;AACD,SAAS,EAAE,CAAC"}

View File

@ -0,0 +1,75 @@
import { PriceService } from './price.service';
export declare class PriceController {
private readonly priceService;
constructor(priceService: PriceService);
getPricesForProduct(id: string): Promise<({
source: {
id: number;
name: string;
url: string;
logo: string | null;
lastScraped: Date | null;
createdAt: Date;
updatedAt: Date;
};
} & {
id: number;
createdAt: Date;
sourceId: number;
regularPrice: number;
discountedPrice: number | null;
discountPercentage: number | null;
unitPrice: string | null;
promotionType: string | null;
promotionStart: Date | null;
promotionEnd: Date | null;
vatIncluded: boolean;
lastUpdated: Date;
productId: number;
})[]>;
comparePrices(ids: string): Promise<{
id: number;
name: string;
description: string | null;
category: string;
availability: boolean;
latestPrice: {
source: {
id: number;
name: string;
url: string;
logo: string | null;
lastScraped: Date | null;
createdAt: Date;
updatedAt: Date;
};
} & {
id: number;
createdAt: Date;
sourceId: number;
regularPrice: number;
discountedPrice: number | null;
discountPercentage: number | null;
unitPrice: string | null;
promotionType: string | null;
promotionStart: Date | null;
promotionEnd: Date | null;
vatIncluded: boolean;
lastUpdated: Date;
productId: number;
};
}[]>;
getPriceHistory(id: string, days?: string): Promise<{
productId: number;
history: unknown[];
priceRange: {
min: number;
max: number;
};
averagePrice: number;
discountStats: {
maxDiscount: number;
averageDiscount: number;
};
}>;
}

View File

@ -0,0 +1,61 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PriceController = void 0;
const common_1 = require("@nestjs/common");
const price_service_1 = require("./price.service");
let PriceController = class PriceController {
priceService;
constructor(priceService) {
this.priceService = priceService;
}
async getPricesForProduct(id) {
return this.priceService.getPricesForProduct(Number(id));
}
async comparePrices(ids) {
const productIds = ids.split(',').map(Number);
return this.priceService.comparePrices(productIds);
}
async getPriceHistory(id, days) {
return this.priceService.getPriceHistory(Number(id), days ? Number(days) : 30);
}
};
exports.PriceController = PriceController;
__decorate([
(0, common_1.Get)('product/:id'),
__param(0, (0, common_1.Param)('id')),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:returntype", Promise)
], PriceController.prototype, "getPricesForProduct", null);
__decorate([
(0, common_1.Get)('compare/:ids'),
__param(0, (0, common_1.Param)('ids')),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:returntype", Promise)
], PriceController.prototype, "comparePrices", null);
__decorate([
(0, common_1.Get)('history/:id'),
__param(0, (0, common_1.Param)('id')),
__param(1, (0, common_1.Query)('days')),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String, String]),
__metadata("design:returntype", Promise)
], PriceController.prototype, "getPriceHistory", null);
exports.PriceController = PriceController = __decorate([
(0, common_1.Controller)('prices'),
__metadata("design:paramtypes", [price_service_1.PriceService])
], PriceController);
//# sourceMappingURL=price.controller.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"price.controller.js","sourceRoot":"","sources":["../../src/price/price.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAA+D;AAC/D,mDAA+C;AAGxC,IAAM,eAAe,GAArB,MAAM,eAAe;IACG;IAA7B,YAA6B,YAA0B;QAA1B,iBAAY,GAAZ,YAAY,CAAc;IAAG,CAAC;IAGrD,AAAN,KAAK,CAAC,mBAAmB,CAAc,EAAU;QAC/C,OAAO,IAAI,CAAC,YAAY,CAAC,mBAAmB,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC;IAC3D,CAAC;IAGK,AAAN,KAAK,CAAC,aAAa,CAAe,GAAW;QAC3C,MAAM,UAAU,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAC9C,OAAO,IAAI,CAAC,YAAY,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC;IACrD,CAAC;IAGK,AAAN,KAAK,CAAC,eAAe,CACN,EAAU,EACR,IAAa;QAE5B,OAAO,IAAI,CAAC,YAAY,CAAC,eAAe,CACtC,MAAM,CAAC,EAAE,CAAC,EACV,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CACzB,CAAC;IACJ,CAAC;CACF,CAAA;AAxBY,0CAAe;AAIpB;IADL,IAAA,YAAG,EAAC,aAAa,CAAC;IACQ,WAAA,IAAA,cAAK,EAAC,IAAI,CAAC,CAAA;;;;0DAErC;AAGK;IADL,IAAA,YAAG,EAAC,cAAc,CAAC;IACC,WAAA,IAAA,cAAK,EAAC,KAAK,CAAC,CAAA;;;;oDAGhC;AAGK;IADL,IAAA,YAAG,EAAC,aAAa,CAAC;IAEhB,WAAA,IAAA,cAAK,EAAC,IAAI,CAAC,CAAA;IACX,WAAA,IAAA,cAAK,EAAC,MAAM,CAAC,CAAA;;;;sDAMf;0BAvBU,eAAe;IAD3B,IAAA,mBAAU,EAAC,QAAQ,CAAC;qCAEwB,4BAAY;GAD5C,eAAe,CAwB3B"}

View File

@ -0,0 +1,2 @@
export declare class PriceModule {
}

View File

@ -0,0 +1,25 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PriceModule = void 0;
const common_1 = require("@nestjs/common");
const price_controller_1 = require("./price.controller");
const price_service_1 = require("./price.service");
const prisma_module_1 = require("../prisma/prisma.module");
let PriceModule = class PriceModule {
};
exports.PriceModule = PriceModule;
exports.PriceModule = PriceModule = __decorate([
(0, common_1.Module)({
imports: [prisma_module_1.PrismaModule],
controllers: [price_controller_1.PriceController],
providers: [price_service_1.PriceService],
exports: [price_service_1.PriceService],
})
], PriceModule);
//# sourceMappingURL=price.module.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"price.module.js","sourceRoot":"","sources":["../../src/price/price.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,yDAAqD;AACrD,mDAA+C;AAC/C,2DAAuD;AAQhD,IAAM,WAAW,GAAjB,MAAM,WAAW;CAAG,CAAA;AAAd,kCAAW;sBAAX,WAAW;IANvB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE,CAAC,4BAAY,CAAC;QACvB,WAAW,EAAE,CAAC,kCAAe,CAAC;QAC9B,SAAS,EAAE,CAAC,4BAAY,CAAC;QACzB,OAAO,EAAE,CAAC,4BAAY,CAAC;KACxB,CAAC;GACW,WAAW,CAAG"}

View File

@ -0,0 +1,75 @@
import { PrismaService } from '../prisma/prisma.service';
export declare class PriceService {
private prisma;
constructor(prisma: PrismaService);
getPricesForProduct(productId: number): Promise<({
source: {
id: number;
name: string;
url: string;
logo: string | null;
lastScraped: Date | null;
createdAt: Date;
updatedAt: Date;
};
} & {
id: number;
createdAt: Date;
sourceId: number;
regularPrice: number;
discountedPrice: number | null;
discountPercentage: number | null;
unitPrice: string | null;
promotionType: string | null;
promotionStart: Date | null;
promotionEnd: Date | null;
vatIncluded: boolean;
lastUpdated: Date;
productId: number;
})[]>;
comparePrices(productIds: number[]): Promise<{
id: number;
name: string;
description: string | null;
category: string;
availability: boolean;
latestPrice: {
source: {
id: number;
name: string;
url: string;
logo: string | null;
lastScraped: Date | null;
createdAt: Date;
updatedAt: Date;
};
} & {
id: number;
createdAt: Date;
sourceId: number;
regularPrice: number;
discountedPrice: number | null;
discountPercentage: number | null;
unitPrice: string | null;
promotionType: string | null;
promotionStart: Date | null;
promotionEnd: Date | null;
vatIncluded: boolean;
lastUpdated: Date;
productId: number;
};
}[]>;
getPriceHistory(productId: number, days?: number): Promise<{
productId: number;
history: unknown[];
priceRange: {
min: number;
max: number;
};
averagePrice: number;
discountStats: {
maxDiscount: number;
averageDiscount: number;
};
}>;
}

View File

@ -0,0 +1,92 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PriceService = void 0;
const common_1 = require("@nestjs/common");
const prisma_service_1 = require("../prisma/prisma.service");
let PriceService = class PriceService {
prisma;
constructor(prisma) {
this.prisma = prisma;
}
async getPricesForProduct(productId) {
return this.prisma.price.findMany({
where: { productId },
include: { source: true },
orderBy: { lastUpdated: 'desc' },
});
}
async comparePrices(productIds) {
const products = await this.prisma.product.findMany({
where: { id: { in: productIds } },
include: {
prices: {
include: { source: true },
orderBy: { lastUpdated: 'desc' },
take: 1,
},
},
});
return products.map((product) => ({
id: product.id,
name: product.name,
description: product.description,
category: product.category,
availability: product.availability,
latestPrice: product.prices[0] || null,
}));
}
async getPriceHistory(productId, days = 30) {
const startDate = new Date();
startDate.setDate(startDate.getDate() - days);
const prices = await this.prisma.price.findMany({
where: {
productId,
lastUpdated: { gte: startDate },
},
include: { source: true },
orderBy: { lastUpdated: 'asc' },
});
const priceHistory = prices.reduce((acc, price) => {
const date = price.lastUpdated.toISOString().split('T')[0];
if (!acc[date]) {
acc[date] = {
date,
regularPrice: price.regularPrice,
discountedPrice: price.discountedPrice,
source: price.source.name,
};
}
return acc;
}, {});
return {
productId,
history: Object.values(priceHistory),
priceRange: {
min: Math.min(...prices.map(p => p.discountedPrice || p.regularPrice)),
max: Math.max(...prices.map(p => p.regularPrice)),
},
averagePrice: prices.reduce((sum, p) => sum + p.regularPrice, 0) / prices.length,
discountStats: {
maxDiscount: Math.max(...prices.map(p => p.discountPercentage || 0)),
averageDiscount: prices.filter(p => p.discountPercentage)
.reduce((sum, p) => sum + (p.discountPercentage || 0), 0) /
prices.filter((p) => p.discountPercentage).length || 0,
},
};
}
};
exports.PriceService = PriceService;
exports.PriceService = PriceService = __decorate([
(0, common_1.Injectable)(),
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
], PriceService);
//# sourceMappingURL=price.service.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"price.service.js","sourceRoot":"","sources":["../../src/price/price.service.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAA4C;AAC5C,6DAAyD;AAGlD,IAAM,YAAY,GAAlB,MAAM,YAAY;IACH;IAApB,YAAoB,MAAqB;QAArB,WAAM,GAAN,MAAM,CAAe;IAAG,CAAC;IAE7C,KAAK,CAAC,mBAAmB,CAAC,SAAiB;QACzC,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC;YAChC,KAAK,EAAE,EAAE,SAAS,EAAE;YACpB,OAAO,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE;YACzB,OAAO,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE;SACjC,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,UAAoB;QACtC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC;YAClD,KAAK,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,UAAU,EAAE,EAAE;YACjC,OAAO,EAAE;gBACP,MAAM,EAAE;oBACN,OAAO,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE;oBACzB,OAAO,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE;oBAChC,IAAI,EAAE,CAAC;iBACR;aACF;SACF,CAAC,CAAC;QAEH,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;YAChC,EAAE,EAAE,OAAO,CAAC,EAAE;YACd,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,WAAW,EAAE,OAAO,CAAC,WAAW;YAChC,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,YAAY,EAAE,OAAO,CAAC,YAAY;YAClC,WAAW,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,IAAI;SACvC,CAAC,CAAC,CAAC;IACN,CAAC;IAED,KAAK,CAAC,eAAe,CAAC,SAAiB,EAAE,OAAe,EAAE;QACxD,MAAM,SAAS,GAAG,IAAI,IAAI,EAAE,CAAC;QAC7B,SAAS,CAAC,OAAO,CAAC,SAAS,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,CAAC;QAE9C,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC;YAC9C,KAAK,EAAE;gBACL,SAAS;gBACT,WAAW,EAAE,EAAE,GAAG,EAAE,SAAS,EAAE;aAChC;YACD,OAAO,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE;YACzB,OAAO,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE;SAChC,CAAC,CAAC;QAGH,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,KAAK,EAAE,EAAE;YAChD,MAAM,IAAI,GAAG,KAAK,CAAC,WAAW,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;YAE3D,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;gBACf,GAAG,CAAC,IAAI,CAAC,GAAG;oBACV,IAAI;oBACJ,YAAY,EAAE,KAAK,CAAC,YAAY;oBAChC,eAAe,EAAE,KAAK,CAAC,eAAe;oBACtC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,IAAI;iBAC1B,CAAC;YACJ,CAAC;YAED,OAAO,GAAG,CAAC;QACb,CAAC,EAAE,EAAE,CAAC,CAAC;QAEP,OAAO;YACL,SAAS;YACT,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,YAAY,CAAC;YACpC,UAAU,EAAE;gBACV,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,eAAe,IAAI,CAAC,CAAC,YAAY,CAAC,CAAC;gBACtE,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC;aAClD;YACD,YAAY,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,YAAY,EAAE,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM;YAChF,aAAa,EAAE;gBACb,WAAW,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,kBAAkB,IAAI,CAAC,CAAC,CAAC;gBACpE,eAAe,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,kBAAkB,CAAC;qBACpD,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC,kBAAkB,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC;oBACzD,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,MAAM,IAAI,CAAC;aAC3D;SACF,CAAC;IACJ,CAAC;CACF,CAAA;AA9EY,oCAAY;uBAAZ,YAAY;IADxB,IAAA,mBAAU,GAAE;qCAEiB,8BAAa;GAD9B,YAAY,CA8ExB"}

View File

@ -0,0 +1,2 @@
export declare class PrismaModule {
}

View File

@ -0,0 +1,21 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PrismaModule = void 0;
const common_1 = require("@nestjs/common");
const prisma_service_1 = require("./prisma.service");
let PrismaModule = class PrismaModule {
};
exports.PrismaModule = PrismaModule;
exports.PrismaModule = PrismaModule = __decorate([
(0, common_1.Module)({
providers: [prisma_service_1.PrismaService],
exports: [prisma_service_1.PrismaService],
})
], PrismaModule);
//# sourceMappingURL=prisma.module.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"prisma.module.js","sourceRoot":"","sources":["../../src/prisma/prisma.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,qDAAiD;AAM1C,IAAM,YAAY,GAAlB,MAAM,YAAY;CAAG,CAAA;AAAf,oCAAY;uBAAZ,YAAY;IAJxB,IAAA,eAAM,EAAC;QACN,SAAS,EAAE,CAAC,8BAAa,CAAC;QAC1B,OAAO,EAAE,CAAC,8BAAa,CAAC;KACzB,CAAC;GACW,YAAY,CAAG"}

View File

@ -0,0 +1,6 @@
import { OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
export declare class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
onModuleInit(): Promise<void>;
onModuleDestroy(): Promise<void>;
}

View File

@ -0,0 +1,24 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.PrismaService = void 0;
const common_1 = require("@nestjs/common");
const client_1 = require("@prisma/client");
let PrismaService = class PrismaService extends client_1.PrismaClient {
async onModuleInit() {
await this.$connect();
}
async onModuleDestroy() {
await this.$disconnect();
}
};
exports.PrismaService = PrismaService;
exports.PrismaService = PrismaService = __decorate([
(0, common_1.Injectable)()
], PrismaService);
//# sourceMappingURL=prisma.service.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"prisma.service.js","sourceRoot":"","sources":["../../src/prisma/prisma.service.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAA2E;AAC3E,2CAA8C;AAGvC,IAAM,aAAa,GAAnB,MAAM,aAAc,SAAQ,qBAAY;IAC7C,KAAK,CAAC,YAAY;QAChB,MAAM,IAAI,CAAC,QAAQ,EAAE,CAAC;IACxB,CAAC;IAED,KAAK,CAAC,eAAe;QACnB,MAAM,IAAI,CAAC,WAAW,EAAE,CAAC;IAC3B,CAAC;CACF,CAAA;AARY,sCAAa;wBAAb,aAAa;IADzB,IAAA,mBAAU,GAAE;GACA,aAAa,CAQzB"}

View File

@ -0,0 +1,154 @@
import { ProductService } from './product.service';
import { Prisma } from '@prisma/client';
export declare class ProductController {
private readonly productService;
constructor(productService: ProductService);
getProduct(id: string): Promise<({
prices: ({
source: {
id: number;
name: string;
url: string;
logo: string | null;
lastScraped: Date | null;
createdAt: Date;
updatedAt: Date;
};
} & {
id: number;
createdAt: Date;
sourceId: number;
regularPrice: number;
discountedPrice: number | null;
discountPercentage: number | null;
unitPrice: string | null;
promotionType: string | null;
promotionStart: Date | null;
promotionEnd: Date | null;
vatIncluded: boolean;
lastUpdated: Date;
productId: number;
})[];
} & {
id: number;
name: string;
createdAt: Date;
updatedAt: Date;
description: string | null;
category: string;
availability: boolean;
sourceProductId: string | null;
sourceId: number;
}) | null>;
getProducts(skip?: string, take?: string, search?: string, maxPrice?: string): Promise<({
prices: ({
source: {
id: number;
name: string;
url: string;
logo: string | null;
lastScraped: Date | null;
createdAt: Date;
updatedAt: Date;
};
} & {
id: number;
createdAt: Date;
sourceId: number;
regularPrice: number;
discountedPrice: number | null;
discountPercentage: number | null;
unitPrice: string | null;
promotionType: string | null;
promotionStart: Date | null;
promotionEnd: Date | null;
vatIncluded: boolean;
lastUpdated: Date;
productId: number;
})[];
} & {
id: number;
name: string;
createdAt: Date;
updatedAt: Date;
description: string | null;
category: string;
availability: boolean;
sourceProductId: string | null;
sourceId: number;
})[]>;
createProduct(data: Prisma.ProductCreateInput): Promise<{
prices: ({
source: {
id: number;
name: string;
url: string;
logo: string | null;
lastScraped: Date | null;
createdAt: Date;
updatedAt: Date;
};
} & {
id: number;
createdAt: Date;
sourceId: number;
regularPrice: number;
discountedPrice: number | null;
discountPercentage: number | null;
unitPrice: string | null;
promotionType: string | null;
promotionStart: Date | null;
promotionEnd: Date | null;
vatIncluded: boolean;
lastUpdated: Date;
productId: number;
})[];
} & {
id: number;
name: string;
createdAt: Date;
updatedAt: Date;
description: string | null;
category: string;
availability: boolean;
sourceProductId: string | null;
sourceId: number;
}>;
updateProduct(id: string, data: Prisma.ProductUpdateInput): Promise<{
prices: ({
source: {
id: number;
name: string;
url: string;
logo: string | null;
lastScraped: Date | null;
createdAt: Date;
updatedAt: Date;
};
} & {
id: number;
createdAt: Date;
sourceId: number;
regularPrice: number;
discountedPrice: number | null;
discountPercentage: number | null;
unitPrice: string | null;
promotionType: string | null;
promotionStart: Date | null;
promotionEnd: Date | null;
vatIncluded: boolean;
lastUpdated: Date;
productId: number;
})[];
} & {
id: number;
name: string;
createdAt: Date;
updatedAt: Date;
description: string | null;
category: string;
availability: boolean;
sourceProductId: string | null;
sourceId: number;
}>;
}

View File

@ -0,0 +1,94 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ProductController = void 0;
const common_1 = require("@nestjs/common");
const product_service_1 = require("./product.service");
const client_1 = require("@prisma/client");
let ProductController = class ProductController {
productService;
constructor(productService) {
this.productService = productService;
}
async getProduct(id) {
return this.productService.getProduct(Number(id));
}
async getProducts(skip, take, search, maxPrice) {
if (search) {
return this.productService.searchProducts(search);
}
return this.productService.getProducts({
skip: skip ? Number(skip) : undefined,
take: take ? Number(take) : undefined,
where: maxPrice
? {
prices: {
some: {
regularPrice: {
lte: Number(maxPrice),
},
},
},
}
: undefined,
});
}
async createProduct(data) {
return this.productService.createProduct(data);
}
async updateProduct(id, data) {
return this.productService.updateProduct({
where: { id: Number(id) },
data,
});
}
};
exports.ProductController = ProductController;
__decorate([
(0, common_1.Get)(':id'),
__param(0, (0, common_1.Param)('id')),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:returntype", Promise)
], ProductController.prototype, "getProduct", null);
__decorate([
(0, common_1.Get)(),
__param(0, (0, common_1.Query)('skip')),
__param(1, (0, common_1.Query)('take')),
__param(2, (0, common_1.Query)('search')),
__param(3, (0, common_1.Query)('maxPrice')),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String, String, String, String]),
__metadata("design:returntype", Promise)
], ProductController.prototype, "getProducts", null);
__decorate([
(0, common_1.Post)(),
__param(0, (0, common_1.Body)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [Object]),
__metadata("design:returntype", Promise)
], ProductController.prototype, "createProduct", null);
__decorate([
(0, common_1.Put)(':id'),
__param(0, (0, common_1.Param)('id')),
__param(1, (0, common_1.Body)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String, Object]),
__metadata("design:returntype", Promise)
], ProductController.prototype, "updateProduct", null);
exports.ProductController = ProductController = __decorate([
(0, common_1.Controller)('products'),
__metadata("design:paramtypes", [product_service_1.ProductService])
], ProductController);
//# sourceMappingURL=product.controller.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"product.controller.js","sourceRoot":"","sources":["../../src/product/product.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAAgF;AAChF,uDAAmD;AACnD,2CAAwC;AAGjC,IAAM,iBAAiB,GAAvB,MAAM,iBAAiB;IACC;IAA7B,YAA6B,cAA8B;QAA9B,mBAAc,GAAd,cAAc,CAAgB;IAAG,CAAC;IAGzD,AAAN,KAAK,CAAC,UAAU,CAAc,EAAU;QACtC,OAAO,IAAI,CAAC,cAAc,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC;IACpD,CAAC;IAGK,AAAN,KAAK,CAAC,WAAW,CACA,IAAa,EACb,IAAa,EACX,MAAe,EACb,QAAiB;QAEpC,IAAI,MAAM,EAAE,CAAC;YACX,OAAO,IAAI,CAAC,cAAc,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;QACpD,CAAC;QAED,OAAO,IAAI,CAAC,cAAc,CAAC,WAAW,CAAC;YACrC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS;YACrC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS;YACrC,KAAK,EAAE,QAAQ;gBACb,CAAC,CAAC;oBACE,MAAM,EAAE;wBACN,IAAI,EAAE;4BACJ,YAAY,EAAE;gCACZ,GAAG,EAAE,MAAM,CAAC,QAAQ,CAAC;6BACtB;yBACF;qBACF;iBACF;gBACH,CAAC,CAAC,SAAS;SACd,CAAC,CAAC;IACL,CAAC;IAGK,AAAN,KAAK,CAAC,aAAa,CAAS,IAA+B;QACzD,OAAO,IAAI,CAAC,cAAc,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;IACjD,CAAC;IAGK,AAAN,KAAK,CAAC,aAAa,CACJ,EAAU,EACf,IAA+B;QAEvC,OAAO,IAAI,CAAC,cAAc,CAAC,aAAa,CAAC;YACvC,KAAK,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC,EAAE;YACzB,IAAI;SACL,CAAC,CAAC;IACL,CAAC;CACF,CAAA;AAnDY,8CAAiB;AAItB;IADL,IAAA,YAAG,EAAC,KAAK,CAAC;IACO,WAAA,IAAA,cAAK,EAAC,IAAI,CAAC,CAAA;;;;mDAE5B;AAGK;IADL,IAAA,YAAG,GAAE;IAEH,WAAA,IAAA,cAAK,EAAC,MAAM,CAAC,CAAA;IACb,WAAA,IAAA,cAAK,EAAC,MAAM,CAAC,CAAA;IACb,WAAA,IAAA,cAAK,EAAC,QAAQ,CAAC,CAAA;IACf,WAAA,IAAA,cAAK,EAAC,UAAU,CAAC,CAAA;;;;oDAqBnB;AAGK;IADL,IAAA,aAAI,GAAE;IACc,WAAA,IAAA,aAAI,GAAE,CAAA;;;;sDAE1B;AAGK;IADL,IAAA,YAAG,EAAC,KAAK,CAAC;IAER,WAAA,IAAA,cAAK,EAAC,IAAI,CAAC,CAAA;IACX,WAAA,IAAA,aAAI,GAAE,CAAA;;;;sDAMR;4BAlDU,iBAAiB;IAD7B,IAAA,mBAAU,EAAC,UAAU,CAAC;qCAEwB,gCAAc;GADhD,iBAAiB,CAmD7B"}

View File

@ -0,0 +1,2 @@
export declare class ProductModule {
}

View File

@ -0,0 +1,24 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ProductModule = void 0;
const common_1 = require("@nestjs/common");
const prisma_module_1 = require("../prisma/prisma.module");
const product_controller_1 = require("./product.controller");
const product_service_1 = require("./product.service");
let ProductModule = class ProductModule {
};
exports.ProductModule = ProductModule;
exports.ProductModule = ProductModule = __decorate([
(0, common_1.Module)({
imports: [prisma_module_1.PrismaModule],
controllers: [product_controller_1.ProductController],
providers: [product_service_1.ProductService],
})
], ProductModule);
//# sourceMappingURL=product.module.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"product.module.js","sourceRoot":"","sources":["../../src/product/product.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,2DAAuD;AACvD,6DAAyD;AACzD,uDAAmD;AAO5C,IAAM,aAAa,GAAnB,MAAM,aAAa;CAAG,CAAA;AAAhB,sCAAa;wBAAb,aAAa;IALzB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE,CAAC,4BAAY,CAAC;QACvB,WAAW,EAAE,CAAC,sCAAiB,CAAC;QAChC,SAAS,EAAE,CAAC,gCAAc,CAAC;KAC5B,CAAC;GACW,aAAa,CAAG"}

View File

@ -0,0 +1,200 @@
import { PrismaService } from '../prisma/prisma.service';
import { Prisma } from '@prisma/client';
export declare class ProductService {
private prisma;
constructor(prisma: PrismaService);
getProduct(id: number): Promise<({
prices: ({
source: {
id: number;
name: string;
url: string;
logo: string | null;
lastScraped: Date | null;
createdAt: Date;
updatedAt: Date;
};
} & {
id: number;
createdAt: Date;
sourceId: number;
regularPrice: number;
discountedPrice: number | null;
discountPercentage: number | null;
unitPrice: string | null;
promotionType: string | null;
promotionStart: Date | null;
promotionEnd: Date | null;
vatIncluded: boolean;
lastUpdated: Date;
productId: number;
})[];
} & {
id: number;
name: string;
createdAt: Date;
updatedAt: Date;
description: string | null;
category: string;
availability: boolean;
sourceProductId: string | null;
sourceId: number;
}) | null>;
getProducts(params: {
skip?: number;
take?: number;
cursor?: Prisma.ProductWhereUniqueInput;
where?: Prisma.ProductWhereInput;
orderBy?: Prisma.ProductOrderByWithRelationInput;
}): Promise<({
prices: ({
source: {
id: number;
name: string;
url: string;
logo: string | null;
lastScraped: Date | null;
createdAt: Date;
updatedAt: Date;
};
} & {
id: number;
createdAt: Date;
sourceId: number;
regularPrice: number;
discountedPrice: number | null;
discountPercentage: number | null;
unitPrice: string | null;
promotionType: string | null;
promotionStart: Date | null;
promotionEnd: Date | null;
vatIncluded: boolean;
lastUpdated: Date;
productId: number;
})[];
} & {
id: number;
name: string;
createdAt: Date;
updatedAt: Date;
description: string | null;
category: string;
availability: boolean;
sourceProductId: string | null;
sourceId: number;
})[]>;
searchProducts(query: string): Promise<({
prices: ({
source: {
id: number;
name: string;
url: string;
logo: string | null;
lastScraped: Date | null;
createdAt: Date;
updatedAt: Date;
};
} & {
id: number;
createdAt: Date;
sourceId: number;
regularPrice: number;
discountedPrice: number | null;
discountPercentage: number | null;
unitPrice: string | null;
promotionType: string | null;
promotionStart: Date | null;
promotionEnd: Date | null;
vatIncluded: boolean;
lastUpdated: Date;
productId: number;
})[];
} & {
id: number;
name: string;
createdAt: Date;
updatedAt: Date;
description: string | null;
category: string;
availability: boolean;
sourceProductId: string | null;
sourceId: number;
})[]>;
createProduct(data: Prisma.ProductCreateInput): Promise<{
prices: ({
source: {
id: number;
name: string;
url: string;
logo: string | null;
lastScraped: Date | null;
createdAt: Date;
updatedAt: Date;
};
} & {
id: number;
createdAt: Date;
sourceId: number;
regularPrice: number;
discountedPrice: number | null;
discountPercentage: number | null;
unitPrice: string | null;
promotionType: string | null;
promotionStart: Date | null;
promotionEnd: Date | null;
vatIncluded: boolean;
lastUpdated: Date;
productId: number;
})[];
} & {
id: number;
name: string;
createdAt: Date;
updatedAt: Date;
description: string | null;
category: string;
availability: boolean;
sourceProductId: string | null;
sourceId: number;
}>;
updateProduct(params: {
where: Prisma.ProductWhereUniqueInput;
data: Prisma.ProductUpdateInput;
}): Promise<{
prices: ({
source: {
id: number;
name: string;
url: string;
logo: string | null;
lastScraped: Date | null;
createdAt: Date;
updatedAt: Date;
};
} & {
id: number;
createdAt: Date;
sourceId: number;
regularPrice: number;
discountedPrice: number | null;
discountPercentage: number | null;
unitPrice: string | null;
promotionType: string | null;
promotionStart: Date | null;
promotionEnd: Date | null;
vatIncluded: boolean;
lastUpdated: Date;
productId: number;
})[];
} & {
id: number;
name: string;
createdAt: Date;
updatedAt: Date;
description: string | null;
category: string;
availability: boolean;
sourceProductId: string | null;
sourceId: number;
}>;
}

View File

@ -0,0 +1,94 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ProductService = void 0;
const common_1 = require("@nestjs/common");
const prisma_service_1 = require("../prisma/prisma.service");
let ProductService = class ProductService {
prisma;
constructor(prisma) {
this.prisma = prisma;
}
async getProduct(id) {
return this.prisma.product.findUnique({
where: { id },
include: {
prices: {
include: { source: true },
orderBy: { lastUpdated: 'desc' }
}
}
});
}
async getProducts(params) {
const { skip, take, cursor, where, orderBy } = params;
return this.prisma.product.findMany({
skip,
take,
cursor,
where,
orderBy,
include: {
prices: {
include: { source: true },
orderBy: { lastUpdated: 'desc' },
take: 1
}
}
});
}
async searchProducts(query) {
return this.prisma.product.findMany({
where: {
OR: [
{ name: { contains: query, mode: 'insensitive' } },
{ description: { contains: query, mode: 'insensitive' } },
{ category: { contains: query, mode: 'insensitive' } }
]
},
include: {
prices: {
include: { source: true },
orderBy: { lastUpdated: 'desc' },
take: 1
}
}
});
}
async createProduct(data) {
return this.prisma.product.create({
data,
include: {
prices: {
include: { source: true }
}
}
});
}
async updateProduct(params) {
const { where, data } = params;
return this.prisma.product.update({
data,
where,
include: {
prices: {
include: { source: true }
}
}
});
}
};
exports.ProductService = ProductService;
exports.ProductService = ProductService = __decorate([
(0, common_1.Injectable)(),
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
], ProductService);
//# sourceMappingURL=product.service.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"product.service.js","sourceRoot":"","sources":["../../src/product/product.service.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAA4C;AAC5C,6DAAyD;AAIlD,IAAM,cAAc,GAApB,MAAM,cAAc;IACL;IAApB,YAAoB,MAAqB;QAArB,WAAM,GAAN,MAAM,CAAe;IAAG,CAAC;IAE7C,KAAK,CAAC,UAAU,CAAC,EAAU;QACzB,OAAO,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC;YACpC,KAAK,EAAE,EAAE,EAAE,EAAE;YACb,OAAO,EAAE;gBACP,MAAM,EAAE;oBACN,OAAO,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE;oBACzB,OAAO,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE;iBACjC;aACF;SACF,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,WAAW,CAAC,MAMjB;QACC,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,MAAM,CAAC;QACtD,OAAO,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC;YAClC,IAAI;YACJ,IAAI;YACJ,MAAM;YACN,KAAK;YACL,OAAO;YACP,OAAO,EAAE;gBACP,MAAM,EAAE;oBACN,OAAO,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE;oBACzB,OAAO,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE;oBAChC,IAAI,EAAE,CAAC;iBACR;aACF;SACF,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,cAAc,CAAC,KAAa;QAChC,OAAO,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC;YAClC,KAAK,EAAE;gBACL,EAAE,EAAE;oBACF,EAAE,IAAI,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,aAAa,EAAE,EAAE;oBAClD,EAAE,WAAW,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,aAAa,EAAE,EAAE;oBACzD,EAAE,QAAQ,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,aAAa,EAAE,EAAE;iBACvD;aACF;YACD,OAAO,EAAE;gBACP,MAAM,EAAE;oBACN,OAAO,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE;oBACzB,OAAO,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE;oBAChC,IAAI,EAAE,CAAC;iBACR;aACF;SACF,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,IAA+B;QACjD,OAAO,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC;YAChC,IAAI;YACJ,OAAO,EAAE;gBACP,MAAM,EAAE;oBACN,OAAO,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE;iBAC1B;aACF;SACF,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,MAGnB;QACC,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,MAAM,CAAC;QAC/B,OAAO,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC;YAChC,IAAI;YACJ,KAAK;YACL,OAAO,EAAE;gBACP,MAAM,EAAE;oBACN,OAAO,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE;iBAC1B;aACF;SACF,CAAC,CAAC;IACL,CAAC;CACF,CAAA;AApFY,wCAAc;yBAAd,cAAc;IAD1B,IAAA,mBAAU,GAAE;qCAEiB,8BAAa;GAD9B,cAAc,CAoF1B"}

View File

@ -0,0 +1,2 @@
export declare class ScraperModule {
}

View File

@ -0,0 +1,25 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ScraperModule = void 0;
const common_1 = require("@nestjs/common");
const axios_1 = require("@nestjs/axios");
const schedule_1 = require("@nestjs/schedule");
const scraper_service_1 = require("./scraper.service");
const prisma_module_1 = require("../prisma/prisma.module");
let ScraperModule = class ScraperModule {
};
exports.ScraperModule = ScraperModule;
exports.ScraperModule = ScraperModule = __decorate([
(0, common_1.Module)({
imports: [prisma_module_1.PrismaModule, axios_1.HttpModule, schedule_1.ScheduleModule.forRoot()],
providers: [scraper_service_1.ScraperService],
exports: [scraper_service_1.ScraperService],
})
], ScraperModule);
//# sourceMappingURL=scraper.module.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"scraper.module.js","sourceRoot":"","sources":["../../src/scraper/scraper.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,yCAA2C;AAC3C,+CAAkD;AAClD,uDAAmD;AACnD,2DAAuD;AAOhD,IAAM,aAAa,GAAnB,MAAM,aAAa;CAAG,CAAA;AAAhB,sCAAa;wBAAb,aAAa;IALzB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE,CAAC,4BAAY,EAAE,kBAAU,EAAE,yBAAc,CAAC,OAAO,EAAE,CAAC;QAC7D,SAAS,EAAE,CAAC,gCAAc,CAAC;QAC3B,OAAO,EAAE,CAAC,gCAAc,CAAC;KAC1B,CAAC;GACW,aAAa,CAAG"}

View File

@ -0,0 +1,17 @@
import { PrismaService } from '../prisma/prisma.service';
import { ConfigService } from '@nestjs/config';
export declare class ScraperService {
private prisma;
private config;
constructor(prisma: PrismaService, config: ConfigService);
private readonly logger;
private parsePrice;
private parseDate;
private parseDateRange;
private getTableSelector;
private calculateDiscountPercentage;
private parseProductRow;
scrapeAllSources(): Promise<void>;
scrapeProducts(sourceUrl: string, sourceId: number): Promise<void>;
manualScrape(sourceId: number): Promise<void>;
}

View File

@ -0,0 +1,245 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var ScraperService_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ScraperService = void 0;
const common_1 = require("@nestjs/common");
const prisma_service_1 = require("../prisma/prisma.service");
const schedule_1 = require("@nestjs/schedule");
const cheerio = require("cheerio");
const axios_1 = require("axios");
const config_1 = require("@nestjs/config");
let ScraperService = ScraperService_1 = class ScraperService {
prisma;
config;
constructor(prisma, config) {
this.prisma = prisma;
this.config = config;
}
logger = new common_1.Logger(ScraperService_1.name);
parsePrice(price) {
const cleanPrice = price.replace(/[^\d.]/g, '');
return cleanPrice ? parseFloat(cleanPrice) : 0;
}
parseDate(dateStr) {
try {
const [day, month, year] = dateStr.split('/').map(Number);
return new Date(year, month - 1, day);
}
catch {
return null;
}
}
parseDateRange(dateRange) {
try {
const [startStr, endStr] = dateRange.split('-').map((s) => s.trim());
return {
start: this.parseDate(startStr),
end: this.parseDate(endStr),
};
}
catch {
return { start: null, end: null };
}
}
getTableSelector(sourceId) {
switch (sourceId) {
case 2:
return 'table:eq(2)';
case 12:
return '#product-table';
default:
throw new Error(`Unsupported source ID: ${sourceId}`);
}
}
calculateDiscountPercentage(regularPrice, discountedPrice) {
if (!discountedPrice || !regularPrice)
return null;
return Math.round(((regularPrice - discountedPrice) / regularPrice) * 100);
}
parseProductRow($, rowElement, sourceId) {
const cells = $(rowElement).find('td');
const getText = (index) => {
const cell = cells.eq(index);
return cell.text().trim();
};
if (sourceId === 2) {
const promotionPeriod = getText(9);
const { start, end } = this.parseDateRange(promotionPeriod);
return {
name: getText(0),
regularPrice: this.parsePrice(getText(1)),
unitPrice: getText(2) || null,
availability: getText(3).toLowerCase() === 'да',
description: getText(4) || '',
category: 'Uncategorized',
discountedPrice: this.parsePrice(getText(6)),
discountPercentage: parseFloat(getText(7)) || null,
promotionType: getText(8) || null,
promotionStart: start,
promotionEnd: end,
};
}
else if (sourceId === 12) {
const name = getText(1);
const regularPrice = this.parsePrice(getText(3));
const description = getText(2) || '';
const discountedPrice = this.parsePrice(getText(4)) || null;
return {
name,
regularPrice,
unitPrice: null,
availability: true,
description,
category: 'Uncategorized',
discountedPrice,
discountPercentage: this.calculateDiscountPercentage(regularPrice, discountedPrice),
promotionType: null,
promotionStart: null,
promotionEnd: null,
};
}
throw new Error(`Unsupported source ID: ${sourceId}`);
}
async scrapeAllSources() {
try {
const sources = await this.prisma.source.findMany();
for (const source of sources) {
try {
await this.scrapeProducts(source.url, source.id);
this.logger.log(`Successfully scraped data from source: ${source.name}`);
}
catch (error) {
this.logger.error(`Failed to scrape source ${source.name}:`, error);
continue;
}
}
}
catch (error) {
this.logger.error('Failed to fetch sources:', error);
}
}
async scrapeProducts(sourceUrl, sourceId) {
const config = {
headers: {
Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.5',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/117.0',
},
};
try {
this.logger.log(`Fetching data from URL: ${sourceUrl}`);
const response = await axios_1.default.get(sourceUrl, config);
const $ = cheerio.load(response.data);
const productTable = $(this.getTableSelector(sourceId));
if (!productTable.length) {
throw new Error('Product table not found');
}
const rows = productTable.find('tr').slice(1);
this.logger.log(`Found ${rows.length} product rows`);
let processedProducts = 0;
for (const row of rows.toArray()) {
try {
const scrapedProduct = this.parseProductRow($, row, sourceId);
if (!scrapedProduct.name)
continue;
this.logger.log(`Processing product: ${scrapedProduct.name}`);
const product = await this.prisma.product.upsert({
where: {
name_sourceId: {
name: scrapedProduct.name,
sourceId: sourceId,
},
},
create: {
name: scrapedProduct.name,
description: scrapedProduct.description,
category: scrapedProduct.category,
availability: scrapedProduct.availability,
sourceId: sourceId,
prices: {
create: {
regularPrice: scrapedProduct.regularPrice,
discountedPrice: scrapedProduct.discountedPrice,
discountPercentage: scrapedProduct.discountPercentage,
unitPrice: scrapedProduct.unitPrice,
promotionType: scrapedProduct.promotionType,
promotionStart: scrapedProduct.promotionStart,
promotionEnd: scrapedProduct.promotionEnd,
sourceId: sourceId,
},
},
},
update: {
availability: scrapedProduct.availability,
description: scrapedProduct.description,
category: scrapedProduct.category,
prices: {
create: {
regularPrice: scrapedProduct.regularPrice,
discountedPrice: scrapedProduct.discountedPrice,
discountPercentage: scrapedProduct.discountPercentage,
unitPrice: scrapedProduct.unitPrice,
promotionType: scrapedProduct.promotionType,
promotionStart: scrapedProduct.promotionStart,
promotionEnd: scrapedProduct.promotionEnd,
sourceId: sourceId,
},
},
},
});
processedProducts++;
this.logger.log(`Successfully processed product: ${product.name}`);
}
catch (error) {
if (error instanceof Error) {
this.logger.error(`Failed to process row: ${error.message}`);
}
else {
this.logger.error('Failed to process row: Unknown error');
}
}
}
this.logger.log(`Successfully processed ${processedProducts} products`);
}
catch (error) {
if (error instanceof Error) {
this.logger.error(`Failed to scrape products from source ${sourceId}: ${error.message}`);
}
else {
this.logger.error(`Failed to scrape products from source ${sourceId}: Unknown error`);
}
throw error;
}
}
async manualScrape(sourceId) {
const source = await this.prisma.source.findUnique({
where: { id: sourceId },
});
if (!source) {
throw new Error(`Source with ID ${sourceId} not found`);
}
return this.scrapeProducts(source.url, source.id);
}
};
exports.ScraperService = ScraperService;
__decorate([
(0, schedule_1.Cron)(schedule_1.CronExpression.EVERY_HOUR),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], ScraperService.prototype, "scrapeAllSources", null);
exports.ScraperService = ScraperService = ScraperService_1 = __decorate([
(0, common_1.Injectable)(),
__metadata("design:paramtypes", [prisma_service_1.PrismaService,
config_1.ConfigService])
], ScraperService);
//# sourceMappingURL=scraper.service.js.map

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,68 @@
import { SourceService } from './source.service';
export declare class SourceController {
private readonly sourceService;
constructor(sourceService: SourceService);
getSources(): Promise<{
id: number;
name: string;
url: string;
logo: string | null;
lastScraped: Date | null;
createdAt: Date;
updatedAt: Date;
}[]>;
getSource(id: string): Promise<{
id: number;
name: string;
url: string;
logo: string | null;
lastScraped: Date | null;
createdAt: Date;
updatedAt: Date;
} | null>;
getProductsFromSource(id: string, skip?: string, take?: string): Promise<{
products: ({
prices: ({
source: {
id: number;
name: string;
url: string;
logo: string | null;
lastScraped: Date | null;
createdAt: Date;
updatedAt: Date;
};
} & {
id: number;
createdAt: Date;
sourceId: number;
regularPrice: number;
discountedPrice: number | null;
discountPercentage: number | null;
unitPrice: string | null;
promotionType: string | null;
promotionStart: Date | null;
promotionEnd: Date | null;
vatIncluded: boolean;
lastUpdated: Date;
productId: number;
})[];
} & {
id: number;
name: string;
createdAt: Date;
updatedAt: Date;
description: string | null;
category: string;
availability: boolean;
sourceProductId: string | null;
sourceId: number;
})[];
pagination: {
total: number;
skip: number;
take: number;
hasMore: boolean;
};
}>;
}

View File

@ -0,0 +1,60 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SourceController = void 0;
const common_1 = require("@nestjs/common");
const source_service_1 = require("./source.service");
let SourceController = class SourceController {
sourceService;
constructor(sourceService) {
this.sourceService = sourceService;
}
async getSources() {
return this.sourceService.getSources();
}
async getSource(id) {
return this.sourceService.getSource(Number(id));
}
async getProductsFromSource(id, skip, take) {
return this.sourceService.getProductsFromSource(Number(id), skip ? Number(skip) : undefined, take ? Number(take) : undefined);
}
};
exports.SourceController = SourceController;
__decorate([
(0, common_1.Get)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", Promise)
], SourceController.prototype, "getSources", null);
__decorate([
(0, common_1.Get)(':id'),
__param(0, (0, common_1.Param)('id')),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:returntype", Promise)
], SourceController.prototype, "getSource", null);
__decorate([
(0, common_1.Get)(':id/products'),
__param(0, (0, common_1.Param)('id')),
__param(1, (0, common_1.Query)('skip')),
__param(2, (0, common_1.Query)('take')),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String, String, String]),
__metadata("design:returntype", Promise)
], SourceController.prototype, "getProductsFromSource", null);
exports.SourceController = SourceController = __decorate([
(0, common_1.Controller)('sources'),
__metadata("design:paramtypes", [source_service_1.SourceService])
], SourceController);
//# sourceMappingURL=source.controller.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"source.controller.js","sourceRoot":"","sources":["../../src/source/source.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAA+D;AAC/D,qDAAiD;AAG1C,IAAM,gBAAgB,GAAtB,MAAM,gBAAgB;IACE;IAA7B,YAA6B,aAA4B;QAA5B,kBAAa,GAAb,aAAa,CAAe;IAAG,CAAC;IAGvD,AAAN,KAAK,CAAC,UAAU;QACd,OAAO,IAAI,CAAC,aAAa,CAAC,UAAU,EAAE,CAAC;IACzC,CAAC;IAGK,AAAN,KAAK,CAAC,SAAS,CAAc,EAAU;QACrC,OAAO,IAAI,CAAC,aAAa,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC;IAClD,CAAC;IAGK,AAAN,KAAK,CAAC,qBAAqB,CACZ,EAAU,EACR,IAAa,EACb,IAAa;QAE5B,OAAO,IAAI,CAAC,aAAa,CAAC,qBAAqB,CAC7C,MAAM,CAAC,EAAE,CAAC,EACV,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,EAC/B,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAChC,CAAC;IACJ,CAAC;CACF,CAAA;AAzBY,4CAAgB;AAIrB;IADL,IAAA,YAAG,GAAE;;;;kDAGL;AAGK;IADL,IAAA,YAAG,EAAC,KAAK,CAAC;IACM,WAAA,IAAA,cAAK,EAAC,IAAI,CAAC,CAAA;;;;iDAE3B;AAGK;IADL,IAAA,YAAG,EAAC,cAAc,CAAC;IAEjB,WAAA,IAAA,cAAK,EAAC,IAAI,CAAC,CAAA;IACX,WAAA,IAAA,cAAK,EAAC,MAAM,CAAC,CAAA;IACb,WAAA,IAAA,cAAK,EAAC,MAAM,CAAC,CAAA;;;;6DAOf;2BAxBU,gBAAgB;IAD5B,IAAA,mBAAU,EAAC,SAAS,CAAC;qCAEwB,8BAAa;GAD9C,gBAAgB,CAyB5B"}

View File

@ -0,0 +1,2 @@
export declare class SourceModule {
}

View File

@ -0,0 +1,25 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SourceModule = void 0;
const common_1 = require("@nestjs/common");
const source_controller_1 = require("./source.controller");
const source_service_1 = require("./source.service");
const prisma_module_1 = require("../prisma/prisma.module");
let SourceModule = class SourceModule {
};
exports.SourceModule = SourceModule;
exports.SourceModule = SourceModule = __decorate([
(0, common_1.Module)({
imports: [prisma_module_1.PrismaModule],
controllers: [source_controller_1.SourceController],
providers: [source_service_1.SourceService],
exports: [source_service_1.SourceService],
})
], SourceModule);
//# sourceMappingURL=source.module.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"source.module.js","sourceRoot":"","sources":["../../src/source/source.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,2DAAuD;AACvD,qDAAiD;AACjD,2DAAuD;AAQhD,IAAM,YAAY,GAAlB,MAAM,YAAY;CAAG,CAAA;AAAf,oCAAY;uBAAZ,YAAY;IANxB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE,CAAC,4BAAY,CAAC;QACvB,WAAW,EAAE,CAAC,oCAAgB,CAAC;QAC/B,SAAS,EAAE,CAAC,8BAAa,CAAC;QAC1B,OAAO,EAAE,CAAC,8BAAa,CAAC;KACzB,CAAC;GACW,YAAY,CAAG"}

View File

@ -0,0 +1,68 @@
import { PrismaService } from '../prisma/prisma.service';
export declare class SourceService {
private prisma;
constructor(prisma: PrismaService);
getSources(): Promise<{
id: number;
name: string;
url: string;
logo: string | null;
lastScraped: Date | null;
createdAt: Date;
updatedAt: Date;
}[]>;
getSource(id: number): Promise<{
id: number;
name: string;
url: string;
logo: string | null;
lastScraped: Date | null;
createdAt: Date;
updatedAt: Date;
} | null>;
getProductsFromSource(sourceId: number, skip?: number, take?: number): Promise<{
products: ({
prices: ({
source: {
id: number;
name: string;
url: string;
logo: string | null;
lastScraped: Date | null;
createdAt: Date;
updatedAt: Date;
};
} & {
id: number;
createdAt: Date;
sourceId: number;
regularPrice: number;
discountedPrice: number | null;
discountPercentage: number | null;
unitPrice: string | null;
promotionType: string | null;
promotionStart: Date | null;
promotionEnd: Date | null;
vatIncluded: boolean;
lastUpdated: Date;
productId: number;
})[];
} & {
id: number;
name: string;
createdAt: Date;
updatedAt: Date;
description: string | null;
category: string;
availability: boolean;
sourceProductId: string | null;
sourceId: number;
})[];
pagination: {
total: number;
skip: number;
take: number;
hasMore: boolean;
};
}>;
}

View File

@ -0,0 +1,76 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SourceService = void 0;
const common_1 = require("@nestjs/common");
const prisma_service_1 = require("../prisma/prisma.service");
let SourceService = class SourceService {
prisma;
constructor(prisma) {
this.prisma = prisma;
}
async getSources() {
return this.prisma.source.findMany({
orderBy: { name: 'asc' },
});
}
async getSource(id) {
return this.prisma.source.findUnique({
where: { id },
});
}
async getProductsFromSource(sourceId, skip, take) {
const products = await this.prisma.product.findMany({
where: {
prices: {
some: {
sourceId,
},
},
},
include: {
prices: {
where: { sourceId },
orderBy: { lastUpdated: 'desc' },
take: 1,
include: { source: true },
},
},
skip,
take: take || 20,
orderBy: { name: 'asc' },
});
const total = await this.prisma.product.count({
where: {
prices: {
some: {
sourceId,
},
},
},
});
return {
products,
pagination: {
total,
skip: skip || 0,
take: take || 20,
hasMore: (skip || 0) + (take || 20) < total,
},
};
}
};
exports.SourceService = SourceService;
exports.SourceService = SourceService = __decorate([
(0, common_1.Injectable)(),
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
], SourceService);
//# sourceMappingURL=source.service.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"source.service.js","sourceRoot":"","sources":["../../src/source/source.service.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAA4C;AAC5C,6DAAyD;AAGlD,IAAM,aAAa,GAAnB,MAAM,aAAa;IACJ;IAApB,YAAoB,MAAqB;QAArB,WAAM,GAAN,MAAM,CAAe;IAAG,CAAC;IAE7C,KAAK,CAAC,UAAU;QACd,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC;YACjC,OAAO,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE;SACzB,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,EAAU;QACxB,OAAO,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC;YACnC,KAAK,EAAE,EAAE,EAAE,EAAE;SACd,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,qBAAqB,CACzB,QAAgB,EAChB,IAAa,EACb,IAAa;QAEb,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC;YAClD,KAAK,EAAE;gBACL,MAAM,EAAE;oBACN,IAAI,EAAE;wBACJ,QAAQ;qBACT;iBACF;aACF;YACD,OAAO,EAAE;gBACP,MAAM,EAAE;oBACN,KAAK,EAAE,EAAE,QAAQ,EAAE;oBACnB,OAAO,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE;oBAChC,IAAI,EAAE,CAAC;oBACP,OAAO,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE;iBAC1B;aACF;YACD,IAAI;YACJ,IAAI,EAAE,IAAI,IAAI,EAAE;YAChB,OAAO,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE;SACzB,CAAC,CAAC;QAGH,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC;YAC5C,KAAK,EAAE;gBACL,MAAM,EAAE;oBACN,IAAI,EAAE;wBACJ,QAAQ;qBACT;iBACF;aACF;SACF,CAAC,CAAC;QAEH,OAAO;YACL,QAAQ;YACR,UAAU,EAAE;gBACV,KAAK;gBACL,IAAI,EAAE,IAAI,IAAI,CAAC;gBACf,IAAI,EAAE,IAAI,IAAI,EAAE;gBAChB,OAAO,EAAE,CAAC,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC,GAAG,KAAK;aAC5C;SACF,CAAC;IACJ,CAAC;CACF,CAAA;AA9DY,sCAAa;wBAAb,aAAa;IADzB,IAAA,mBAAU,GAAE;qCAEiB,8BAAa;GAD9B,aAAa,CA8DzB"}

1
price-compare-api/dist/test-db.d.ts vendored Normal file
View File

@ -0,0 +1 @@
export {};

79
price-compare-api/dist/test-db.js vendored Normal file
View File

@ -0,0 +1,79 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const client_1 = require("@prisma/client");
const prisma = new client_1.PrismaClient();
async function main() {
try {
const testSource = await prisma.source.upsert({
where: {
name: 'Test Source',
},
update: {
url: 'http://example.com',
},
create: {
name: 'Test Source',
url: 'http://example.com',
},
});
console.log('Test source created/updated successfully:', testSource);
const products = await Promise.all([
prisma.product.create({
data: {
name: 'iPhone 15',
description: 'Latest Apple smartphone',
category: 'Electronics',
sourceId: testSource.id,
prices: {
create: {
regularPrice: 999.99,
discountedPrice: 899.99,
discountPercentage: 10,
sourceId: testSource.id,
unitPrice: '$999.99/unit'
}
}
},
}),
prisma.product.create({
data: {
name: 'Samsung Galaxy S24',
description: 'Premium Android smartphone',
category: 'Electronics',
sourceId: testSource.id,
prices: {
create: {
regularPrice: 899.99,
sourceId: testSource.id,
unitPrice: '$899.99/unit'
}
}
},
}),
prisma.product.create({
data: {
name: 'Sony PlayStation 5',
description: 'Gaming console with advanced graphics',
category: 'Gaming',
sourceId: testSource.id,
prices: {
create: {
regularPrice: 499.99,
sourceId: testSource.id,
unitPrice: '$499.99/unit'
}
}
},
}),
]);
console.log('Sample products created successfully:', products);
}
catch (error) {
console.error('Error creating test data:', error);
}
finally {
await prisma.$disconnect();
}
}
main();
//# sourceMappingURL=test-db.js.map

1
price-compare-api/dist/test-db.js.map vendored Normal file
View File

@ -0,0 +1 @@
{"version":3,"file":"test-db.js","sourceRoot":"","sources":["../src/test-db.ts"],"names":[],"mappings":";;AAAA,2CAA8C;AAE9C,MAAM,MAAM,GAAG,IAAI,qBAAY,EAAE,CAAC;AAElC,KAAK,UAAU,IAAI;IACjB,IAAI,CAAC;QAEH,MAAM,UAAU,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC;YAC5C,KAAK,EAAE;gBACL,IAAI,EAAE,aAAa;aACpB;YACD,MAAM,EAAE;gBACN,GAAG,EAAE,oBAAoB;aAC1B;YACD,MAAM,EAAE;gBACN,IAAI,EAAE,aAAa;gBACnB,GAAG,EAAE,oBAAoB;aAC1B;SACF,CAAC,CAAC;QACH,OAAO,CAAC,GAAG,CAAC,2CAA2C,EAAE,UAAU,CAAC,CAAC;QAGrE,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;YACjC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC;gBACpB,IAAI,EAAE;oBACJ,IAAI,EAAE,WAAW;oBACjB,WAAW,EAAE,yBAAyB;oBACtC,QAAQ,EAAE,aAAa;oBACvB,QAAQ,EAAE,UAAU,CAAC,EAAE;oBACvB,MAAM,EAAE;wBACN,MAAM,EAAE;4BACN,YAAY,EAAE,MAAM;4BACpB,eAAe,EAAE,MAAM;4BACvB,kBAAkB,EAAE,EAAE;4BACtB,QAAQ,EAAE,UAAU,CAAC,EAAE;4BACvB,SAAS,EAAE,cAAc;yBAC1B;qBACF;iBACF;aACF,CAAC;YACF,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC;gBACpB,IAAI,EAAE;oBACJ,IAAI,EAAE,oBAAoB;oBAC1B,WAAW,EAAE,4BAA4B;oBACzC,QAAQ,EAAE,aAAa;oBACvB,QAAQ,EAAE,UAAU,CAAC,EAAE;oBACvB,MAAM,EAAE;wBACN,MAAM,EAAE;4BACN,YAAY,EAAE,MAAM;4BACpB,QAAQ,EAAE,UAAU,CAAC,EAAE;4BACvB,SAAS,EAAE,cAAc;yBAC1B;qBACF;iBACF;aACF,CAAC;YACF,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC;gBACpB,IAAI,EAAE;oBACJ,IAAI,EAAE,oBAAoB;oBAC1B,WAAW,EAAE,uCAAuC;oBACpD,QAAQ,EAAE,QAAQ;oBAClB,QAAQ,EAAE,UAAU,CAAC,EAAE;oBACvB,MAAM,EAAE;wBACN,MAAM,EAAE;4BACN,YAAY,EAAE,MAAM;4BACpB,QAAQ,EAAE,UAAU,CAAC,EAAE;4BACvB,SAAS,EAAE,cAAc;yBAC1B;qBACF;iBACF;aACF,CAAC;SACH,CAAC,CAAC;QAEH,OAAO,CAAC,GAAG,CAAC,uCAAuC,EAAE,QAAQ,CAAC,CAAC;IACjE,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,2BAA2B,EAAE,KAAK,CAAC,CAAC;IACpD,CAAC;YAAS,CAAC;QACT,MAAM,MAAM,CAAC,WAAW,EAAE,CAAC;IAC7B,CAAC;AACH,CAAC;AAED,IAAI,EAAE,CAAC"}

View File

@ -0,0 +1 @@
export {};

22
price-compare-api/dist/test-scraper.js vendored Normal file
View File

@ -0,0 +1,22 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const core_1 = require("@nestjs/core");
const app_module_1 = require("./app.module");
const scraper_service_1 = require("./scraper/scraper.service");
async function bootstrap() {
const app = await core_1.NestFactory.create(app_module_1.AppModule);
const scraperService = app.get(scraper_service_1.ScraperService);
try {
const sourceId = 2;
await scraperService.manualScrape(sourceId);
console.log('Scraping completed successfully');
}
catch (error) {
console.error('Scraping failed:', error);
}
finally {
await app.close();
}
}
bootstrap();
//# sourceMappingURL=test-scraper.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"test-scraper.js","sourceRoot":"","sources":["../src/test-scraper.ts"],"names":[],"mappings":";;AAAA,uCAA2C;AAC3C,6CAAyC;AACzC,+DAA2D;AAE3D,KAAK,UAAU,SAAS;IACtB,MAAM,GAAG,GAAG,MAAM,kBAAW,CAAC,MAAM,CAAC,sBAAS,CAAC,CAAC;IAChD,MAAM,cAAc,GAAG,GAAG,CAAC,GAAG,CAAC,gCAAc,CAAC,CAAC;IAE/C,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,CAAC,CAAC;QACnB,MAAM,cAAc,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;QAC5C,OAAO,CAAC,GAAG,CAAC,iCAAiC,CAAC,CAAC;IACjD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,kBAAkB,EAAE,KAAK,CAAC,CAAC;IAC3C,CAAC;YAAS,CAAC;QACT,MAAM,GAAG,CAAC,KAAK,EAAE,CAAC;IACpB,CAAC;AACH,CAAC;AAED,SAAS,EAAE,CAAC"}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,139 @@
import { UserService } from './user.service';
export declare class UserController {
private readonly userService;
constructor(userService: UserService);
getWatchlist(userId: string): Promise<({
product: {
prices: ({
source: {
id: number;
name: string;
url: string;
logo: string | null;
lastScraped: Date | null;
createdAt: Date;
updatedAt: Date;
};
} & {
id: number;
createdAt: Date;
sourceId: number;
regularPrice: number;
discountedPrice: number | null;
discountPercentage: number | null;
unitPrice: string | null;
promotionType: string | null;
promotionStart: Date | null;
promotionEnd: Date | null;
vatIncluded: boolean;
lastUpdated: Date;
productId: number;
})[];
} & {
id: number;
name: string;
createdAt: Date;
updatedAt: Date;
description: string | null;
category: string;
availability: boolean;
sourceProductId: string | null;
sourceId: number;
};
} & {
id: number;
createdAt: Date;
productId: number;
userId: number;
})[]>;
addToWatchlist(userId: string, data: {
productId: number;
}): Promise<{
id: number;
createdAt: Date;
productId: number;
userId: number;
}>;
removeFromWatchlist(userId: string, productId: string): Promise<{
id: number;
createdAt: Date;
productId: number;
userId: number;
}>;
getNotifications(userId: string): Promise<({
product: {
prices: ({
source: {
id: number;
name: string;
url: string;
logo: string | null;
lastScraped: Date | null;
createdAt: Date;
updatedAt: Date;
};
} & {
id: number;
createdAt: Date;
sourceId: number;
regularPrice: number;
discountedPrice: number | null;
discountPercentage: number | null;
unitPrice: string | null;
promotionType: string | null;
promotionStart: Date | null;
promotionEnd: Date | null;
vatIncluded: boolean;
lastUpdated: Date;
productId: number;
})[];
} & {
id: number;
name: string;
createdAt: Date;
updatedAt: Date;
description: string | null;
category: string;
availability: boolean;
sourceProductId: string | null;
sourceId: number;
};
} & {
id: number;
createdAt: Date;
updatedAt: Date;
productId: number;
userId: number;
priceThreshold: number | null;
percentThreshold: number | null;
notifyOnAnyChange: boolean;
isActive: boolean;
})[]>;
configureNotification(userId: string, data: {
productId: number;
priceThreshold?: number;
percentThreshold?: number;
notifyOnAnyChange?: boolean;
}): Promise<{
id: number;
createdAt: Date;
updatedAt: Date;
productId: number;
userId: number;
priceThreshold: number | null;
percentThreshold: number | null;
notifyOnAnyChange: boolean;
isActive: boolean;
}>;
removeNotification(userId: string, productId: string): Promise<{
id: number;
createdAt: Date;
updatedAt: Date;
productId: number;
userId: number;
priceThreshold: number | null;
percentThreshold: number | null;
notifyOnAnyChange: boolean;
isActive: boolean;
}>;
}

View File

@ -0,0 +1,93 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.UserController = void 0;
const common_1 = require("@nestjs/common");
const user_service_1 = require("./user.service");
let UserController = class UserController {
userService;
constructor(userService) {
this.userService = userService;
}
async getWatchlist(userId) {
return this.userService.getWatchlist(Number(userId));
}
async addToWatchlist(userId, data) {
return this.userService.addToWatchlist(Number(userId), data.productId);
}
async removeFromWatchlist(userId, productId) {
return this.userService.removeFromWatchlist(Number(userId), Number(productId));
}
async getNotifications(userId) {
return this.userService.getNotifications(Number(userId));
}
async configureNotification(userId, data) {
return this.userService.configureNotification(Number(userId), data);
}
async removeNotification(userId, productId) {
return this.userService.removeNotification(Number(userId), Number(productId));
}
};
exports.UserController = UserController;
__decorate([
(0, common_1.Get)(':userId/watchlist'),
__param(0, (0, common_1.Param)('userId')),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:returntype", Promise)
], UserController.prototype, "getWatchlist", null);
__decorate([
(0, common_1.Post)(':userId/watchlist'),
__param(0, (0, common_1.Param)('userId')),
__param(1, (0, common_1.Body)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String, Object]),
__metadata("design:returntype", Promise)
], UserController.prototype, "addToWatchlist", null);
__decorate([
(0, common_1.Delete)(':userId/watchlist/:productId'),
__param(0, (0, common_1.Param)('userId')),
__param(1, (0, common_1.Param)('productId')),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String, String]),
__metadata("design:returntype", Promise)
], UserController.prototype, "removeFromWatchlist", null);
__decorate([
(0, common_1.Get)(':userId/notifications'),
__param(0, (0, common_1.Param)('userId')),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:returntype", Promise)
], UserController.prototype, "getNotifications", null);
__decorate([
(0, common_1.Post)(':userId/notifications'),
__param(0, (0, common_1.Param)('userId')),
__param(1, (0, common_1.Body)()),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String, Object]),
__metadata("design:returntype", Promise)
], UserController.prototype, "configureNotification", null);
__decorate([
(0, common_1.Delete)(':userId/notifications/:productId'),
__param(0, (0, common_1.Param)('userId')),
__param(1, (0, common_1.Param)('productId')),
__metadata("design:type", Function),
__metadata("design:paramtypes", [String, String]),
__metadata("design:returntype", Promise)
], UserController.prototype, "removeNotification", null);
exports.UserController = UserController = __decorate([
(0, common_1.Controller)('users'),
__metadata("design:paramtypes", [user_service_1.UserService])
], UserController);
//# sourceMappingURL=user.controller.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"user.controller.js","sourceRoot":"","sources":["../../src/user/user.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,2CAAqF;AACrF,iDAA6C;AAGtC,IAAM,cAAc,GAApB,MAAM,cAAc;IACI;IAA7B,YAA6B,WAAwB;QAAxB,gBAAW,GAAX,WAAW,CAAa;IAAG,CAAC;IAGnD,AAAN,KAAK,CAAC,YAAY,CAAkB,MAAc;QAChD,OAAO,IAAI,CAAC,WAAW,CAAC,YAAY,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;IACvD,CAAC;IAGK,AAAN,KAAK,CAAC,cAAc,CACD,MAAc,EACvB,IAA2B;QAEnC,OAAO,IAAI,CAAC,WAAW,CAAC,cAAc,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;IACzE,CAAC;IAGK,AAAN,KAAK,CAAC,mBAAmB,CACN,MAAc,EACX,SAAiB;QAErC,OAAO,IAAI,CAAC,WAAW,CAAC,mBAAmB,CACzC,MAAM,CAAC,MAAM,CAAC,EACd,MAAM,CAAC,SAAS,CAAC,CAClB,CAAC;IACJ,CAAC;IAGK,AAAN,KAAK,CAAC,gBAAgB,CAAkB,MAAc;QACpD,OAAO,IAAI,CAAC,WAAW,CAAC,gBAAgB,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;IAC3D,CAAC;IAGK,AAAN,KAAK,CAAC,qBAAqB,CACR,MAAc,EAE/B,IAKC;QAED,OAAO,IAAI,CAAC,WAAW,CAAC,qBAAqB,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC;IACtE,CAAC;IAGK,AAAN,KAAK,CAAC,kBAAkB,CACL,MAAc,EACX,SAAiB;QAErC,OAAO,IAAI,CAAC,WAAW,CAAC,kBAAkB,CACxC,MAAM,CAAC,MAAM,CAAC,EACd,MAAM,CAAC,SAAS,CAAC,CAClB,CAAC;IACJ,CAAC;CACF,CAAA;AAxDY,wCAAc;AAInB;IADL,IAAA,YAAG,EAAC,mBAAmB,CAAC;IACL,WAAA,IAAA,cAAK,EAAC,QAAQ,CAAC,CAAA;;;;kDAElC;AAGK;IADL,IAAA,aAAI,EAAC,mBAAmB,CAAC;IAEvB,WAAA,IAAA,cAAK,EAAC,QAAQ,CAAC,CAAA;IACf,WAAA,IAAA,aAAI,GAAE,CAAA;;;;oDAGR;AAGK;IADL,IAAA,eAAM,EAAC,8BAA8B,CAAC;IAEpC,WAAA,IAAA,cAAK,EAAC,QAAQ,CAAC,CAAA;IACf,WAAA,IAAA,cAAK,EAAC,WAAW,CAAC,CAAA;;;;yDAMpB;AAGK;IADL,IAAA,YAAG,EAAC,uBAAuB,CAAC;IACL,WAAA,IAAA,cAAK,EAAC,QAAQ,CAAC,CAAA;;;;sDAEtC;AAGK;IADL,IAAA,aAAI,EAAC,uBAAuB,CAAC;IAE3B,WAAA,IAAA,cAAK,EAAC,QAAQ,CAAC,CAAA;IACf,WAAA,IAAA,aAAI,GAAE,CAAA;;;;2DASR;AAGK;IADL,IAAA,eAAM,EAAC,kCAAkC,CAAC;IAExC,WAAA,IAAA,cAAK,EAAC,QAAQ,CAAC,CAAA;IACf,WAAA,IAAA,cAAK,EAAC,WAAW,CAAC,CAAA;;;;wDAMpB;yBAvDU,cAAc;IAD1B,IAAA,mBAAU,EAAC,OAAO,CAAC;qCAEwB,0BAAW;GAD1C,cAAc,CAwD1B"}

View File

@ -0,0 +1,2 @@
export declare class UserModule {
}

View File

@ -0,0 +1,25 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.UserModule = void 0;
const common_1 = require("@nestjs/common");
const user_controller_1 = require("./user.controller");
const user_service_1 = require("./user.service");
const prisma_module_1 = require("../prisma/prisma.module");
let UserModule = class UserModule {
};
exports.UserModule = UserModule;
exports.UserModule = UserModule = __decorate([
(0, common_1.Module)({
imports: [prisma_module_1.PrismaModule],
controllers: [user_controller_1.UserController],
providers: [user_service_1.UserService],
exports: [user_service_1.UserService],
})
], UserModule);
//# sourceMappingURL=user.module.js.map

View File

@ -0,0 +1 @@
{"version":3,"file":"user.module.js","sourceRoot":"","sources":["../../src/user/user.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAAwC;AACxC,uDAAmD;AACnD,iDAA6C;AAC7C,2DAAuD;AAQhD,IAAM,UAAU,GAAhB,MAAM,UAAU;CAAG,CAAA;AAAb,gCAAU;qBAAV,UAAU;IANtB,IAAA,eAAM,EAAC;QACN,OAAO,EAAE,CAAC,4BAAY,CAAC;QACvB,WAAW,EAAE,CAAC,gCAAc,CAAC;QAC7B,SAAS,EAAE,CAAC,0BAAW,CAAC;QACxB,OAAO,EAAE,CAAC,0BAAW,CAAC;KACvB,CAAC;GACW,UAAU,CAAG"}

Some files were not shown because too many files have changed in this diff Show More