change repo

This commit is contained in:
Ruben Kallinich
2024-07-11 10:04:05 +02:00
commit eb3870fa81
48 changed files with 7830 additions and 0 deletions

25
.eslintrc.js Normal file
View File

@@ -0,0 +1,25 @@
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',
},
};

4
.prettierrc Normal file
View File

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

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"nuxt.isNuxtApp": false
}

73
README.md Normal file
View File

@@ -0,0 +1,73 @@
<p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="200" 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://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></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"/></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"></a>
</p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Installation
```bash
$ pnpm install
```
## Running the app
```bash
# development
$ pnpm run start
# watch mode
$ pnpm run start:dev
# production mode
$ pnpm run start:prod
```
## Test
```bash
# unit tests
$ pnpm run test
# e2e tests
$ pnpm run test:e2e
# test coverage
$ pnpm run test:cov
```
## 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://kamilmysliwiec.com)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](LICENSE).

8
nest-cli.json Normal file
View File

@@ -0,0 +1,8 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}

80
package.json Normal file
View File

@@ -0,0 +1,80 @@
{
"name": "page-speed-service",
"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": "^3.0.1",
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.0.0",
"@nestjs/mapped-types": "*",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/swagger": "^7.2.0",
"@nestjs/typeorm": "^10.0.1",
"axios": "^1.6.5",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"mysql2": "^3.7.0",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
"swagger-ui-express": "^5.0.0",
"typeorm": "^0.3.19"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
"@types/node": "^20.3.1",
"@types/supertest": "^6.0.0",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"eslint": "^8.42.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"jest": "^29.5.0",
"prettier": "^3.0.0",
"source-map-support": "^0.5.21",
"supertest": "^6.3.3",
"ts-jest": "^29.1.0",
"ts-loader": "^9.4.3",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.1.3"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

5544
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

53
src/app.module.ts Normal file
View File

@@ -0,0 +1,53 @@
import { HttpModule } from '@nestjs/axios';
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ControllersController } from './controller/controllers/controllers.controller';
import { ControllersService } from './controller/controllers/controllers.service';
import { CustomerController } from './service/customer/customer.controller';
import { CustomerModule } from './service/customer/customer.module';
import { CustomerService } from './service/customer/customer.service';
import { Customer } from './service/customer/entities/customer.entity';
import { PageSpeedData } from './service/pagespeed/entities/pagespeeddata.entity';
import { PagespeedModule } from './service/pagespeed/pagespeed.module';
import { PagespeedService } from './service/pagespeed/pagespeed.service';
import { Website } from './service/website/entities/website.entity';
import { WebsiteController } from './service/website/website.controller';
import { WebsiteService } from './service/website/website.service';
@Module({
imports: [
HttpModule,
PagespeedModule,
TypeOrmModule.forRootAsync({
imports: [
ConfigModule.forRoot({
ignoreEnvFile: false,
envFilePath: '.env',
}),
],
inject: [ConfigService],
useFactory: async (configService: ConfigService) => ({
type: 'mysql',
host: configService.get<string>('HOST'),
port: parseInt(configService.get<string>('PORT')),
username: configService.get<string>('DB_USER'),
password: configService.get<string>('PASSWORD'),
database: configService.get<string>('DATABASE'),
entities: [PageSpeedData, Customer, Website],
synchronize: true,
logging: false,
}),
}),
CustomerModule,
],
controllers: [CustomerController, WebsiteController, ControllersController],
providers: [
CustomerService,
WebsiteService,
PagespeedService,
ControllersService,
ConfigService,
],
})
export class AppModule {}

View File

@@ -0,0 +1,20 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ControllersController } from './controllers.controller';
import { ControllersService } from './controllers.service';
describe('ControllersController', () => {
let controller: ControllersController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [ControllersController],
providers: [ControllersService],
}).compile();
controller = module.get<ControllersController>(ControllersController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@@ -0,0 +1,92 @@
import {
Body,
ClassSerializerInterceptor,
Controller,
Get,
Param,
Post,
Res,
UseInterceptors,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Response } from 'express';
import { CustomerService } from 'src/service/customer/customer.service';
import { PageSpeedData } from 'src/service/pagespeed/entities/pagespeeddata.entity';
import { PagespeedService } from 'src/service/pagespeed/pagespeed.service';
import { WebsiteService } from 'src/service/website/website.service';
@ApiTags('pagespeed')
@Controller('controllers')
export class ControllersController {
constructor(
private readonly customerService: CustomerService,
private readonly websiteService: WebsiteService,
private readonly pageSpeedService: PagespeedService,
) {}
@Post('newpage')
async getPageSpeedResult(
@Body()
body: {
firstName: string;
lastName: string;
url: string;
displayName: string;
email: string;
dsgvo: boolean;
},
@Res() res: Response,
): Promise<any> {
try {
const url = body.url;
const firstName = body.firstName;
const lastName = body.lastName;
const email = body.email;
const displayName = body.displayName;
const dsgvo = body.dsgvo;
const customer = await this.customerService.createOrUpdateCustomer({
firstName,
lastName,
email,
dsgvo,
});
const website = await this.websiteService.createOrUpdateWebsite({
url,
displayName,
customer,
});
const result = await this.pageSpeedService.getPageSpeedResult(
url,
website.id,
);
const pageSpeedId = result.id; // Extract the pageSpeedId from the backend response to utilize it within the frontend.
// This allows us to reference the specific page speed data on the client side,
// enabling dynamic content updates and user-specific interactions based on the
// performance metrics of the requested web page.
return res.status(200).json({
message: 'PageSpeed data retrieved and saved successfully.',
pageSpeedId,
});
} catch (error) {
console.error('Error in getPageSpeedResult:', error);
return res.status(500).json({
message: 'An error occurred while retrieving PageSpeed data.',
error: error.message,
});
}
}
@Get('currentresult/:id')
@UseInterceptors(ClassSerializerInterceptor)
@UsePipes(new ValidationPipe({ transform: true }))
async findTheHole(@Param('id') id: string): Promise<PageSpeedData[]> {
return this.pageSpeedService.getAllPageSpeeds(id);
}
@Get('gettall/:id')
async findAll(@Param('id') id: string): Promise<PageSpeedData[]> {
return this.pageSpeedService.getAllPageSpeeds(id);
}
}

View File

@@ -0,0 +1,29 @@
import { HttpModule } from '@nestjs/axios';
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CustomerService } from 'src/service/customer/customer.service';
import { Customer } from 'src/service/customer/entities/customer.entity';
import { PageSpeedData } from 'src/service/pagespeed/entities/pagespeeddata.entity';
import { PagespeedService } from 'src/service/pagespeed/pagespeed.service';
import { Website } from 'src/service/website/entities/website.entity';
import { WebsiteService } from 'src/service/website/website.service';
import { ControllersController } from './controllers.controller';
import { ControllersService } from './controllers.service';
@Module({
imports: [
ConfigModule,
HttpModule,
TypeOrmModule.forFeature([Customer, Website, PageSpeedData]),
],
controllers: [ControllersController],
providers: [
ControllersService,
CustomerService,
WebsiteService,
PagespeedService,
],
exports: [CustomerService, WebsiteService, PagespeedService, TypeOrmModule],
})
export class ControllersModule {}

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ControllersService } from './controllers.service';
describe('ControllersService', () => {
let service: ControllersService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [ControllersService],
}).compile();
service = module.get<ControllersService>(ControllersService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@@ -0,0 +1,4 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export class ControllersService {}

View File

@@ -0,0 +1 @@
export class Controller {}

View File

@@ -0,0 +1,32 @@
import {
CallHandler,
ExecutionContext,
HttpException,
HttpStatus,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class CurrentResultInterceptor implements NestInterceptor {
intercept(
context: ExecutionContext,
next: CallHandler<any>,
): Observable<any> | Promise<Observable<any>> {
const request = context.switchToHttp().getRequest();
const { displayName, url } = request.body;
if (!displayName || !url) {
console.log('Name', displayName, 'URL', url, 'HOLE:', request.body);
throw new HttpException('Please enter propertys', HttpStatus.BAD_REQUEST);
}
return next.handle().pipe(
map((data) => {
return data;
}),
);
}
}

34
src/main.ts Normal file
View File

@@ -0,0 +1,34 @@
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { AppModule } from './app.module';
import { CreateWebsiteDto } from './service/website/dto/create-website.dto';
import { CreateCustomerDto } from './service/customer/dto/create-customer.dto';
import { CreatePageSpeedDto } from './service/pagespeed/dto/create-pagespeed.dto';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const config = new DocumentBuilder()
.setTitle('PageSpeed API')
.setDescription('PageSpeed API description')
.setVersion('1.0')
.addTag('pagespeed')
.build();
const document = SwaggerModule.createDocument(app, config, {
extraModels: [CreateWebsiteDto, CreateCustomerDto, CreatePageSpeedDto],
});
SwaggerModule.setup('api', app, document);
app.useGlobalPipes(
new ValidationPipe({
transform: true, //activates the transformation from plain JSO.
whitelist: true, // removes all charataristics from DTO.
forbidNonWhitelisted: true, //deleting errors that is not wihtelisted.
}),
);
//#region acception corse
app.enableCors();
//#endregion
await app.listen(3001);
}
bootstrap();

View File

@@ -0,0 +1,20 @@
import { Test, TestingModule } from '@nestjs/testing';
import { CustomerController } from './customer.controller';
import { CustomerService } from './customer.service';
describe('CustomerController', () => {
let controller: CustomerController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [CustomerController],
providers: [CustomerService],
}).compile();
controller = module.get<CustomerController>(CustomerController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@@ -0,0 +1,54 @@
import { Controller } from '@nestjs/common';
import { PagespeedService } from '../pagespeed/pagespeed.service';
import { WebsiteService } from '../website/website.service';
import { CustomerService } from './customer.service';
@Controller('customer')
export class CustomerController {
// constructor(
// private readonly customerService: CustomerService,
// private readonly websiteService: WebsiteService,
// private readonly pageSpeedService: PagespeedService,
// ) {}
// @Post()
// async createCustomer(
// @Body() createCustomerDto: CreateCustomerDto,
// ): Promise<Customer> {
// return this.customerService.createOrUpdateCustomer(createCustomerDto);
// }
// @Post()
// async createCustomer(
// @Body() createCustomerDto: CreateCustomerDto,
// ): Promise<Customer> {
// return this.customerService.createOrUpdateCustomer(createCustomerDto);
// }
//
// @Get('all')
// findAll(): Promise<Customer[]> {
// return this.customerService.getAllCustomers();
// }
// @Get('id/:id')
// async findOneCustomerById(@Param('id') id: string): Promise<Customer> {
// return this.customerService.getCustomerById(id);
// }
// @Get('name/:name')
// async findOneCustomerByName(@Param('name') name: string): Promise<Customer> {
// return this.customerService.getCustomerByName(name);
// }
// @Get('website/:id')
// async findOneWebsiteById(@Param('id') id: string): Promise<Website> {
// return this.websiteService.getWebsiteById(id);
// }
// @Get('website/:id/pagespeed')
// async getPageSpeedByWebsiteId(
// @Param('id') id: string,
// ): Promise<PageSpeedData[]> {
// return this.pageSpeedService.getPageSpeedsByWebsiteId(id);
// }
// @Get('display/:displayName')
// async findOneWebsiteByDisplayName(
// @Param('displayName') displayName: string,
// ): Promise<Website> {
// return this.websiteService.getWebsiteByDisplayName(displayName);
// }
}

View File

@@ -0,0 +1,23 @@
import { HttpModule } from '@nestjs/axios';
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { PagespeedService } from '../pagespeed/pagespeed.service';
import { WebsiteService } from '../website/website.service';
import { PageSpeedData } from '../pagespeed/entities/pagespeeddata.entity';
import { Website } from '../website/entities/website.entity';
import { CustomerController } from './customer.controller';
import { CustomerService } from './customer.service';
import { Customer } from './entities/customer.entity';
@Module({
imports: [
ConfigModule,
HttpModule,
TypeOrmModule.forFeature([Customer, Website, PageSpeedData]),
],
controllers: [CustomerController],
providers: [CustomerService, WebsiteService, PagespeedService],
exports: [CustomerService, WebsiteService, PagespeedService, TypeOrmModule],
})
export class CustomerModule {}

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { CustomerService } from './customer.service';
describe('CustomerService', () => {
let service: CustomerService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [CustomerService],
}).compile();
service = module.get<CustomerService>(CustomerService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@@ -0,0 +1,39 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateCustomerDto } from './dto/create-customer.dto';
import { Customer } from './entities/customer.entity';
@Injectable()
export class CustomerService {
constructor(
@InjectRepository(Customer)
private customerRepository: Repository<Customer>,
) {}
async createOrUpdateCustomer(
data: Partial<CreateCustomerDto>,
): Promise<Customer> {
let customer = await this.customerRepository.findOne({
where: { email: data.email },
});
if (!customer) {
customer = new Customer();
customer.firstName = data.firstName;
customer.lastName = data.lastName;
customer.email = data.email;
customer.dsgvo = data.dsgvo;
await this.customerRepository.save(customer);
}
return customer;
}
async getAllCustomers(): Promise<Customer[]> {
return this.customerRepository.find();
}
async getCustomerById(id: string): Promise<Customer> {
return this.customerRepository.findOne({ where: { id: id } });
}
async getCustomerByName(name: string): Promise<Customer> {
return this.customerRepository.findOne({ where: { firstName: name } });
}
}

View File

@@ -0,0 +1,32 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsBoolean, IsEmail, IsString } from 'class-validator';
import { CreateWebsiteDto } from '../../website/dto/create-website.dto';
export class CreateCustomerDto {
@ApiProperty({ description: 'Customer Id' })
@IsString()
id: string;
@ApiProperty({ description: 'First name' })
@IsString()
firstName: string;
@ApiProperty({ description: 'Last name ' })
@IsString()
lastName: string;
@ApiProperty({ description: 'E-Mail adress' })
@IsString()
@IsEmail()
email: string;
@ApiProperty({
type: () => CreateWebsiteDto,
description: 'Website Association',
})
website: CreateWebsiteDto[];
@ApiProperty({ description: 'DSGVO confirmation' })
@IsBoolean()
dsgvo: boolean;
}

View File

@@ -0,0 +1,24 @@
import { ApiProperty } from '@nestjs/swagger';
import { LoadWebsiteDto } from '../../website/dto/load-website.dto';
import { IsString } from 'class-validator';
export class LoadCustomerDto {
@ApiProperty()
@IsString()
id: string;
@ApiProperty()
@IsString()
firstName: string;
@ApiProperty()
@IsString()
lastName: string;
@ApiProperty()
@IsString()
email: string;
@ApiProperty({ type: () => LoadWebsiteDto })
website: LoadCustomerDto;
}

View File

@@ -0,0 +1,25 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsEmail } from 'class-validator';
import { UpdateWebsiteDto } from '../../website/dto/update-website.dto';
export class UpdateCustomerDto {
@ApiProperty()
@IsString()
id: string;
@ApiProperty()
@IsString()
firstName: string;
@ApiProperty()
@IsString()
lastName: string;
@ApiProperty()
@IsString()
@IsEmail()
email: string;
@ApiProperty({ type: () => UpdateWebsiteDto })
website: UpdateWebsiteDto[];
}

