update server
This commit is contained in:
parent
805a6361d7
commit
35d407cb4e
|
|
@ -1 +0,0 @@
|
||||||
BASE_URL = 'https://int.ipsupply.com.au/api/'
|
|
||||||
|
|
@ -1,30 +1,56 @@
|
||||||
|
# compiled output
|
||||||
|
/dist
|
||||||
|
/node_modules
|
||||||
|
/build
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
|
pnpm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
pnpm-debug.log*
|
|
||||||
lerna-debug.log*
|
lerna-debug.log*
|
||||||
|
|
||||||
node_modules
|
# OS
|
||||||
dist
|
|
||||||
dist-content
|
|
||||||
dist-ssr
|
|
||||||
auto-post-facebook-extensions
|
|
||||||
*.local
|
|
||||||
|
|
||||||
# Editor directories and files
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/extensions.json
|
|
||||||
.idea
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.suo
|
|
||||||
*.ntvs*
|
# Tests
|
||||||
*.njsproj
|
/coverage
|
||||||
*.sln
|
/.nyc_output
|
||||||
*.sw?
|
|
||||||
|
# IDEs and editors
|
||||||
|
/.idea
|
||||||
|
.project
|
||||||
|
.classpath
|
||||||
|
.c9/
|
||||||
|
*.launch
|
||||||
|
.settings/
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
# IDE - VSCode
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
.env
|
.env
|
||||||
product-cache*
|
.env.development.local
|
||||||
meta.json
|
.env.test.local
|
||||||
data.json
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# temp directory
|
||||||
|
.temp
|
||||||
|
.tmp
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
|
||||||
|
|
@ -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).
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
[
|
|
||||||
{
|
|
||||||
"title": "Cisco AIR-AP1542D-Z-K9 802.11ac W2 Value Outdoor AP, Direct. Ant, Z Reg Dom.",
|
|
||||||
"price": 2563,
|
|
||||||
"el": {},
|
|
||||||
"date": "2025-08-27T06:04:03.253Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Cisco A9K-8T-B 8 Port 10GE Line Card Requires XFPs ASR 9010 Fully Tested",
|
|
||||||
"price": 850,
|
|
||||||
"el": {},
|
|
||||||
"date": "2025-08-27T06:04:03.253Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Brand New Cisco C9130AXI-Z 802.11ac 9130AX Wireless Access Poiny Z Domain Reg",
|
|
||||||
"price": 975,
|
|
||||||
"el": {},
|
|
||||||
"date": "2025-08-27T06:04:03.253Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Dell PowerEdge R715 32 Core Server AMD Opteron 128Gb Ram 2x 300Gb HDD TaxInv - R715, SERVER",
|
|
||||||
"price": 672,
|
|
||||||
"el": {},
|
|
||||||
"date": "2025-08-27T06:04:03.253Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "CISCO2911-V/K9 Router UC License + PVDM3-32 Latest IOS SPA.155-3.M4a 1YrWty",
|
|
||||||
"price": 125,
|
|
||||||
"el": {},
|
|
||||||
"date": "2025-08-27T06:04:03.253Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "HP DL380 G9 2U Server 2x CPU E5-2650V3 32Gb RAM 600Gb HDD Dual PSU Rail Kit - DL380 G9, server",
|
|
||||||
"price": 2245,
|
|
||||||
"el": {},
|
|
||||||
"date": "2025-08-27T06:04:03.253Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Cisco AIR-PWRINJ6 Power Injector 8023at for Aironet Access Points",
|
|
||||||
"price": 95,
|
|
||||||
"el": {},
|
|
||||||
"date": "2025-08-27T06:04:03.253Z"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
module.exports = {
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
parserOptions: {
|
||||||
|
project: 'tsconfig.json',
|
||||||
|
tsconfigRootDir: __dirname,
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
plugins: ['@typescript-eslint/eslint-plugin'],
|
||||||
|
extends: [
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:prettier/recommended',
|
||||||
|
],
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
jest: true,
|
||||||
|
},
|
||||||
|
ignorePatterns: ['.eslintrc.js'],
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/interface-name-prefix': 'off',
|
||||||
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
|
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
},
|
||||||
|
'prettier/prettier': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
endOfLine: 'auto',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/nest-cli",
|
||||||
|
"collection": "@nestjs/schematics",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"compilerOptions": {
|
||||||
|
"deleteOutDir": true
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,21 +1,85 @@
|
||||||
{
|
{
|
||||||
"name": "server",
|
"name": "server-v2",
|
||||||
"version": "1.0.0",
|
"version": "0.0.1",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
|
||||||
"scripts": {
|
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
|
||||||
"dev": "node server.js"
|
|
||||||
},
|
|
||||||
"keywords": [],
|
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"private": true,
|
||||||
"type": "module",
|
"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": {
|
"dependencies": {
|
||||||
"axios": "^1.11.0",
|
"@nestjs/common": "^11.0.1",
|
||||||
"cors": "^2.8.5",
|
"@nestjs/config": "^4.0.2",
|
||||||
"dotenv": "^17.2.1",
|
"@nestjs/core": "^11.0.1",
|
||||||
"express": "^5.1.0",
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
"lodash": "^4.17.21"
|
"@nestjs/schedule": "^6.0.1",
|
||||||
|
"@nestjs/typeorm": "^11.0.0",
|
||||||
|
"axios": "^1.12.2",
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
|
"class-validator": "^0.14.2",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"mysql2": "^3.15.1",
|
||||||
|
"nestjs-paginate": "^12.8.3",
|
||||||
|
"node-cache": "^5.1.2",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
|
"rxjs": "^7.8.1",
|
||||||
|
"typeorm": "^0.3.27"
|
||||||
|
},
|
||||||
|
"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/cron": "^2.0.1",
|
||||||
|
"@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",
|
||||||
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1,4 +1,4 @@
|
||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
|
|
@ -44,10 +44,19 @@
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto,
|
font-family:
|
||||||
Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji";
|
ui-sans-serif,
|
||||||
|
system-ui,
|
||||||
|
-apple-system,
|
||||||
|
Segoe UI,
|
||||||
|
Roboto,
|
||||||
|
Helvetica,
|
||||||
|
Arial,
|
||||||
|
'Apple Color Emoji',
|
||||||
|
'Segoe UI Emoji';
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
background: radial-gradient(
|
background:
|
||||||
|
radial-gradient(
|
||||||
1200px 800px at 80% -10%,
|
1200px 800px at 80% -10%,
|
||||||
rgba(106, 166, 255, 0.18),
|
rgba(106, 166, 255, 0.18),
|
||||||
transparent 60%
|
transparent 60%
|
||||||
|
|
@ -76,7 +85,7 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.header::after {
|
.header::after {
|
||||||
content: "";
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: -1px;
|
inset: -1px;
|
||||||
border-radius: inherit;
|
border-radius: inherit;
|
||||||
|
|
@ -158,8 +167,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.kbd {
|
.kbd {
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
font-family:
|
||||||
"Liberation Mono", "Courier New", monospace;
|
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||||
|
'Liberation Mono', 'Courier New', monospace;
|
||||||
background: rgba(255, 255, 255, 0.06);
|
background: rgba(255, 255, 255, 0.06);
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-bottom-width: 2px;
|
border-bottom-width: 2px;
|
||||||
|
|
@ -207,7 +217,7 @@
|
||||||
<time datetime="2025-08-25">August 25, 2025</time>
|
<time datetime="2025-08-25">August 25, 2025</time>
|
||||||
</div>
|
</div>
|
||||||
<div>•</div>
|
<div>•</div>
|
||||||
<div><span class="kbd">Version 1.3</span></div>
|
<div><span class="kbd">Version 2.0</span></div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit e7fd818c75881047115599e0227d093b4a460f4e
|
|
||||||
211
server/server.js
211
server/server.js
|
|
@ -1,211 +0,0 @@
|
||||||
import express from "express";
|
|
||||||
import fs from "fs";
|
|
||||||
import bodyParser from "body-parser";
|
|
||||||
import cors from "cors";
|
|
||||||
import axios from "axios";
|
|
||||||
import path from "path";
|
|
||||||
import _ from "lodash";
|
|
||||||
|
|
||||||
import dotenv from "dotenv";
|
|
||||||
|
|
||||||
dotenv.config();
|
|
||||||
|
|
||||||
const metaPath = path.join(process.cwd(), "meta.json");
|
|
||||||
const dataFile = "data.json";
|
|
||||||
const cachePrefix = "product-cache-";
|
|
||||||
const productsFile = path.join(process.cwd(), "products.json");
|
|
||||||
const app = express();
|
|
||||||
|
|
||||||
// Cho phép tất cả origin gọi API
|
|
||||||
app.use(cors());
|
|
||||||
|
|
||||||
app.use(express.json({ limit: "10mb" }));
|
|
||||||
// parse JSON body
|
|
||||||
app.use(bodyParser.json());
|
|
||||||
|
|
||||||
// POST thêm product (tự động update nếu id đã tồn tại)
|
|
||||||
app.post("/products", (req, res) => {
|
|
||||||
const { id, ...rest } = req.body;
|
|
||||||
if (!id) return res.status(400).json({ error: "Product must have an id" });
|
|
||||||
|
|
||||||
// Đọc dữ liệu cũ (nếu có)
|
|
||||||
let data = {};
|
|
||||||
if (fs.existsSync(productsFile)) {
|
|
||||||
data = JSON.parse(fs.readFileSync(productsFile, "utf8") || "{}");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Thêm / ghi đè theo id
|
|
||||||
data[id] = { id, ...rest };
|
|
||||||
|
|
||||||
// Ghi lại file
|
|
||||||
fs.writeFileSync(productsFile, JSON.stringify(data, null, 2));
|
|
||||||
|
|
||||||
res.json({ success: true, product: data[id] });
|
|
||||||
});
|
|
||||||
|
|
||||||
// GET product theo id
|
|
||||||
app.get("/products/:id", (req, res) => {
|
|
||||||
if (!fs.existsSync(productsFile)) return res.json({});
|
|
||||||
const data = JSON.parse(fs.readFileSync(productsFile, "utf8") || "{}");
|
|
||||||
res.json(data[req.params.id] || null);
|
|
||||||
});
|
|
||||||
|
|
||||||
// API lưu dữ liệu
|
|
||||||
app.post("/sync", (req, res) => {
|
|
||||||
const data = req.body;
|
|
||||||
|
|
||||||
if (!Array.isArray(data)) {
|
|
||||||
return res.status(400).json({ error: "Dữ liệu phải là array" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chuẩn hoá dữ liệu (có thêm date)
|
|
||||||
const mapped = data.map((item) => ({
|
|
||||||
...item,
|
|
||||||
date: new Date().toISOString(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Đọc dữ liệu cũ
|
|
||||||
let oldData = [];
|
|
||||||
try {
|
|
||||||
oldData = JSON.parse(fs.readFileSync(dataFile, "utf-8"));
|
|
||||||
} catch (e) {
|
|
||||||
oldData = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// So sánh (bỏ qua field date vì nó luôn khác)
|
|
||||||
const stripDate = (arr) => arr.map(({ date, ...rest }) => rest);
|
|
||||||
|
|
||||||
const oldStripped = stripDate(oldData);
|
|
||||||
const newStripped = stripDate(mapped);
|
|
||||||
|
|
||||||
if (!_.isEqual(oldStripped, newStripped)) {
|
|
||||||
// Nếu khác → xoá cache
|
|
||||||
const files = fs.readdirSync(".");
|
|
||||||
files.forEach((file) => {
|
|
||||||
if (file.startsWith(cachePrefix)) {
|
|
||||||
fs.unlinkSync(path.join(".", file));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
console.log("Dữ liệu thay đổi → xoá cache");
|
|
||||||
} else {
|
|
||||||
console.log("Dữ liệu không đổi → giữ cache");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ghi dữ liệu mới
|
|
||||||
fs.writeFileSync(dataFile, JSON.stringify(mapped, null, 2));
|
|
||||||
|
|
||||||
res.json({ message: "Đã lưu dữ liệu thành công", saved: mapped.length });
|
|
||||||
});
|
|
||||||
|
|
||||||
// API đọc lại dữ liệu
|
|
||||||
app.get("/data", (req, res) => {
|
|
||||||
if (!fs.existsSync("data.json")) {
|
|
||||||
return res.json([]);
|
|
||||||
}
|
|
||||||
const content = fs.readFileSync("data.json", "utf-8");
|
|
||||||
res.json(JSON.parse(content));
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post("/", async (req, res) => {
|
|
||||||
try {
|
|
||||||
// ===== Helpers =====
|
|
||||||
const readJSON = (path, fallback = null) => {
|
|
||||||
try {
|
|
||||||
return JSON.parse(fs.readFileSync(path, "utf-8"));
|
|
||||||
} catch {
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const writeJSON = (path, data) => {
|
|
||||||
try {
|
|
||||||
fs.writeFileSync(path, JSON.stringify(data, null, 2));
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("Không ghi được file:", e.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ===== Meta =====
|
|
||||||
let meta = readJSON(metaPath, { total: 4000 });
|
|
||||||
|
|
||||||
const { info, ...filter } = req.body?.filter || {};
|
|
||||||
const originalFilter = { ...filter };
|
|
||||||
const status = filter?.where?.status_listing;
|
|
||||||
|
|
||||||
if (["listed", "unlisted"].includes(status)) {
|
|
||||||
filter.skip = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Cache check =====
|
|
||||||
const cacheFile = `${cachePrefix}${JSON.stringify(originalFilter)}.json`;
|
|
||||||
if (fs.existsSync(cacheFile)) {
|
|
||||||
return res.json(readJSON(cacheFile, {}));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Load publisted data nếu cần =====
|
|
||||||
let publistedData = [];
|
|
||||||
if (status && meta.total) {
|
|
||||||
filter.limit = meta.total;
|
|
||||||
publistedData = readJSON("data.json", []);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Call API gốc =====
|
|
||||||
const { data } = await axios({
|
|
||||||
headers: { Authorization: req.headers.authorization },
|
|
||||||
url: "transferGetData",
|
|
||||||
baseURL: process.env.BASE_URL,
|
|
||||||
method: "POST",
|
|
||||||
data: { ...req.body, filter },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update meta
|
|
||||||
if (typeof data.total === "number") {
|
|
||||||
writeJSON(metaPath, { total: data.total });
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Xử lý listed/unlisted =====
|
|
||||||
if (status === "listed" || status === "unlisted") {
|
|
||||||
const skip = originalFilter.skip || 0;
|
|
||||||
const limit = originalFilter.limit || data.data.length;
|
|
||||||
|
|
||||||
let filteredData = data.data.filter((item) => {
|
|
||||||
const key = item?.title.includes(item.code)
|
|
||||||
? `${item.title}`.toLowerCase()
|
|
||||||
: `${item.title} - ${item.code}`.toLowerCase();
|
|
||||||
|
|
||||||
if (status === "listed") {
|
|
||||||
return publistedData.some(
|
|
||||||
(i) =>
|
|
||||||
`${i.title}`.toLowerCase().includes(key) &&
|
|
||||||
i?.url_info == info?.url
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return !publistedData.some(
|
|
||||||
(i) =>
|
|
||||||
`${i.title}`.toLowerCase().includes(key) &&
|
|
||||||
i?.url_info == info?.url
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
data.total = filteredData.length;
|
|
||||||
data.data = filteredData.slice(skip, skip + limit);
|
|
||||||
data.filter = { ...originalFilter, skip, limit };
|
|
||||||
|
|
||||||
writeJSON(cacheFile, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json(data);
|
|
||||||
} catch (err) {
|
|
||||||
console.error("API error:", err.message);
|
|
||||||
res.status(500).json({ error: err.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Route trả về privacy policy
|
|
||||||
app.get("/policy", (req, res) => {
|
|
||||||
res.sendFile(path.join(process.cwd(), "views", "policy.html"));
|
|
||||||
});
|
|
||||||
|
|
||||||
app.listen(3000, () => {
|
|
||||||
console.log("Server chạy tại http://localhost:3000");
|
|
||||||
});
|
|
||||||
|
|
@ -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,12 @@
|
||||||
|
import { Controller, Get } from '@nestjs/common';
|
||||||
|
import { AppService } from './app.service';
|
||||||
|
|
||||||
|
@Controller()
|
||||||
|
export class AppController {
|
||||||
|
constructor(private readonly appService: AppService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
getHello(): string {
|
||||||
|
return this.appService.getHello();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { AppController } from './app.controller';
|
||||||
|
import { AppService } from './app.service';
|
||||||
|
import { CacheModule } from './modules/cache/cache.module';
|
||||||
|
import { ProductsModule } from './modules/products/products.module';
|
||||||
|
import { SyncModule } from './modules/sync/sync.module';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
|
import { ScheduleModule } from '@nestjs/schedule';
|
||||||
|
import { TaskModule } from './modules/task/task.module';
|
||||||
|
import { PolicyModule } from './modules/policy/policy.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
isGlobal: true,
|
||||||
|
}),
|
||||||
|
CacheModule,
|
||||||
|
ProductsModule,
|
||||||
|
TaskModule,
|
||||||
|
SyncModule,
|
||||||
|
TypeOrmModule.forRootAsync({
|
||||||
|
imports: [ConfigModule],
|
||||||
|
inject: [ConfigService],
|
||||||
|
useFactory: (config: ConfigService) => ({
|
||||||
|
type: 'mysql',
|
||||||
|
host: config.get<string>('DB_HOST'),
|
||||||
|
port: config.get<number>('DB_PORT'),
|
||||||
|
username: config.get<string>('DB_USER'),
|
||||||
|
password: config.get<string>('DB_PASS'),
|
||||||
|
database: config.get<string>('DB_NAME'),
|
||||||
|
autoLoadEntities: true,
|
||||||
|
synchronize: true, // chỉ bật dev
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
ScheduleModule.forRoot(),
|
||||||
|
PolicyModule,
|
||||||
|
],
|
||||||
|
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,26 @@
|
||||||
|
import { EConfig } from 'src/system/enum/config-enum';
|
||||||
|
import {
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
|
||||||
|
@Entity('configs')
|
||||||
|
export class Config {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ type: 'enum', enum: EConfig, unique: true })
|
||||||
|
key_name: EConfig;
|
||||||
|
|
||||||
|
@Column({ type: 'json' })
|
||||||
|
value: string;
|
||||||
|
|
||||||
|
@CreateDateColumn({ type: 'timestamp', name: 'created_at' })
|
||||||
|
created_at: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn({ type: 'timestamp', name: 'updated_at' })
|
||||||
|
updated_at: Date;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
import {
|
||||||
|
BeforeInsert,
|
||||||
|
BeforeUpdate,
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
Entity,
|
||||||
|
JoinColumn,
|
||||||
|
ManyToOne,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Unique,
|
||||||
|
UpdateDateColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { Product } from './product.entity';
|
||||||
|
import * as crypto from 'crypto';
|
||||||
|
|
||||||
|
@Entity('product_listed')
|
||||||
|
@Unique(['product', 'info_hash'])
|
||||||
|
export class ProductListed {
|
||||||
|
@PrimaryGeneratedColumn('increment')
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@ManyToOne(() => Product, (product) => product.listedProducts, {
|
||||||
|
onDelete: 'CASCADE',
|
||||||
|
})
|
||||||
|
@JoinColumn({ name: 'product_id' })
|
||||||
|
product: Product;
|
||||||
|
|
||||||
|
@Column({ type: 'json', nullable: true })
|
||||||
|
info: Record<string, any>;
|
||||||
|
|
||||||
|
// Cho phép null tạm, vì sẽ được fill tự động
|
||||||
|
@Column({ type: 'varchar', length: 255, nullable: true, default: null })
|
||||||
|
info_hash: string | null;
|
||||||
|
|
||||||
|
@CreateDateColumn({ type: 'timestamp', name: 'created_at' })
|
||||||
|
created_at: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn({ type: 'timestamp', name: 'updated_at' })
|
||||||
|
updated_at: Date;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
PrimaryColumn,
|
||||||
|
OneToOne,
|
||||||
|
OneToMany,
|
||||||
|
} from 'typeorm';
|
||||||
|
import { ProductListed } from './product-listed.entity';
|
||||||
|
|
||||||
|
@Entity('products')
|
||||||
|
export class Product {
|
||||||
|
@PrimaryColumn('varchar')
|
||||||
|
ebayListingId: string;
|
||||||
|
|
||||||
|
@Column('varchar', { nullable: true, default: null })
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
code: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true, default: null })
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@Column('simple-array', { nullable: true, default: null })
|
||||||
|
listImage: string[];
|
||||||
|
|
||||||
|
@Column('simple-array', { nullable: true, default: null })
|
||||||
|
package_contain: string[];
|
||||||
|
|
||||||
|
@Column({ type: 'longtext', nullable: true, default: null })
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
@Column({ type: 'longtext', nullable: true, default: null })
|
||||||
|
shortDescription: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true, default: null })
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true, default: null })
|
||||||
|
category: string; // để đơn giản, lưu string thôi
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', nullable: true, default: null })
|
||||||
|
price: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true, default: null })
|
||||||
|
conditionEbay: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true, default: null })
|
||||||
|
condition: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true, default: null })
|
||||||
|
manufactor: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true, default: null })
|
||||||
|
location: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true, default: null })
|
||||||
|
listingId: string;
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn()
|
||||||
|
updatedAt: Date;
|
||||||
|
|
||||||
|
@OneToMany(() => ProductListed, (listed) => listed.product)
|
||||||
|
listedProducts: ProductListed[];
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { Product } from 'src/entities/product.entity';
|
||||||
|
|
||||||
|
const productKeys = [
|
||||||
|
'ebayListingId', // thêm dòng này
|
||||||
|
'id',
|
||||||
|
'code',
|
||||||
|
'listImage',
|
||||||
|
'package_contain',
|
||||||
|
'description',
|
||||||
|
'shortDescription',
|
||||||
|
'title',
|
||||||
|
'category',
|
||||||
|
'price',
|
||||||
|
'conditionEbay',
|
||||||
|
'condition',
|
||||||
|
'manufactor',
|
||||||
|
'location',
|
||||||
|
'listingId',
|
||||||
|
'name',
|
||||||
|
'createdAt',
|
||||||
|
'updatedAt',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
type ProductInput = {
|
||||||
|
[K in (typeof productKeys)[number]]?: Product[K] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function mapToProduct(
|
||||||
|
raw: Record<string, any>,
|
||||||
|
): ProductInput & { ebayListingId: string } {
|
||||||
|
const mapped: any = {};
|
||||||
|
|
||||||
|
mapped.ebayListingId = raw.ebayListingId; // bắt buộc map khóa chính
|
||||||
|
|
||||||
|
for (const key of productKeys) {
|
||||||
|
const value = raw[key];
|
||||||
|
|
||||||
|
if (value !== undefined && value !== null) {
|
||||||
|
if (key === 'createdAt' || key === 'updatedAt') {
|
||||||
|
mapped[key] = new Date(value);
|
||||||
|
} else if (key === 'listImage' || key === 'package_contain') {
|
||||||
|
mapped[key] = Array.isArray(value) ? value.map(String) : [];
|
||||||
|
} else {
|
||||||
|
mapped[key] = value;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mapped[key] =
|
||||||
|
key === 'listImage' || key === 'package_contain' ? [] : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapped;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import { AppModule } from './app.module';
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
const app = await NestFactory.create(AppModule);
|
||||||
|
|
||||||
|
app.setGlobalPrefix('api');
|
||||||
|
|
||||||
|
app.enableCors({ origin: '*' });
|
||||||
|
|
||||||
|
await app.listen(process.env.PORT ?? 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
void bootstrap();
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { CacheService } from './cache.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
providers: [CacheService],
|
||||||
|
})
|
||||||
|
export class CacheModule {}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { CacheService } from './cache.service';
|
||||||
|
|
||||||
|
describe('CacheService', () => {
|
||||||
|
let service: CacheService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [CacheService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<CacheService>(CacheService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
// src/cache/cache.service.ts
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import * as NodeCache from 'node-cache';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CacheService {
|
||||||
|
private cache = new NodeCache({ stdTTL: 60 * 5 }); // 5 phút
|
||||||
|
|
||||||
|
get<T>(key: string): T | undefined {
|
||||||
|
return this.cache.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
set<T>(key: string, value: T, ttl: number | string = 10000) {
|
||||||
|
this.cache.set(key, value, ttl);
|
||||||
|
}
|
||||||
|
|
||||||
|
del(key: string) {
|
||||||
|
this.cache.del(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
flush() {
|
||||||
|
this.cache.flushAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ConfigService } from './config.service';
|
||||||
|
import { Config } from 'src/entities/config.entity';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([Config])],
|
||||||
|
providers: [ConfigService],
|
||||||
|
exports: [ConfigService],
|
||||||
|
})
|
||||||
|
export class ConfigModule {}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { ConfigService } from './config.service';
|
||||||
|
|
||||||
|
describe('ConfigService', () => {
|
||||||
|
let service: ConfigService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [ConfigService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<ConfigService>(ConfigService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Config } from 'src/entities/config.entity';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ConfigService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(Config)
|
||||||
|
public readonly repo: Repository<Config>,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { PolicyController } from './policy.controller';
|
||||||
|
|
||||||
|
describe('PolicyController', () => {
|
||||||
|
let controller: PolicyController;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
controllers: [PolicyController],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
controller = module.get<PolicyController>(PolicyController);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(controller).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { Controller, Get, Res } from '@nestjs/common';
|
||||||
|
import { Response } from 'express';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { existsSync } from 'fs';
|
||||||
|
|
||||||
|
@Controller('policy')
|
||||||
|
export class PolicyController {
|
||||||
|
@Get()
|
||||||
|
getPolicyPage(@Res() res: Response) {
|
||||||
|
// 🔥 Đi ngược từ dist/ ra thư mục gốc, tìm đến public/
|
||||||
|
const filePath = join(process.cwd(), 'public', 'views', 'policy.html');
|
||||||
|
|
||||||
|
if (!existsSync(filePath)) {
|
||||||
|
return res
|
||||||
|
.status(404)
|
||||||
|
.send('Policy page not found. (Missing public/views/policy.html)');
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.sendFile(filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { PolicyController } from './policy.controller';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [PolicyController]
|
||||||
|
})
|
||||||
|
export class PolicyModule {}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { ProductsController } from './products.controller';
|
||||||
|
|
||||||
|
describe('ProductsController', () => {
|
||||||
|
let controller: ProductsController;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
controllers: [ProductsController],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
controller = module.get<ProductsController>(ProductsController);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(controller).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
|
||||||
|
import { Paginate, PaginateQuery } from 'nestjs-paginate';
|
||||||
|
import { Product } from 'src/entities/product.entity';
|
||||||
|
import { ProductsService } from './products.service';
|
||||||
|
|
||||||
|
@Controller('products')
|
||||||
|
export class ProductsController {
|
||||||
|
constructor(private readonly productsService: ProductsService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
async index(@Paginate() query: PaginateQuery) {
|
||||||
|
return this.productsService.index(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
async show(@Param('id') id: Product['id']) {
|
||||||
|
return this.productsService.show(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('publish/:id')
|
||||||
|
async publish(@Param('id') id: Product['id'], @Body() data: any) {
|
||||||
|
return this.productsService.publish(id, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('unlist/:id')
|
||||||
|
async unlist(@Param('id') id: Product['id'], @Body() data: any) {
|
||||||
|
return this.productsService.unlist(id, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { Product } from 'src/entities/product.entity';
|
||||||
|
import { ProductsController } from './products.controller';
|
||||||
|
import { ProductsService } from './products.service';
|
||||||
|
import { ProductListed } from 'src/entities/product-listed.entity';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([Product, ProductListed])],
|
||||||
|
providers: [ProductsService],
|
||||||
|
controllers: [ProductsController],
|
||||||
|
exports: [ProductsService],
|
||||||
|
})
|
||||||
|
export class ProductsModule {}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { ProductsService } from './products.service';
|
||||||
|
|
||||||
|
describe('ProductsService', () => {
|
||||||
|
let service: ProductsService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [ProductsService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<ProductsService>(ProductsService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,162 @@
|
||||||
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
Injectable,
|
||||||
|
NotFoundException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { paginate, PaginateQuery } from 'nestjs-paginate';
|
||||||
|
import axios from 'axios';
|
||||||
|
import * as crypto from 'crypto';
|
||||||
|
import { ConfigService as CSL } from '@nestjs/config';
|
||||||
|
import { Product } from 'src/entities/product.entity';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import AppResponse from 'src/system/response/app-response';
|
||||||
|
import { SystemLang } from 'src/system/lang/system.lang';
|
||||||
|
import { ProductListed } from 'src/entities/product-listed.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ProductsService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(Product)
|
||||||
|
public readonly repo: Repository<Product>,
|
||||||
|
@InjectRepository(ProductListed)
|
||||||
|
public readonly repoProductlisted: Repository<ProductListed>,
|
||||||
|
private readonly csl: CSL,
|
||||||
|
readonly configService: ConfigService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async index(query: PaginateQuery) {
|
||||||
|
const qb = this.repo.createQueryBuilder('product');
|
||||||
|
|
||||||
|
let info_hash: string | null = null;
|
||||||
|
|
||||||
|
// Nếu có filter.info → hash nó lại (ở đây bạn truyền sẵn hash)
|
||||||
|
if (query.filter?.info) {
|
||||||
|
info_hash = (query.filter.info as string) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nếu có info_hash → chỉ join những bản ghi có info_hash đó
|
||||||
|
if (info_hash) {
|
||||||
|
qb.leftJoinAndMapMany(
|
||||||
|
'product.listedProducts',
|
||||||
|
'product_listed',
|
||||||
|
'listed',
|
||||||
|
'listed.product_id = product.ebayListingId AND listed.info_hash = :info_hash',
|
||||||
|
{ info_hash },
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
qb.leftJoinAndMapMany(
|
||||||
|
'product.listedProducts',
|
||||||
|
'product_listed',
|
||||||
|
'listed',
|
||||||
|
'listed.product_id = product.ebayListingId',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----- Lọc theo status -----
|
||||||
|
if (query.filter?.status === 'listed') {
|
||||||
|
qb.where('listed.id IS NOT NULL');
|
||||||
|
} else if (query.filter?.status === 'unlist') {
|
||||||
|
if (info_hash) {
|
||||||
|
qb.where(
|
||||||
|
`product.ebayListingId NOT IN (
|
||||||
|
SELECT pl.product_id FROM product_listed pl WHERE pl.info_hash = :info_hash
|
||||||
|
)`,
|
||||||
|
{ info_hash },
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
qb.where('listed.id IS NULL');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await paginate(query, qb, {
|
||||||
|
sortableColumns: ['createdAt'],
|
||||||
|
searchableColumns: ['id', 'code', 'name', 'title'],
|
||||||
|
defaultLimit: 10,
|
||||||
|
defaultSortBy: [['createdAt', 'DESC'] as any],
|
||||||
|
maxLimit: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
return AppResponse.toPagination<Product>(data, true, Product);
|
||||||
|
}
|
||||||
|
|
||||||
|
async show(id: Product['ebayListingId']) {
|
||||||
|
const product = await this.repo.findOne({ where: { ebayListingId: id } });
|
||||||
|
|
||||||
|
if (!product) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
AppResponse.toResponse(null, {
|
||||||
|
message: SystemLang.getText('messages', 'not_found'),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const a = await axios({
|
||||||
|
method: 'POST',
|
||||||
|
headers: { Authorization: `Bearer ${this.csl.get('VITE_API_TOKEN')}` },
|
||||||
|
url: 'transferGetData',
|
||||||
|
baseURL: this.csl.get('BASE_URL'),
|
||||||
|
data: {
|
||||||
|
filter: {
|
||||||
|
where: {
|
||||||
|
account: 'prology_net',
|
||||||
|
condition: product.condition,
|
||||||
|
productModelCode: product.code,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pageCurrent: '/ebaytools/listing-ebay',
|
||||||
|
urlAPI: '/api/ebay-listing/get-image-listing',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return AppResponse.toResponse({ ...product, ...a.data?.data });
|
||||||
|
}
|
||||||
|
|
||||||
|
async publish(id: Product['ebayListingId'], data: any) {
|
||||||
|
const info_hash = data?.hash_info || '';
|
||||||
|
|
||||||
|
const prev = await this.repoProductlisted.findOne({
|
||||||
|
where: { product: { ebayListingId: id }, info_hash: info_hash },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (prev) {
|
||||||
|
return AppResponse.toResponse(prev);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await this.repoProductlisted.save({
|
||||||
|
product: { ebayListingId: id },
|
||||||
|
info: data?.info || {},
|
||||||
|
info_hash: info_hash,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result)
|
||||||
|
throw new BadRequestException(
|
||||||
|
AppResponse.toResponse(null, {
|
||||||
|
message: SystemLang.getText('messages', 'error'),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return AppResponse.toResponse(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
async unlist(id: Product['ebayListingId'], data: any) {
|
||||||
|
const info_hash = data?.hash_info || '';
|
||||||
|
|
||||||
|
const prev = await this.repoProductlisted.findOne({
|
||||||
|
where: { product: { ebayListingId: id }, info_hash: info_hash },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!prev) {
|
||||||
|
throw new NotFoundException(
|
||||||
|
AppResponse.toResponse(false, {
|
||||||
|
message: SystemLang.getText('messages', 'not_found'),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.repoProductlisted.delete({ id: prev.id });
|
||||||
|
|
||||||
|
return AppResponse.toResponse(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { SyncController } from './sync.controller';
|
||||||
|
|
||||||
|
describe('SyncController', () => {
|
||||||
|
let controller: SyncController;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
controllers: [SyncController],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
controller = module.get<SyncController>(SyncController);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(controller).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { Controller, Post } from '@nestjs/common';
|
||||||
|
import { SyncService } from './sync.service';
|
||||||
|
|
||||||
|
@Controller('sync')
|
||||||
|
export class SyncController {
|
||||||
|
constructor(readonly syncService: SyncService) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
async sync() {
|
||||||
|
return this.syncService.sync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { SyncService } from './sync.service';
|
||||||
|
import { ConfigModule } from '../config/config.module';
|
||||||
|
import { ProductsModule } from '../products/products.module';
|
||||||
|
import { SyncController } from './sync.controller';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [ConfigModule, ProductsModule],
|
||||||
|
providers: [SyncService],
|
||||||
|
controllers: [SyncController],
|
||||||
|
exports: [SyncService],
|
||||||
|
})
|
||||||
|
export class SyncModule {}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { SyncService } from './sync.service';
|
||||||
|
|
||||||
|
describe('SyncService', () => {
|
||||||
|
let service: SyncService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [SyncService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<SyncService>(SyncService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ConfigService as CSL } from '@nestjs/config';
|
||||||
|
import axios from 'axios';
|
||||||
|
import { ConfigService } from '../config/config.service';
|
||||||
|
import { mapToProduct } from 'src/features/product';
|
||||||
|
import { ProductsService } from '../products/products.service';
|
||||||
|
import { EConfig } from 'src/system/enum/config-enum';
|
||||||
|
import AppResponse from 'src/system/response/app-response';
|
||||||
|
import { SystemLang } from 'src/system/lang/system.lang';
|
||||||
|
import { Product } from 'src/entities/product.entity';
|
||||||
|
import * as _ from 'lodash';
|
||||||
|
@Injectable()
|
||||||
|
export class SyncService {
|
||||||
|
constructor(
|
||||||
|
private readonly csl: CSL,
|
||||||
|
private readonly configAppService: ConfigService,
|
||||||
|
private readonly productService: ProductsService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async sync() {
|
||||||
|
const limit = await this.configAppService.repo.findOne({
|
||||||
|
where: { key_name: EConfig.LIMIT },
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data } = await axios({
|
||||||
|
headers: { Authorization: `Bearer ${this.csl.get('VITE_API_TOKEN')}` },
|
||||||
|
url: 'transferGetData',
|
||||||
|
baseURL: this.csl.get('BASE_URL'),
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
urlAPI: '/api/ebay-listing/listing-get-list',
|
||||||
|
filter: {
|
||||||
|
skip: 0,
|
||||||
|
limit: limit?.value || 4180,
|
||||||
|
order: 'updatedAt desc',
|
||||||
|
where: {
|
||||||
|
account: 'prology_net',
|
||||||
|
status: 'Updated',
|
||||||
|
productModelCode: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const rawProducts = data?.data || [];
|
||||||
|
|
||||||
|
const productsToSave = rawProducts.map((item: any) => {
|
||||||
|
return this.productService.repo.create(mapToProduct(item) as any);
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await this.productService.repo.upsert(productsToSave, [
|
||||||
|
'ebayListingId',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (data?.total && data?.total != limit?.value) {
|
||||||
|
if (!limit) {
|
||||||
|
await this.configAppService.repo.save({
|
||||||
|
key_name: EConfig.LIMIT,
|
||||||
|
value: data?.total,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await this.configAppService.repo.update(
|
||||||
|
{ key_name: EConfig.LIMIT },
|
||||||
|
{
|
||||||
|
key_name: EConfig.LIMIT,
|
||||||
|
value: data?.total,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return AppResponse.toResponse(true, {
|
||||||
|
message: SystemLang.getText('messages', 'success'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TasksService } from './task.service';
|
||||||
|
import { SyncModule } from '../sync/sync.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [SyncModule],
|
||||||
|
providers: [TasksService],
|
||||||
|
})
|
||||||
|
export class TaskModule {}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { TasksService } from './task.service';
|
||||||
|
|
||||||
|
describe('TaskService', () => {
|
||||||
|
let service: TasksService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [TasksService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<TasksService>(TasksService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
// tasks.service.ts
|
||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||||
|
import { SyncService } from '../sync/sync.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TasksService {
|
||||||
|
private readonly logger = new Logger(TasksService.name);
|
||||||
|
|
||||||
|
private isRunning = false; // flag để lock
|
||||||
|
|
||||||
|
constructor(readonly syncService: SyncService) {}
|
||||||
|
|
||||||
|
@Cron(CronExpression.EVERY_30_MINUTES)
|
||||||
|
async handleCron() {
|
||||||
|
if (this.isRunning) {
|
||||||
|
this.logger.warn('Cron job đang chạy, bỏ qua lần trigger mới.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.isRunning = true;
|
||||||
|
this.logger.debug('Cron job bắt đầu...');
|
||||||
|
try {
|
||||||
|
await this.syncService.sync();
|
||||||
|
this.logger.debug('Cron job hoàn tất.');
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error('Cron job lỗi:', err);
|
||||||
|
} finally {
|
||||||
|
this.isRunning = false; // unlock
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export enum EConfig {
|
||||||
|
LIMIT = 'LIMIT',
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,113 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||||
|
export class SystemLang {
|
||||||
|
private static texts = {
|
||||||
|
messages: {
|
||||||
|
vi: {
|
||||||
|
error: 'Đã xảy ra lỗi!',
|
||||||
|
not_found: 'Không tìm thấy dữ liệu!',
|
||||||
|
file_not_found: 'Không tìm thấy tệp tin!',
|
||||||
|
unauthorized: 'Bạn không có quyền truy cập!',
|
||||||
|
forbidden: 'Truy cập bị từ chối!',
|
||||||
|
success: 'Thành công!',
|
||||||
|
server_error: 'Lỗi máy chủ, vui lòng thử lại sau!',
|
||||||
|
connection_lost: 'Mất kết nối, kiểm tra lại mạng!',
|
||||||
|
try_again: 'Vui lòng thử lại!',
|
||||||
|
logout_success: 'Bạn đã đăng xuất thành công!',
|
||||||
|
login_error_input: 'Tên đăng nhập hoặc mật khẩu không chính xác',
|
||||||
|
login_error_valid: 'Input phải là username hoặc email',
|
||||||
|
login_required: 'Vui lòng đăng nhập để tiếp tục!',
|
||||||
|
login_success: 'Đăng nhập thành công!',
|
||||||
|
save_success: 'Lưu thành công!',
|
||||||
|
delete_success: 'Xóa thành công!',
|
||||||
|
delete_failure: 'Xóa không thành công!',
|
||||||
|
can_not_delete: 'Không thể xóa @key',
|
||||||
|
no_data: 'Không có dữ liệu để hiển thị!',
|
||||||
|
default_action: `Hành động này không thể thực hiện được!`,
|
||||||
|
register_failure: 'Đăng ký tài khoản không thành công!',
|
||||||
|
register_success: 'Đăng ký tài khoản thành công!',
|
||||||
|
create_success: 'Đã tạo @key thành công!',
|
||||||
|
create_failed: 'Không thể tạo @key!',
|
||||||
|
update_success: 'Đã cập nhật @key thành công!',
|
||||||
|
update_failed: 'Không thể cập nhật @key!',
|
||||||
|
already_exists: '@key đã tồn tại!',
|
||||||
|
required: '@key là bắt buộc!',
|
||||||
|
invalid: '@key không hợp lệ!',
|
||||||
|
no_permission: 'Bạn không có quyền thực hiện thao tác với @key!',
|
||||||
|
password_invalid: 'Mật khẩu không hợp lệ !',
|
||||||
|
change_pass_success:
|
||||||
|
'Đổi mật khẩu thành công. Vui lòng đăng nhập lại !',
|
||||||
|
too_many_request: 'Yêu cầu vượt quá mức quy định',
|
||||||
|
feature_disabled: 'Tính năng này đang tạm ngưng',
|
||||||
|
id_not_match: 'ID không khớp!',
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
error: 'An error occurred!',
|
||||||
|
id_not_match: 'ID is not match!',
|
||||||
|
not_found: 'Data not found!',
|
||||||
|
file_not_found: 'File not found!',
|
||||||
|
unauthorized: 'You are not authorized!',
|
||||||
|
forbidden: 'Access denied!',
|
||||||
|
success: 'Success!',
|
||||||
|
server_error: 'Server error, please try again later!',
|
||||||
|
connection_lost: 'Connection lost, check your network!',
|
||||||
|
try_again: 'Please try again!',
|
||||||
|
logout_success: 'You have successfully logged out!',
|
||||||
|
login_error_input: 'Username or Password is cornet',
|
||||||
|
login_error_valid: 'Input must be a valid username or email',
|
||||||
|
login_required: 'Please log in to continue!',
|
||||||
|
login_success: 'Login Success!',
|
||||||
|
save_success: 'Saved successfully!',
|
||||||
|
delete_success: 'Delete success!',
|
||||||
|
delete_failure: 'Delete fail!',
|
||||||
|
can_not_delete: "Can't delete @key",
|
||||||
|
no_data: 'No data available!',
|
||||||
|
default_action: `This action cannot be performed`,
|
||||||
|
register_failure: 'Register account is fail!',
|
||||||
|
register_success: 'Register account is success!',
|
||||||
|
create_success: '@key created successfully!',
|
||||||
|
create_failed: 'Failed to create @key!',
|
||||||
|
update_success: '@key updated successfully!',
|
||||||
|
update_failed: 'Failed to update @key!',
|
||||||
|
already_exists: '@key already exists!',
|
||||||
|
required: '@key is required!',
|
||||||
|
invalid: '@key is invalid!',
|
||||||
|
no_permission:
|
||||||
|
'You do not have permission to perform this action on @key!',
|
||||||
|
password_invalid: 'Password is invalid !',
|
||||||
|
change_pass_success: 'Change password success. Please re-login !',
|
||||||
|
too_many_request: 'To many request',
|
||||||
|
feature_disabled: 'This feature is disabled',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// default lang is VIỆT NAM
|
||||||
|
public static lang = 'en';
|
||||||
|
|
||||||
|
/** ✅ Lấy text từ messages hoặc labels */
|
||||||
|
static getText<T extends keyof typeof SystemLang.texts>(
|
||||||
|
type: T,
|
||||||
|
key: keyof (typeof SystemLang.texts)[T]['vi'],
|
||||||
|
after?: string,
|
||||||
|
): string {
|
||||||
|
const text: string = SystemLang.texts[type][SystemLang.lang][key];
|
||||||
|
|
||||||
|
return after
|
||||||
|
? text.includes('@key')
|
||||||
|
? text.replaceAll('@key', after)
|
||||||
|
: `${text} ${after}`
|
||||||
|
: (text ?? `Missing ${type}: ${String(key)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** ✅ Trả về text từ custom object, fallback nếu không có */
|
||||||
|
static getCustomText(
|
||||||
|
texts: { vi?: string; en?: string },
|
||||||
|
fallbackType: keyof typeof SystemLang.texts = 'messages',
|
||||||
|
): string {
|
||||||
|
return (
|
||||||
|
texts[SystemLang.lang] ||
|
||||||
|
Object.values(texts)[0] ||
|
||||||
|
SystemLang.getText(fallbackType, 'error')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
import { HttpStatus } from '@nestjs/common';
|
||||||
|
import { plainToClass, plainToInstance } from 'class-transformer';
|
||||||
|
import { FilterOperator, FilterSuffix, Paginated } from 'nestjs-paginate';
|
||||||
|
import { Column } from 'nestjs-paginate/lib/helper';
|
||||||
|
import { SystemLang } from '../lang/system.lang';
|
||||||
|
|
||||||
|
export default class AppResponse {
|
||||||
|
public static toResponse(
|
||||||
|
data: any,
|
||||||
|
options?: { message?: string; status_code?: number; bonus?: any },
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
message:
|
||||||
|
options?.message ||
|
||||||
|
(!Boolean(data)
|
||||||
|
? SystemLang.getText('messages', 'default_action')
|
||||||
|
: SystemLang.getText('messages', 'success')),
|
||||||
|
status_code:
|
||||||
|
options?.status_code || (!Boolean(data) ? HttpStatus.BAD_REQUEST : 200),
|
||||||
|
data: data,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
...(options?.bonus || {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static plainToInStance<M>(this: new (...args: any[]) => M, obj: M): M {
|
||||||
|
return plainToClass(this, obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static toPagination<M>(
|
||||||
|
{ data, meta }: Paginated<M>,
|
||||||
|
transform = true,
|
||||||
|
dtoClass?: new () => M,
|
||||||
|
) {
|
||||||
|
let transformedData: any[] = data;
|
||||||
|
|
||||||
|
if (transform && dtoClass) {
|
||||||
|
transformedData = plainToInstance(dtoClass, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentPage = meta.currentPage ?? 1;
|
||||||
|
const itemsPerPage = meta.itemsPerPage ?? 0;
|
||||||
|
const totalItems = meta.totalItems ?? 0;
|
||||||
|
const totalPages = meta.totalPages ?? 0;
|
||||||
|
|
||||||
|
return this.toResponse(transformedData, {
|
||||||
|
bonus: {
|
||||||
|
current_page: currentPage,
|
||||||
|
from: currentPage * itemsPerPage - (itemsPerPage - 1),
|
||||||
|
to:
|
||||||
|
currentPage * itemsPerPage > totalItems
|
||||||
|
? totalItems
|
||||||
|
: currentPage * itemsPerPage,
|
||||||
|
last_page: totalPages,
|
||||||
|
per_page: itemsPerPage,
|
||||||
|
total: totalItems,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static processFilters<T>(
|
||||||
|
queryFilters: any,
|
||||||
|
filterableColumns: {
|
||||||
|
[key in Column<T> | (string & {})]?:
|
||||||
|
| (FilterOperator | FilterSuffix)[]
|
||||||
|
| true;
|
||||||
|
},
|
||||||
|
): any {
|
||||||
|
if (!queryFilters) return {};
|
||||||
|
|
||||||
|
const processedFilters = { ...queryFilters };
|
||||||
|
|
||||||
|
for (const key in queryFilters) {
|
||||||
|
const value = queryFilters[key];
|
||||||
|
|
||||||
|
if (filterableColumns[key]) {
|
||||||
|
const columnConfig = filterableColumns[key];
|
||||||
|
|
||||||
|
if (Array.isArray(columnConfig)) {
|
||||||
|
for (const operator of columnConfig) {
|
||||||
|
if (
|
||||||
|
operator === FilterOperator.ILIKE &&
|
||||||
|
!value.startsWith('$ilike')
|
||||||
|
) {
|
||||||
|
processedFilters[key] = `$ilike:%${value}%`; // Thêm ký tự đại diện %
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (operator === FilterOperator.LTE && !value.startsWith('$lte')) {
|
||||||
|
processedFilters[key] = `$lte:${value}`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (operator === FilterOperator.GTE && !value.startsWith('$gte')) {
|
||||||
|
processedFilters[key] = `$gte:${value}`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (operator === FilterOperator.EQ && !value.startsWith('$eq')) {
|
||||||
|
processedFilters[key] = `$eq:${value}`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Nếu cột chỉ có giá trị `true` (mặc định EQ)
|
||||||
|
else if (columnConfig === true && !value.startsWith('$eq')) {
|
||||||
|
processedFilters[key] = `$eq:${value}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return processedFilters;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { INestApplication } from '@nestjs/common';
|
||||||
|
import * as request from 'supertest';
|
||||||
|
import { App } from 'supertest/types';
|
||||||
|
import { AppModule } from './../src/app.module';
|
||||||
|
|
||||||
|
describe('AppController (e2e)', () => {
|
||||||
|
let app: INestApplication<App>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||||
|
imports: [AppModule],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
app = moduleFixture.createNestApplication();
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('/ (GET)', () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get('/')
|
||||||
|
.expect(200)
|
||||||
|
.expect('Hello World!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"moduleFileExtensions": ["js", "json", "ts"],
|
||||||
|
"rootDir": ".",
|
||||||
|
"testEnvironment": "node",
|
||||||
|
"testRegex": ".e2e-spec.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"declaration": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"target": "ES2023",
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"incremental": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"strictBindCallApply": false,
|
||||||
|
"noFallthroughCasesInSwitch": false
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue