initial commit
This commit is contained in:
parent
f7429cb3cd
commit
100d018d3c
27 changed files with 15616 additions and 2 deletions
8
.env
Normal file
8
.env
Normal file
|
@ -0,0 +1,8 @@
|
|||
DATABASE_HOST=localhost
|
||||
DATABASE_PORT=5432
|
||||
DATABASE_TEST_PORT=5433
|
||||
DATABASE_USERNAME=user
|
||||
DATABASE_PASSWORD=pwd
|
||||
DATABASE_NAME=database
|
||||
MIGRATIONS_RUN=true
|
||||
NODE_ENV=development
|
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
node_modules
|
||||
dist
|
10
.vscode/settings.json
vendored
Normal file
10
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"editor.formatOnSave": true,
|
||||
"typescript.preferences.importModuleSpecifier": "relative",
|
||||
"editor.codeActionsOnSave": [
|
||||
"source.fixAll",
|
||||
"source.organizeImports",
|
||||
"source.addMissingImports"
|
||||
],
|
||||
}
|
||||
|
96
README.md
96
README.md
|
@ -1,2 +1,94 @@
|
|||
# public-hiring-test
|
||||
Technical tests for hiring software engineering @Greenly
|
||||
# How to use
|
||||
|
||||
Stack: NestJs + TypeORM + Postgres
|
||||
|
||||
## Requirements
|
||||
|
||||
- Docker / Docker Compose
|
||||
- Node.js 18
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
$ yarn
|
||||
```
|
||||
|
||||
## Running the app
|
||||
|
||||
First you need to start, migrate and seed the db :
|
||||
|
||||
```bash
|
||||
$ yarn init-project
|
||||
```
|
||||
|
||||
you can then start the server:
|
||||
|
||||
```bash
|
||||
# development
|
||||
$ yarn start
|
||||
|
||||
# watch mode
|
||||
$ yarn start:dev
|
||||
```
|
||||
|
||||
## Test
|
||||
|
||||
To run unit tests:
|
||||
|
||||
```bash
|
||||
# unit tests
|
||||
$ yarn test
|
||||
|
||||
# e2e tests
|
||||
$ yarn test:e2e
|
||||
|
||||
# test coverage
|
||||
$ yarn test:cov
|
||||
```
|
||||
|
||||
## Database Migration
|
||||
|
||||
When the data schema is updated, the database needs to be synchronised with the code. This is done by creating a migration with Typeorm using the following command:
|
||||
|
||||
```bash
|
||||
migrationName=<name> yarn migration:generate
|
||||
```
|
||||
|
||||
##################################################################################################################################
|
||||
|
||||
# Hiring Test
|
||||
|
||||
##################################################################################################################################
|
||||
|
||||
When working on the following exercise, in addition to answering the product need, to give particular attention to the following points:
|
||||
|
||||
- Readability
|
||||
- Maintainability
|
||||
- Unit testing
|
||||
- Handling of corner cases
|
||||
- Error-handling
|
||||
|
||||
We want to compute the Agrybalise carbonfootprint of a foodproduct (e.g.: a hamCheesePizza) that we characterize by its ingredients as shown below
|
||||
|
||||
```js
|
||||
const hamCheesePizza = {
|
||||
ingredients: [
|
||||
{ name: "ham", quantity: 0.1, unit: "kg" },
|
||||
{ name: "cheese", quantity: 0.15, unit: "kg" },
|
||||
{ name: "tomato", quantity: 0.4, unit: "kg" },
|
||||
{ name: "floor", quantity: 0.7, unit: "kg" },
|
||||
{ name: "oliveOil", quantity: 0.3, unit: "kg" },
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
The calculation of the Agrybalise carbon footprint can be described as below:
|
||||
|
||||
- The Agrybalise carbon footprint of one ingredient is obtained by multiplying the quantity of the ingredient by the emission of a matching emission factor (same name and same unit).
|
||||
- The carbon footprint of the food product is then obtained by summing the carbon footprint of all ingredients.
|
||||
- If the carbon footprint of one ingredient cannot be calculated, then the carbon footprint of the whole product is set to null.
|
||||
|
||||
The tasks of this exercice are as follows:
|
||||
1/ Implement the carbon footprint calculation of a product and persist the results in database.
|
||||
2/ Implement a GET endpoint to retrieve the result.
|
||||
3/ Implement a POST endpoint to trigger the calculation and the saving in the database.
|
||||
|
|
63
config/dataSource.ts
Normal file
63
config/dataSource.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
import { registerAs } from "@nestjs/config";
|
||||
import { config as dotenvConfig } from "dotenv";
|
||||
import { join } from "path";
|
||||
import { DataSource, DataSourceOptions } from "typeorm";
|
||||
|
||||
dotenvConfig({ path: ".env" });
|
||||
|
||||
const dataSourceOptions: DataSourceOptions = {
|
||||
type: "postgres",
|
||||
port: parseInt(
|
||||
`${process.env.NODE_ENV === "test" ? process.env.DATABASE_TEST_PORT : process.env.DATABASE_PORT}`
|
||||
),
|
||||
username: process.env.DATABASE_USERNAME,
|
||||
password: process.env.DATABASE_PASSWORD,
|
||||
database: process.env.DATABASE_NAME,
|
||||
synchronize: false,
|
||||
logging: false,
|
||||
entities: [join(__dirname, "../src/**/*.entity.{js,ts}")],
|
||||
migrations: [join(__dirname, "../migrations", "*.*")],
|
||||
migrationsTableName: "migrations",
|
||||
migrationsRun: process.env.MIGRATIONS_RUN === "true",
|
||||
};
|
||||
|
||||
export const typeorm = registerAs("typeorm", () => dataSourceOptions);
|
||||
|
||||
export class GreenlyDataSource {
|
||||
private static dataSource: DataSource;
|
||||
private static testDataSource: DataSource;
|
||||
|
||||
public static getInstance() {
|
||||
if (process.env.NODE_ENV === "test") {
|
||||
if (this.testDataSource) {
|
||||
console.log(`testDataSource already set for ${process.env.NODE_ENV}`);
|
||||
return this.testDataSource;
|
||||
}
|
||||
this.testDataSource = new DataSource(dataSourceOptions);
|
||||
|
||||
return this.testDataSource;
|
||||
}
|
||||
if (this.dataSource) {
|
||||
console.log(`dataSource already set for ${process.env.NODE_ENV}`);
|
||||
return this.dataSource;
|
||||
}
|
||||
this.dataSource = new DataSource(dataSourceOptions);
|
||||
|
||||
return this.dataSource;
|
||||
}
|
||||
|
||||
public static async cleanDatabase(): Promise<void> {
|
||||
try {
|
||||
const entities = dataSource.entityMetadatas;
|
||||
const tableNames = entities
|
||||
.map((entity) => `"${entity.tableName}"`)
|
||||
.join(", ");
|
||||
|
||||
await dataSource.query(`TRUNCATE ${tableNames} CASCADE;`);
|
||||
} catch (error) {
|
||||
throw new Error(`ERROR: Cleaning test database: ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const dataSource = GreenlyDataSource.getInstance();
|
20
docker-compose.yml
Normal file
20
docker-compose.yml
Normal file
|
@ -0,0 +1,20 @@
|
|||
services:
|
||||
db:
|
||||
image: postgres
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_USER: ${DATABASE_USERNAME}
|
||||
POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
|
||||
POSTGRES_DB: ${DATABASE_NAME}
|
||||
ports:
|
||||
- "${DATABASE_PORT}:5432"
|
||||
db_test:
|
||||
image: postgres
|
||||
restart: always
|
||||
environment:
|
||||
POSTGRES_USER: ${DATABASE_USERNAME}
|
||||
POSTGRES_PASSWORD: ${DATABASE_PASSWORD}
|
||||
POSTGRES_DB: ${DATABASE_NAME}
|
||||
# PGPORT
|
||||
ports:
|
||||
- "${DATABASE_TEST_PORT}:5432"
|
9
jest.json
Normal file
9
jest.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"moduleFileExtensions": ["js", "json", "ts"],
|
||||
"rootDir": "./src",
|
||||
"testEnvironment": "node",
|
||||
"testRegex": ".test.ts",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
}
|
||||
}
|
15
migrations/1708367794381-carbonEmissionFactor.ts
Normal file
15
migrations/1708367794381-carbonEmissionFactor.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class CarbonEmissionFactor1708367794381 implements MigrationInterface {
|
||||
name = "CarbonEmissionFactor1708367794381";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "carbon_emission_factors" ("id" SERIAL NOT NULL, "name" character varying NOT NULL, "unit" character varying NOT NULL, "emissionCO2eInKgPerUnit" float NOT NULL, "source" character varying NOT NULL, CONSTRAINT "PK_e6a201ea58a7b4cdec0ca1c0c61" PRIMARY KEY ("id"))`
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP TABLE "carbon_emission_factors"`);
|
||||
}
|
||||
}
|
8
nest-cli.json
Normal file
8
nest-cli.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/nest-cli",
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
}
|
||||
}
|
113
notes.txt
Normal file
113
notes.txt
Normal file
|
@ -0,0 +1,113 @@
|
|||
## Sources
|
||||
to integrate typeorm and generate migrations
|
||||
|
||||
https://dev.to/amirfakour/using-typeorm-migration-in-nestjs-with-postgres-database-3c75
|
||||
|
||||
|
||||
|
||||
|
||||
Technical
|
||||
|
||||
- Start from a boiler plate with simple database:
|
||||
-postgre /Typeorm/express//jest
|
||||
|
||||
```js
|
||||
const carbonEmissionFactors = [
|
||||
{
|
||||
Name: "ham",
|
||||
unit: "kg",
|
||||
valueInKgCO2: 0.12,
|
||||
source: "Agrybalise",
|
||||
},
|
||||
{
|
||||
name: cheese,
|
||||
unit: "kg",
|
||||
valueInKgCO2: 0.12,
|
||||
source: "Agrybalise",
|
||||
},
|
||||
{
|
||||
name: tomato,
|
||||
unit: "kg",
|
||||
valueInKgCO2: 0.12,
|
||||
source: "Agrybalise",
|
||||
},
|
||||
{
|
||||
name: oliveOil,
|
||||
unit: l,
|
||||
valueInKgCO2: 0.12,
|
||||
source: "Agrybalise",
|
||||
},
|
||||
];
|
||||
|
||||
```
|
||||
|
||||
|
||||
When working on the following exercise, give particular attention to the following points:
|
||||
Readability of your code
|
||||
Unit Testing
|
||||
Architecture and organization of your functions
|
||||
Handling of corner cases and errors
|
||||
Overall performance of your code
|
||||
|
||||
1/ Create an endpoint to compute carbonFootprint of food and persist data in database.
|
||||
```js
|
||||
const hamCheesePizza = {
|
||||
ingredients: [
|
||||
{ name: "ham", value: "2", unit: "g" },
|
||||
{ name: "cheese", value: "15", unit: "g" },
|
||||
{ name: "tomato", value: "4", unit: "g" },
|
||||
{ name: "floor", value: "7", unit: "g" },
|
||||
{ name: "oliveOil", value: "0.7", unit: "l" },
|
||||
],
|
||||
};
|
||||
```
|
||||
2/Agrybalise has updated its coefficients and we want to update accordingly our knowledge base. In order to do that, we need to develop and endpoint allowing to update and/or insert new values in our referential of emission factors.
|
||||
|
||||
In particular, we want to add these values:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Test Criterion for technical test:
|
||||
Problem solving solution: should solve efficiently the problem
|
||||
Clean code aspects: readability (naming, architecture), test
|
||||
Performance of code & algorithmic
|
||||
Knowledge of database
|
||||
Error handling & exceptions
|
||||
|
||||
Test Criterion for technical test:
|
||||
Clarity of explanation of approach and database
|
||||
General engineering culture
|
||||
Reactivity on advices & inputs
|
||||
|
||||
|
||||
|
||||
|
||||
Questions:
|
||||
|
||||
|
||||
|
||||
2/
|
||||
|
||||
Fonction buggé
|
||||
|
||||
|
||||
Performance
|
||||
|
||||
Asynchronous feature
|
||||
Utiliser un ORM
|
||||
Rest API
|
||||
Base de donnée
|
||||
|
||||
Bonus:
|
||||
authentification
|
9517
package-lock.json
generated
Normal file
9517
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
83
package.json
Normal file
83
package.json
Normal file
|
@ -0,0 +1,83 @@
|
|||
{
|
||||
"name": "food-carbon-calculator",
|
||||
"version": "0.0.1",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
"license": "UNLICENSED",
|
||||
"scripts": {
|
||||
"init-project": "yarn build && yarn start-docker && yarn migration:run && yarn seed",
|
||||
"start": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"start-docker": "docker-compose down --remove-orphans && docker-compose up --remove-orphans -d",
|
||||
"build": "nest build",
|
||||
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||
"test": "NODE_ENV=test jest --config ./jest.json",
|
||||
"test:watch": "jest --watch --config ./jest.json",
|
||||
"test:cov": "jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "NODE_ENV=test jest --config ./test-e2e/jest-e2e.json",
|
||||
"typeorm": "ts-node ./node_modules/typeorm/cli -d ./config/dataSource.ts",
|
||||
"migration:run": "yarn build && yarn typeorm migration:run",
|
||||
"migration:generate": "yarn build && yarn typeorm -- migration:generate ./src/migrations/$migrationName",
|
||||
"seed": "ts-node ./src/seed-dev-data.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^10.0.0",
|
||||
"@nestjs/config": "^3.2.0",
|
||||
"@nestjs/core": "^10.0.0",
|
||||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@nestjs/typeorm": "^10.0.2",
|
||||
"@types/lodash": "^4.14.202",
|
||||
"dotenv": "^16.4.4",
|
||||
"lodash": "^4.17.21",
|
||||
"pg": "^8.11.3",
|
||||
"postgresql": "^0.0.1",
|
||||
"reflect-metadata": "^0.2.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"typeorm": "^0.3.20"
|
||||
},
|
||||
"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.2",
|
||||
"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"
|
||||
}
|
||||
}
|
21
src/app.module.ts
Normal file
21
src/app.module.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { Module } from "@nestjs/common";
|
||||
import { ConfigModule, ConfigService } from "@nestjs/config";
|
||||
import { TypeOrmModule } from "@nestjs/typeorm";
|
||||
import { typeorm } from "../config/dataSource";
|
||||
import { CarbonEmissionFactorsModule } from "./carbonEmissionFactor/carbonEmissionFactors.module";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
load: [typeorm],
|
||||
}),
|
||||
TypeOrmModule.forRootAsync({
|
||||
inject: [ConfigService],
|
||||
useFactory: async (configService: ConfigService) =>
|
||||
configService.getOrThrow("typeorm"),
|
||||
}),
|
||||
CarbonEmissionFactorsModule,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
37
src/carbonEmissionFactor/carbonEmissionFactor.entity.test.ts
Normal file
37
src/carbonEmissionFactor/carbonEmissionFactor.entity.test.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
import { GreenlyDataSource, dataSource } from "../../config/dataSource";
|
||||
import { CarbonEmissionFactor } from "../carbonEmissionFactor/carbonEmissionFactor.entity";
|
||||
|
||||
let chickenEmissionFactor: CarbonEmissionFactor;
|
||||
beforeAll(async () => {
|
||||
await dataSource.initialize();
|
||||
chickenEmissionFactor = new CarbonEmissionFactor({
|
||||
emissionCO2eInKgPerUnit: 2.4,
|
||||
unit: "kg",
|
||||
name: "chicken",
|
||||
source: "Agrybalise",
|
||||
});
|
||||
});
|
||||
beforeEach(async () => {
|
||||
await GreenlyDataSource.cleanDatabase();
|
||||
});
|
||||
describe("FoodProductEntity", () => {
|
||||
describe("constructor", () => {
|
||||
it("should create an emission factor", () => {
|
||||
expect(chickenEmissionFactor.name).toBe("chicken");
|
||||
});
|
||||
it("should throw an error if the source is empty", () => {
|
||||
expect(() => {
|
||||
const carbonEmissionFactor = new CarbonEmissionFactor({
|
||||
emissionCO2eInKgPerUnit: 2.4,
|
||||
unit: "kg",
|
||||
name: "chicken",
|
||||
source: "",
|
||||
});
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await dataSource.destroy();
|
||||
});
|
49
src/carbonEmissionFactor/carbonEmissionFactor.entity.ts
Normal file
49
src/carbonEmissionFactor/carbonEmissionFactor.entity.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
||||
|
||||
@Entity("carbon_emission_factors")
|
||||
export class CarbonEmissionFactor extends BaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({
|
||||
nullable: false,
|
||||
})
|
||||
name: string;
|
||||
|
||||
@Column({
|
||||
nullable: false,
|
||||
})
|
||||
unit: string;
|
||||
|
||||
@Column({
|
||||
type: "float",
|
||||
nullable: false,
|
||||
})
|
||||
emissionCO2eInKgPerUnit: number;
|
||||
|
||||
@Column({
|
||||
nullable: false,
|
||||
})
|
||||
source: string;
|
||||
|
||||
sanitize() {
|
||||
if (this.source === "") {
|
||||
throw new Error("Source cannot be empty");
|
||||
}
|
||||
}
|
||||
|
||||
constructor(props: {
|
||||
name: string;
|
||||
unit: string;
|
||||
emissionCO2eInKgPerUnit: number;
|
||||
source: string;
|
||||
}) {
|
||||
super();
|
||||
|
||||
this.name = props?.name;
|
||||
this.unit = props?.unit;
|
||||
this.emissionCO2eInKgPerUnit = props?.emissionCO2eInKgPerUnit;
|
||||
this.source = props?.source;
|
||||
this.sanitize();
|
||||
}
|
||||
}
|
30
src/carbonEmissionFactor/carbonEmissionFactors.controller.ts
Normal file
30
src/carbonEmissionFactor/carbonEmissionFactors.controller.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { Body, Controller, Get, Logger, Post } from "@nestjs/common";
|
||||
import { CarbonEmissionFactor } from "./carbonEmissionFactor.entity";
|
||||
import { CarbonEmissionFactorsService } from "./carbonEmissionFactors.service";
|
||||
import { CreateCarbonEmissionFactorDto } from "./dto/create-carbonEmissionFactor.dto";
|
||||
|
||||
@Controller("carbon-emission-factors")
|
||||
export class CarbonEmissionFactorsController {
|
||||
constructor(
|
||||
private readonly carbonEmissionFactorService: CarbonEmissionFactorsService
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
getCarbonEmissionFactors(): Promise<CarbonEmissionFactor[]> {
|
||||
Logger.log(
|
||||
`[carbon-emission-factors] [GET] CarbonEmissionFactor: getting all CarbonEmissionFactors`
|
||||
);
|
||||
return this.carbonEmissionFactorService.findAll();
|
||||
}
|
||||
|
||||
@Post()
|
||||
createCarbonEmissionFactors(
|
||||
@Body() carbonEmissionFactors: CreateCarbonEmissionFactorDto[]
|
||||
): Promise<CarbonEmissionFactor[] | null> {
|
||||
``;
|
||||
Logger.log(
|
||||
`[carbon-emission-factors] [POST] CarbonEmissionFactor: ${carbonEmissionFactors} created`
|
||||
);
|
||||
return this.carbonEmissionFactorService.save(carbonEmissionFactors);
|
||||
}
|
||||
}
|
12
src/carbonEmissionFactor/carbonEmissionFactors.module.ts
Normal file
12
src/carbonEmissionFactor/carbonEmissionFactors.module.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { Module } from "@nestjs/common";
|
||||
import { TypeOrmModule } from "@nestjs/typeorm";
|
||||
import { CarbonEmissionFactor } from "./carbonEmissionFactor.entity";
|
||||
import { CarbonEmissionFactorsService } from "./carbonEmissionFactors.service";
|
||||
import { CarbonEmissionFactorsController } from "./carbonEmissionFactors.controller";
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([CarbonEmissionFactor])],
|
||||
providers: [CarbonEmissionFactorsService],
|
||||
controllers: [CarbonEmissionFactorsController],
|
||||
})
|
||||
export class CarbonEmissionFactorsModule {}
|
|
@ -0,0 +1,44 @@
|
|||
import { GreenlyDataSource, dataSource } from "../../config/dataSource";
|
||||
import { getTestEmissionFactor } from "../seed-dev-data";
|
||||
import { CarbonEmissionFactor } from "./carbonEmissionFactor.entity";
|
||||
import { CarbonEmissionFactorsService } from "./carbonEmissionFactors.service";
|
||||
|
||||
let flourEmissionFactor = getTestEmissionFactor("flour");
|
||||
let hamEmissionFactor = getTestEmissionFactor("ham");
|
||||
let olivedOilEmissionFactor = getTestEmissionFactor("oliveOil");
|
||||
let carbonEmissionFactorService: CarbonEmissionFactorsService;
|
||||
|
||||
beforeAll(async () => {
|
||||
await dataSource.initialize();
|
||||
carbonEmissionFactorService = new CarbonEmissionFactorsService(
|
||||
dataSource.getRepository(CarbonEmissionFactor)
|
||||
);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await GreenlyDataSource.cleanDatabase();
|
||||
await dataSource
|
||||
.getRepository(CarbonEmissionFactor)
|
||||
.save(olivedOilEmissionFactor);
|
||||
});
|
||||
|
||||
describe("CarbonEmissionFactors.service", () => {
|
||||
it("should save new emissionFactors", async () => {
|
||||
await carbonEmissionFactorService.save([
|
||||
hamEmissionFactor,
|
||||
flourEmissionFactor,
|
||||
]);
|
||||
const retrieveChickenEmissionFactor = await dataSource
|
||||
.getRepository(CarbonEmissionFactor)
|
||||
.findOne({ where: { name: "flour" } });
|
||||
expect(retrieveChickenEmissionFactor?.name).toBe("flour");
|
||||
});
|
||||
it("should retrieve emission Factors", async () => {
|
||||
const carbonEmissionFactors = await carbonEmissionFactorService.findAll();
|
||||
expect(carbonEmissionFactors).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await dataSource.destroy();
|
||||
});
|
23
src/carbonEmissionFactor/carbonEmissionFactors.service.ts
Normal file
23
src/carbonEmissionFactor/carbonEmissionFactors.service.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { Injectable } from "@nestjs/common";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import { Repository } from "typeorm";
|
||||
import { CarbonEmissionFactor } from "./carbonEmissionFactor.entity";
|
||||
import { CreateCarbonEmissionFactorDto } from "./dto/create-carbonEmissionFactor.dto";
|
||||
|
||||
@Injectable()
|
||||
export class CarbonEmissionFactorsService {
|
||||
constructor(
|
||||
@InjectRepository(CarbonEmissionFactor)
|
||||
private carbonEmissionFactorRepository: Repository<CarbonEmissionFactor>
|
||||
) {}
|
||||
|
||||
findAll(): Promise<CarbonEmissionFactor[]> {
|
||||
return this.carbonEmissionFactorRepository.find();
|
||||
}
|
||||
|
||||
save(
|
||||
carbonEmissionFactor: CreateCarbonEmissionFactorDto[]
|
||||
): Promise<CarbonEmissionFactor[] | null> {
|
||||
return this.carbonEmissionFactorRepository.save(carbonEmissionFactor);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
export class CreateCarbonEmissionFactorDto {
|
||||
name: string;
|
||||
unit: string;
|
||||
emissionCO2eInKgPerUnit: number;
|
||||
source: string;
|
||||
}
|
17
src/main.ts
Normal file
17
src/main.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { Logger } from "@nestjs/common";
|
||||
import { NestFactory } from "@nestjs/core";
|
||||
import { dataSource } from "../config/dataSource";
|
||||
import { AppModule } from "./app.module";
|
||||
|
||||
async function bootstrap() {
|
||||
if (!dataSource.isInitialized) {
|
||||
await dataSource.initialize();
|
||||
}
|
||||
|
||||
const app = await NestFactory.create(AppModule, {
|
||||
logger: ["error", "warn", "log"],
|
||||
});
|
||||
await app.listen(3000);
|
||||
}
|
||||
Logger.log(`Server running on http://localhost:3000`, "Bootstrap");
|
||||
bootstrap();
|
86
src/seed-dev-data.ts
Normal file
86
src/seed-dev-data.ts
Normal file
|
@ -0,0 +1,86 @@
|
|||
import { dataSource } from "../config/dataSource";
|
||||
import { CarbonEmissionFactor } from "./carbonEmissionFactor/carbonEmissionFactor.entity";
|
||||
|
||||
export const TEST_CARBON_EMISSION_FACTORS = [
|
||||
{
|
||||
name: "ham",
|
||||
unit: "kg",
|
||||
emissionCO2eInKgPerUnit: 0.11,
|
||||
source: "Agrybalise",
|
||||
},
|
||||
{
|
||||
name: "cheese",
|
||||
unit: "kg",
|
||||
emissionCO2eInKgPerUnit: 0.12,
|
||||
source: "Agrybalise",
|
||||
},
|
||||
{
|
||||
name: "tomato",
|
||||
unit: "kg",
|
||||
emissionCO2eInKgPerUnit: 0.13,
|
||||
source: "Agrybalise",
|
||||
},
|
||||
{
|
||||
name: "flour",
|
||||
unit: "kg",
|
||||
emissionCO2eInKgPerUnit: 0.14,
|
||||
source: "Agrybalise",
|
||||
},
|
||||
{
|
||||
name: "blueCheese",
|
||||
unit: "kg",
|
||||
emissionCO2eInKgPerUnit: 0.34,
|
||||
source: "Agrybalise",
|
||||
},
|
||||
{
|
||||
name: "vinegar",
|
||||
unit: "kg",
|
||||
emissionCO2eInKgPerUnit: 0.14,
|
||||
source: "Agrybalise",
|
||||
},
|
||||
{
|
||||
name: "beef",
|
||||
unit: "kg",
|
||||
emissionCO2eInKgPerUnit: 14,
|
||||
source: "Agrybalise",
|
||||
},
|
||||
{
|
||||
name: "oliveOil",
|
||||
unit: "kg",
|
||||
emissionCO2eInKgPerUnit: 0.15,
|
||||
source: "Agrybalise",
|
||||
},
|
||||
].map((args) => {
|
||||
return new CarbonEmissionFactor({
|
||||
name: args.name,
|
||||
unit: args.unit,
|
||||
emissionCO2eInKgPerUnit: args.emissionCO2eInKgPerUnit,
|
||||
source: args.source,
|
||||
});
|
||||
});
|
||||
|
||||
export const getTestEmissionFactor = (name: string) => {
|
||||
const emissionFactor = TEST_CARBON_EMISSION_FACTORS.find(
|
||||
(ef) => ef.name === name
|
||||
);
|
||||
if (!emissionFactor) {
|
||||
throw new Error(
|
||||
`test emission factor with name ${name} could not be found`
|
||||
);
|
||||
}
|
||||
return emissionFactor;
|
||||
};
|
||||
|
||||
export const seedTestCarbonEmissionFactors = async () => {
|
||||
if (!dataSource.isInitialized) {
|
||||
await dataSource.initialize();
|
||||
}
|
||||
const carbonEmissionFactorsService =
|
||||
dataSource.getRepository(CarbonEmissionFactor);
|
||||
|
||||
await carbonEmissionFactorsService.save(TEST_CARBON_EMISSION_FACTORS);
|
||||
};
|
||||
|
||||
if (require.main === module) {
|
||||
seedTestCarbonEmissionFactors().catch((e) => console.error(e));
|
||||
}
|
63
test-e2e/carbonEmissionFactors.e2e-test.ts
Normal file
63
test-e2e/carbonEmissionFactors.e2e-test.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
import { INestApplication } from "@nestjs/common";
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import * as request from "supertest";
|
||||
import { dataSource } from "../config/dataSource";
|
||||
import { AppModule } from "../src/app.module";
|
||||
import { CarbonEmissionFactor } from "../src/carbonEmissionFactor/carbonEmissionFactor.entity";
|
||||
import { getTestEmissionFactor } from "../src/seed-dev-data";
|
||||
|
||||
beforeAll(async () => {
|
||||
await dataSource.initialize();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await dataSource.destroy();
|
||||
});
|
||||
|
||||
describe("CarbonEmissionFactorsController", () => {
|
||||
let app: INestApplication;
|
||||
let defaultCarbonEmissionFactors: CarbonEmissionFactor[];
|
||||
|
||||
beforeEach(async () => {
|
||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
app = moduleFixture.createNestApplication();
|
||||
await app.init();
|
||||
|
||||
await dataSource
|
||||
.getRepository(CarbonEmissionFactor)
|
||||
.save([getTestEmissionFactor("ham"), getTestEmissionFactor("beef")]);
|
||||
|
||||
defaultCarbonEmissionFactors = await dataSource
|
||||
.getRepository(CarbonEmissionFactor)
|
||||
.find();
|
||||
});
|
||||
|
||||
it("GET /carbon-emission-factors", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get("/carbon-emission-factors")
|
||||
.expect(200)
|
||||
.expect(({ body }) => {
|
||||
expect(body).toEqual(defaultCarbonEmissionFactors);
|
||||
});
|
||||
});
|
||||
|
||||
it("POST /carbon-emission-factors", async () => {
|
||||
const carbonEmissionFactorArgs = {
|
||||
name: "Test Carbon Emission Factor",
|
||||
unit: "kg",
|
||||
emissionCO2eInKgPerUnit: 12,
|
||||
source: "Test Source",
|
||||
};
|
||||
return request(app.getHttpServer())
|
||||
.post("/carbon-emission-factors")
|
||||
.send([carbonEmissionFactorArgs])
|
||||
.expect(201)
|
||||
.expect(({ body }) => {
|
||||
expect(body.length).toEqual(1);
|
||||
expect(body[0]).toMatchObject(carbonEmissionFactorArgs);
|
||||
});
|
||||
});
|
||||
});
|
9
test-e2e/jest-e2e.json
Normal file
9
test-e2e/jest-e2e.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"moduleFileExtensions": ["js", "json", "ts"],
|
||||
"rootDir": ".",
|
||||
"testEnvironment": "node",
|
||||
"testRegex": ".e2e-test.ts",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
}
|
||||
}
|
4
tsconfig.build.json
Normal file
4
tsconfig.build.json
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||
}
|
21
tsconfig.json
Normal file
21
tsconfig.json
Normal 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": true,
|
||||
"noImplicitAny": true,
|
||||
"strictBindCallApply": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue