From f9752a8800b52d95b5427a863075e7f536d33789 Mon Sep 17 00:00:00 2001 From: Timi Ajiboye Date: Fri, 9 Feb 2018 16:09:39 +0100 Subject: [PATCH 1/9] chore: renamed RequestController and changed routes --- .../{RequestController.js => NeedController.js} | 0 server/server-web.js | 8 ++++---- 2 files changed, 4 insertions(+), 4 deletions(-) rename server/controllers/{RequestController.js => NeedController.js} (100%) diff --git a/server/controllers/RequestController.js b/server/controllers/NeedController.js similarity index 100% rename from server/controllers/RequestController.js rename to server/controllers/NeedController.js diff --git a/server/server-web.js b/server/server-web.js index 08de7f4d..7b9040ce 100644 --- a/server/server-web.js +++ b/server/server-web.js @@ -2,7 +2,7 @@ const cors = require('./middleware/cors'); const getOrCreateUser = require('./middleware/getOrCreateUser'); const StatusController = require('./controllers/StatusController'); -const RequestController = require('./controllers/RequestController'); +const NeedController = require('./controllers/NeedController'); const MissionController = require('./controllers/MissionController'); // Create thrift connection to Captain @@ -24,9 +24,9 @@ app.get('/healthy', (req, res) => { app.get('/status', StatusController.getStatus); -app.get('/request/new', RequestController.newRequest); -app.get('/request/cancel', RequestController.cancelRequest); -app.get('/choose_bid', RequestController.chooseBid); +app.post('/requests', NeedController.newRequest); +app.delete('/requests/:requestID', NeedController.cancelRequest); +app.get('/choose_bid', NeedController.chooseBid); app.get('/mission_command', MissionController.command); From 36ada3a4a7f4eadb501760d84166f69d1b1b93a3 Mon Sep 17 00:00:00 2001 From: Timi Ajiboye Date: Fri, 9 Feb 2018 16:16:34 +0100 Subject: [PATCH 2/9] chore: changed cancelRequest to work with request param --- server/controllers/NeedController.js | 2 +- server/server-web.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/controllers/NeedController.js b/server/controllers/NeedController.js index c7928b73..9246ca11 100644 --- a/server/controllers/NeedController.js +++ b/server/controllers/NeedController.js @@ -16,7 +16,7 @@ const newRequest = async (req, res) => { }; const cancelRequest = async (req, res) => { - const { requestId } = req.query; + const requestId = req.param('requestId'); const request = await getRequest(requestId); if (request) { await deleteRequest(requestId); diff --git a/server/server-web.js b/server/server-web.js index 7b9040ce..45234a40 100644 --- a/server/server-web.js +++ b/server/server-web.js @@ -25,7 +25,7 @@ app.get('/healthy', (req, res) => { app.get('/status', StatusController.getStatus); app.post('/requests', NeedController.newRequest); -app.delete('/requests/:requestID', NeedController.cancelRequest); +app.delete('/requests/:requestId', NeedController.cancelRequest); app.get('/choose_bid', NeedController.chooseBid); app.get('/mission_command', MissionController.command); From 6f5f92f7da823b56a5953a83625b3c979b444985 Mon Sep 17 00:00:00 2001 From: Timi Ajiboye Date: Fri, 9 Feb 2018 18:52:28 +0100 Subject: [PATCH 3/9] chore: added body parser, refractor: changed more things fron requests to need --- package.json | 3 ++- server/controllers/NeedController.js | 35 ++++++++++++++-------------- server/middleware/cors.js | 1 + server/server-web.js | 6 +++-- 4 files changed, 25 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index c8c4ef66..15c64f76 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "randomstring": "^1.1.5", "redis": "^2.8.0", "request": "^2.83.0", - "thrift": "^0.11.0" + "thrift": "^0.11.0", + "body-parser": "^1.18.2" }, "devDependencies": { "gulp": "^3.9.1", diff --git a/server/controllers/NeedController.js b/server/controllers/NeedController.js index 9246ca11..35ae1f97 100644 --- a/server/controllers/NeedController.js +++ b/server/controllers/NeedController.js @@ -1,44 +1,45 @@ -const { createRequest, getRequest, deleteRequest } = require('../store/requests'); -const { deleteBidsForRequest } = require('../store/bids'); -const { createMission } = require('../store/missions'); -const { updateVehicleStatus } = require('../store/vehicles'); +const {createRequest, getRequest, deleteRequest} = require('../store/requests'); +const {deleteBidsForRequest} = require('../store/bids'); +const {createMission} = require('../store/missions'); +const {updateVehicleStatus} = require('../store/vehicles'); -const newRequest = async (req, res) => { - const { user_id, pickup, dropoff, requested_pickup_time, size, weight } = req.query; +const create = async (req, res) => { + const {user_id} = req.query; + const {pickup, dropoff, requested_pickup_time, size, weight} = req.body const requestId = await createRequest({ user_id, pickup, dropoff, requested_pickup_time, size, weight }); if (requestId) { - res.json({ requestId }); + res.json({requestId}); } else { res.status(500).send('Something broke!'); } }; -const cancelRequest = async (req, res) => { - const requestId = req.param('requestId'); - const request = await getRequest(requestId); - if (request) { - await deleteRequest(requestId); - await deleteBidsForRequest(requestId); - res.send('request cancelled'); +const cancel = async (req, res) => { + const {needId} = req.params; + const need = await getRequest(needId); + if (need) { + await deleteRequest(need); + await deleteBidsForRequest(need); + res.send('need cancelled'); } else { res.status(500).send('Something broke!'); } }; const chooseBid = async (req, res) => { - const { user_id, bid_id } = req.query; + const {user_id, bid_id} = req.query; const mission = await createMission({ user_id, bid_id, }); if (mission) { await updateVehicleStatus(mission.vehicle_id, 'contract_received'); - res.json({ mission }); + res.json({mission}); } else { res.status(500).send('Something broke!'); } }; -module.exports = { newRequest, cancelRequest, chooseBid }; +module.exports = {create, cancel, chooseBid}; diff --git a/server/middleware/cors.js b/server/middleware/cors.js index 506bf65a..02e7ecf1 100644 --- a/server/middleware/cors.js +++ b/server/middleware/cors.js @@ -4,5 +4,6 @@ module.exports = (req, res, next) => { 'Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept', ); + res.header("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE"); next(); }; diff --git a/server/server-web.js b/server/server-web.js index 45234a40..26634cda 100644 --- a/server/server-web.js +++ b/server/server-web.js @@ -13,9 +13,11 @@ require('./client-thrift').start({ const express = require('express'); const app = express(); +const bodyParser = require('body-parser'); const port = process.env.WEB_SERVER_PORT || 8888; app.use(cors); +app.use(bodyParser.json()); app.use(getOrCreateUser); app.get('/healthy', (req, res) => { @@ -24,8 +26,8 @@ app.get('/healthy', (req, res) => { app.get('/status', StatusController.getStatus); -app.post('/requests', NeedController.newRequest); -app.delete('/requests/:requestId', NeedController.cancelRequest); +app.post('/needs', NeedController.create); +app.delete('/needs/:needId', NeedController.cancel); app.get('/choose_bid', NeedController.chooseBid); app.get('/mission_command', MissionController.command); From 099fd0da0a2ebd4bfb844a18b8d539c8eb16bd72 Mon Sep 17 00:00:00 2001 From: Timi Ajiboye Date: Sun, 11 Feb 2018 17:42:14 +0100 Subject: [PATCH 4/9] refractor: changed request to need everywhere --- server/config/index.js | 2 +- server/controllers/NeedController.js | 16 ++++++------ server/controllers/StatusController.js | 6 ++--- server/store/bids.js | 36 +++++++++++++------------- server/store/missions.js | 12 ++++----- server/store/{requests.js => needs.js} | 36 +++++++++++++------------- 6 files changed, 54 insertions(+), 54 deletions(-) rename server/store/{requests.js => needs.js} (53%) diff --git a/server/config/index.js b/server/config/index.js index d158dce6..c0c104e1 100644 --- a/server/config/index.js +++ b/server/config/index.js @@ -1,5 +1,5 @@ const config = { - requests_ttl: 43200, + needs_ttl: 43200, bids_ttl: 3600, vehicles_ttl: 86400, }; diff --git a/server/controllers/NeedController.js b/server/controllers/NeedController.js index 35ae1f97..845fc63f 100644 --- a/server/controllers/NeedController.js +++ b/server/controllers/NeedController.js @@ -1,16 +1,16 @@ -const {createRequest, getRequest, deleteRequest} = require('../store/requests'); -const {deleteBidsForRequest} = require('../store/bids'); +const {createNeed, getNeed, deleteNeed} = require('../store/needs'); +const {deleteBidsForNeed} = require('../store/bids'); const {createMission} = require('../store/missions'); const {updateVehicleStatus} = require('../store/vehicles'); const create = async (req, res) => { const {user_id} = req.query; const {pickup, dropoff, requested_pickup_time, size, weight} = req.body - const requestId = await createRequest({ + const needId = await createNeed({ user_id, pickup, dropoff, requested_pickup_time, size, weight }); - if (requestId) { - res.json({requestId}); + if (needId) { + res.json({needId}); } else { res.status(500).send('Something broke!'); } @@ -18,10 +18,10 @@ const create = async (req, res) => { const cancel = async (req, res) => { const {needId} = req.params; - const need = await getRequest(needId); + const need = await getNeed(needId); if (need) { - await deleteRequest(need); - await deleteBidsForRequest(need); + await deleteNeed(need); + await deleteBidsForNeed(need); res.send('need cancelled'); } else { res.status(500).send('Something broke!'); diff --git a/server/controllers/StatusController.js b/server/controllers/StatusController.js index 3948fc21..81d8abc4 100644 --- a/server/controllers/StatusController.js +++ b/server/controllers/StatusController.js @@ -1,5 +1,5 @@ const {getVehiclesInRange, updateVehicleStatus, getVehicle, getVehicles, updateVehiclePosition, getPosition, getLatestPositionUpdate} = require('../store/vehicles'); -const {getBidsForRequest} = require('../store/bids'); +const {getBidsForNeed} = require('../store/bids'); const {getLatestMission, updateMission} = require('../store/missions'); const {createMissionUpdate} = require('../store/mission_updates'); const {hasStore} = require('../lib/environment'); @@ -7,10 +7,10 @@ const missionProgress = require('../simulation/missionProgress'); const {calculateNextCoordinate} = require('../simulation/vehicles'); const getStatus = async (req, res) => { - const {lat, long, requestId, user_id} = req.query; + const {lat, long, needId, user_id} = req.query; const status = 'idle'; const latestMission = await getLatestMission(user_id); - const bids = (!hasStore() || !requestId) ? [] : await getBidsForRequest(requestId); + const bids = (!hasStore() || !needId) ? [] : await getBidsForNeed(needId); let vehicles = []; if (hasStore()) { if (bids.length > 0) { diff --git a/server/store/bids.js b/server/store/bids.js index 4cd1c70c..a797e0b2 100644 --- a/server/store/bids.js +++ b/server/store/bids.js @@ -2,14 +2,14 @@ const redis = require('./redis'); const config = require('../config'); const { randomBid } = require('../simulation/vehicles'); const { getVehicle } = require('../store/vehicles'); -const { getRequest } = require('../store/requests'); +const { getNeed } = require('./needs'); -const saveBid = async ({ vehicle_id, price, time_to_pickup, time_to_dropoff }, requestId, userId) => { +const saveBid = async ({ vehicle_id, price, time_to_pickup, time_to_dropoff }, needId, userId) => { // get new unique id for bid const bidId = await redis.incrAsync('next_bid_id'); - // Save bid id in request_bids - redis.rpushAsync(`request_bids:${requestId}`, bidId); + // Save bid id in need_bids + redis.rpushAsync(`need_bids:${needId}`, bidId); // Add bid to bids redis.hmsetAsync(`bids:${bidId}`, @@ -19,7 +19,7 @@ const saveBid = async ({ vehicle_id, price, time_to_pickup, time_to_dropoff }, r 'price', price, 'time_to_pickup', time_to_pickup, 'time_to_dropoff', time_to_dropoff, - 'request_id', requestId, + 'need_id', needId, ); // Set TTL for bid @@ -33,15 +33,15 @@ const getBid = async bidId => { return await redis.hgetallAsync(`bids:${bidId}`); }; -const getBidsForRequest = async requestId => { +const getBidsForNeed = async needId => { // get request details - const request = await getRequest(requestId); - if (!request) return []; + const need = await getNeed(needId); + if (!need) return []; - const userId = request.user_id; + const userId = need.user_id; - // get bids for request - const bidIds = await redis.lrangeAsync(`request_bids:${requestId}`, 0, -1); + // get bids for need + const bidIds = await redis.lrangeAsync(`need_bids:${needId}`, 0, -1); const bids = await Promise.all( bidIds.map(bidId => { redis.expire(`bids:${bidId}`, config('bids_ttl')); @@ -51,7 +51,7 @@ const getBidsForRequest = async requestId => { // If not enough bids, make some up if (bidIds.length < 10) { - const { pickup_long, pickup_lat, dropoff_lat, dropoff_long } = request; + const { pickup_long, pickup_lat, dropoff_lat, dropoff_long } = need; const pickup = { lat: pickup_lat, long: pickup_long }; const dropoff = { lat: dropoff_lat, long: dropoff_long }; const vehicleIds = await redis.georadiusAsync( @@ -69,7 +69,7 @@ const getBidsForRequest = async requestId => { const origin = { lat: vehicle.lat, long: vehicle.long }; let newBid = randomBid(origin, pickup, dropoff); newBid.vehicle_id = vehicleId; - const newBidId = await saveBid(newBid, requestId, userId); + const newBidId = await saveBid(newBid, needId, userId); newBid.id = newBidId; bids.push(newBid); } @@ -78,16 +78,16 @@ const getBidsForRequest = async requestId => { return bids; }; -const deleteBidsForRequest = async requestId => { - const bidIds = await redis.lrangeAsync(`request_bids:${requestId}`, 0, -1); +const deleteBidsForNeed = async needId => { + const bidIds = await redis.lrangeAsync(`need_bids:${needId}`, 0, -1); await Promise.all( bidIds.map(bidId => redis.del(`bids:${bidId}`)) ); - return await redis.del(`request_bids:${requestId}`); + return await redis.del(`need_bids:${needId}`); }; module.exports = { - getBidsForRequest, + getBidsForNeed, getBid, - deleteBidsForRequest, + deleteBidsForNeed, }; diff --git a/server/store/missions.js b/server/store/missions.js index efd54135..1c60a948 100644 --- a/server/store/missions.js +++ b/server/store/missions.js @@ -1,6 +1,6 @@ const redis = require('./redis'); const { getBid } = require('./bids'); -const { getRequest } = require('./requests'); +const { getNeed } = require('./needs'); const { createMissionUpdate } = require('./mission_updates'); const getMission = async missionId => { @@ -28,11 +28,11 @@ const updateMission = async (id, params) => { const createMission = async ({ user_id, bid_id }) => { // get bid details const bid = await getBid(bid_id); - const { vehicle_id, price, time_to_pickup, time_to_dropoff, request_id } = bid; + const { vehicle_id, price, time_to_pickup, time_to_dropoff, need_id } = bid; - // get request details - const request = await getRequest(request_id); - const { pickup_lat, pickup_long, dropoff_lat, dropoff_long, requested_pickup_time, size, weight } = request; + // get neeed details + const need = await getNeed(need_id); + const { pickup_lat, pickup_long, dropoff_lat, dropoff_long, requested_pickup_time, size, weight } = need; // get new unique id for mission const missionId = await redis.incrAsync('next_mission_id'); @@ -51,7 +51,7 @@ const createMission = async ({ user_id, bid_id }) => { 'price', price, 'time_to_pickup', time_to_pickup, 'time_to_dropoff', time_to_dropoff, - 'request_id', request_id, + 'need_id', need_id, 'pickup_lat', pickup_lat, 'pickup_long', pickup_long, 'dropoff_lat', dropoff_lat, diff --git a/server/store/requests.js b/server/store/needs.js similarity index 53% rename from server/store/requests.js rename to server/store/needs.js index 5339b8ef..0384aaca 100644 --- a/server/store/requests.js +++ b/server/store/needs.js @@ -2,21 +2,21 @@ const redis = require('./redis'); const config = require('../config'); const { getVehiclesInRange } = require('./vehicles'); -const getRequest = async requestId => { - // Set TTL for request - redis.expire(`requests:${requestId}`, config('requests_ttl')); - return await redis.hgetallAsync(`requests:${requestId}`); +const getNeed = async needId => { + // Set TTL for need + redis.expire(`needs:${needId}`, config('needs_ttl')); + return await redis.hgetallAsync(`needs:${needId}`); }; -const createRequest = async requestDetails => { - // get new unique id for request - const requestId = await redis.incrAsync('next_request_id'); +const createNeed = async needDetails => { + // get new unique id for need + const needId = await redis.incrAsync('next_need_id'); - // create a new request entry in Redis - const {user_id, pickup, dropoff, requested_pickup_time, size, weight} = requestDetails; + // create a new need entry in Redis + const {user_id, pickup, dropoff, requested_pickup_time, size, weight} = needDetails; const [pickup_lat, pickup_long] = pickup.split(','); const [dropoff_lat, dropoff_long] = dropoff.split(','); - redis.hmsetAsync(`requests:${requestId}`, + redis.hmsetAsync(`needs:${needId}`, 'user_id', user_id, 'pickup_lat', pickup_lat, 'pickup_long', pickup_long, @@ -30,17 +30,17 @@ const createRequest = async requestDetails => { // See if there are any vehicles around the pickup position, if not a few vehicles will be generated there getVehiclesInRange({ lat: parseFloat(pickup_lat), long: parseFloat(pickup_long) }, 7000); - // Set TTL for request - redis.expire(`requests:${requestId}`, config('requests_ttl')); - return requestId; + // Set TTL for need + redis.expire(`needs:${needId}`, config('needs_ttl')); + return needId; }; -const deleteRequest = async requestId => { - return await redis.del(`requests:${requestId}`); +const deleteNeed = async needId => { + return await redis.del(`needs:${needId}`); }; module.exports = { - createRequest, - getRequest, - deleteRequest, + createNeed, + getNeed, + deleteNeed, }; From 8f3bb1f8ae278b97ba40930c93ca442c708fc323 Mon Sep 17 00:00:00 2001 From: Timi Ajiboye Date: Tue, 13 Feb 2018 01:15:43 +0100 Subject: [PATCH 5/9] chore: added complete constraints object for creating need --- package-lock.json | 3 + package.json | 3 +- server/controllers/NeedController.js | 3 + server/controllers/constraints/need/create.js | 65 +++++++++++++++++++ 4 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 server/controllers/constraints/need/create.js diff --git a/package-lock.json b/package-lock.json index b2c6a0a5..7c479a72 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10118,6 +10118,9 @@ "spdx-expression-parse": "1.0.4" } }, + "validate.js": { + "version": "github:ansman/validate.js#cccc345aa70cda2a59bccdbc240ffd52b7528bda" + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/package.json b/package.json index 15c64f76..a1e1194b 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,8 @@ "redis": "^2.8.0", "request": "^2.83.0", "thrift": "^0.11.0", - "body-parser": "^1.18.2" + "body-parser": "^1.18.2", + "validate.js": "ansman/validate.js#master" }, "devDependencies": { "gulp": "^3.9.1", diff --git a/server/controllers/NeedController.js b/server/controllers/NeedController.js index 845fc63f..27695a62 100644 --- a/server/controllers/NeedController.js +++ b/server/controllers/NeedController.js @@ -2,6 +2,8 @@ const {createNeed, getNeed, deleteNeed} = require('../store/needs'); const {deleteBidsForNeed} = require('../store/bids'); const {createMission} = require('../store/missions'); const {updateVehicleStatus} = require('../store/vehicles'); +const validate = require('validate.js'); +const createConstraints = require('./constraints/create'); const create = async (req, res) => { const {user_id} = req.query; @@ -16,6 +18,7 @@ const create = async (req, res) => { } }; + const cancel = async (req, res) => { const {needId} = req.params; const need = await getNeed(needId); diff --git a/server/controllers/constraints/need/create.js b/server/controllers/constraints/need/create.js new file mode 100644 index 00000000..fc3134fd --- /dev/null +++ b/server/controllers/constraints/need/create.js @@ -0,0 +1,65 @@ +module.exports = { + pickup_at: { + numericality: { + greaterThan: Date.now() + } + }, + pickup_latitude: { + presence: true, + numericality: { + lessThanOrEqualTo: 90, + } + }, + pickup_longitude: { + presence: true, + numericality: { + lessThanOrEqualTo: 180, + } + }, + dropoff_latitude: { + presence: true, + numericality: { + lessThanOrEqualTo: 90, + } + }, + dropoff_longitude: { + presence: true, + numericality: { + lessThanOrEqualTo: 180, + } + }, + requester_name: { + length: { + minimum: 3, + } + }, + requester_phone_number: { + length: { + minimum: 8, + } + }, + cargo_type: { + presence: true, + type: 'number', + inclusion: { + within: Array.from(new Array(18), (x,i) => i + 1) + } + }, + hazardous_goods: { + type: 'number', + inclusion: { + within: Array.from(new Array(9), (x,i) => i + 1) + } + }, + insurance_required: { + type: 'boolean' + }, + insured_value: { + type: 'number' + }, + insured_value_currency: { + length: { + is: 3 + } + } +}; From 1376a252132f368308c552645e730555bf0a6016 Mon Sep 17 00:00:00 2001 From: Timi Ajiboye Date: Tue, 13 Feb 2018 03:29:15 +0100 Subject: [PATCH 6/9] feat: added validation for new Need parameters, refractor: changed code app wide to support new parameters --- package.json | 3 +- server/controllers/NeedController.js | 27 +- server/controllers/StatusController.js | 4 +- server/controllers/constraints/need/create.js | 18 + server/lib/validate.js | 1241 +++++++++++++++++ server/simulation/vehicles.js | 12 +- server/store/bids.js | 10 +- server/store/missions.js | 26 +- server/store/needs.js | 19 +- 9 files changed, 1306 insertions(+), 54 deletions(-) create mode 100644 server/lib/validate.js diff --git a/package.json b/package.json index a1e1194b..15c64f76 100644 --- a/package.json +++ b/package.json @@ -26,8 +26,7 @@ "redis": "^2.8.0", "request": "^2.83.0", "thrift": "^0.11.0", - "body-parser": "^1.18.2", - "validate.js": "ansman/validate.js#master" + "body-parser": "^1.18.2" }, "devDependencies": { "gulp": "^3.9.1", diff --git a/server/controllers/NeedController.js b/server/controllers/NeedController.js index 27695a62..f18875df 100644 --- a/server/controllers/NeedController.js +++ b/server/controllers/NeedController.js @@ -2,19 +2,26 @@ const {createNeed, getNeed, deleteNeed} = require('../store/needs'); const {deleteBidsForNeed} = require('../store/bids'); const {createMission} = require('../store/missions'); const {updateVehicleStatus} = require('../store/vehicles'); -const validate = require('validate.js'); -const createConstraints = require('./constraints/create'); +const createConstraints = require('./constraints/need/create'); + +// using downloaded validate.js file because latest version does not have type checking and docker fails with git source in package.json +const validate = require('../lib/validate'); const create = async (req, res) => { - const {user_id} = req.query; - const {pickup, dropoff, requested_pickup_time, size, weight} = req.body - const needId = await createNeed({ - user_id, pickup, dropoff, requested_pickup_time, size, weight - }); - if (needId) { - res.json({needId}); + const params = req.body + const validationErrors = validate(params, createConstraints); + if (validationErrors) { + res.status(422).json(validationErrors); } else { - res.status(500).send('Something broke!'); + const allowedParamsKeys = Object.keys(createConstraints); + Object.keys(params).forEach(key => {if (!allowedParamsKeys.includes(key)) delete params[key]}) + params.user_id = req.query.user_id + const needId = await createNeed(params); + if (needId) { + res.json({needId}); + } else { + res.status(500).send('Something broke!'); + } } }; diff --git a/server/controllers/StatusController.js b/server/controllers/StatusController.js index 81d8abc4..1e3e3002 100644 --- a/server/controllers/StatusController.js +++ b/server/controllers/StatusController.js @@ -33,8 +33,8 @@ const getStatus = async (req, res) => { await updateMission(latestMission.mission_id, { 'vehicle_signed_at': Date.now(), 'status': 'in_progress', - 'vehicle_start_long': vehicle.long, - 'vehicle_start_lat': vehicle.lat + 'vehicle_start_longitude': vehicle.long, + 'vehicle_start_latitude': vehicle.lat }); await updateVehicleStatus(latestMission.vehicle_id, 'travelling_pickup'); await createMissionUpdate(latestMission.mission_id, 'travelling_pickup'); diff --git a/server/controllers/constraints/need/create.js b/server/controllers/constraints/need/create.js index fc3134fd..c607401d 100644 --- a/server/controllers/constraints/need/create.js +++ b/server/controllers/constraints/need/create.js @@ -51,6 +51,24 @@ module.exports = { within: Array.from(new Array(9), (x,i) => i + 1) } }, + ip_protection_level: { + type: 'number', + inclusion: { + within: Array.from(new Array(69), (x,i) => i + 54) + } + }, + height: { + type: 'number' + }, + width: { + type: 'number' + }, + length: { + type: 'number' + }, + weight: { + type: 'number' + }, insurance_required: { type: 'boolean' }, diff --git a/server/lib/validate.js b/server/lib/validate.js new file mode 100644 index 00000000..610d0bc9 --- /dev/null +++ b/server/lib/validate.js @@ -0,0 +1,1241 @@ +/*! + * validate.js 0.12.0 + * + * (c) 2013-2017 Nicklas Ansman, 2013 Wrapp + * Validate.js may be freely distributed under the MIT license. + * For all details and documentation: + * http://validatejs.org/ + */ + +(function(exports, module, define) { + "use strict"; + + // The main function that calls the validators specified by the constraints. + // The options are the following: + // - format (string) - An option that controls how the returned value is formatted + // * flat - Returns a flat array of just the error messages + // * grouped - Returns the messages grouped by attribute (default) + // * detailed - Returns an array of the raw validation data + // - fullMessages (boolean) - If `true` (default) the attribute name is prepended to the error. + // + // Please note that the options are also passed to each validator. + var validate = function(attributes, constraints, options) { + options = v.extend({}, v.options, options); + + var results = v.runValidations(attributes, constraints, options) + , attr + , validator; + + if (results.some(function(r) { return v.isPromise(r.error); })) { + throw new Error("Use validate.async if you want support for promises"); + } + return validate.processValidationResults(results, options); + }; + + var v = validate; + + // Copies over attributes from one or more sources to a single destination. + // Very much similar to underscore's extend. + // The first argument is the target object and the remaining arguments will be + // used as sources. + v.extend = function(obj) { + [].slice.call(arguments, 1).forEach(function(source) { + for (var attr in source) { + obj[attr] = source[attr]; + } + }); + return obj; + }; + + v.extend(validate, { + // This is the version of the library as a semver. + // The toString function will allow it to be coerced into a string + version: { + major: 0, + minor: 12, + patch: 0, + metadata: "development", + toString: function() { + var version = v.format("%{major}.%{minor}.%{patch}", v.version); + if (!v.isEmpty(v.version.metadata)) { + version += "+" + v.version.metadata; + } + return version; + } + }, + + // Below is the dependencies that are used in validate.js + + // The constructor of the Promise implementation. + // If you are using Q.js, RSVP or any other A+ compatible implementation + // override this attribute to be the constructor of that promise. + // Since jQuery promises aren't A+ compatible they won't work. + Promise: typeof Promise !== "undefined" ? Promise : /* istanbul ignore next */ null, + + EMPTY_STRING_REGEXP: /^\s*$/, + + // Runs the validators specified by the constraints object. + // Will return an array of the format: + // [{attribute: "", error: ""}, ...] + runValidations: function(attributes, constraints, options) { + var results = [] + , attr + , validatorName + , value + , validators + , validator + , validatorOptions + , error; + + if (v.isDomElement(attributes) || v.isJqueryElement(attributes)) { + attributes = v.collectFormValues(attributes); + } + + // Loops through each constraints, finds the correct validator and run it. + for (attr in constraints) { + value = v.getDeepObjectValue(attributes, attr); + // This allows the constraints for an attribute to be a function. + // The function will be called with the value, attribute name, the complete dict of + // attributes as well as the options and constraints passed in. + // This is useful when you want to have different + // validations depending on the attribute value. + validators = v.result(constraints[attr], value, attributes, attr, options, constraints); + + for (validatorName in validators) { + validator = v.validators[validatorName]; + + if (!validator) { + error = v.format("Unknown validator %{name}", {name: validatorName}); + throw new Error(error); + } + + validatorOptions = validators[validatorName]; + // This allows the options to be a function. The function will be + // called with the value, attribute name, the complete dict of + // attributes as well as the options and constraints passed in. + // This is useful when you want to have different + // validations depending on the attribute value. + validatorOptions = v.result(validatorOptions, value, attributes, attr, options, constraints); + if (!validatorOptions) { + continue; + } + results.push({ + attribute: attr, + value: value, + validator: validatorName, + globalOptions: options, + attributes: attributes, + options: validatorOptions, + error: validator.call(validator, + value, + validatorOptions, + attr, + attributes, + options) + }); + } + } + + return results; + }, + + // Takes the output from runValidations and converts it to the correct + // output format. + processValidationResults: function(errors, options) { + errors = v.pruneEmptyErrors(errors, options); + errors = v.expandMultipleErrors(errors, options); + errors = v.convertErrorMessages(errors, options); + + var format = options.format || "grouped"; + + if (typeof v.formatters[format] === 'function') { + errors = v.formatters[format](errors); + } else { + throw new Error(v.format("Unknown format %{format}", options)); + } + + return v.isEmpty(errors) ? undefined : errors; + }, + + // Runs the validations with support for promises. + // This function will return a promise that is settled when all the + // validation promises have been completed. + // It can be called even if no validations returned a promise. + async: function(attributes, constraints, options) { + options = v.extend({}, v.async.options, options); + + var WrapErrors = options.wrapErrors || function(errors) { + return errors; + }; + + // Removes unknown attributes + if (options.cleanAttributes !== false) { + attributes = v.cleanAttributes(attributes, constraints); + } + + var results = v.runValidations(attributes, constraints, options); + + return new v.Promise(function(resolve, reject) { + v.waitForResults(results).then(function() { + var errors = v.processValidationResults(results, options); + if (errors) { + reject(new WrapErrors(errors, options, attributes, constraints)); + } else { + resolve(attributes); + } + }, function(err) { + reject(err); + }); + }); + }, + + single: function(value, constraints, options) { + options = v.extend({}, v.single.options, options, { + format: "flat", + fullMessages: false + }); + return v({single: value}, {single: constraints}, options); + }, + + // Returns a promise that is resolved when all promises in the results array + // are settled. The promise returned from this function is always resolved, + // never rejected. + // This function modifies the input argument, it replaces the promises + // with the value returned from the promise. + waitForResults: function(results) { + // Create a sequence of all the results starting with a resolved promise. + return results.reduce(function(memo, result) { + // If this result isn't a promise skip it in the sequence. + if (!v.isPromise(result.error)) { + return memo; + } + + return memo.then(function() { + return result.error.then(function(error) { + result.error = error || null; + }); + }); + }, new v.Promise(function(r) { r(); })); // A resolved promise + }, + + // If the given argument is a call: function the and: function return the value + // otherwise just return the value. Additional arguments will be passed as + // arguments to the function. + // Example: + // ``` + // result('foo') // 'foo' + // result(Math.max, 1, 2) // 2 + // ``` + result: function(value) { + var args = [].slice.call(arguments, 1); + if (typeof value === 'function') { + value = value.apply(null, args); + } + return value; + }, + + // Checks if the value is a number. This function does not consider NaN a + // number like many other `isNumber` functions do. + isNumber: function(value) { + return typeof value === 'number' && !isNaN(value); + }, + + // Returns false if the object is not a function + isFunction: function(value) { + return typeof value === 'function'; + }, + + // A simple check to verify that the value is an integer. Uses `isNumber` + // and a simple modulo check. + isInteger: function(value) { + return v.isNumber(value) && value % 1 === 0; + }, + + // Checks if the value is a boolean + isBoolean: function(value) { + return typeof value === 'boolean'; + }, + + // Uses the `Object` function to check if the given argument is an object. + isObject: function(obj) { + return obj === Object(obj); + }, + + // Simply checks if the object is an instance of a date + isDate: function(obj) { + return obj instanceof Date; + }, + + // Returns false if the object is `null` of `undefined` + isDefined: function(obj) { + return obj !== null && obj !== undefined; + }, + + // Checks if the given argument is a promise. Anything with a `then` + // function is considered a promise. + isPromise: function(p) { + return !!p && v.isFunction(p.then); + }, + + isJqueryElement: function(o) { + return o && v.isString(o.jquery); + }, + + isDomElement: function(o) { + if (!o) { + return false; + } + + if (!o.querySelectorAll || !o.querySelector) { + return false; + } + + if (v.isObject(document) && o === document) { + return true; + } + + // http://stackoverflow.com/a/384380/699304 + /* istanbul ignore else */ + if (typeof HTMLElement === "object") { + return o instanceof HTMLElement; + } else { + return o && + typeof o === "object" && + o !== null && + o.nodeType === 1 && + typeof o.nodeName === "string"; + } + }, + + isEmpty: function(value) { + var attr; + + // Null and undefined are empty + if (!v.isDefined(value)) { + return true; + } + + // functions are non empty + if (v.isFunction(value)) { + return false; + } + + // Whitespace only strings are empty + if (v.isString(value)) { + return v.EMPTY_STRING_REGEXP.test(value); + } + + // For arrays we use the length property + if (v.isArray(value)) { + return value.length === 0; + } + + // Dates have no attributes but aren't empty + if (v.isDate(value)) { + return false; + } + + // If we find at least one property we consider it non empty + if (v.isObject(value)) { + for (attr in value) { + return false; + } + return true; + } + + return false; + }, + + // Formats the specified strings with the given values like so: + // ``` + // format("Foo: %{foo}", {foo: "bar"}) // "Foo bar" + // ``` + // If you want to write %{...} without having it replaced simply + // prefix it with % like this `Foo: %%{foo}` and it will be returned + // as `"Foo: %{foo}"` + format: v.extend(function(str, vals) { + if (!v.isString(str)) { + return str; + } + return str.replace(v.format.FORMAT_REGEXP, function(m0, m1, m2) { + if (m1 === '%') { + return "%{" + m2 + "}"; + } else { + return String(vals[m2]); + } + }); + }, { + // Finds %{key} style patterns in the given string + FORMAT_REGEXP: /(%?)%\{([^\}]+)\}/g + }), + + // "Prettifies" the given string. + // Prettifying means replacing [.\_-] with spaces as well as splitting + // camel case words. + prettify: function(str) { + if (v.isNumber(str)) { + // If there are more than 2 decimals round it to two + if ((str * 100) % 1 === 0) { + return "" + str; + } else { + return parseFloat(Math.round(str * 100) / 100).toFixed(2); + } + } + + if (v.isArray(str)) { + return str.map(function(s) { return v.prettify(s); }).join(", "); + } + + if (v.isObject(str)) { + return str.toString(); + } + + // Ensure the string is actually a string + str = "" + str; + + return str + // Splits keys separated by periods + .replace(/([^\s])\.([^\s])/g, '$1 $2') + // Removes backslashes + .replace(/\\+/g, '') + // Replaces - and - with space + .replace(/[_-]/g, ' ') + // Splits camel cased words + .replace(/([a-z])([A-Z])/g, function(m0, m1, m2) { + return "" + m1 + " " + m2.toLowerCase(); + }) + .toLowerCase(); + }, + + stringifyValue: function(value, options) { + var prettify = options && options.prettify || v.prettify; + return prettify(value); + }, + + isString: function(value) { + return typeof value === 'string'; + }, + + isArray: function(value) { + return {}.toString.call(value) === '[object Array]'; + }, + + // Checks if the object is a hash, which is equivalent to an object that + // is neither an array nor a function. + isHash: function(value) { + return v.isObject(value) && !v.isArray(value) && !v.isFunction(value); + }, + + contains: function(obj, value) { + if (!v.isDefined(obj)) { + return false; + } + if (v.isArray(obj)) { + return obj.indexOf(value) !== -1; + } + return value in obj; + }, + + unique: function(array) { + if (!v.isArray(array)) { + return array; + } + return array.filter(function(el, index, array) { + return array.indexOf(el) == index; + }); + }, + + forEachKeyInKeypath: function(object, keypath, callback) { + if (!v.isString(keypath)) { + return undefined; + } + + var key = "" + , i + , escape = false; + + for (i = 0; i < keypath.length; ++i) { + switch (keypath[i]) { + case '.': + if (escape) { + escape = false; + key += '.'; + } else { + object = callback(object, key, false); + key = ""; + } + break; + + case '\\': + if (escape) { + escape = false; + key += '\\'; + } else { + escape = true; + } + break; + + default: + escape = false; + key += keypath[i]; + break; + } + } + + return callback(object, key, true); + }, + + getDeepObjectValue: function(obj, keypath) { + if (!v.isObject(obj)) { + return undefined; + } + + return v.forEachKeyInKeypath(obj, keypath, function(obj, key) { + if (v.isObject(obj)) { + return obj[key]; + } + }); + }, + + // This returns an object with all the values of the form. + // It uses the input name as key and the value as value + // So for example this: + // + // would return: + // {email: "foo@bar.com"} + collectFormValues: function(form, options) { + var values = {} + , i + , j + , input + , inputs + , option + , value; + + if (v.isJqueryElement(form)) { + form = form[0]; + } + + if (!form) { + return values; + } + + options = options || {}; + + inputs = form.querySelectorAll("input[name], textarea[name]"); + for (i = 0; i < inputs.length; ++i) { + input = inputs.item(i); + + if (v.isDefined(input.getAttribute("data-ignored"))) { + continue; + } + + var name = input.name.replace(/\./g, "\\\\."); + value = v.sanitizeFormValue(input.value, options); + if (input.type === "number") { + value = value ? +value : null; + } else if (input.type === "checkbox") { + if (input.attributes.value) { + if (!input.checked) { + value = values[name] || null; + } + } else { + value = input.checked; + } + } else if (input.type === "radio") { + if (!input.checked) { + value = values[name] || null; + } + } + values[name] = value; + } + + inputs = form.querySelectorAll("select[name]"); + for (i = 0; i < inputs.length; ++i) { + input = inputs.item(i); + if (v.isDefined(input.getAttribute("data-ignored"))) { + continue; + } + + if (input.multiple) { + value = []; + for (j in input.options) { + option = input.options[j]; + if (option && option.selected) { + value.push(v.sanitizeFormValue(option.value, options)); + } + } + } else { + var _val = typeof input.options[input.selectedIndex] !== 'undefined' ? input.options[input.selectedIndex].value : /* istanbul ignore next */ ''; + value = v.sanitizeFormValue(_val, options); + } + values[input.name] = value; + } + + return values; + }, + + sanitizeFormValue: function(value, options) { + if (options.trim && v.isString(value)) { + value = value.trim(); + } + + if (options.nullify !== false && value === "") { + return null; + } + return value; + }, + + capitalize: function(str) { + if (!v.isString(str)) { + return str; + } + return str[0].toUpperCase() + str.slice(1); + }, + + // Remove all errors who's error attribute is empty (null or undefined) + pruneEmptyErrors: function(errors) { + return errors.filter(function(error) { + return !v.isEmpty(error.error); + }); + }, + + // In + // [{error: ["err1", "err2"], ...}] + // Out + // [{error: "err1", ...}, {error: "err2", ...}] + // + // All attributes in an error with multiple messages are duplicated + // when expanding the errors. + expandMultipleErrors: function(errors) { + var ret = []; + errors.forEach(function(error) { + // Removes errors without a message + if (v.isArray(error.error)) { + error.error.forEach(function(msg) { + ret.push(v.extend({}, error, {error: msg})); + }); + } else { + ret.push(error); + } + }); + return ret; + }, + + // Converts the error mesages by prepending the attribute name unless the + // message is prefixed by ^ + convertErrorMessages: function(errors, options) { + options = options || {}; + + var ret = [] + , prettify = options.prettify || v.prettify; + errors.forEach(function(errorInfo) { + var error = v.result(errorInfo.error, + errorInfo.value, + errorInfo.attribute, + errorInfo.options, + errorInfo.attributes, + errorInfo.globalOptions); + + if (!v.isString(error)) { + ret.push(errorInfo); + return; + } + + if (error[0] === '^') { + error = error.slice(1); + } else if (options.fullMessages !== false) { + error = v.capitalize(prettify(errorInfo.attribute)) + " " + error; + } + error = error.replace(/\\\^/g, "^"); + error = v.format(error, { + value: v.stringifyValue(errorInfo.value, options) + }); + ret.push(v.extend({}, errorInfo, {error: error})); + }); + return ret; + }, + + // In: + // [{attribute: "", ...}] + // Out: + // {"": [{attribute: "", ...}]} + groupErrorsByAttribute: function(errors) { + var ret = {}; + errors.forEach(function(error) { + var list = ret[error.attribute]; + if (list) { + list.push(error); + } else { + ret[error.attribute] = [error]; + } + }); + return ret; + }, + + // In: + // [{error: "", ...}, {error: "", ...}] + // Out: + // ["", ""] + flattenErrorsToArray: function(errors) { + return errors + .map(function(error) { return error.error; }) + .filter(function(value, index, self) { + return self.indexOf(value) === index; + }); + }, + + cleanAttributes: function(attributes, whitelist) { + function whitelistCreator(obj, key, last) { + if (v.isObject(obj[key])) { + return obj[key]; + } + return (obj[key] = last ? true : {}); + } + + function buildObjectWhitelist(whitelist) { + var ow = {} + , lastObject + , attr; + for (attr in whitelist) { + if (!whitelist[attr]) { + continue; + } + v.forEachKeyInKeypath(ow, attr, whitelistCreator); + } + return ow; + } + + function cleanRecursive(attributes, whitelist) { + if (!v.isObject(attributes)) { + return attributes; + } + + var ret = v.extend({}, attributes) + , w + , attribute; + + for (attribute in attributes) { + w = whitelist[attribute]; + + if (v.isObject(w)) { + ret[attribute] = cleanRecursive(ret[attribute], w); + } else if (!w) { + delete ret[attribute]; + } + } + return ret; + } + + if (!v.isObject(whitelist) || !v.isObject(attributes)) { + return {}; + } + + whitelist = buildObjectWhitelist(whitelist); + return cleanRecursive(attributes, whitelist); + }, + + exposeModule: function(validate, root, exports, module, define) { + if (exports) { + if (module && module.exports) { + exports = module.exports = validate; + } + exports.validate = validate; + } else { + root.validate = validate; + if (validate.isFunction(define) && define.amd) { + define([], function () { return validate; }); + } + } + }, + + warn: function(msg) { + if (typeof console !== "undefined" && console.warn) { + console.warn("[validate.js] " + msg); + } + }, + + error: function(msg) { + if (typeof console !== "undefined" && console.error) { + console.error("[validate.js] " + msg); + } + } + }); + + validate.validators = { + // Presence validates that the value isn't empty + presence: function(value, options) { + options = v.extend({}, this.options, options); + if (options.allowEmpty !== false ? !v.isDefined(value) : v.isEmpty(value)) { + return options.message || this.message || "can't be blank"; + } + }, + length: function(value, options, attribute) { + // Empty values are allowed + if (!v.isDefined(value)) { + return; + } + + options = v.extend({}, this.options, options); + + var is = options.is + , maximum = options.maximum + , minimum = options.minimum + , tokenizer = options.tokenizer || function(val) { return val; } + , err + , errors = []; + + value = tokenizer(value); + var length = value.length; + if(!v.isNumber(length)) { + return options.message || this.notValid || "has an incorrect length"; + } + + // Is checks + if (v.isNumber(is) && length !== is) { + err = options.wrongLength || + this.wrongLength || + "is the wrong length (should be %{count} characters)"; + errors.push(v.format(err, {count: is})); + } + + if (v.isNumber(minimum) && length < minimum) { + err = options.tooShort || + this.tooShort || + "is too short (minimum is %{count} characters)"; + errors.push(v.format(err, {count: minimum})); + } + + if (v.isNumber(maximum) && length > maximum) { + err = options.tooLong || + this.tooLong || + "is too long (maximum is %{count} characters)"; + errors.push(v.format(err, {count: maximum})); + } + + if (errors.length > 0) { + return options.message || errors; + } + }, + numericality: function(value, options, attribute, attributes, globalOptions) { + // Empty values are fine + if (!v.isDefined(value)) { + return; + } + + options = v.extend({}, this.options, options); + + var errors = [] + , name + , count + , checks = { + greaterThan: function(v, c) { return v > c; }, + greaterThanOrEqualTo: function(v, c) { return v >= c; }, + equalTo: function(v, c) { return v === c; }, + lessThan: function(v, c) { return v < c; }, + lessThanOrEqualTo: function(v, c) { return v <= c; }, + divisibleBy: function(v, c) { return v % c === 0; } + } + , prettify = options.prettify || + (globalOptions && globalOptions.prettify) || + v.prettify; + + // Strict will check that it is a valid looking number + if (v.isString(value) && options.strict) { + var pattern = "^-?(0|[1-9]\\d*)"; + if (!options.onlyInteger) { + pattern += "(\\.\\d+)?"; + } + pattern += "$"; + + if (!(new RegExp(pattern).test(value))) { + return options.message || + options.notValid || + this.notValid || + this.message || + "must be a valid number"; + } + } + + // Coerce the value to a number unless we're being strict. + if (options.noStrings !== true && v.isString(value) && !v.isEmpty(value)) { + value = +value; + } + + // If it's not a number we shouldn't continue since it will compare it. + if (!v.isNumber(value)) { + return options.message || + options.notValid || + this.notValid || + this.message || + "is not a number"; + } + + // Same logic as above, sort of. Don't bother with comparisons if this + // doesn't pass. + if (options.onlyInteger && !v.isInteger(value)) { + return options.message || + options.notInteger || + this.notInteger || + this.message || + "must be an integer"; + } + + for (name in checks) { + count = options[name]; + if (v.isNumber(count) && !checks[name](value, count)) { + // This picks the default message if specified + // For example the greaterThan check uses the message from + // this.notGreaterThan so we capitalize the name and prepend "not" + var key = "not" + v.capitalize(name); + var msg = options[key] || + this[key] || + this.message || + "must be %{type} %{count}"; + + errors.push(v.format(msg, { + count: count, + type: prettify(name) + })); + } + } + + if (options.odd && value % 2 !== 1) { + errors.push(options.notOdd || + this.notOdd || + this.message || + "must be odd"); + } + if (options.even && value % 2 !== 0) { + errors.push(options.notEven || + this.notEven || + this.message || + "must be even"); + } + + if (errors.length) { + return options.message || errors; + } + }, + datetime: v.extend(function(value, options) { + if (!v.isFunction(this.parse) || !v.isFunction(this.format)) { + throw new Error("Both the parse and format functions needs to be set to use the datetime/date validator"); + } + + // Empty values are fine + if (!v.isDefined(value)) { + return; + } + + options = v.extend({}, this.options, options); + + var err + , errors = [] + , earliest = options.earliest ? this.parse(options.earliest, options) : NaN + , latest = options.latest ? this.parse(options.latest, options) : NaN; + + value = this.parse(value, options); + + // 86400000 is the number of milliseconds in a day, this is used to remove + // the time from the date + if (isNaN(value) || options.dateOnly && value % 86400000 !== 0) { + err = options.notValid || + options.message || + this.notValid || + "must be a valid date"; + return v.format(err, {value: arguments[0]}); + } + + if (!isNaN(earliest) && value < earliest) { + err = options.tooEarly || + options.message || + this.tooEarly || + "must be no earlier than %{date}"; + err = v.format(err, { + value: this.format(value, options), + date: this.format(earliest, options) + }); + errors.push(err); + } + + if (!isNaN(latest) && value > latest) { + err = options.tooLate || + options.message || + this.tooLate || + "must be no later than %{date}"; + err = v.format(err, { + date: this.format(latest, options), + value: this.format(value, options) + }); + errors.push(err); + } + + if (errors.length) { + return v.unique(errors); + } + }, { + parse: null, + format: null + }), + date: function(value, options) { + options = v.extend({}, options, {dateOnly: true}); + return v.validators.datetime.call(v.validators.datetime, value, options); + }, + format: function(value, options) { + if (v.isString(options) || (options instanceof RegExp)) { + options = {pattern: options}; + } + + options = v.extend({}, this.options, options); + + var message = options.message || this.message || "is invalid" + , pattern = options.pattern + , match; + + // Empty values are allowed + if (!v.isDefined(value)) { + return; + } + if (!v.isString(value)) { + return message; + } + + if (v.isString(pattern)) { + pattern = new RegExp(options.pattern, options.flags); + } + match = pattern.exec(value); + if (!match || match[0].length != value.length) { + return message; + } + }, + inclusion: function(value, options) { + // Empty values are fine + if (!v.isDefined(value)) { + return; + } + if (v.isArray(options)) { + options = {within: options}; + } + options = v.extend({}, this.options, options); + if (v.contains(options.within, value)) { + return; + } + var message = options.message || + this.message || + "^%{value} is not included in the list"; + return v.format(message, {value: value}); + }, + exclusion: function(value, options) { + // Empty values are fine + if (!v.isDefined(value)) { + return; + } + if (v.isArray(options)) { + options = {within: options}; + } + options = v.extend({}, this.options, options); + if (!v.contains(options.within, value)) { + return; + } + var message = options.message || this.message || "^%{value} is restricted"; + if (v.isString(options.within[value])) { + value = options.within[value]; + } + return v.format(message, {value: value}); + }, + email: v.extend(function(value, options) { + options = v.extend({}, this.options, options); + var message = options.message || this.message || "is not a valid email"; + // Empty values are fine + if (!v.isDefined(value)) { + return; + } + if (!v.isString(value)) { + return message; + } + if (!this.PATTERN.exec(value)) { + return message; + } + }, { + PATTERN: /^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$/i + }), + equality: function(value, options, attribute, attributes, globalOptions) { + if (!v.isDefined(value)) { + return; + } + + if (v.isString(options)) { + options = {attribute: options}; + } + options = v.extend({}, this.options, options); + var message = options.message || + this.message || + "is not equal to %{attribute}"; + + if (v.isEmpty(options.attribute) || !v.isString(options.attribute)) { + throw new Error("The attribute must be a non empty string"); + } + + var otherValue = v.getDeepObjectValue(attributes, options.attribute) + , comparator = options.comparator || function(v1, v2) { + return v1 === v2; + } + , prettify = options.prettify || + (globalOptions && globalOptions.prettify) || + v.prettify; + + if (!comparator(value, otherValue, options, attribute, attributes)) { + return v.format(message, {attribute: prettify(options.attribute)}); + } + }, + // A URL validator that is used to validate URLs with the ability to + // restrict schemes and some domains. + url: function(value, options) { + if (!v.isDefined(value)) { + return; + } + + options = v.extend({}, this.options, options); + + var message = options.message || this.message || "is not a valid url" + , schemes = options.schemes || this.schemes || ['http', 'https'] + , allowLocal = options.allowLocal || this.allowLocal || false; + + if (!v.isString(value)) { + return message; + } + + // https://gist.github.com/dperini/729294 + var regex = + "^" + + // protocol identifier + "(?:(?:" + schemes.join("|") + ")://)" + + // user:pass authentication + "(?:\\S+(?::\\S*)?@)?" + + "(?:"; + + var tld = "(?:\\.(?:[a-z\\u00a1-\\uffff]{2,}))"; + + if (allowLocal) { + tld += "?"; + } else { + regex += + // IP address exclusion + // private & local networks + "(?!(?:10|127)(?:\\.\\d{1,3}){3})" + + "(?!(?:169\\.254|192\\.168)(?:\\.\\d{1,3}){2})" + + "(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})"; + } + + regex += + // IP address dotted notation octets + // excludes loopback network 0.0.0.0 + // excludes reserved space >= 224.0.0.0 + // excludes network & broacast addresses + // (first & last IP address of each class) + "(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" + + "(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" + + "(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" + + "|" + + // host name + "(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)" + + // domain name + "(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*" + + tld + + ")" + + // port number + "(?::\\d{2,5})?" + + // resource path + "(?:[/?#]\\S*)?" + + "$"; + + var PATTERN = new RegExp(regex, 'i'); + if (!PATTERN.exec(value)) { + return message; + } + }, + type: v.extend(function(value, originalOptions, attribute, attributes, globalOptions) { + if (v.isString(originalOptions)) { + originalOptions = {type: originalOptions}; + } + + if (!v.isDefined(value)) { + return; + } + + var options = v.extend({}, this.options, originalOptions); + + var type = options.type; + if (!v.isDefined(type)) { + throw new Error("No type was specified"); + } + + var check; + if (v.isFunction(type)) { + check = type; + } else { + check = this.types[type]; + } + + if (!v.isFunction(check)) { + throw new Error("validate.validators.type.types." + type + " must be a function."); + } + + if (!check(value, options, attribute, attributes, globalOptions)) { + var message = originalOptions.message || + this.messages[type] || + this.message || + options.message || + (v.isFunction(type) ? "must be of the correct type" : "must be of type %{type}"); + + if (v.isFunction(message)) { + message = message(value, originalOptions, attribute, attributes, globalOptions); + } + + return v.format(message, {attribute: v.prettify(attribute), type: type}); + } + }, { + types: { + object: function(value) { + return v.isObject(value) && !v.isArray(value); + }, + array: v.isArray, + integer: v.isInteger, + number: v.isNumber, + string: v.isString, + date: v.isDate, + boolean: v.isBoolean + }, + messages: {} + }) + }; + + validate.formatters = { + detailed: function(errors) {return errors;}, + flat: v.flattenErrorsToArray, + grouped: function(errors) { + var attr; + + errors = v.groupErrorsByAttribute(errors); + for (attr in errors) { + errors[attr] = v.flattenErrorsToArray(errors[attr]); + } + return errors; + }, + constraint: function(errors) { + var attr; + errors = v.groupErrorsByAttribute(errors); + for (attr in errors) { + errors[attr] = errors[attr].map(function(result) { + return result.validator; + }).sort(); + } + return errors; + } + }; + + validate.exposeModule(validate, this, exports, module, define); +}).call(this, + typeof exports !== 'undefined' ? /* istanbul ignore next */ exports : null, + typeof module !== 'undefined' ? /* istanbul ignore next */ module : null, + typeof define !== 'undefined' ? /* istanbul ignore next */ define : null); diff --git a/server/simulation/vehicles.js b/server/simulation/vehicles.js index 2fbeb3ba..952794aa 100644 --- a/server/simulation/vehicles.js +++ b/server/simulation/vehicles.js @@ -27,8 +27,8 @@ const calculateNextCoordinate = async (vehicle, mission, leg, positionLastUpdate const legStartTime = leg === 'pickup' ? mission.vehicle_signed_at : mission.travelling_dropoff_at; let legCompletionTime = parseFloat(legStartTime) + parseFloat(mission['time_to_' + leg]); - const destinationLong = mission[leg + '_long']; - const destinationLat = mission[leg + '_lat']; + const destinationLong = mission[leg + '_longitude']; + const destinationLat = mission[leg + '_latitude']; const timeLeftAtPreviousPosition = legCompletionTime - positionLastUpdatedAt; @@ -44,13 +44,13 @@ const calculateNextCoordinate = async (vehicle, mission, leg, positionLastUpdate switch(leg){ case 'pickup':{ - long = dontMoveAtDestination(destinationLong, mission.vehicle_start_long, long); - lat = dontMoveAtDestination(destinationLat, mission.vehicle_start_lat, lat); + long = dontMoveAtDestination(destinationLong, mission.vehicle_start_longitude, long); + lat = dontMoveAtDestination(destinationLat, mission.vehicle_start_latitude, lat); break; } case 'dropoff':{ - long = dontMoveAtDestination(destinationLong, mission.pickup_long, long); - lat = dontMoveAtDestination(destinationLat, mission.pickup_lat, lat); + long = dontMoveAtDestination(destinationLong, mission.pickup_longitude, long); + lat = dontMoveAtDestination(destinationLat, mission.pickup_latitude, lat); break; } } diff --git a/server/store/bids.js b/server/store/bids.js index a797e0b2..853a54f0 100644 --- a/server/store/bids.js +++ b/server/store/bids.js @@ -51,13 +51,13 @@ const getBidsForNeed = async needId => { // If not enough bids, make some up if (bidIds.length < 10) { - const { pickup_long, pickup_lat, dropoff_lat, dropoff_long } = need; - const pickup = { lat: pickup_lat, long: pickup_long }; - const dropoff = { lat: dropoff_lat, long: dropoff_long }; + const { pickup_longitude, pickup_latitude, dropoff_latitude, dropoff_longitude } = need; + const pickup = { lat: pickup_latitude, long: pickup_longitude }; + const dropoff = { lat: dropoff_latitude, long: dropoff_longitude }; const vehicleIds = await redis.georadiusAsync( 'vehicle_positions', - pickup_long, - pickup_lat, + pickup_longitude, + pickup_latitude, 2000, 'm', ); diff --git a/server/store/missions.js b/server/store/missions.js index 1c60a948..2566d5eb 100644 --- a/server/store/missions.js +++ b/server/store/missions.js @@ -32,7 +32,7 @@ const createMission = async ({ user_id, bid_id }) => { // get neeed details const need = await getNeed(need_id); - const { pickup_lat, pickup_long, dropoff_lat, dropoff_long, requested_pickup_time, size, weight } = need; + const { pickup_latitude, pickup_longitude, dropoff_latitude, dropoff_longitude, start_at, cargo_type, weight } = need; // get new unique id for mission const missionId = await redis.incrAsync('next_mission_id'); @@ -52,12 +52,12 @@ const createMission = async ({ user_id, bid_id }) => { 'time_to_pickup', time_to_pickup, 'time_to_dropoff', time_to_dropoff, 'need_id', need_id, - 'pickup_lat', pickup_lat, - 'pickup_long', pickup_long, - 'dropoff_lat', dropoff_lat, - 'dropoff_long', dropoff_long, - 'requested_pickup_time', requested_pickup_time, - 'size', size, + 'pickup_latitude', pickup_latitude, + 'pickup_longitude', pickup_longitude, + 'dropoff_latitude', dropoff_latitude, + 'dropoff_longitude', dropoff_longitude, + 'start_at', start_at, + 'cargo_type', cargo_type, 'weight', weight, 'status', 'awaiting_signatures', 'user_signed_at', user_signed_at, @@ -68,12 +68,12 @@ const createMission = async ({ user_id, bid_id }) => { price, time_to_pickup, time_to_dropoff, - pickup_lat, - pickup_long, - dropoff_lat, - dropoff_long, - requested_pickup_time, - size, + pickup_latitude, + pickup_longitude, + dropoff_latitude, + dropoff_longitude, + start_at, + cargo_type, weight, user_signed_at, }; diff --git a/server/store/needs.js b/server/store/needs.js index 0384aaca..001db5d2 100644 --- a/server/store/needs.js +++ b/server/store/needs.js @@ -11,24 +11,11 @@ const getNeed = async needId => { const createNeed = async needDetails => { // get new unique id for need const needId = await redis.incrAsync('next_need_id'); - - // create a new need entry in Redis - const {user_id, pickup, dropoff, requested_pickup_time, size, weight} = needDetails; - const [pickup_lat, pickup_long] = pickup.split(','); - const [dropoff_lat, dropoff_long] = dropoff.split(','); - redis.hmsetAsync(`needs:${needId}`, - 'user_id', user_id, - 'pickup_lat', pickup_lat, - 'pickup_long', pickup_long, - 'dropoff_lat', dropoff_lat, - 'dropoff_long', dropoff_long, - 'requested_pickup_time', requested_pickup_time, - 'size', size, - 'weight', weight, - ); + const key_value_array = [].concat(...Object.entries(needDetails)); + redis.hmsetAsync(`needs:${needId}`, ...key_value_array); // See if there are any vehicles around the pickup position, if not a few vehicles will be generated there - getVehiclesInRange({ lat: parseFloat(pickup_lat), long: parseFloat(pickup_long) }, 7000); + getVehiclesInRange({ lat: parseFloat(needDetails.pickup_latitude), long: parseFloat(needDetails.pickup_longitude) }, 7000); // Set TTL for need redis.expire(`needs:${needId}`, config('needs_ttl')); From 5557ad90840e9705dc8cdc997dd8aaf349bced8e Mon Sep 17 00:00:00 2001 From: Timi Ajiboye Date: Tue, 13 Feb 2018 03:38:44 +0100 Subject: [PATCH 7/9] fix: lint issues --- server/controllers/NeedController.js | 6 +++--- server/controllers/constraints/need/create.js | 8 ++++---- server/lib/validate.js | 2 ++ server/middleware/cors.js | 2 +- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/server/controllers/NeedController.js b/server/controllers/NeedController.js index f18875df..776593ba 100644 --- a/server/controllers/NeedController.js +++ b/server/controllers/NeedController.js @@ -8,14 +8,14 @@ const createConstraints = require('./constraints/need/create'); const validate = require('../lib/validate'); const create = async (req, res) => { - const params = req.body + const params = req.body; const validationErrors = validate(params, createConstraints); if (validationErrors) { res.status(422).json(validationErrors); } else { const allowedParamsKeys = Object.keys(createConstraints); - Object.keys(params).forEach(key => {if (!allowedParamsKeys.includes(key)) delete params[key]}) - params.user_id = req.query.user_id + Object.keys(params).forEach(key => {if (!allowedParamsKeys.includes(key)) delete params[key];}); + params.user_id = req.query.user_id; const needId = await createNeed(params); if (needId) { res.json({needId}); diff --git a/server/controllers/constraints/need/create.js b/server/controllers/constraints/need/create.js index c607401d..c7e6c738 100644 --- a/server/controllers/constraints/need/create.js +++ b/server/controllers/constraints/need/create.js @@ -42,19 +42,19 @@ module.exports = { presence: true, type: 'number', inclusion: { - within: Array.from(new Array(18), (x,i) => i + 1) + within: Array.from(new Array(18), (x, i) => i + 1) } }, hazardous_goods: { type: 'number', inclusion: { - within: Array.from(new Array(9), (x,i) => i + 1) + within: Array.from(new Array(9), (x, i) => i + 1) } }, ip_protection_level: { type: 'number', inclusion: { - within: Array.from(new Array(69), (x,i) => i + 54) + within: Array.from(new Array(69), (x, i) => i + 54) } }, height: { @@ -70,7 +70,7 @@ module.exports = { type: 'number' }, insurance_required: { - type: 'boolean' + type: 'boolean' }, insured_value: { type: 'number' diff --git a/server/lib/validate.js b/server/lib/validate.js index 610d0bc9..5c2b1dd5 100644 --- a/server/lib/validate.js +++ b/server/lib/validate.js @@ -1,3 +1,5 @@ +/* eslint-disable */ + /*! * validate.js 0.12.0 * diff --git a/server/middleware/cors.js b/server/middleware/cors.js index 02e7ecf1..58b4b197 100644 --- a/server/middleware/cors.js +++ b/server/middleware/cors.js @@ -4,6 +4,6 @@ module.exports = (req, res, next) => { 'Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept', ); - res.header("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE"); + res.header('Access-Control-Allow-Methods', 'GET, PUT, POST, DELETE'); next(); }; From 514966e87dc5aea567dd3dfc165cfe670b3ee09f Mon Sep 17 00:00:00 2001 From: Timi Ajiboye Date: Wed, 14 Feb 2018 15:12:45 +0100 Subject: [PATCH 8/9] fix: fix validation issues, changed start_at to pickup_at --- server/controllers/NeedController.js | 2 - server/controllers/constraints/need/create.js | 50 +++++++++++++------ server/lib/validate.js | 4 ++ server/store/missions.js | 6 +-- 4 files changed, 42 insertions(+), 20 deletions(-) diff --git a/server/controllers/NeedController.js b/server/controllers/NeedController.js index 776593ba..615709e1 100644 --- a/server/controllers/NeedController.js +++ b/server/controllers/NeedController.js @@ -3,8 +3,6 @@ const {deleteBidsForNeed} = require('../store/bids'); const {createMission} = require('../store/missions'); const {updateVehicleStatus} = require('../store/vehicles'); const createConstraints = require('./constraints/need/create'); - -// using downloaded validate.js file because latest version does not have type checking and docker fails with git source in package.json const validate = require('../lib/validate'); const create = async (req, res) => { diff --git a/server/controllers/constraints/need/create.js b/server/controllers/constraints/need/create.js index c7e6c738..af185f4f 100644 --- a/server/controllers/constraints/need/create.js +++ b/server/controllers/constraints/need/create.js @@ -1,31 +1,35 @@ module.exports = { pickup_at: { numericality: { - greaterThan: Date.now() + greaterThanOrEqualTo: Date.now() } }, pickup_latitude: { presence: true, numericality: { lessThanOrEqualTo: 90, + greaterThanOrEqualTo: -90 } }, pickup_longitude: { presence: true, numericality: { lessThanOrEqualTo: 180, + greaterThanOrEqualTo: -180 } }, dropoff_latitude: { presence: true, numericality: { lessThanOrEqualTo: 90, + greaterThanOrEqualTo: -90 } }, dropoff_longitude: { presence: true, numericality: { lessThanOrEqualTo: 180, + greaterThanOrEqualTo: -180 } }, requester_name: { @@ -40,40 +44,56 @@ module.exports = { }, cargo_type: { presence: true, - type: 'number', - inclusion: { - within: Array.from(new Array(18), (x, i) => i + 1) + numericality: { + onlyInteger: true, + strict: true, + lessThanOrEqualTo: 18, + greaterThanOrEqualTo: 1 } }, hazardous_goods: { - type: 'number', - inclusion: { - within: Array.from(new Array(9), (x, i) => i + 1) + numericality: { + onlyInteger: true, + strict: true, + lessThanOrEqualTo: 9, + greaterThanOrEqualTo: 1 } }, ip_protection_level: { - type: 'number', - inclusion: { - within: Array.from(new Array(69), (x, i) => i + 54) + numericality: { + onlyInteger: true, + strict: true, + lessThanOrEqualTo: 69, + greaterThanOrEqualTo: 54 } }, height: { - type: 'number' + numericality: { + greaterThan: 0 + } }, width: { - type: 'number' + numericality: { + greaterThan: 0 + } }, length: { - type: 'number' + numericality: { + greaterThan: 0 + } }, weight: { - type: 'number' + numericality: { + greaterThan: 0 + } }, insurance_required: { type: 'boolean' }, insured_value: { - type: 'number' + numericality: { + greaterThan: 0 + } }, insured_value_currency: { length: { diff --git a/server/lib/validate.js b/server/lib/validate.js index 5c2b1dd5..89152334 100644 --- a/server/lib/validate.js +++ b/server/lib/validate.js @@ -1,5 +1,9 @@ /* eslint-disable */ +/* using downloaded validate.js file because latest version on npm does not have type checking + * and docker fails with git source in package.json + */ + /*! * validate.js 0.12.0 * diff --git a/server/store/missions.js b/server/store/missions.js index 2566d5eb..3adde3b0 100644 --- a/server/store/missions.js +++ b/server/store/missions.js @@ -32,7 +32,7 @@ const createMission = async ({ user_id, bid_id }) => { // get neeed details const need = await getNeed(need_id); - const { pickup_latitude, pickup_longitude, dropoff_latitude, dropoff_longitude, start_at, cargo_type, weight } = need; + const { pickup_latitude, pickup_longitude, dropoff_latitude, dropoff_longitude, pickup_at, cargo_type, weight } = need; // get new unique id for mission const missionId = await redis.incrAsync('next_mission_id'); @@ -56,7 +56,7 @@ const createMission = async ({ user_id, bid_id }) => { 'pickup_longitude', pickup_longitude, 'dropoff_latitude', dropoff_latitude, 'dropoff_longitude', dropoff_longitude, - 'start_at', start_at, + 'pickup_at', pickup_at, 'cargo_type', cargo_type, 'weight', weight, 'status', 'awaiting_signatures', @@ -72,7 +72,7 @@ const createMission = async ({ user_id, bid_id }) => { pickup_longitude, dropoff_latitude, dropoff_longitude, - start_at, + pickup_at, cargo_type, weight, user_signed_at, From be89e9f6d22c4109090b65147cd5a66fba654693 Mon Sep 17 00:00:00 2001 From: trevortosi Date: Sun, 18 Feb 2018 01:04:55 -0500 Subject: [PATCH 9/9] Removed redundant license footer --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 3a22b834..c29470c0 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,4 @@ As an organization committed to extreme transparency, collaboration, and open-so For help in getting started, please be sure to read our [contribution guidelines](https://github.com/DAVFoundation/missioncontrol/blob/master/CONTRIBUTING.md). -### License -Licensed under [MIT](https://github.com/DAVFoundation/missioncontrol/blob/master/LICENSE).