View File

@@ -0,0 +1,36 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail } from 'class-validator';
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
import { Website } from '../../website/entities/website.entity';
@Entity()
export class Customer {
@PrimaryGeneratedColumn('uuid')
@ApiProperty()
id: string;
@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
@ApiProperty()
createdAt: Date;
@Column({ nullable: false })
@ApiProperty()
firstName: string;
@Column({ nullable: false })
@ApiProperty()
lastName: string;
@Column({ nullable: false })
@ApiProperty()
@IsEmail()
email: string;
@ApiProperty()
@OneToMany(() => Website, (website) => website.customers)
websites: Website[];
@ApiProperty()
@Column({ nullable: false })
dsgvo: boolean;
}

View File

@@ -0,0 +1,209 @@
import { ApiProperty } from '@nestjs/swagger';
import { Exclude } from 'class-transformer';
import { CreateWebsiteDto } from '../../website/dto/create-website.dto';
import { IsNumber, IsOptional, IsString } from 'class-validator';
export class CreatePageSpeedDto {
@ApiProperty({ description: 'Pagespeed Id' })
@IsString()
id: string;
@ApiProperty({
type: () => CreateWebsiteDto,
description: 'Website Foring Key',
})
website: CreateWebsiteDto;
@Exclude()
@ApiProperty({ description: 'Hole page-speed result ' })
lighthouseObject: any;
@ApiProperty()
@IsString()
firstContentfulPaintDisplayValue: string;
@ApiProperty()
@IsNumber()
firstContentfulPaintScore: number;
@ApiProperty()
@IsNumber()
firstContentfulPaintNumericValue: number;
@ApiProperty()
@IsNumber()
firstMeaningfulPaintScore: number;
@ApiProperty()
@IsString()
firstMeaningfulPaintDisplayValue: string;
@ApiProperty()
@IsNumber()
firstMeaningfulPaintNumericValue: number;
@ApiProperty()
@IsString()
firstMeaningfulPaintNumericUnit: string;
@ApiProperty()
@IsString()
mainThreadWorkBreakdownDisplayValue: string;
@ApiProperty()
@IsNumber()
mainThreadWorkBreakdownNumricValue: number;
@ApiProperty()
@IsString()
mainThreadWorkBreakdownNumericUnit: string;
@ApiProperty({ type: () => [String] })
@IsString()
mainThreadWorkBreakdownItemsGroupLabel: string[];
@ApiProperty({ type: () => [Number] })
@IsNumber()
mainThreadWorkBreakdownItemsDuration: number[];
@ApiProperty()
@IsNumber()
speedIndexScore: number;
@ApiProperty()
@IsString()
speedIndexDisplayValue: string;
@ApiProperty()
@IsNumber()
speedIndexNumericValue: number;
@ApiProperty()
@IsString()
speedIndexNumericUnit: string;
@ApiProperty()
@IsString()
@IsOptional()
largestContentfulPaintScore: string;
@ApiProperty()
@IsString()
@IsOptional()
largestContentfulPaintDisplayValue: string;
@ApiProperty()
@IsString()
@IsOptional()
largestContentfulPaintNumericValue: string;
@ApiProperty()
@IsString()
@IsOptional()
largestContentfulPaintNumericUnit: string;
@ApiProperty()
@IsString()
totalBlockingTimeScore: string;
@ApiProperty()
@IsString()
totalBlockingTimeDisplayValue: string;
@ApiProperty()
@IsString()
totalBlockingTimeNumericValue: string;
@ApiProperty()
@IsString()
totalBlockingTimeNumericUnit: string;
@ApiProperty({ type: () => [String] })
@IsString()
@IsOptional()
unusedCssRulesItems: string[];
@ApiProperty()
@IsOptional()
thirdPartySummaryDisplayValue: string | null;
@ApiProperty({ type: () => [String] })
@IsString()
@IsOptional()
thirdPartySummaryItemsUrl: string[];
@ApiProperty({ type: () => [Number] })
@IsNumber()
@IsOptional()
thirdPartySummaryItemsTransfer: number[];
@ApiProperty({ type: () => [Number] })
@IsNumber()
@IsOptional()
thirdPartySummaryItemsMainThred: number[];
@ApiProperty({ type: () => [Number] })
@IsNumber()
@IsOptional()
thirdPartySummaryItemsBlockingTime: number[];
@ApiProperty()
@IsString()
timeToInteractiveScore: string;
@ApiProperty()
@IsString()
timeToInteractiveDisplayValue: string;
@ApiProperty()
@IsString()
timeToInteractiveNumericValue: string;
@ApiProperty()
@IsString()
timeToInteractiveNumericUnit: string;
@ApiProperty()
@IsOptional()
totalByteWeightScore: number | null;
@ApiProperty()
@IsString()
totalByteWeightDisplayValue: string;
@ApiProperty()
@IsNumber()
totalByteWeightNumericValue: number;
@ApiProperty()
@IsString()
totalByteWeightNumericUnit: string;
@ApiProperty({ type: () => [String] })
@IsString()
totalByteWeightItemsUrl: string[];
@ApiProperty({ type: () => [Number] })
@IsNumber()
totalByteWeightItemsTotalBytes: number[];
@ApiProperty()
domSizeScore: number | null;
@ApiProperty()
@IsString()
domSizeDisplayValue: string;
@ApiProperty()
@IsString()
domSizeNumericValue: string;
@ApiProperty()
@IsString()
domSizeNumericUnit: string;
@ApiProperty({ type: () => [String] })
@IsString()
@IsOptional()
unusedJavaScript: string[];
}

View File

