Initial commit

This commit is contained in:
dbroqua 2020-03-02 20:50:33 +01:00
parent a13ceef804
commit 8641fc9723
19 changed files with 303532 additions and 0 deletions

1
.eslintignore Normal file
View file

@ -0,0 +1 @@
/node_modules/

31
.eslintrc.js Normal file
View file

@ -0,0 +1,31 @@
module.exports = {
parser: "babel-eslint",
extends: ["airbnb-base", "prettier", "plugin:jest/recommended"],
plugins: ["prettier", "jest"],
env: {
es6: true,
browser: false,
node: true,
"jest/globals": true
},
globals: {
__DEV__: true
},
rules: {
// enable additional rules
"no-plusplus": [2, { allowForLoopAfterthoughts: true }],
"no-underscore-dangle": "off",
indent: ["error", 2, { SwitchCase: 1 }],
"linebreak-style": ["error", "unix"],
quotes: ["error", "double"],
semi: ["error", "always"],
// Forbid the use of extraneous packages
// https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-extraneous-dependencies.md
"import/no-extraneous-dependencies": ["error", { packageDir: "." }],
// ESLint plugin for prettier formatting
// https://github.com/prettier/eslint-plugin-prettier
"prettier/prettier": "error",
"func-names": ["error", "never"],
"no-restricted-globals": ["error", "event", "fdescribe"]
}
};

25
.gitignore vendored Normal file
View file

@ -0,0 +1,25 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
/public/gasStations.xml
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

38
app.js Normal file
View file

@ -0,0 +1,38 @@
import express from "express";
import cookieParser from "cookie-parser";
import Pino from "express-pino-logger";
import cors from "cors";
import routerStations from "./routes/v1/stations";
import config from "./config";
import { formatResponseError } from "./libs/Format";
// Initialisation du logger
const pino = new Pino({
prettyPrint: true
});
pino.logger.info(`Server started on port ${config.port}`);
const app = express();
app.use(cors());
// Permet de sauvegarder l'ip du client et non du proxy
app.set("trust proxy", config.trustProxy);
app.use(cookieParser());
// Utilisation de Pino
app.use(pino);
// Déclaration des routes
app.use("/v1/stations", routerStations);
// Gestion des erreurs
app.use((err, req, res, next) => {
res.error = err;
if (res.headersSent) {
next(err);
} else {
formatResponseError(res, err);
}
});
module.exports = app;

11
config/index.js Normal file
View file

@ -0,0 +1,11 @@
module.exports = {
port: process.env.PORT || 3000,
trustProxy: process.env.TRUST_PROXY || "loopback",
mongodbUrl:
process.env.DATABASE_URL || "mongodb://localhost:27017/prix-carburants",
mongodbDebug: process.env.DEBUG || true,
mondeDbOptions: {
useUnifiedTopology: true,
useNewUrlParser: true
}
};

143
importGasStations.js Normal file
View file

