Initial commit
This commit is contained in:
parent
a13ceef804
commit
8641fc9723
19 changed files with 303532 additions and 0 deletions
1
.eslintignore
Normal file
1
.eslintignore
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/node_modules/
|
31
.eslintrc.js
Normal file
31
.eslintrc.js
Normal 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
25
.gitignore
vendored
Normal 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
38
app.js
Normal 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
11
config/index.js
Normal 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
143
importGasStations.js
Normal 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
62
libs/Format.js
Normal 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
34
middleware/Stations.js
Normal 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
42
models/Stations.js
Normal 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
37
models/index.js
Normal 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
70
package.json
Normal 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
298704
public/gas-stations.xml
Normal file
File diff suppressed because it is too large
Load diff
15
routes/v1/stations.js
Normal file
15
routes/v1/stations.js
Normal 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
7
server.js
Normal 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
19
start-server.js
Normal 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
6
views/error.jade
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
extends layout
|
||||||
|
|
||||||
|
block content
|
||||||
|
h1= message
|
||||||
|
h2= error.status
|
||||||
|
pre #{error.stack}
|
5
views/index.jade
Normal file
5
views/index.jade
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
extends layout
|
||||||
|
|
||||||
|
block content
|
||||||
|
h1= title
|
||||||
|
p Welcome to #{title}
|
7
views/layout.jade
Normal file
7
views/layout.jade
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
doctype html
|
||||||
|
html
|
||||||
|
head
|
||||||
|
title= title
|
||||||
|
link(rel='stylesheet', href='/stylesheets/style.css')
|
||||||
|
body
|
||||||
|
block content
|
Loading…
Reference in a new issue