@@ -0,0 +1,152 @@
import { LoadWebsiteDto } from '../../website/dto/load-website.dto';
import { ApiProperty } from '@nestjs/swagger';
import { Exclude } from 'class-transformer';
import { IsString } from 'class-validator';
export class LoadPageSpeedDto {
@ApiProperty()
@IsString()
id: string;
@ApiProperty()
createdAt: Date;
@ApiProperty({ type: () => LoadWebsiteDto })
website: LoadWebsiteDto;
@Exclude()
@ApiProperty({ type: () => [String] })
lighthouseObjet: string[];
@ApiProperty()
firstContentfulPaintDisplayValue: string;
@ApiProperty()
firstContentfulPaintScore: number;
@ApiProperty()
firstContentfulPaintNumericValue: number;
@ApiProperty()
firstMeaningfulPaintScore: number;
@ApiProperty()
firstMeaningfulPaintDisplayValue: string;
@ApiProperty()
firstMeaningfulPaintNumericValue: number;
@ApiProperty()
firstMeaningfulPaintNumericUnit: string;
@ApiProperty()
mainThreadWorkBreakdownDisplayValue: string;
@ApiProperty()
mainThreadWorkBreakdownNumricValue: number;
@ApiProperty()
mainThreadWorkBreakdownNumericUnit: string;
@ApiProperty({ type: () => [String] })
mainThreadWorkBreakdownItemsGroupLabel: string[];
@ApiProperty({ type: () => [Number] })
mainThreadWorkBreakdownItemsDuration: number[];
@ApiProperty()
speedIndexScore: number;
@ApiProperty()
speedIndexDisplayValue: string;
@ApiProperty()
speedIndexNumericValue: number;
@ApiProperty()
speedIndexNumericUnit: string;
@ApiProperty()
largestContentfulPaintScore: string;
@ApiProperty()
largestContentfulPaintDisplayValue: string;
@ApiProperty()
largestContentfulPaintNumericValue: string;
@ApiProperty()
largestContentfulPaintNumericUnit: string;
@ApiProperty()
totalBlockingTimeScore: string;
@ApiProperty()
totalBlockingTimeDisplayValue: string;
@ApiProperty()
totalBlockingTimeNumericValue: string;
@ApiProperty()
totalBlockingTimeNumericUnit: string;
@ApiProperty({ type: () => [String] })
unusedCssRulesItems: string[];
@ApiProperty()
thirdPartySummaryDisplayValue: string | null;
@ApiProperty({ type: () => [String] })
thirdPartySummaryItemsUrl: string[];
@ApiProperty({ type: () => [Number] })
thirdPartySummaryItemsTransfer: number[];
@ApiProperty({ type: () => [Number] })
thirdPartySummaryItemsMainThred: number[];
@ApiProperty({ type: () => [Number] })
thirdPartySummaryItemsBlockingTime: number[];
@ApiProperty()
timeToInteractiveScore: string;
@ApiProperty()
timeToInteractiveDisplayValue: string;
@ApiProperty()
timeToInteractiveNumericValue: string;
@ApiProperty()
timeToInteractiveNumericUnit: string;
@ApiProperty()
totalByteWeightScore: number | null;
@ApiProperty()
totalByteWeightDisplayValue: string;
@ApiProperty()
totalByteWeightNumericValue: number;
@ApiProperty()
totalByteWeightNumericUnit: string;
@ApiProperty({ type: () => [String] })
totalByteWeightItemsUrl: string[];
@ApiProperty({ type: () => [Number] })
totalByteWeightItemsTotalBytes: number[];
@ApiProperty()
domSizeScore: number | null;
@ApiProperty()
domSizeDisplayValue: string;
@ApiProperty()
domSizeNumericValue: string;
@ApiProperty()
domSizeNumericUnit: string;
}

View File

@@ -0,0 +1,190 @@
import { ApiProperty } from '@nestjs/swagger';
import { Exclude } from 'class-transformer';
import { UpdateWebsiteDto } from '../../website/dto/update-website.dto';
import { IsNumber, IsString } from 'class-validator';
export class UpdatePageSpeedDto {
@ApiProperty()
@IsString()
id: string;
@ApiProperty({ type: () => UpdateWebsiteDto })
website: UpdateWebsiteDto;
@Exclude()
@ApiProperty()
lighthouseObject: any;
@ApiProperty()
@IsString()
firstContentfulPaintDisplayValue: string;
@ApiProperty()
@IsNumber()
firstContentfulPaintScore: number;
@ApiProperty()
@IsNumber()
firstContentfulPaintNumericValue: number;
@ApiProperty()
@IsNumber()
firstMeaningfulPaintScore: number;
@ApiProperty()
@IsString()
firstMeaningfulPaintDisplayValue: string;
@ApiProperty()
@IsNumber()
firstMeaningfulPaintNumericValue: number;
@ApiProperty()
@IsString()
firstMeaningfulPaintNumericUnit: string;
@ApiProperty()
@IsString()
mainThreadWorkBreakdownDisplayValue: string;
@ApiProperty()
@IsNumber()
mainThreadWorkBreakdownNumricValue: number;
@ApiProperty()
@IsString()
mainThreadWorkBreakdownNumericUnit: string;
@ApiProperty({ type: () => [String] })
@IsString()
mainThreadWorkBreakdownItemsGroupLabel: string[];
@ApiProperty({ type: () => [Number] })
@IsNumber()
mainThreadWorkBreakdownItemsDuration: number[];
@ApiProperty()
@IsNumber()
speedIndexScore: number;
@ApiProperty()
@IsString()
speedIndexDisplayValue: string;
@ApiProperty()
@IsNumber()
speedIndexNumericValue: number;
@ApiProperty()
@IsString()
speedIndexNumericUnit: string;
@ApiProperty()
@IsString()
largestContentfulPaintScore: string;
@ApiProperty()
@IsString()
largestContentfulPaintDisplayValue: string;
@ApiProperty()
@IsString()
largestContentfulPaintNumericValue: string;
@ApiProperty()
@IsString()
largestContentfulPaintNumericUnit: string;
@ApiProperty()
@IsString()
totalBlockingTimeScore: string;
@ApiProperty()
@IsString()
totalBlockingTimeDisplayValue: string;
@ApiProperty()
@IsString()
totalBlockingTimeNumericValue: string;
@ApiProperty()
@IsString()
totalBlockingTimeNumericUnit: string;
@ApiProperty({ type: () => [String] })
@IsString()
unusedCssRulesItems: string[];
@ApiProperty()
thirdPartySummaryDisplayValue: string | null;
@ApiProperty({ type: () => [String] })
@IsString()
thirdPartySummaryItemsUrl: string[];
@ApiProperty({ type: () => [Number] })
@IsNumber()
thirdPartySummaryItemsTransfer: number[];
@ApiProperty({ type: () => [Number] })
@IsNumber()
thirdPartySummaryItemsMainThred: number[];
@ApiProperty({ type: () => [Number] })
@IsNumber()
thirdPartySummaryItemsBlockingTime: number[];
@ApiProperty()
@IsString()
timeToInteractiveScore: string;
@ApiProperty()
@IsString()
timeToInteractiveDisplayValue: string;
@ApiProperty()
@IsString()
timeToInteractiveNumericValue: string;
@ApiProperty()
@IsString()
timeToInteractiveNumericUnit: string;
@ApiProperty()
totalByteWeightScore: number | null;
@ApiProperty()
@IsString()
totalByteWeightDisplayValue: string;
@ApiProperty()
@IsNumber()
totalByteWeightNumericValue: number;
@ApiProperty()
@IsString()
totalByteWeightNumericUnit: string;
@ApiProperty({ type: () => [String] })
@IsString()
totalByteWeightItemsUrl: string[];
@ApiProperty({ type: () => [Number] })
@IsNumber()
totalByteWeightItemsTotalBytes: number[];
@ApiProperty()
domSizeScore: number | null;
@ApiProperty()
@IsString()
domSizeDisplayValue: string;
@ApiProperty()
@IsString()
domSizeNumericValue: string;
@ApiProperty()
@IsString()
domSizeNumericUnit: string;
}

View File

@@ -0,0 +1,218 @@
import { ApiProperty } from '@nestjs/swagger';
import { Exclude } from 'class-transformer';
import { Website } from '../../website/entities/website.entity';
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
PrimaryGeneratedColumn,
ManyToOne,
} from 'typeorm';
@Entity()
export class PageSpeedData {
@PrimaryGeneratedColumn('uuid')
@ApiProperty()
id: string;
@CreateDateColumn()
@ApiProperty()
createdAt: Date;
@ManyToOne(() => Website, (website) => website.pageSpeedDatas)
@JoinColumn({ name: 'websiteId' })
@ApiProperty()
website: Website;
@Exclude()
@Column('json')
@ApiProperty({ type: () => [String] })
lighthouseObject: string[];
//#region firstContentfulPaint
@Column()
@ApiProperty()
firstContentfulPaintDisplayValue: string;
@Column()
@ApiProperty()
firstContentfulPaintScore: number;
@Column()
@ApiProperty()
firstContentfulPaintNumericValue: number;
//#endregion
//#region firstMeaningfulPaint
@Column()
@ApiProperty()
firstMeaningfulPaintScore: number;
@Column()
@ApiProperty()
firstMeaningfulPaintDisplayValue: string;
@Column()
@ApiProperty()
firstMeaningfulPaintNumericValue: number;
@Column()
@ApiProperty()
firstMeaningfulPaintNumericUnit: string;
//#endregion
//#region mainThreadWorkBreakdown
@Column()
@ApiProperty()
mainThreadWorkBreakdownDisplayValue: string;
@Column()
@ApiProperty()
mainThreadWorkBreakdownNumricValue: number;
@Column()
@ApiProperty()
mainThreadWorkBreakdownNumericUnit: string;
@Column('json')
@ApiProperty({ type: () => [String] })
mainThreadWorkBreakdownItemsGroupLabel: string[];
@Column('json')
@ApiProperty({ type: () => [Number] })
mainThreadWorkBreakdownItemsDuration: number[];
//#endregion
@Column()
@ApiProperty()
speedIndexScore: number;
@Column()
@ApiProperty()
speedIndexDisplayValue: string;
@Column()
@ApiProperty()
speedIndexNumericValue: number;
@Column()
@ApiProperty()
speedIndexNumericUnit: string;
@Column({ nullable: true })
@ApiProperty()
largestContentfulPaintScore: string;
@Column({ nullable: true })
@ApiProperty()
largestContentfulPaintDisplayValue: string;
@Column({ nullable: true })
@ApiProperty()
largestContentfulPaintNumericValue: string;
@Column({ nullable: true })
@ApiProperty()
largestContentfulPaintNumericUnit: string;
@Column()
@ApiProperty()
totalBlockingTimeScore: string;
@Column()
@ApiProperty()
totalBlockingTimeDisplayValue: string;
@Column()
@ApiProperty()
totalBlockingTimeNumericValue: string;
@Column()
@ApiProperty()
totalBlockingTimeNumericUnit: string;
@Column('json')
@ApiProperty({ type: () => [String], nullable: true })
unusedCssRulesItems: string[];
@Column({ nullable: true })
@ApiProperty()
thirdPartySummaryDisplayValue: string | null;
@Column('json', { nullable: true })
@ApiProperty({ type: () => [String] })
thirdPartySummaryItemsUrl: string[];
@Column('json', { nullable: true })
@ApiProperty({ type: () => [Number] })
thirdPartySummaryItemsTransfer: number[];
@Column('json', { nullable: true })
@ApiProperty({ type: () => [Number] })
thirdPartySummaryItemsMainThred: number[];
@Column('json', { nullable: true })
@ApiProperty({ type: () => [Number] })
thirdPartySummaryItemsBlockingTime: number[];
@Column()
@ApiProperty()
timeToInteractiveScore: string;
@Column()
@ApiProperty()
timeToInteractiveDisplayValue: string;
@Column()
@ApiProperty()
timeToInteractiveNumericValue: string;
@Column()
@ApiProperty()
timeToInteractiveNumericUnit: string;
@Column({ nullable: true })
@ApiProperty()
totalByteWeightScore: number | null;
@Column()
@ApiProperty()
totalByteWeightDisplayValue: string;
@Column()
@ApiProperty()
totalByteWeightNumericValue: number;
@Column()
@ApiProperty()
totalByteWeightNumericUnit: string;
@Column('json')
@ApiProperty({ type: () => [String] })
totalByteWeightItemsUrl: string[];
@Column('json')
@ApiProperty({ type: () => [Number] })
totalByteWeightItemsTotalBytes: number[];
@Column({ nullable: true })
@ApiProperty()
domSizeScore: number | null;
@Column()
@ApiProperty()
domSizeDisplayValue: string;
@Column()
@ApiProperty()
domSizeNumericValue: string;
@Column()
@ApiProperty()
domSizeNumericUnit: string;
@Column('json')
@ApiProperty({ type: () => [String], nullable: true })
unusedJavaScript: string[];
}

View File

@@ -0,0 +1,20 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PagespeedController } from './pagespeed.controller';
import { PagespeedService } from './pagespeed.service';
describe('PagespeedController', () => {
let controller: PagespeedController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [PagespeedController],
providers: [PagespeedService],
}).compile();
controller = module.get<PagespeedController>(PagespeedController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@@ -0,0 +1,4 @@
import { Controller } from '@nestjs/common';
@Controller('pagespeed')
export class PagespeedController {}

View File

@@ -0,0 +1,21 @@
import { HttpModule } from '@nestjs/axios';
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { PageSpeedData } from './entities/pagespeeddata.entity';
import { PagespeedController } from './pagespeed.controller';
import { PagespeedService } from './pagespeed.service';
import { WebsiteModule } from '../website/website.module';
@Module({
imports: [
HttpModule,
ConfigModule,
WebsiteModule,
TypeOrmModule.forFeature([PageSpeedData]),
],
controllers: [PagespeedController],
providers: [PagespeedService],
exports: [PagespeedService],
})
export class PagespeedModule {}

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PagespeedService } from './pagespeed.service';
describe('PagespeedService', () => {
let service: PagespeedService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [PagespeedService],
}).compile();
service = module.get<PagespeedService>(PagespeedService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@@ -0,0 +1,78 @@
import { HttpService } from '@nestjs/axios';
import { HttpException, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectRepository } from '@nestjs/typeorm';
import { firstValueFrom } from 'rxjs';
import { map } from 'rxjs/operators';
import { Repository } from 'typeorm';
import { WebsiteService } from '../website/website.service';
import { PageSpeedData } from './entities/pagespeeddata.entity';
import {
convertDTOToEntity,
createPageSpeedDTOFromApiResponse,
} from './pagespeed.utils';
@Injectable()
export class PagespeedService {
constructor(
private readonly httpService: HttpService,
private readonly configService: ConfigService,
private readonly websiteService: WebsiteService,
@InjectRepository(PageSpeedData)
private readonly pageSpeedRepository: Repository<PageSpeedData>,
) {}
API_KEY = this.configService.get<string>('API_KEY');
//#region Request with URL and API Key to Google Lighthouse to get PageSpeedResults
// It's essential to note that the Google API only accepts URLs with a protocol (such as "http://" or "https://").
// If a domain is registered only with a subdomain, you can only test it with the subdomain included.
// Without a subdomain, it will result in a 500 Error.
async pageSpeedRequest(url: string): Promise<any> {
if (!url.startsWith('https://') && !url.startsWith('http://')) {
url = 'https://' + url;
}
const expression = new RegExp(
/(https:\/\/www\.|http:\/\/www\.|https:\/\/|http:\/\/)?[a-zA-Z]{2,}(\.[a-zA-Z]{2,})(\.[a-zA-Z]{2,})?\/[a-zA-Z0-9]{2,}|((https:\/\/www\.|http:\/\/www\.|https:\/\/|http:\/\/)?[a-zA-Z]{2,}(\.[a-zA-Z]{2,})(\.[a-zA-Z]{2,})?)|(https:\/\/www\.|http:\/\/www\.|https:\/\/|http:\/\/)?[a-zA-Z0-9]{2,}\.[a-zA-Z0-9]{2,}\.[a-zA-Z0-9]{2,}(\.[a-zA-Z0-9]{2,})?/g,
);
if (!url || url === '' || url.match(expression) === null) {
throw new HttpException(
"Bad Request: The URL must begin with 'http://' or 'https://' and have a valid domain format.",
400,
);
}
const encodedUrl = encodeURI(url);
const apiUrl = `https://www.googleapis.com/pagespeedonline/v5/runPagespeed?url=${encodedUrl}&key=${this.API_KEY}`;
const response$ = this.httpService
.get(apiUrl)
.pipe(map((response) => response.data));
const data = await firstValueFrom(response$);
return data;
}
//#endregion
async getPageSpeedResult(
url: string,
websiteId: string,
): Promise<PageSpeedData> {
const data = await this.pageSpeedRequest(url);
const website = await this.websiteService.getWebsiteById(websiteId);
if (!website) {
throw new Error(`Website with WebsiteID ${websiteId} not found`);
}
// Create DTO with API results, with request "data"
const pageSpeedDTO = createPageSpeedDTOFromApiResponse(data);
// Convert DTO to Entity it is a function in utils that gets 2 objets to convertDTOToEntity in entitys.
const entity = convertDTOToEntity(pageSpeedDTO, website);
// Save Entity into Database the entity that crated in the convertfunction will saved in the entity
await this.pageSpeedRepository.save(entity);
return entity;
}
async getPageSpeedsByWebsiteId(websiteId: string): Promise<PageSpeedData[]> {
return await this.pageSpeedRepository.find({
where: { website: { id: websiteId } },
});
}
async getAllPageSpeeds(webId: string): Promise<PageSpeedData[]> {
return await this.pageSpeedRepository.find({ where: { id: webId } });
}
}

View File

@@ -0,0 +1,319 @@
import { Website } from '../website/entities/website.entity';
import { CreatePageSpeedDto } from './dto/create-pagespeed.dto';
import { PageSpeedData } from './entities/pagespeeddata.entity';
export function createPageSpeedDTOFromApiResponse(
data: any,
): CreatePageSpeedDto {
const pageSpeedDTO = new CreatePageSpeedDto();
// Datas from pagespeed request
const mainLighthouseObjet = data.lighthouseResult;
const firstContentfulPaintData =
data.lighthouseResult.audits['first-contentful-paint'];
const firstMeaningfulPaintData =
data.lighthouseResult.audits['first-meaningful-paint'];
const mainThreadWorkBreakdownData =
data.lighthouseResult.audits['mainthread-work-breakdown'];
const unusedCssRulesData = data.lighthouseResult.audits['unused-css-rules'];
const speedIndexData = data.lighthouseResult.audits['speed-index'];
const thirdPartySummaryData =
data.lighthouseResult.audits['third-party-summary'];
const totalByteWeightData = data.lighthouseResult.audits['total-byte-weight'];
const totalBlockingTimeData =
data.lighthouseResult.audits['total-blocking-time'];
const timeToInteractiveData = data.lighthouseResult.audits['interactive'];
const domSizeData = data.lighthouseResult.audits['dom-size'];
const largestContentfulPaintData =
data.lighthouseResult.audits['largest-contentful-paint'];
const unusedJavaScript = data.lighthouseResult.audits['unused-javascript'];
// Save howl object
pageSpeedDTO.lighthouseObject = mainLighthouseObjet;
// First Contentful Paint Data
pageSpeedDTO.firstContentfulPaintScore = firstContentfulPaintData.score;
pageSpeedDTO.firstContentfulPaintNumericValue =
firstContentfulPaintData.numericUnit;
pageSpeedDTO.firstContentfulPaintNumericValue =
firstContentfulPaintData.numericValue;
pageSpeedDTO.firstContentfulPaintDisplayValue =
firstContentfulPaintData.displayValue;
// First Meaningful Paint Data
pageSpeedDTO.firstMeaningfulPaintScore = firstMeaningfulPaintData.score;
pageSpeedDTO.firstMeaningfulPaintNumericValue =
firstMeaningfulPaintData.numericValue;
pageSpeedDTO.firstMeaningfulPaintNumericUnit =
firstMeaningfulPaintData.numericUnit;
pageSpeedDTO.firstMeaningfulPaintDisplayValue =
firstMeaningfulPaintData.displayValue;
// Main Thread Work Breakdown Data
pageSpeedDTO.mainThreadWorkBreakdownDisplayValue =
mainThreadWorkBreakdownData.displayValue;
pageSpeedDTO.mainThreadWorkBreakdownNumricValue =
mainThreadWorkBreakdownData.numericValue;
pageSpeedDTO.mainThreadWorkBreakdownNumericUnit =
mainThreadWorkBreakdownData.numericUnit;
pageSpeedDTO.mainThreadWorkBreakdownItemsDuration = [];
const items = mainThreadWorkBreakdownData.details.items;
for (let i = 0; i < items.length; i++) {
const item = items[i];
pageSpeedDTO.mainThreadWorkBreakdownItemsDuration.push(item.duration);
}
pageSpeedDTO.mainThreadWorkBreakdownItemsGroupLabel = [];
const group = mainThreadWorkBreakdownData.details.items;
for (let i = 0; i < group.length; i++) {
const item = group[i];
pageSpeedDTO.mainThreadWorkBreakdownItemsGroupLabel.push(item.groupLabel);
}
// Largest Contentful Paint Data
pageSpeedDTO.largestContentfulPaintScore = largestContentfulPaintData.score;
pageSpeedDTO.largestContentfulPaintDisplayValue =
largestContentfulPaintData.displayValue;
pageSpeedDTO.largestContentfulPaintNumericValue =
largestContentfulPaintData.numericValue;
pageSpeedDTO.largestContentfulPaintNumericUnit =
largestContentfulPaintData.numericUnit;
// Unused CSS Rules Data
if (unusedCssRulesData && unusedCssRulesData.details) {
pageSpeedDTO.unusedCssRulesItems = [];
const cssdata = unusedCssRulesData.details.items;
for (let i = 0; i < cssdata.length; i++) {
const item = cssdata[i];
pageSpeedDTO.unusedCssRulesItems.push(item);
}
}
// Unused JavaScript
if (unusedJavaScript && unusedJavaScript.details) {
pageSpeedDTO.unusedJavaScript = [];
const javadata = unusedJavaScript.details.items;
for (let i = 0; i < javadata.length; i++) {
const item = javadata[i];
pageSpeedDTO.unusedJavaScript.push(item);
}
}
// Speed Index Data
pageSpeedDTO.speedIndexScore = speedIndexData.score;
pageSpeedDTO.speedIndexDisplayValue = speedIndexData.displayValue;
pageSpeedDTO.speedIndexNumericValue = speedIndexData.numericValue;
pageSpeedDTO.speedIndexNumericUnit = speedIndexData.numericUnit;
// Third Party Summary Data
pageSpeedDTO.thirdPartySummaryDisplayValue =
thirdPartySummaryData.displayValue;
// Third Party Summary Url
if (
thirdPartySummaryData &&
thirdPartySummaryData.detail &&
thirdPartySummaryData.detail.items
) {
pageSpeedDTO.thirdPartySummaryItemsUrl = [];
for (let i = 0; i < thirdPartySummaryData.details.items.length; i++) {
const item = thirdPartySummaryData.details.items[i];
for (let j = 0; j < item.subItems.items.length; j++) {
const subItem = item.subItems.items[j];
pageSpeedDTO.thirdPartySummaryItemsUrl.push(subItem.url);
}
}
}
// Third Party Summary Transfer Size
if (
thirdPartySummaryData &&
thirdPartySummaryData.details &&
thirdPartySummaryData.details.items
) {
pageSpeedDTO.thirdPartySummaryItemsTransfer = [];
for (let i = 0; i < thirdPartySummaryData.details.items.length; i++) {
const item = thirdPartySummaryData.details.items[i];
for (let j = 0; j < item.subItems.items.length; j++) {
const subItem = item.subItems.items[j];
pageSpeedDTO.thirdPartySummaryItemsTransfer.push(subItem.transferSize);
}
}
}
// Third Party Summary Main Thread Time
if (
thirdPartySummaryData &&
thirdPartySummaryData.details &&
thirdPartySummaryData.details.items
) {
pageSpeedDTO.thirdPartySummaryItemsMainThred = [];
for (let i = 0; i < thirdPartySummaryData.details.items.length; i++) {
const item = thirdPartySummaryData.details.items[i];
for (let j = 0; j < item.subItems.items.length; j++) {
const subItem = item.subItems.items[j];
pageSpeedDTO.thirdPartySummaryItemsMainThred.push(
subItem.mainThreadTime,
);
}
}
}
// Third Party Summary Blocking Time
if (
thirdPartySummaryData &&
thirdPartySummaryData.details &&
thirdPartySummaryData.details.items
) {
pageSpeedDTO.thirdPartySummaryItemsBlockingTime = [];
for (let i = 0; i < thirdPartySummaryData.details.items.length; i++) {
const item = thirdPartySummaryData.details.items[i];
for (let j = 0; j < item.subItems.items.length; j++) {
const subItem = item.subItems.items[j];
pageSpeedDTO.thirdPartySummaryItemsBlockingTime.push(
subItem.blockingTime,
);
}
}
}
// Total Byte Weight Data
pageSpeedDTO.totalByteWeightScore = totalByteWeightData.score;
pageSpeedDTO.totalByteWeightDisplayValue = totalByteWeightData.displayValue;
pageSpeedDTO.totalByteWeightNumericValue = totalByteWeightData.numericValue;
pageSpeedDTO.totalByteWeightNumericUnit = totalByteWeightData.numericUnit;
pageSpeedDTO.totalByteWeightItemsUrl = [];
const byteUrl = totalByteWeightData.details.items;
for (let i = 0; i < byteUrl.length; i++) {
const item = byteUrl[i];
pageSpeedDTO.totalByteWeightItemsUrl.push(item.url);
}
pageSpeedDTO.totalByteWeightItemsTotalBytes = [];
const totalByte = totalByteWeightData.details.items;
for (let i = 0; i < totalByte.length; i++) {
const item = totalByte[i];
pageSpeedDTO.totalByteWeightItemsUrl.push(item.url);
}
// Total Blocking Time Data
pageSpeedDTO.totalBlockingTimeScore = totalBlockingTimeData.score;
pageSpeedDTO.totalBlockingTimeDisplayValue =
totalBlockingTimeData.displayValue;
pageSpeedDTO.totalBlockingTimeNumericValue =
totalBlockingTimeData.numericValue;
pageSpeedDTO.totalBlockingTimeNumericUnit = totalBlockingTimeData.numericUnit;
// Time To Interactive Data
pageSpeedDTO.timeToInteractiveScore = timeToInteractiveData.score;
pageSpeedDTO.timeToInteractiveDisplayValue =
timeToInteractiveData.displayValue;
pageSpeedDTO.timeToInteractiveNumericValue =
timeToInteractiveData.numericValue;
pageSpeedDTO.timeToInteractiveNumericUnit = timeToInteractiveData.numericUnit;
// DOM Size Data
pageSpeedDTO.domSizeScore = domSizeData.score;
pageSpeedDTO.domSizeDisplayValue = domSizeData.displayValue;
pageSpeedDTO.domSizeNumericValue = domSizeData.numericValue;
pageSpeedDTO.domSizeNumericUnit = domSizeData.numericUnit;
return pageSpeedDTO;
}
export function convertDTOToEntity(
dto: CreatePageSpeedDto,
website: Website,
): PageSpeedData {
// Create new PageSpeedData entity
const entity = new PageSpeedData();
// Datas from pagespeed request
if (!website) {
throw new Error(`Website with WebsiteID ${website} not found`);
}
entity.website = website;
// Save howl object
entity.lighthouseObject = dto.lighthouseObject;
// First Contentful Paint Data
entity.firstContentfulPaintScore = dto.firstContentfulPaintScore;
entity.firstContentfulPaintNumericValue =
dto.firstContentfulPaintNumericValue;
entity.firstContentfulPaintDisplayValue =
dto.firstContentfulPaintDisplayValue;
// First Meaningful Paint Data
entity.firstMeaningfulPaintScore = dto.firstMeaningfulPaintScore;
entity.firstMeaningfulPaintNumericValue =
dto.firstMeaningfulPaintNumericValue;
entity.firstMeaningfulPaintNumericUnit = dto.firstMeaningfulPaintNumericUnit;
entity.firstMeaningfulPaintDisplayValue =
dto.firstMeaningfulPaintDisplayValue;
// Main Thread Work Breakdown Data
entity.mainThreadWorkBreakdownDisplayValue =
dto.mainThreadWorkBreakdownDisplayValue;
entity.mainThreadWorkBreakdownNumricValue =
dto.mainThreadWorkBreakdownNumricValue;
entity.mainThreadWorkBreakdownNumericUnit =
dto.mainThreadWorkBreakdownNumericUnit;
entity.mainThreadWorkBreakdownItemsDuration =
dto.mainThreadWorkBreakdownItemsDuration;
entity.mainThreadWorkBreakdownItemsGroupLabel =
dto.mainThreadWorkBreakdownItemsGroupLabel;
// Largest Contentful Paint Data
entity.largestContentfulPaintScore = dto.largestContentfulPaintScore;
entity.largestContentfulPaintDisplayValue =
dto.largestContentfulPaintDisplayValue;
entity.largestContentfulPaintNumericValue =
dto.largestContentfulPaintNumericValue;
entity.largestContentfulPaintNumericUnit =
dto.largestContentfulPaintNumericUnit;
// Unused CSS Rules Data
entity.unusedCssRulesItems = dto.unusedCssRulesItems;
// Unused Java Script Data
entity.unusedJavaScript = dto.unusedJavaScript;
// Speed Index Data
entity.speedIndexScore = dto.speedIndexScore;
entity.speedIndexDisplayValue = dto.speedIndexDisplayValue;
entity.speedIndexNumericValue = dto.speedIndexNumericValue;
entity.speedIndexNumericUnit = dto.speedIndexNumericUnit;
// Third Party Summary Data
entity.thirdPartySummaryDisplayValue = dto.thirdPartySummaryDisplayValue;
// Third Party Summary Url
entity.thirdPartySummaryItemsUrl = dto.thirdPartySummaryItemsUrl;
// Third Party Summary Transfer Size
entity.thirdPartySummaryItemsTransfer = dto.thirdPartySummaryItemsTransfer;
// Third Party Summary Main Thread Time
entity.thirdPartySummaryItemsMainThred = dto.thirdPartySummaryItemsMainThred;
// Third Party Summary Blocking Time
entity.thirdPartySummaryItemsBlockingTime =
dto.thirdPartySummaryItemsBlockingTime;
// Total Byte Weight Data
entity.totalByteWeightScore = dto.totalByteWeightScore;
entity.totalByteWeightDisplayValue = dto.totalByteWeightDisplayValue;
entity.totalByteWeightNumericValue = dto.totalByteWeightNumericValue;
entity.totalByteWeightNumericUnit = dto.totalByteWeightNumericUnit;
entity.totalByteWeightItemsUrl = dto.totalByteWeightItemsUrl;
entity.totalByteWeightItemsTotalBytes = dto.totalByteWeightItemsTotalBytes;
// Total Blocking Time Data
entity.totalBlockingTimeScore = dto.totalBlockingTimeScore;
entity.totalBlockingTimeDisplayValue = dto.totalBlockingTimeDisplayValue;
entity.totalBlockingTimeNumericValue = dto.totalBlockingTimeNumericValue;
entity.totalBlockingTimeNumericUnit = dto.totalBlockingTimeNumericUnit;
// Time To Interactive Data
entity.timeToInteractiveScore = dto.timeToInteractiveScore;
entity.timeToInteractiveDisplayValue = dto.timeToInteractiveDisplayValue;
entity.timeToInteractiveNumericValue = dto.timeToInteractiveNumericValue;
entity.timeToInteractiveNumericUnit = dto.timeToInteractiveNumericUnit;
// DOM Size Data
entity.domSizeScore = dto.domSizeScore;
entity.domSizeDisplayValue = dto.domSizeDisplayValue;
entity.domSizeNumericValue = dto.domSizeNumericValue;
entity.domSizeNumericUnit = dto.domSizeNumericUnit;
return entity;
}

View File

@@ -0,0 +1,29 @@
import { CreateCustomerDto } from '../../customer/dto/create-customer.dto';
import { CreatePageSpeedDto } from '../../pagespeed/dto/create-pagespeed.dto';
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class CreateWebsiteDto {
@ApiProperty({ description: 'Website Id' })
@IsString()
id: string;
@ApiProperty({ description: 'Company Name' })
@IsString()
displayName: string;
@ApiProperty({ type: () => CreateCustomerDto, description: 'Forign Key' })
@IsNotEmpty()
customer: { id: string };
@ApiProperty({ description: 'Website Neme' })
@IsString()
url: string;
@ApiProperty({
type: () => CreatePageSpeedDto,
description: 'PageSpeed Association',
})
@IsNotEmpty()
pageSpeedDatas: CreatePageSpeedDto[];
}

View File

@@ -0,0 +1,24 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString } from 'class-validator';
import { LoadCustomerDto } from '../../customer/dto/load-customer.dto';
import { LoadPageSpeedDto } from '../../pagespeed/dto/load-pagespeed.dto';
export class LoadWebsiteDto {
@ApiProperty()
@IsString()
websiteId: string;
@ApiProperty()
@IsString()
displayName: string;
@ApiProperty({ type: () => LoadCustomerDto })
customer: LoadCustomerDto;
@ApiProperty()
@IsString()
url: string;
@ApiProperty({ type: () => LoadPageSpeedDto })
pageSpeedDatas: LoadPageSpeedDto[];
}

