first
This commit is contained in:
commit
0fa3c7ac48
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
app/node_modules
|
||||
price-compare-api/node_modules
|
||||
node_modules
|
||||
8
.idea/.gitignore
vendored
Normal file
8
.idea/.gitignore
vendored
Normal 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
12
.idea/dataSources.xml
Normal 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>
|
||||
6
.idea/inspectionProfiles/Project_Default.xml
Normal file
6
.idea/inspectionProfiles/Project_Default.xml
Normal 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
8
.idea/modules.xml
Normal 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
6
.idea/prettier.xml
Normal 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
12
.idea/sporedi1.iml
Normal 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
11
.idea/vcs.xml
Normal 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
123
README.md
Normal 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
41
app/.gitignore
vendored
Normal 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
36
app/README.md
Normal 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
16
app/eslint.config.mjs
Normal 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
7
app/next.config.ts
Normal 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
5927
app/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
app/package.json
Normal file
28
app/package.json
Normal 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
5
app/postcss.config.mjs
Normal file
@ -0,0 +1,5 @@
|
||||
const config = {
|
||||
plugins: ["@tailwindcss/postcss"],
|
||||
};
|
||||
|
||||
export default config;
|
||||
1
app/public/file.svg
Normal file
1
app/public/file.svg
Normal 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
1
app/public/globe.svg
Normal 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
1
app/public/next.svg
Normal 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
1
app/public/vercel.svg
Normal 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
1
app/public/window.svg
Normal 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
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
26
app/src/app/globals.css
Normal 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
41
app/src/app/layout.tsx
Normal 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
92
app/src/app/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
49
app/src/components/ProductCard.tsx
Normal file
49
app/src/components/ProductCard.tsx
Normal 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
41
app/src/lib/api.ts
Normal 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
27
app/tsconfig.json
Normal 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
293
docs/improvment_plan.md
Normal 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
293
docs/requirements.md
Normal 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
2
price-compare-api/.env
Normal 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"
|
||||
4
price-compare-api/.prettierrc
Normal file
4
price-compare-api/.prettierrc
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
98
price-compare-api/README.md
Normal file
98
price-compare-api/README.md
Normal 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>
|
||||
<!--[](https://opencollective.com/nest#backer)
|
||||
[](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).
|
||||
9
price-compare-api/dist/app.controller.d.ts
vendored
Normal file
9
price-compare-api/dist/app.controller.d.ts
vendored
Normal 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>;
|
||||
}
|
||||
52
price-compare-api/dist/app.controller.js
vendored
Normal file
52
price-compare-api/dist/app.controller.js
vendored
Normal 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
|
||||
1
price-compare-api/dist/app.controller.js.map
vendored
Normal file
1
price-compare-api/dist/app.controller.js.map
vendored
Normal 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"}
|
||||
2
price-compare-api/dist/app.module.d.ts
vendored
Normal file
2
price-compare-api/dist/app.module.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
export declare class AppModule {
|
||||
}
|
||||
42
price-compare-api/dist/app.module.js
vendored
Normal file
42
price-compare-api/dist/app.module.js
vendored
Normal 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
|
||||
1
price-compare-api/dist/app.module.js.map
vendored
Normal file
1
price-compare-api/dist/app.module.js.map
vendored
Normal 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"}
|
||||
3
price-compare-api/dist/app.service.d.ts
vendored
Normal file
3
price-compare-api/dist/app.service.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
export declare class AppService {
|
||||
getHello(): string;
|
||||
}
|
||||
20
price-compare-api/dist/app.service.js
vendored
Normal file
20
price-compare-api/dist/app.service.js
vendored
Normal 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
|
||||
1
price-compare-api/dist/app.service.js.map
vendored
Normal file
1
price-compare-api/dist/app.service.js.map
vendored
Normal 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"}
|
||||
1
price-compare-api/dist/create-source.d.ts
vendored
Normal file
1
price-compare-api/dist/create-source.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export {};
|
||||
37
price-compare-api/dist/create-source.js
vendored
Normal file
37
price-compare-api/dist/create-source.js
vendored
Normal 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
|
||||
1
price-compare-api/dist/create-source.js.map
vendored
Normal file
1
price-compare-api/dist/create-source.js.map
vendored
Normal 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
1
price-compare-api/dist/main.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export {};
|
||||
15
price-compare-api/dist/main.js
vendored
Normal file
15
price-compare-api/dist/main.js
vendored
Normal 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
1
price-compare-api/dist/main.js.map
vendored
Normal 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"}
|
||||
75
price-compare-api/dist/price/price.controller.d.ts
vendored
Normal file
75
price-compare-api/dist/price/price.controller.d.ts
vendored
Normal 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;
|
||||
};
|
||||
}>;
|
||||
}
|
||||
61
price-compare-api/dist/price/price.controller.js
vendored
Normal file
61
price-compare-api/dist/price/price.controller.js
vendored
Normal 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
|
||||
1
price-compare-api/dist/price/price.controller.js.map
vendored
Normal file
1
price-compare-api/dist/price/price.controller.js.map
vendored
Normal 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"}
|
||||
2
price-compare-api/dist/price/price.module.d.ts
vendored
Normal file
2
price-compare-api/dist/price/price.module.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
export declare class PriceModule {
|
||||
}
|
||||
25
price-compare-api/dist/price/price.module.js
vendored
Normal file
25
price-compare-api/dist/price/price.module.js
vendored
Normal 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
|
||||
1
price-compare-api/dist/price/price.module.js.map
vendored
Normal file
1
price-compare-api/dist/price/price.module.js.map
vendored
Normal 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"}
|
||||
75
price-compare-api/dist/price/price.service.d.ts
vendored
Normal file
75
price-compare-api/dist/price/price.service.d.ts
vendored
Normal 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;
|
||||
};
|
||||
}>;
|
||||
}
|
||||
92
price-compare-api/dist/price/price.service.js
vendored
Normal file
92
price-compare-api/dist/price/price.service.js
vendored
Normal 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
|
||||
1
price-compare-api/dist/price/price.service.js.map
vendored
Normal file
1
price-compare-api/dist/price/price.service.js.map
vendored
Normal 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"}
|
||||
2
price-compare-api/dist/prisma/prisma.module.d.ts
vendored
Normal file
2
price-compare-api/dist/prisma/prisma.module.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
export declare class PrismaModule {
|
||||
}
|
||||
21
price-compare-api/dist/prisma/prisma.module.js
vendored
Normal file
21
price-compare-api/dist/prisma/prisma.module.js
vendored
Normal 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
|
||||
1
price-compare-api/dist/prisma/prisma.module.js.map
vendored
Normal file
1
price-compare-api/dist/prisma/prisma.module.js.map
vendored
Normal 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"}
|
||||
6
price-compare-api/dist/prisma/prisma.service.d.ts
vendored
Normal file
6
price-compare-api/dist/prisma/prisma.service.d.ts
vendored
Normal 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>;
|
||||
}
|
||||
24
price-compare-api/dist/prisma/prisma.service.js
vendored
Normal file
24
price-compare-api/dist/prisma/prisma.service.js
vendored
Normal 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
|
||||
1
price-compare-api/dist/prisma/prisma.service.js.map
vendored
Normal file
1
price-compare-api/dist/prisma/prisma.service.js.map
vendored
Normal 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"}
|
||||
154
price-compare-api/dist/product/product.controller.d.ts
vendored
Normal file
154
price-compare-api/dist/product/product.controller.d.ts
vendored
Normal 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;
|
||||
}>;
|
||||
}
|
||||
94
price-compare-api/dist/product/product.controller.js
vendored
Normal file
94
price-compare-api/dist/product/product.controller.js
vendored
Normal 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
|
||||
1
price-compare-api/dist/product/product.controller.js.map
vendored
Normal file
1
price-compare-api/dist/product/product.controller.js.map
vendored
Normal 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"}
|
||||
2
price-compare-api/dist/product/product.module.d.ts
vendored
Normal file
2
price-compare-api/dist/product/product.module.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
export declare class ProductModule {
|
||||
}
|
||||
24
price-compare-api/dist/product/product.module.js
vendored
Normal file
24
price-compare-api/dist/product/product.module.js
vendored
Normal 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
|
||||
1
price-compare-api/dist/product/product.module.js.map
vendored
Normal file
1
price-compare-api/dist/product/product.module.js.map
vendored
Normal 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"}
|
||||
200
price-compare-api/dist/product/product.service.d.ts
vendored
Normal file
200
price-compare-api/dist/product/product.service.d.ts
vendored
Normal 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;
|
||||
}>;
|
||||
}
|
||||
94
price-compare-api/dist/product/product.service.js
vendored
Normal file
94
price-compare-api/dist/product/product.service.js
vendored
Normal 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
|
||||
1
price-compare-api/dist/product/product.service.js.map
vendored
Normal file
1
price-compare-api/dist/product/product.service.js.map
vendored
Normal 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"}
|
||||
2
price-compare-api/dist/scraper/scraper.module.d.ts
vendored
Normal file
2
price-compare-api/dist/scraper/scraper.module.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
export declare class ScraperModule {
|
||||
}
|
||||
25
price-compare-api/dist/scraper/scraper.module.js
vendored
Normal file
25
price-compare-api/dist/scraper/scraper.module.js
vendored
Normal 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
|
||||
1
price-compare-api/dist/scraper/scraper.module.js.map
vendored
Normal file
1
price-compare-api/dist/scraper/scraper.module.js.map
vendored
Normal 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"}
|
||||
17
price-compare-api/dist/scraper/scraper.service.d.ts
vendored
Normal file
17
price-compare-api/dist/scraper/scraper.service.d.ts
vendored
Normal 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>;
|
||||
}
|
||||
245
price-compare-api/dist/scraper/scraper.service.js
vendored
Normal file
245
price-compare-api/dist/scraper/scraper.service.js
vendored
Normal 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
|
||||
1
price-compare-api/dist/scraper/scraper.service.js.map
vendored
Normal file
1
price-compare-api/dist/scraper/scraper.service.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
68
price-compare-api/dist/source/source.controller.d.ts
vendored
Normal file
68
price-compare-api/dist/source/source.controller.d.ts
vendored
Normal 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;
|
||||
};
|
||||
}>;
|
||||
}
|
||||
60
price-compare-api/dist/source/source.controller.js
vendored
Normal file
60
price-compare-api/dist/source/source.controller.js
vendored
Normal 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
|
||||
1
price-compare-api/dist/source/source.controller.js.map
vendored
Normal file
1
price-compare-api/dist/source/source.controller.js.map
vendored
Normal 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"}
|
||||
2
price-compare-api/dist/source/source.module.d.ts
vendored
Normal file
2
price-compare-api/dist/source/source.module.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
export declare class SourceModule {
|
||||
}
|
||||
25
price-compare-api/dist/source/source.module.js
vendored
Normal file
25
price-compare-api/dist/source/source.module.js
vendored
Normal 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
|
||||
1
price-compare-api/dist/source/source.module.js.map
vendored
Normal file
1
price-compare-api/dist/source/source.module.js.map
vendored
Normal 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"}
|
||||
68
price-compare-api/dist/source/source.service.d.ts
vendored
Normal file
68
price-compare-api/dist/source/source.service.d.ts
vendored
Normal 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;
|
||||
};
|
||||
}>;
|
||||
}
|
||||
76
price-compare-api/dist/source/source.service.js
vendored
Normal file
76
price-compare-api/dist/source/source.service.js
vendored
Normal 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
|
||||
1
price-compare-api/dist/source/source.service.js.map
vendored
Normal file
1
price-compare-api/dist/source/source.service.js.map
vendored
Normal 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
1
price-compare-api/dist/test-db.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export {};
|
||||
79
price-compare-api/dist/test-db.js
vendored
Normal file
79
price-compare-api/dist/test-db.js
vendored
Normal 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
1
price-compare-api/dist/test-db.js.map
vendored
Normal 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"}
|
||||
1
price-compare-api/dist/test-scraper.d.ts
vendored
Normal file
1
price-compare-api/dist/test-scraper.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export {};
|
||||
22
price-compare-api/dist/test-scraper.js
vendored
Normal file
22
price-compare-api/dist/test-scraper.js
vendored
Normal 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
|
||||
1
price-compare-api/dist/test-scraper.js.map
vendored
Normal file
1
price-compare-api/dist/test-scraper.js.map
vendored
Normal 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"}
|
||||
1
price-compare-api/dist/tsconfig.build.tsbuildinfo
vendored
Normal file
1
price-compare-api/dist/tsconfig.build.tsbuildinfo
vendored
Normal file
File diff suppressed because one or more lines are too long
139
price-compare-api/dist/user/user.controller.d.ts
vendored
Normal file
139
price-compare-api/dist/user/user.controller.d.ts
vendored
Normal 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;
|
||||
}>;
|
||||
}
|
||||
93
price-compare-api/dist/user/user.controller.js
vendored
Normal file
93
price-compare-api/dist/user/user.controller.js
vendored
Normal 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
|
||||
1
price-compare-api/dist/user/user.controller.js.map
vendored
Normal file
1
price-compare-api/dist/user/user.controller.js.map
vendored
Normal 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"}
|
||||
2
price-compare-api/dist/user/user.module.d.ts
vendored
Normal file
2
price-compare-api/dist/user/user.module.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
export declare class UserModule {
|
||||
}
|
||||
25
price-compare-api/dist/user/user.module.js
vendored
Normal file
25
price-compare-api/dist/user/user.module.js
vendored
Normal 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
|
||||
1
price-compare-api/dist/user/user.module.js.map
vendored
Normal file
1
price-compare-api/dist/user/user.module.js.map
vendored
Normal 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
Loading…
Reference in New Issue
Block a user