first
This commit is contained in:
commit
0fa3c7ac48
140 files changed
+24289
No files matched your search
@@ -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"
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
|
||||
@@ -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
@@ -0,0 +1,2 @@
|
||||
export declare class AppModule {
|
||||
}
|
||||
Vendored
+42
@@ -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
@@ -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
@@ -0,0 +1,3 @@
|
||||
export declare class AppService {
|
||||
getHello(): string;
|
||||
}
|
||||
+20
@@ -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
@@ -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
@@ -0,0 +1 @@
|
||||
export {};
|
||||
+37
@@ -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
@@ -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"}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
export {};
|
||||
Vendored
+15
@@ -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
|
||||
Vendored
+1
@@ -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"}
|
||||
@@ -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;
|
||||
};
|
||||
}>;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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"}
|
||||
@@ -0,0 +1,2 @@
|
||||
export declare class PriceModule {
|
||||
}
|
||||
+25
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
|
||||
@@ -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"}
|
||||
@@ -0,0 +1,2 @@
|
||||
export declare class PrismaModule {
|
||||
}
|
||||
+21
@@ -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
|
||||
@@ -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"}
|
||||
@@ -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
@@ -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
|
||||
@@ -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"}
|
||||
@@ -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;
|
||||
}>;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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"}
|
||||
@@ -0,0 +1,2 @@
|
||||
export declare class ProductModule {
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
@@ -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;
|
||||
}>;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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"}
|
||||
@@ -0,0 +1,2 @@
|
||||
export declare class ScraperModule {
|
||||
}
|
||||
@@ -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
|
||||
@@ -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"}
|
||||
@@ -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
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"scraper.service.js","sourceRoot":"","sources":["../../src/scraper/scraper.service.ts"],"names":[],"mappings":"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 truncated
|
||||
@@ -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;
|
||||
};
|
||||
}>;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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"}
|
||||
@@ -0,0 +1,2 @@
|
||||
export declare class SourceModule {
|
||||
}
|
||||
+25
@@ -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
|
||||
@@ -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"}
|
||||
@@ -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
@@ -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
|
||||
@@ -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"}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
export {};
|
||||
Vendored
+79
@@ -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
@@ -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
@@ -0,0 +1 @@
|
||||
export {};
|
||||
+22
@@ -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
@@ -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"}
|
||||
@@ -0,0 +1 @@
|
||||
{"fileNames":["../node_modules/typescript/lib/lib.es5.d.ts","../node_modules/typescript/lib/lib.es2015.d.ts","../node_modules/typescript/lib/lib.es2016.d.ts","../node_modules/typescript/lib/lib.es2017.d.ts","../node_modules/typescript/lib/lib.es2018.d.ts","../node_modules/typescript/lib/lib.es2019.d.ts","../node_modules/typescript/lib/lib.es2020.d.ts","../node_modules/typescript/lib/lib.es2021.d.ts","../node_modules/typescript/lib/lib.es2022.d.ts","../node_modules/typescript/lib/lib.es2023.d.ts","../node_modules/typescript/lib/lib.dom.d.ts","../node_modules/typescript/lib/lib.dom.iterable.d.ts","../node_modules/typescript/lib/lib.dom.asynciterable.d.ts","../node_modules/typescript/lib/lib.webworker.importscripts.d.ts","../node_modules/typescript/lib/lib.scripthost.d.ts","../node_modules/typescript/lib/lib.es2015.core.d.ts","../node_modules/typescript/lib/lib.es2015.collection.d.ts","../node_modules/typescript/lib/lib.es2015.generator.d.ts","../node_modules/typescript/lib/lib.es2015.iterable.d.ts","../node_modules/typescript/lib/lib.es2015.promise.d.ts","../node_modules/typescript/lib/lib.es2015.proxy.d.ts","../node_modules/typescript/lib/lib.es2015.reflect.d.ts","../node_modules/typescript/lib/lib.es2015.symbol.d.ts","../node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","../node_modules/typescript/lib/lib.es2016.array.include.d.ts","../node_modules/typescript/lib/lib.es2016.intl.d.ts","../node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","../node_modules/typescript/lib/lib.es2017.date.d.ts","../node_modules/typescript/lib/lib.es2017.object.d.ts","../node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","../node_modules/typescript/lib/lib.es2017.string.d.ts","../node_modules/typescript/lib/lib.es2017.intl.d.ts","../node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","../node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","../node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","../node_modules/typescript/lib/lib.es2018.intl.d.ts","../node_modules/typescript/lib/lib.es2018.promise.d.ts","../node_modules/typescript/lib/lib.es2018.regexp.d.ts","../node_modules/typescript/lib/lib.es2019.array.d.ts","../node_modules/typescript/lib/lib.es2019.object.d.ts","../node_modules/typescript/lib/lib.es2019.string.d.ts","../node_modules/typescript/lib/lib.es2019.symbol.d.ts","../node_modules/typescript/lib/lib.es2019.intl.d.ts","../node_modules/typescript/lib/lib.es2020.bigint.d.ts","../node_modules/typescript/lib/lib.es2020.date.d.ts","../node_modules/typescript/lib/lib.es2020.promise.d.ts","../node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","../node_modules/typescript/lib/lib.es2020.string.d.ts","../node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","../node_modules/typescript/lib/lib.es2020.intl.d.ts","../node_modules/typescript/lib/lib.es2020.number.d.ts","../node_modules/typescript/lib/lib.es2021.promise.d.ts","../node_modules/typescript/lib/lib.es2021.string.d.ts","../node_modules/typescript/lib/lib.es2021.weakref.d.ts","../node_modules/typescript/lib/lib.es2021.intl.d.ts","../node_modules/typescript/lib/lib.es2022.array.d.ts","../node_modules/typescript/lib/lib.es2022.error.d.ts","../node_modules/typescript/lib/lib.es2022.intl.d.ts","../node_modules/typescript/lib/lib.es2022.object.d.ts","../node_modules/typescript/lib/lib.es2022.string.d.ts","../node_modules/typescript/lib/lib.es2022.regexp.d.ts","../node_modules/typescript/lib/lib.es2023.array.d.ts","../node_modules/typescript/lib/lib.es2023.collection.d.ts","../node_modules/typescript/lib/lib.es2023.intl.d.ts","../node_modules/typescript/lib/lib.decorators.d.ts","../node_modules/typescript/lib/lib.decorators.legacy.d.ts","../node_modules/typescript/lib/lib.es2023.full.d.ts","../node_modules/reflect-metadata/index.d.ts","../node_modules/@nestjs/common/decorators/core/bind.decorator.d.ts","../node_modules/@nestjs/common/interfaces/abstract.interface.d.ts","../node_modules/@nestjs/common/interfaces/controllers/controller-metadata.interface.d.ts","../node_modules/@nestjs/common/interfaces/controllers/controller.interface.d.ts","../node_modules/@nestjs/common/interfaces/features/arguments-host.interface.d.ts","../node_modules/@nestjs/common/interfaces/exceptions/exception-filter.interface.d.ts","../node_modules/rxjs/dist/types/internal/Subscription.d.ts","../node_modules/rxjs/dist/types/internal/Subscriber.d.ts","../node_modules/rxjs/dist/types/internal/Operator.d.ts","../node_modules/rxjs/dist/types/internal/Observable.d.ts","../node_modules/rxjs/dist/types/internal/types.d.ts","../node_modules/rxjs/dist/types/internal/operators/audit.d.ts","../node_modules/rxjs/dist/types/internal/operators/auditTime.d.ts","../node_modules/rxjs/dist/types/internal/operators/buffer.d.ts","../node_modules/rxjs/dist/types/internal/operators/bufferCount.d.ts","../node_modules/rxjs/dist/types/internal/operators/bufferTime.d.ts","../node_modules/rxjs/dist/types/internal/operators/bufferToggle.d.ts","../node_modules/rxjs/dist/tLine truncated
|
||||
+139
@@ -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
@@ -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
|
||||
@@ -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"}
|
||||
@@ -0,0 +1,2 @@
|
||||
export declare class UserModule {
|
||||
}
|
||||
+25
@@ -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
|
||||
@@ -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"}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
export declare class UserService {
|
||||
private prisma;
|
||||
constructor(prisma: PrismaService);
|
||||
getWatchlist(userId: number): 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: number, productId: number): Promise<{
|
||||
id: number;
|
||||
createdAt: Date;
|
||||
productId: number;
|
||||
userId: number;
|
||||
}>;
|
||||
removeFromWatchlist(userId: number, productId: number): Promise<{
|
||||
id: number;
|
||||
createdAt: Date;
|
||||
productId: number;
|
||||
userId: number;
|
||||
}>;
|
||||
getNotifications(userId: number): 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: number, 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: number, productId: number): Promise<{
|
||||
id: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
productId: number;
|
||||
userId: number;
|
||||
priceThreshold: number | null;
|
||||
percentThreshold: number | null;
|
||||
notifyOnAnyChange: boolean;
|
||||
isActive: boolean;
|
||||
}>;
|
||||
}
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
"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.UserService = void 0;
|
||||
const common_1 = require("@nestjs/common");
|
||||
const prisma_service_1 = require("../prisma/prisma.service");
|
||||
let UserService = class UserService {
|
||||
prisma;
|
||||
constructor(prisma) {
|
||||
this.prisma = prisma;
|
||||
}
|
||||
async getWatchlist(userId) {
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
});
|
||||
if (!user) {
|
||||
throw new common_1.NotFoundException(`User with ID ${userId} not found`);
|
||||
}
|
||||
return this.prisma.watchlistItem.findMany({
|
||||
where: { userId },
|
||||
include: {
|
||||
product: {
|
||||
include: {
|
||||
prices: {
|
||||
orderBy: { lastUpdated: 'desc' },
|
||||
take: 1,
|
||||
include: { source: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
async addToWatchlist(userId, productId) {
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
});
|
||||
if (!user) {
|
||||
throw new common_1.NotFoundException(`User with ID ${userId} not found`);
|
||||
}
|
||||
const product = await this.prisma.product.findUnique({
|
||||
where: { id: productId },
|
||||
});
|
||||
if (!product) {
|
||||
throw new common_1.NotFoundException(`Product with ID ${productId} not found`);
|
||||
}
|
||||
return this.prisma.watchlistItem.upsert({
|
||||
where: {
|
||||
userId_productId: {
|
||||
userId,
|
||||
productId,
|
||||
},
|
||||
},
|
||||
update: {},
|
||||
create: {
|
||||
userId,
|
||||
productId,
|
||||
},
|
||||
});
|
||||
}
|
||||
async removeFromWatchlist(userId, productId) {
|
||||
const watchlistItem = await this.prisma.watchlistItem.findUnique({
|
||||
where: {
|
||||
userId_productId: {
|
||||
userId,
|
||||
productId,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!watchlistItem) {
|
||||
throw new common_1.NotFoundException(`Watchlist item for user ${userId} and product ${productId} not found`);
|
||||
}
|
||||
return this.prisma.watchlistItem.delete({
|
||||
where: {
|
||||
userId_productId: {
|
||||
userId,
|
||||
productId,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
async getNotifications(userId) {
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
});
|
||||
if (!user) {
|
||||
throw new common_1.NotFoundException(`User with ID ${userId} not found`);
|
||||
}
|
||||
return this.prisma.notification.findMany({
|
||||
where: { userId },
|
||||
include: {
|
||||
product: {
|
||||
include: {
|
||||
prices: {
|
||||
orderBy: { lastUpdated: 'desc' },
|
||||
take: 1,
|
||||
include: { source: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
async configureNotification(userId, data) {
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
});
|
||||
if (!user) {
|
||||
throw new common_1.NotFoundException(`User with ID ${userId} not found`);
|
||||
}
|
||||
const product = await this.prisma.product.findUnique({
|
||||
where: { id: data.productId },
|
||||
});
|
||||
if (!product) {
|
||||
throw new common_1.NotFoundException(`Product with ID ${data.productId} not found`);
|
||||
}
|
||||
return this.prisma.notification.upsert({
|
||||
where: {
|
||||
userId_productId: {
|
||||
userId,
|
||||
productId: data.productId,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
priceThreshold: data.priceThreshold,
|
||||
percentThreshold: data.percentThreshold,
|
||||
notifyOnAnyChange: data.notifyOnAnyChange ?? false,
|
||||
isActive: true,
|
||||
},
|
||||
create: {
|
||||
userId,
|
||||
productId: data.productId,
|
||||
priceThreshold: data.priceThreshold,
|
||||
percentThreshold: data.percentThreshold,
|
||||
notifyOnAnyChange: data.notifyOnAnyChange ?? false,
|
||||
},
|
||||
});
|
||||
}
|
||||
async removeNotification(userId, productId) {
|
||||
const notification = await this.prisma.notification.findUnique({
|
||||
where: {
|
||||
userId_productId: {
|
||||
userId,
|
||||
productId,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!notification) {
|
||||
throw new common_1.NotFoundException(`Notification for user ${userId} and product ${productId} not found`);
|
||||
}
|
||||
return this.prisma.notification.delete({
|
||||
where: {
|
||||
userId_productId: {
|
||||
userId,
|
||||
productId,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
exports.UserService = UserService;
|
||||
exports.UserService = UserService = __decorate([
|
||||
(0, common_1.Injectable)(),
|
||||
__metadata("design:paramtypes", [prisma_service_1.PrismaService])
|
||||
], UserService);
|
||||
//# sourceMappingURL=user.service.js.map
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"user.service.js","sourceRoot":"","sources":["../../src/user/user.service.ts"],"names":[],"mappings":"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"}
|
||||
@@ -0,0 +1,34 @@
|
||||
// @ts-check
|
||||
import eslint from '@eslint/js';
|
||||
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
|
||||
import globals from 'globals';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
ignores: ['eslint.config.mjs'],
|
||||
},
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
eslintPluginPrettierRecommended,
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.node,
|
||||
...globals.jest,
|
||||
},
|
||||
sourceType: 'commonjs',
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-floating-promises': 'warn',
|
||||
'@typescript-eslint/no-unsafe-argument': 'warn'
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
||||
Generated
+12753
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,81 @@
|
||||
{
|
||||
"name": "price-compare-api",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
"license": "UNLICENSED",
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"start": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/axios": "^4.0.0",
|
||||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/schedule": "^6.0.0",
|
||||
"@prisma/client": "^6.6.0",
|
||||
"axios": "^1.8.4",
|
||||
"cheerio": "^1.0.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "^9.18.0",
|
||||
"@nestjs/cli": "^11.0.0",
|
||||
"@nestjs/schematics": "^11.0.0",
|
||||
"@nestjs/testing": "^11.0.1",
|
||||
"@swc/cli": "^0.6.0",
|
||||
"@swc/core": "^1.10.7",
|
||||
"@types/cheerio": "^0.22.35",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-prettier": "^5.2.2",
|
||||
"globals": "^16.0.0",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.4.2",
|
||||
"prisma": "^6.6.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^7.0.0",
|
||||
"ts-jest": "^29.2.5",
|
||||
"ts-loader": "^9.5.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.20.0"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"testRegex": ".*\\.spec\\.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"collectCoverageFrom": [
|
||||
"**/*.(t|j)s"
|
||||
],
|
||||
"coverageDirectory": "../coverage",
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Product" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"category" TEXT NOT NULL,
|
||||
"availability" BOOLEAN NOT NULL DEFAULT true,
|
||||
"sourceProductId" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"sourceId" INTEGER NOT NULL,
|
||||
|
||||
CONSTRAINT "Product_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Price" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"productId" INTEGER NOT NULL,
|
||||
"sourceId" INTEGER NOT NULL,
|
||||
"regularPrice" DOUBLE PRECISION NOT NULL,
|
||||
"discountedPrice" DOUBLE PRECISION,
|
||||
"discountPercentage" DOUBLE PRECISION,
|
||||
"unitPrice" TEXT,
|
||||
"promotionType" TEXT,
|
||||
"promotionStart" TIMESTAMP(3),
|
||||
"promotionEnd" TIMESTAMP(3),
|
||||
"vatIncluded" BOOLEAN NOT NULL DEFAULT true,
|
||||
"lastUpdated" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "Price_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Source" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"url" TEXT NOT NULL,
|
||||
"logo" TEXT,
|
||||
"lastScraped" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Source_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Product_name_sourceId_key" ON "Product"("name", "sourceId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Source_name_key" ON "Source"("name");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Price" ADD CONSTRAINT "Price_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Price" ADD CONSTRAINT "Price_sourceId_fkey" FOREIGN KEY ("sourceId") REFERENCES "Source"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,56 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"email" TEXT NOT NULL,
|
||||
"name" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "WatchlistItem" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"userId" INTEGER NOT NULL,
|
||||
"productId" INTEGER NOT NULL,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "WatchlistItem_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Notification" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"userId" INTEGER NOT NULL,
|
||||
"productId" INTEGER NOT NULL,
|
||||
"priceThreshold" DOUBLE PRECISION,
|
||||
"percentThreshold" DOUBLE PRECISION,
|
||||
"notifyOnAnyChange" BOOLEAN NOT NULL DEFAULT false,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Notification_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "WatchlistItem_userId_productId_key" ON "WatchlistItem"("userId", "productId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Notification_userId_productId_key" ON "Notification"("userId", "productId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "WatchlistItem" ADD CONSTRAINT "WatchlistItem_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "WatchlistItem" ADD CONSTRAINT "WatchlistItem_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Notification" ADD CONSTRAINT "Notification_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Notification" ADD CONSTRAINT "Notification_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
@@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "postgresql"
|
||||
@@ -0,0 +1,92 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
directUrl = env("DIRECT_URL")
|
||||
}
|
||||
|
||||
model Product {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
description String?
|
||||
category String
|
||||
availability Boolean @default(true)
|
||||
sourceProductId String? // Original ID from the source
|
||||
prices Price[]
|
||||
watchlistItems WatchlistItem[]
|
||||
notifications Notification[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
sourceId Int // Added for the composite unique constraint
|
||||
|
||||
@@unique([name, sourceId])
|
||||
}
|
||||
|
||||
model Price {
|
||||
id Int @id @default(autoincrement())
|
||||
productId Int
|
||||
product Product @relation(fields: [productId], references: [id])
|
||||
sourceId Int
|
||||
source Source @relation(fields: [sourceId], references: [id])
|
||||
regularPrice Float
|
||||
discountedPrice Float?
|
||||
discountPercentage Float?
|
||||
unitPrice String?
|
||||
promotionType String?
|
||||
promotionStart DateTime?
|
||||
promotionEnd DateTime?
|
||||
vatIncluded Boolean @default(true)
|
||||
lastUpdated DateTime @default(now())
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model Source {
|
||||
id Int @id @default(autoincrement())
|
||||
name String @unique
|
||||
url String
|
||||
logo String?
|
||||
lastScraped DateTime?
|
||||
prices Price[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model User {
|
||||
id Int @id @default(autoincrement())
|
||||
email String @unique
|
||||
name String?
|
||||
watchlist WatchlistItem[]
|
||||
notifications Notification[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model WatchlistItem {
|
||||
id Int @id @default(autoincrement())
|
||||
userId Int
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
productId Int
|
||||
product Product @relation(fields: [productId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@unique([userId, productId])
|
||||
}
|
||||
|
||||
model Notification {
|
||||
id Int @id @default(autoincrement())
|
||||
userId Int
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
productId Int
|
||||
product Product @relation(fields: [productId], references: [id])
|
||||
priceThreshold Float?
|
||||
percentThreshold Float?
|
||||
notifyOnAnyChange Boolean @default(false)
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([userId, productId])
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
|
||||
describe('AppController', () => {
|
||||
let appController: AppController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const app: TestingModule = await Test.createTestingModule({
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
}).compile();
|
||||
|
||||
appController = app.get<AppController>(AppController);
|
||||
});
|
||||
|
||||
describe('root', () => {
|
||||
it('should return "Hello World!"', () => {
|
||||
expect(appController.getHello()).toBe('Hello World!');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Controller, Get, Param, ParseIntPipe } from '@nestjs/common';
|
||||
import { AppService } from './app.service';
|
||||
import { ScraperService } from './scraper/scraper.service';
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
constructor(
|
||||
private readonly appService: AppService,
|
||||
private readonly scraperService: ScraperService,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
getHello(): string {
|
||||
return this.appService.getHello();
|
||||
}
|
||||
|
||||
@Get('scrape/:sourceId')
|
||||
async triggerScrape(@Param('sourceId', ParseIntPipe) sourceId: number) {
|
||||
return this.scraperService.manualScrape(sourceId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { PrismaModule } from './prisma/prisma.module';
|
||||
import { ProductModule } from './product/product.module';
|
||||
import { ScraperModule } from './scraper/scraper.module';
|
||||
import { PriceModule } from './price/price.module';
|
||||
import { SourceModule } from './source/source.module';
|
||||
import { UserModule } from './user/user.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
}),
|
||||
ScheduleModule.forRoot(),
|
||||
PrismaModule,
|
||||
ProductModule,
|
||||
ScraperModule,
|
||||
PriceModule,
|
||||
SourceModule,
|
||||
UserModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [AppService],
|
||||
})
|
||||
export class AppModule {}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
getHello(): string {
|
||||
return 'Hello World!';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
// Array of sources to create/update
|
||||
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',
|
||||
},
|
||||
|
||||
// Add more sources as needed
|
||||
];
|
||||
|
||||
// Upsert all sources
|
||||
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();
|
||||
@@ -0,0 +1,16 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
// Enable CORS for frontend application
|
||||
app.enableCors({
|
||||
origin: true, // Allow requests from any origin
|
||||
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
|
||||
credentials: true,
|
||||
});
|
||||
|
||||
await app.listen(3001);
|
||||
}
|
||||
bootstrap();
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Controller, Get, Param, Query } from '@nestjs/common';
|
||||
import { PriceService } from './price.service';
|
||||
|
||||
@Controller('prices')
|
||||
export class PriceController {
|
||||
constructor(private readonly priceService: PriceService) {}
|
||||
|
||||
@Get('product/:id')
|
||||
async getPricesForProduct(@Param('id') id: string) {
|
||||
return this.priceService.getPricesForProduct(Number(id));
|
||||
}
|
||||
|
||||
@Get('compare/:ids')
|
||||
async comparePrices(@Param('ids') ids: string) {
|
||||
const productIds = ids.split(',').map(Number);
|
||||
return this.priceService.comparePrices(productIds);
|
||||
}
|
||||
|
||||
@Get('history/:id')
|
||||
async getPriceHistory(@Param('id') id: string, @Query('days') days?: string) {
|
||||
return this.priceService.getPriceHistory(
|
||||
Number(id),
|
||||
days ? Number(days) : 30,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PriceController } from './price.controller';
|
||||
import { PriceService } from './price.service';
|
||||
import { PrismaModule } from '../prisma/prisma.module';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
controllers: [PriceController],
|
||||
providers: [PriceService],
|
||||
exports: [PriceService],
|
||||
})
|
||||
export class PriceModule {}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
|
||||
@Injectable()
|
||||
export class PriceService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
async getPricesForProduct(productId: number) {
|
||||
return this.prisma.price.findMany({
|
||||
where: { productId },
|
||||
include: { source: true },
|
||||
orderBy: { lastUpdated: 'desc' },
|
||||
});
|
||||
}
|
||||
|
||||
async comparePrices(productIds: number[]) {
|
||||
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: number, days: number = 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' },
|
||||
});
|
||||
|
||||
// Group prices by date for charting
|
||||
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,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PrismaService } from './prisma.service';
|
||||
|
||||
@Module({
|
||||
providers: [PrismaService],
|
||||
exports: [PrismaService],
|
||||
})
|
||||
export class PrismaModule {}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
@Injectable()
|
||||
export class PrismaService
|
||||
extends PrismaClient
|
||||
implements OnModuleInit, OnModuleDestroy
|
||||
{
|
||||
async onModuleInit() {
|
||||
await this.$connect();
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
await this.$disconnect();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Controller, Get, Post, Put, Body, Param, Query } from '@nestjs/common';
|
||||
import { ProductService } from './product.service';
|
||||
import { Prisma } from '@prisma/client';
|
||||
|
||||
@Controller('products')
|
||||
export class ProductController {
|
||||
constructor(private readonly productService: ProductService) {}
|
||||
|
||||
@Get(':id')
|
||||
async getProduct(@Param('id') id: string) {
|
||||
return this.productService.getProduct(Number(id));
|
||||
}
|
||||
|
||||
@Get()
|
||||
async getProducts(
|
||||
@Query('skip') skip?: string,
|
||||
@Query('take') take?: string,
|
||||
@Query('search') search?: string,
|
||||
@Query('maxPrice') maxPrice?: string,
|
||||
) {
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
@Post()
|
||||
async createProduct(@Body() data: Prisma.ProductCreateInput) {
|
||||
return this.productService.createProduct(data);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
async updateProduct(
|
||||
@Param('id') id: string,
|
||||
@Body() data: Prisma.ProductUpdateInput,
|
||||
) {
|
||||
return this.productService.updateProduct({
|
||||
where: { id: Number(id) },
|
||||
data,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PrismaModule } from '../prisma/prisma.module';
|
||||
import { ProductController } from './product.controller';
|
||||
import { ProductService } from './product.service';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
controllers: [ProductController],
|
||||
providers: [ProductService],
|
||||
})
|
||||
export class ProductModule {}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { Prisma } from '@prisma/client';
|
||||
|
||||
@Injectable()
|
||||
export class ProductService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
async getProduct(id: number) {
|
||||
return this.prisma.product.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
prices: {
|
||||
include: { source: true },
|
||||
orderBy: { lastUpdated: 'desc' },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getProducts(params: {
|
||||
skip?: number;
|
||||
take?: number;
|
||||
cursor?: Prisma.ProductWhereUniqueInput;
|
||||
where?: Prisma.ProductWhereInput;
|
||||
orderBy?: Prisma.ProductOrderByWithRelationInput;
|
||||
}) {
|
||||
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: string) {
|
||||
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: Prisma.ProductCreateInput) {
|
||||
return this.prisma.product.create({
|
||||
data,
|
||||
include: {
|
||||
prices: {
|
||||
include: { source: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async updateProduct(params: {
|
||||
where: Prisma.ProductWhereUniqueInput;
|
||||
data: Prisma.ProductUpdateInput;
|
||||
}) {
|
||||
const { where, data } = params;
|
||||
return this.prisma.product.update({
|
||||
data,
|
||||
where,
|
||||
include: {
|
||||
prices: {
|
||||
include: { source: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
import { ScheduleModule } from '@nestjs/schedule';
|
||||
import { ScraperService } from './scraper.service';
|
||||
import { PrismaModule } from '../prisma/prisma.module';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, HttpModule, ScheduleModule.forRoot()],
|
||||
providers: [ScraperService],
|
||||
exports: [ScraperService],
|
||||
})
|
||||
export class ScraperModule {}
|
||||
@@ -0,0 +1,275 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||
import * as cheerio from 'cheerio';
|
||||
import axios, { AxiosRequestConfig } from 'axios';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
interface ScrapedProduct {
|
||||
name: string;
|
||||
regularPrice: number;
|
||||
unitPrice: string | null;
|
||||
availability: boolean;
|
||||
description: string;
|
||||
category: string;
|
||||
discountedPrice: number | null;
|
||||
discountPercentage: number | null;
|
||||
promotionType: string | null;
|
||||
promotionStart: Date | null;
|
||||
promotionEnd: Date | null;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ScraperService {
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
private config: ConfigService,
|
||||
) {}
|
||||
private readonly logger = new Logger(ScraperService.name);
|
||||
|
||||
private parsePrice(price: string): number {
|
||||
const cleanPrice = price.replace(/[^\d.]/g, '');
|
||||
return cleanPrice ? parseFloat(cleanPrice) : 0;
|
||||
}
|
||||
|
||||
private parseDate(dateStr: string): Date | null {
|
||||
try {
|
||||
const [day, month, year] = dateStr.split('/').map(Number);
|
||||
return new Date(year, month - 1, day);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private parseDateRange(dateRange: string): {
|
||||
start: Date | null;
|
||||
end: Date | null;
|
||||
} {
|
||||
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 };
|
||||
}
|
||||
}
|
||||
|
||||
private getTableSelector(sourceId: number): string {
|
||||
switch (sourceId) {
|
||||
case 2: // Vero
|
||||
return 'table:eq(2)';
|
||||
case 12: // DIM
|
||||
// DIM shows its products in an embedded iframe on dim.marketceni.mk
|
||||
return '#product-table'; // Target the product table directly by ID
|
||||
default:
|
||||
throw new Error(`Unsupported source ID: ${sourceId}`);
|
||||
}
|
||||
}
|
||||
|
||||
private calculateDiscountPercentage(
|
||||
regularPrice: number,
|
||||
discountedPrice: number | null,
|
||||
): number | null {
|
||||
if (!discountedPrice || !regularPrice) return null;
|
||||
return Math.round(((regularPrice - discountedPrice) / regularPrice) * 100);
|
||||
}
|
||||
|
||||
private parseProductRow(
|
||||
$: ReturnType<typeof cheerio.load>,
|
||||
rowElement: cheerio.Element,
|
||||
sourceId: number,
|
||||
): ScrapedProduct {
|
||||
const cells = $(rowElement).find('td');
|
||||
|
||||
const getText = (index: number): string => {
|
||||
const cell = cells.eq(index);
|
||||
return cell.text().trim();
|
||||
};
|
||||
|
||||
if (sourceId === 2) {
|
||||
// Vero parsing logic
|
||||
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) {
|
||||
// DIM specific parsing logic
|
||||
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}`);
|
||||
}
|
||||
|
||||
@Cron(CronExpression.EVERY_HOUR)
|
||||
async scrapeAllSources(): Promise<void> {
|
||||
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: string, sourceId: number): Promise<void> {
|
||||
const config: AxiosRequestConfig = {
|
||||
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.get<string>(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); // Skip header row
|
||||
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: unknown) {
|
||||
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: unknown) {
|
||||
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: number): Promise<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Controller, Get, Param, Query } from '@nestjs/common';
|
||||
import { SourceService } from './source.service';
|
||||
|
||||
@Controller('sources')
|
||||
export class SourceController {
|
||||
constructor(private readonly sourceService: SourceService) {}
|
||||
|
||||
@Get()
|
||||
async getSources() {
|
||||
return this.sourceService.getSources();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
async getSource(@Param('id') id: string) {
|
||||
return this.sourceService.getSource(Number(id));
|
||||
}
|
||||
|
||||
@Get(':id/products')
|
||||
async getProductsFromSource(
|
||||
@Param('id') id: string,
|
||||
@Query('skip') skip?: string,
|
||||
@Query('take') take?: string,
|
||||
) {
|
||||
return this.sourceService.getProductsFromSource(
|
||||
Number(id),
|
||||
skip ? Number(skip) : undefined,
|
||||
take ? Number(take) : undefined,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { SourceController } from './source.controller';
|
||||
import { SourceService } from './source.service';
|
||||
import { PrismaModule } from '../prisma/prisma.module';
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
controllers: [SourceController],
|
||||
providers: [SourceService],
|
||||
exports: [SourceService],
|
||||
})
|
||||
export class SourceModule {}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
|
||||
@Injectable()
|
||||
export class SourceService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
async getSources() {
|
||||
return this.prisma.source.findMany({
|
||||
orderBy: { name: 'asc' },
|
||||
});
|
||||
}
|
||||
|
||||
async getSource(id: number) {
|
||||
return this.prisma.source.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
}
|
||||
|
||||
async getProductsFromSource(sourceId: number, skip?: number, take?: number) {
|
||||
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' },
|
||||
});
|
||||
|
||||
// Count total products for pagination
|
||||
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,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
Loaded 100 of 140 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user