View File

@@ -0,0 +1,24 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsString } from 'class-validator';
import { UpdateCustomerDto } from '../../customer/dto/update-customer.dto';
import { UpdatePageSpeedDto } from '../../pagespeed/dto/update-pagespeed.dto';
export class UpdateWebsiteDto {
@ApiProperty()
@IsString()
id: string;
@ApiProperty()
@IsString()
displayName: string;
@ApiProperty({ type: () => UpdateCustomerDto })
customer: UpdateCustomerDto;
@ApiProperty()
@IsString()
url: string;
@ApiProperty({ type: () => UpdatePageSpeedDto })
pageSpeedDatas: UpdatePageSpeedDto[];
}

View File

@@ -0,0 +1,39 @@
import {
Entity,
OneToMany,
PrimaryGeneratedColumn,
ManyToOne,
JoinColumn,
} from 'typeorm';
import { Column } from 'typeorm';
import { ApiProperty } from '@nestjs/swagger';
import { PageSpeedData } from '../../pagespeed/entities/pagespeeddata.entity';
import { Customer } from '../../customer/entities/customer.entity';
@Entity()
export class Website {
@PrimaryGeneratedColumn('uuid')
@ApiProperty()
id: string;
@Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
@ApiProperty()
createdAt: Date;
@Column()
@ApiProperty()
displayName: string;
@ManyToOne(() => Customer, (customer) => customer.websites)
@JoinColumn({ name: 'customerId' })
@ApiProperty()
customers: Customer;
@Column({ nullable: true })
@ApiProperty()
url: string;
@OneToMany(() => PageSpeedData, (pageSpeedData) => pageSpeedData.website)
@ApiProperty()
pageSpeedDatas: PageSpeedData[];
}

