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
|
# How to use
|
||||||
Technical tests for hiring software engineering @Greenly
|
|
||||||
|
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