@ -0,0 +1,143 @@
/* eslint-disable no-console */
/* eslint-disable global-require */
/* eslint-disable import/no-dynamic-require */
const fs = require("fs");
const parser = require("xml2json");
const moment = require("moment");
const Mongoose = require("mongoose");
const iconv = require("iconv-lite");
const config = require("./config");
// TODO: envoyer mail lors d'une erreur
Mongoose.set("useNewUrlParser", true);
Mongoose.set("useFindAndModify", false);
Mongoose.set("useCreateIndex", true);
Mongoose.set("debug", config.mongodbDebug);
Mongoose.connect(config.mongodbUrl, config.mondeDbOptions);
const db = () => {
const m = {};
fs.readdirSync("./models")
.filter(file => {
return (
file.indexOf(".") !== 0 &&
file.slice(-3) === ".js" &&
file !== "index.js"
);
})
.forEach(file => {
const model = require(`./models/${file.replace(".js", "")}`)(Mongoose);
m[model.modelName] = model;
});
return m;
};
const models = db();
let foundGasStations = 0;
let done = 0;
const extractPrice = station => {
const prices = [];
if (station.prix) {
for (let i = 0; i < station.prix.length; i += 1) {
const currentPrice = station.prix[i];
prices.push({
gasType: currentPrice.nom,
price: parseInt(currentPrice.valeur, 10) / 1000,
updatedAt: moment(currentPrice.maj)
});
}
}
return prices;
};
const createOrUpdateGasStation = (station, callback) => {
models.Stations.findOne({
stationId: station.stationId
})
.then(item => {
if (item) {
item
.update(station)
.then(() => {
callback(null);
})
.catch(callback);
} else {
// Create item
const newItem = new models.Stations(station);
newItem
.save()
.then(() => {
callback(null);
})
.catch(callback);
}
})
.catch(callback);
};
const next = () => {
if (done === foundGasStations) {
console.log(`DONE ${done} !`);
process.exit();
}
};
fs.readFile("public/gas-stations.xml", function(err, data) {
if (err) {
console.log("ERR:", err);
process.exit();
return false;
}
const json = parser.toJson(iconv.decode(data, "ISO-8859-1"));
const rawStations = JSON.parse(json).pdv_liste.pdv;
// console.log("STATIONS:", rawStations.length);
// console.log("RANDOM STATION:", rawStations[12]);
const stations = [];
for (let i = 0; i < rawStations.length; i += 1) {
const currentStation = rawStations[i];
stations.push({
stationId: currentStation.id,
location: {
type: "Point",
coordinates: [
Number(currentStation.longitude) / 100000,
Number(currentStation.latitude) / 100000
]
},
prices: extractPrice(currentStation),
services:
currentStation.services && currentStation.services.service
? currentStation.services.service
: [],
postCode: currentStation.cp.toString(),
address: currentStation.adresse,
city: currentStation.ville
});
}
foundGasStations = stations.length;
// console.log(stations[12]);
stations.forEach(type => {
createOrUpdateGasStation(type, () => {
done += 1;
next();
});
});
return true;
});

62
libs/Format.js Normal file
View file

@ -0,0 +1,62 @@
/**
* Fonction permettant de formater une réponse API
* @param {Object} req
* @param {Object} res
* @param {Functiont} next
* @param {Object} err
* @param {Object} response {code: Integer, res: Array/Object}
*/
const _formatResponse = (req, res, next, err, response) => {
if (err) {
req.response = response;
next(err);
} else {
switch (req.method) {
case "GET":
res
.status(response ? 200 : 204)
.json(response)
.end();
break;
case "PATCH":
res
.status(200)
.json(response)
.end();
break;
case "DELETE":
res
.status(200)
.json(response)
.end();
break;
case "POST":
res
.status(201)
.json(response)
.end();
break;
default:
next(new Error("Not implemented"));
break;
}
}
};
/**
* Fonction permettant de formater une erreur
* @param {Object} res
* @param {Object} err
*/
const _formatResponseError = (res, err) => {
const code = err.errorCode || 500;
const response = {
code,
message: err.message
};
res.status(Math.trunc(code)).json(response);
};
export const formatResponse = _formatResponse;
export const formatResponseError = _formatResponseError;

34
middleware/Stations.js Normal file
View file

@ -0,0 +1,34 @@
import models from "../models";
class Stations {
static getAll(req, callback) {
const { radius, lon, lat, start, limit } = req.query;
const query = {
location: {
$near: {
$maxDistance: Number(radius),
$geometry: {
type: "Point",
coordinates: [Number(lon), Number(lat)]
}
}
}
};
const skip = start || 0;
const _limit = limit && limit <= 50 ? limit : 20;
models.Stations.count(query)
.then(count => {
models.Stations.find(query)
.skip(parseInt(skip, 10))
.limit(parseInt(_limit, 10))
.then(items => {
callback(null, { total: count, items });
});
})
.catch(callback);
}
}
export default Stations;

42
models/Stations.js Normal file
View file

@ -0,0 +1,42 @@
/**
* Model permettant de bufferiser les events reçus de Kafka
*/
module.exports = mongoose => {
const schema = new mongoose.Schema({
stationId: String,
location: {
type: { type: String },
coordinates: []
},
prices: [
{
gasType: String,
price: Number,
updatedAt: Object
}
],
services: [],
postCode: String,
address: String,
city: String
});
schema.index({ location: "2dsphere" });
const Stations = mongoose.model("Stations", schema);
Stations.createIndexes();
return Stations;
};
// INFO:
/*
schema.location = {
type: 'Point',
coordinates: [
Number(longitude),
Number(latitude)
]
}
*/

37
models/index.js Normal file
View file

@ -0,0 +1,37 @@
/* eslint-disable global-require */
/* eslint-disable import/no-dynamic-require */
import fs from "fs";
import path from "path";
import Mongoose from "mongoose";
import config from "../config";
const basename = path.basename(__filename);
Mongoose.set("useNewUrlParser", true);
Mongoose.set("useFindAndModify", false);
Mongoose.set("useCreateIndex", true);
Mongoose.set("debug", config.mongodbDebug);
Mongoose.connect(config.mongodbUrl, config.mondeDbOptions);
const db = () => {
const m = {};
fs.readdirSync(__dirname)
.filter(file => {
return (
file.indexOf(".") !== 0 && file !== basename && file.slice(-3) === ".js"
);
})
.forEach(file => {
const model = require(path.resolve(__dirname, file))(Mongoose);
m[model.modelName] = model;
});
return m;
};
const models = db();
export const mongoose = Mongoose;
export default models;

70
package.json Normal file
View file

@ -0,0 +1,70 @@
{
"name": "prix-carburants-api",
"version": "0.1.0",
"description": "API pour le projet prix-carburants",
"private": false,
"main": "index.js",
"scripts": {
"start": "node start-server.js",
"start:local": "nodemon start-server.js",
"lint": "./node_modules/.bin/eslint . --fix"
},
"repository": {
"type": "git",
"url": "git@framagit.org:dbroqua/prix-carburants-api.git"
},
"author": "Damien Broqua <contact@darkou.fr>",
"license": "ISC",
"engines": {
"node": "13.x",
"yarn": "1.x"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"*.js": [
"./node_modules/.bin/eslint --fix",
"git add"
]
},
"nodemonConfig": {
"ext": "*.js,*.json,*yaml"
},
"dependencies": {
"cookie-parser": "~1.4.4",
"cors": "^2.8.5",
"debug": "~2.6.9",
"express": "~4.16.1",
"express-pino-logger": "^4.0.0",
"file-stream-rotator": "^0.5.7",
"http-errors": "~1.6.3",
"iconv-lite": "^0.5.1",
"moment": "^2.24.0",
"mongoose": "^5.9.2",
"morgan": "~1.9.1",
"pino-pretty": "^3.6.1",
"xml2json": "^0.12.0"
},
"devDependencies": {
"@babel/core": "^7.8.6",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-proposal-object-rest-spread": "^7.8.3",
"@babel/plugin-transform-runtime": "^7.8.3",
"@babel/preset-env": "^7.8.6",
"@babel/register": "^7.8.6",
"babel-eslint": "^10.1.0",
"eslint": "^6.8.0",
"eslint-config-airbnb-base": "^14.0.0",
"eslint-config-prettier": "^6.10.0",
"eslint-plugin-import": "^2.20.1",
"eslint-plugin-jest": "^23.8.1",
"eslint-plugin-prettier": "^3.1.2",
"husky": "^4.2.3",
"lint-staged": "^10.0.8",
"nodemon": "^2.0.2",
"prettier": "^1.19.1"
}
}

298704
public/gas-stations.xml Normal file

File diff suppressed because it is too large Load diff

15
routes/v1/stations.js Normal file
View file

@ -0,0 +1,15 @@
import express from "express";
import Stations from "../../middleware/Stations";
import { formatResponse } from "../../libs/Format";
const router = express.Router();
/**
* Ensemble des routes permettant à un client de s'inscrire sur des events
*/
router.route("/").get(function(req, res, next) {
Stations.getAll(req, (err, response) => {
formatResponse(req, res, next, err, response);
});
});
export default router;

7
server.js Normal file
View file

@ -0,0 +1,7 @@
import app from "./app";
const config = require("./config");
const server = app.listen(config.port, () => {});
module.exports = server;

19
start-server.js Normal file
View file

@ -0,0 +1,19 @@
require("@babel/register")({
plugins: [
"@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-object-rest-spread",
"@babel/plugin-transform-runtime"
],
presets: [
[
"@babel/preset-env",
{
targets: {
node: "current"
}
}
]
]
});
module.exports = require("./server.js");

6
views/error.jade Normal file
View file

@ -0,0 +1,6 @@
extends layout
block content
h1= message
h2= error.status
pre #{error.stack}

5
views/index.jade Normal file
View file

@ -0,0 +1,5 @@
extends layout
block content
h1= title
p Welcome to #{title}

7
views/layout.jade Normal file
View file

@ -0,0 +1,7 @@
doctype html
html
head
title= title
link(rel='stylesheet', href='/stylesheets/style.css')
body
block content

4275
yarn.lock Normal file

File diff suppressed because it is too large Load diff