View File

@@ -0,0 +1,20 @@
import { Test, TestingModule } from '@nestjs/testing';
import { WebsiteController } from './website.controller';
import { WebsiteService } from './website.service';
describe('WebsiteController', () => {
let controller: WebsiteController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [WebsiteController],
providers: [WebsiteService],
}).compile();
controller = module.get<WebsiteController>(WebsiteController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@@ -0,0 +1,4 @@
import { Controller } from '@nestjs/common';
@Controller('website')
export class WebsiteController {}

View File

@@ -0,0 +1,25 @@
import { HttpModule } from '@nestjs/axios';
import { Module, forwardRef } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Customer } from '../customer/entities/customer.entity';
import { PageSpeedData } from '../pagespeed/entities/pagespeeddata.entity';
import { Website } from './entities/website.entity';
import { WebsiteController } from './website.controller';
import { WebsiteService } from './website.service';
import { CustomerModule } from '../customer/customer.module';
import { ConfigModule } from '@nestjs/config';
import { PagespeedModule } from '../pagespeed/pagespeed.module';
@Module({
imports: [
TypeOrmModule.forFeature([Customer, Website, PageSpeedData]),
forwardRef(() => PagespeedModule),
CustomerModule,
HttpModule,
ConfigModule,
],
controllers: [WebsiteController],
providers: [WebsiteService],
exports: [WebsiteService, TypeOrmModule],
})
export class WebsiteModule {}

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { WebsiteService } from './website.service';
describe('WebsiteService', () => {
let service: WebsiteService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [WebsiteService],
}).compile();
service = module.get<WebsiteService>(WebsiteService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@@ -0,0 +1,69 @@
import {
BadRequestException,
Injectable,
InternalServerErrorException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Customer } from '../customer/entities/customer.entity';
import { CreateWebsiteDto } from './dto/create-website.dto';
import { Website } from './entities/website.entity';
@Injectable()
export class WebsiteService {
constructor(
@InjectRepository(Customer)
private customerRepository: Repository<Customer>,
@InjectRepository(Website)
private websiteRepository: Repository<Website>,
) {}
async createOrUpdateWebsite(
data: Partial<CreateWebsiteDto>,
websiteId?: Website['id'],
): Promise<Website> {
if (!data.customer.id) {
throw new InternalServerErrorException();
}
const customer = await this.customerRepository.findOne({
where: { id: data.customer.id },
});
if (!customer) {
throw new BadRequestException(
`Customer with id ${data.customer.id} not found`,
);
}
await this.websiteRepository.upsert(
{
id: websiteId,
url: data.url,
displayName: data.displayName,
customers: customer,
pageSpeedDatas: data.pageSpeedDatas,
},
['id'],
);
return this.websiteRepository.findOne({ where: { id: websiteId } });
}
async getAllWebsites(): Promise<Website[]> {
return this.websiteRepository.find();
}
async getWebsiteById(id: Website['id']): Promise<Website> {
return this.websiteRepository.findOne({ where: { id: id } });
}
async getAllWebsitesByCustomerId(id: string): Promise<Website[]> {
return this.websiteRepository
.createQueryBuilder('website')
.leftJoinAndSelect('website.customer', 'customer')
.where('customer.id = :id', { id })
.getMany();
}
async getWebsiteByDisplayName(displayName: string): Promise<Website> {
return this.websiteRepository.findOne({
where: { displayName: displayName },
});
}
}

24
test/app.e2e-spec.ts Normal file
View File

@@ -0,0 +1,24 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';
describe('AppController (e2e)', () => {
let app: INestApplication;
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!');
});
});

9
test/jest-e2e.json Normal file
View File

@@ -0,0 +1,9 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}

4
tsconfig.build.json Normal file
View File

@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

21
tsconfig.json Normal file
View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2021",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": false,
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false
}
}