From a2394ef9627d5735babb548f9f3fe61a4a5b57b1 Mon Sep 17 00:00:00 2001 From: Maciej Ziarkowski Date: Wed, 14 Aug 2019 10:54:13 +0100 Subject: [PATCH 1/6] Move JSON API into its own directory --- app/src/api/api.ts | 280 +++++++++++++++++++++++++ app/src/api/{ => services}/building.ts | 4 +- app/src/api/{ => services}/search.ts | 2 +- app/src/api/{ => services}/user.ts | 2 +- app/src/server.tsx | 275 +----------------------- 5 files changed, 289 insertions(+), 274 deletions(-) create mode 100644 app/src/api/api.ts rename app/src/api/{ => services}/building.ts (99%) rename app/src/api/{ => services}/search.ts (96%) rename app/src/api/{ => services}/user.ts (99%) diff --git a/app/src/api/api.ts b/app/src/api/api.ts new file mode 100644 index 00000000..3401bf39 --- /dev/null +++ b/app/src/api/api.ts @@ -0,0 +1,280 @@ +import express from 'express'; + + +import { authUser, createUser, getUserById, authAPIUser, getNewUserAPIKey } from './services/user'; +import { + queryBuildingsAtPoint, + queryBuildingsByReference, + getBuildingById, + getBuildingLikeById, + getBuildingUPRNsById, + saveBuilding, + likeBuilding, + unlikeBuilding +} from './services/building'; +import { queryLocation } from './services/search'; + +const server = express.Router(); + +// GET buildings +// not implemented - may be useful to GET all buildings, paginated + +// GET buildings at point +server.get('/buildings/locate', function (req, res) { + const { lng, lat } = req.query; + queryBuildingsAtPoint(lng, lat).then(function (result) { + res.send(result); + }).catch(function (error) { + console.error(error); + res.send({ error: 'Database error' }) + }) +}); + +// GET buildings by reference (UPRN/TOID or other identifier) +server.get('/buildings/reference', function (req, res) { + const { key, id } = req.query; + queryBuildingsByReference(key, id).then(function (result) { + res.send(result); + }).catch(function (error) { + console.error(error); + res.send({ error: 'Database error' }) + }) +}); + +// GET individual building, POST building updates +server.route('/building/:building_id.json') + .get(function (req, res) { + const { building_id } = req.params; + getBuildingById(building_id).then(function (result) { + res.send(result); + }).catch(function (error) { + console.error(error); + res.send({ error: 'Database error' }) + }) + }) + .post(function (req, res) { + if (req.session.user_id) { + updateBuilding(req, res, req.session.user_id); + } else if (req.query.api_key) { + authAPIUser(req.query.api_key) + .then(function (user) { + updateBuilding(req, res, user.user_id) + }) + .catch(function (err) { + console.error(err); + res.send({ error: 'Must be logged in' }); + }); + } else { + res.send({ error: 'Must be logged in' }); + } + }) + +function updateBuilding(req, res, userId) { + const { building_id } = req.params; + const building = req.body; + saveBuilding(building_id, building, userId).then(building => { + if (building.error) { + res.send(building) + return + } + if (typeof (building) === 'undefined') { + res.send({ error: 'Database error' }) + return + } + res.send(building) + }).catch( + () => res.send({ error: 'Database error' }) + ) +} + +// GET building UPRNs +server.get('/building/:building_id/uprns.json', function (req, res) { + const { building_id } = req.params; + getBuildingUPRNsById(building_id).then(function (result) { + if (typeof (result) === 'undefined') { + res.send({ error: 'Database error' }) + return + } + res.send({ + uprns: result + }); + }).catch(function (error) { + console.error(error); + res.send({ error: 'Database error' }) + }) +}) + +// GET/POST like building +server.route('/building/:building_id/like.json') + .get(function (req, res) { + if (!req.session.user_id) { + res.send({ like: false }); // not logged in, so cannot have liked + return + } + const { building_id } = req.params; + getBuildingLikeById(building_id, req.session.user_id).then(like => { + // any value returned means like + res.send({ like: like }) + }).catch( + () => res.send({ error: 'Database error' }) + ) + }) + .post(function (req, res) { + if (!req.session.user_id) { + res.send({ error: 'Must be logged in' }); + return + } + const { building_id } = req.params; + const { like } = req.body; + if (like) { + likeBuilding(building_id, req.session.user_id).then(building => { + if (building.error) { + res.send(building) + return + } + if (typeof (building) === 'undefined') { + res.send({ error: 'Database error' }) + return + } + res.send(building) + }).catch( + () => res.send({ error: 'Database error' }) + ) + } else { + unlikeBuilding(building_id, req.session.user_id).then(building => { + if (building.error) { + res.send(building) + return + } + if (typeof (building) === 'undefined') { + res.send({ error: 'Database error' }) + return + } + res.send(building) + }).catch( + () => res.send({ error: 'Database error' }) + ) + } + }) + +// POST new user +server.post('/users', function (req, res) { + const user = req.body; + if (req.session.user_id) { + res.send({ error: 'Already signed in' }); + return + } + + if (user.email) { + if (user.email != user.confirm_email) { + res.send({ error: 'Email did not match confirmation.' }); + return + } + } else { + user.email = null; + } + + createUser(user).then(function (result) { + if (result.user_id) { + req.session.user_id = result.user_id; + res.send({ user_id: result.user_id }); + } else { + req.session.user_id = undefined; + res.send({ error: result.error }); + } + }).catch(function (err) { + console.error(err); + res.send(err) + }); +}); + +// POST user auth +server.post('/login', function (req, res) { + authUser(req.body.username, req.body.password).then(function (user: any) { // TODO: remove any + if (user.user_id) { + req.session.user_id = user.user_id; + } else { + req.session.user_id = undefined; + } + res.send(user); + }).catch(function (error) { + res.send(error); + }) +}); + +// POST user logout +server.post('/logout', function (req, res) { + req.session.user_id = undefined; + req.session.destroy(function (err) { + if (err) { + console.error(err); + res.send({ error: 'Failed to end session' }) + } + res.send({ success: true }); + }); +}); + +// GET own user info +server.get('/users/me', function (req, res) { + if (!req.session.user_id) { + res.send({ error: 'Must be logged in' }); + return + } + + getUserById(req.session.user_id).then(function (user) { + res.send(user); + }).catch(function (error) { + res.send(error); + }); +}); + +// POST generate API key +server.post('/api/key', function (req, res) { + if (!req.session.user_id) { + res.send({ error: 'Must be logged in' }); + return + } + + getNewUserAPIKey(req.session.user_id).then(function (apiKey) { + res.send(apiKey); + }).catch(function (error) { + res.send(error); + }); +}) + +// GET search +server.get('/search', function (req, res) { + const searchTerm = req.query.q; + if (!searchTerm) { + res.send({ + error: 'Please provide a search term' + }) + return + } + queryLocation(searchTerm).then((results) => { + if (typeof (results) === 'undefined') { + res.send({ + error: 'Database error' + }) + return + } + res.send({ + results: results.map(item => { + // map from DB results to GeoJSON Feature objects + const geom = JSON.parse(item.st_asgeojson) + return { + type: 'Feature', + attributes: { + label: item.search_str, + zoom: item.zoom || 9 + }, + geometry: geom + } + }) + }) + }).catch(function (error) { + res.send(error); + }); +}) + +export default server; \ No newline at end of file diff --git a/app/src/api/building.ts b/app/src/api/services/building.ts similarity index 99% rename from app/src/api/building.ts rename to app/src/api/services/building.ts index 39bf8d10..4d4befcc 100644 --- a/app/src/api/building.ts +++ b/app/src/api/services/building.ts @@ -2,8 +2,8 @@ * Building data access * */ -import db from '../db'; -import { removeAllAtBbox } from '../tiles/cache'; +import db from '../../db'; +import { removeAllAtBbox } from '../../tiles/cache'; // data type note: PostgreSQL bigint (64-bit) is handled as string in JavaScript, because of // JavaScript numerics are 64-bit double, giving only partial coverage. diff --git a/app/src/api/search.ts b/app/src/api/services/search.ts similarity index 96% rename from app/src/api/search.ts rename to app/src/api/services/search.ts index 08fbebe3..0defa117 100644 --- a/app/src/api/search.ts +++ b/app/src/api/services/search.ts @@ -6,7 +6,7 @@ * - this DOES expose geometry, another reason to keep this clearly separated from building * data */ -import db from '../db'; +import db from '../../db'; function queryLocation(term) { const limit = 5; diff --git a/app/src/api/user.ts b/app/src/api/services/user.ts similarity index 99% rename from app/src/api/user.ts rename to app/src/api/services/user.ts index 7cb3b6c2..da7e2e5d 100644 --- a/app/src/api/user.ts +++ b/app/src/api/services/user.ts @@ -2,7 +2,7 @@ * User data access * */ -import db from '../db'; +import db from '../../db'; function createUser(user) { if (!user.password || user.password.length < 8) { diff --git a/app/src/server.tsx b/app/src/server.tsx index fb80bbe9..581d772e 100644 --- a/app/src/server.tsx +++ b/app/src/server.tsx @@ -16,19 +16,14 @@ import pgConnect from 'connect-pg-simple'; import App from './frontend/app'; import db from './db'; -import { authUser, createUser, getUserById, authAPIUser, getNewUserAPIKey } from './api/user'; +import { getUserById } from './api/services/user'; import { - queryBuildingsAtPoint, - queryBuildingsByReference, getBuildingById, getBuildingLikeById, - getBuildingUPRNsById, - saveBuilding, - likeBuilding, - unlikeBuilding -} from './api/building'; -import { queryLocation } from './api/search'; + getBuildingUPRNsById +} from './api/services/building'; import tileserver from './tiles/tileserver'; +import apiRouter from './api/api'; import { parseBuildingURL } from './parse'; // create server @@ -162,268 +157,8 @@ function renderHTML(context, data, req, res) { } } -// GET tiles server.use('/tiles', tileserver); -// GET buildings -// not implemented - may be useful to GET all buildings, paginated - -// GET buildings at point -server.get('/buildings/locate', function (req, res) { - const { lng, lat } = req.query; - queryBuildingsAtPoint(lng, lat).then(function (result) { - res.send(result); - }).catch(function (error) { - console.error(error); - res.send({ error: 'Database error' }) - }) -}); - -// GET buildings by reference (UPRN/TOID or other identifier) -server.get('/buildings/reference', function (req, res) { - const { key, id } = req.query; - queryBuildingsByReference(key, id).then(function (result) { - res.send(result); - }).catch(function (error) { - console.error(error); - res.send({ error: 'Database error' }) - }) -}); - -// GET individual building, POST building updates -server.route('/building/:building_id.json') - .get(function (req, res) { - const { building_id } = req.params; - getBuildingById(building_id).then(function (result) { - res.send(result); - }).catch(function (error) { - console.error(error); - res.send({ error: 'Database error' }) - }) - }) - .post(function (req, res) { - if (req.session.user_id) { - updateBuilding(req, res, req.session.user_id); - } else if (req.query.api_key) { - authAPIUser(req.query.api_key) - .then(function (user) { - updateBuilding(req, res, user.user_id) - }) - .catch(function (err) { - console.error(err); - res.send({ error: 'Must be logged in' }); - }); - } else { - res.send({ error: 'Must be logged in' }); - } - }) - -function updateBuilding(req, res, userId) { - const { building_id } = req.params; - const building = req.body; - saveBuilding(building_id, building, userId).then(building => { - if (building.error) { - res.send(building) - return - } - if (typeof (building) === 'undefined') { - res.send({ error: 'Database error' }) - return - } - res.send(building) - }).catch( - () => res.send({ error: 'Database error' }) - ) -} - -// GET building UPRNs -server.get('/building/:building_id/uprns.json', function (req, res) { - const { building_id } = req.params; - getBuildingUPRNsById(building_id).then(function (result) { - if (typeof (result) === 'undefined') { - res.send({ error: 'Database error' }) - return - } - res.send({ - uprns: result - }); - }).catch(function (error) { - console.error(error); - res.send({ error: 'Database error' }) - }) -}) - -// GET/POST like building -server.route('/building/:building_id/like.json') - .get(function (req, res) { - if (!req.session.user_id) { - res.send({ like: false }); // not logged in, so cannot have liked - return - } - const { building_id } = req.params; - getBuildingLikeById(building_id, req.session.user_id).then(like => { - // any value returned means like - res.send({ like: like }) - }).catch( - () => res.send({ error: 'Database error' }) - ) - }) - .post(function (req, res) { - if (!req.session.user_id) { - res.send({ error: 'Must be logged in' }); - return - } - const { building_id } = req.params; - const { like } = req.body; - if (like) { - likeBuilding(building_id, req.session.user_id).then(building => { - if (building.error) { - res.send(building) - return - } - if (typeof (building) === 'undefined') { - res.send({ error: 'Database error' }) - return - } - res.send(building) - }).catch( - () => res.send({ error: 'Database error' }) - ) - } else { - unlikeBuilding(building_id, req.session.user_id).then(building => { - if (building.error) { - res.send(building) - return - } - if (typeof (building) === 'undefined') { - res.send({ error: 'Database error' }) - return - } - res.send(building) - }).catch( - () => res.send({ error: 'Database error' }) - ) - } - }) - -// POST new user -server.post('/users', function (req, res) { - const user = req.body; - if (req.session.user_id) { - res.send({ error: 'Already signed in' }); - return - } - - if (user.email) { - if (user.email != user.confirm_email) { - res.send({ error: 'Email did not match confirmation.' }); - return - } - } else { - user.email = null; - } - - createUser(user).then(function (result) { - if (result.user_id) { - req.session.user_id = result.user_id; - res.send({ user_id: result.user_id }); - } else { - req.session.user_id = undefined; - res.send({ error: result.error }); - } - }).catch(function (err) { - console.error(err); - res.send(err) - }); -}); - -// POST user auth -server.post('/login', function (req, res) { - authUser(req.body.username, req.body.password).then(function (user: any) { // TODO: remove any - if (user.user_id) { - req.session.user_id = user.user_id; - } else { - req.session.user_id = undefined; - } - res.send(user); - }).catch(function (error) { - res.send(error); - }) -}); - -// POST user logout -server.post('/logout', function (req, res) { - req.session.user_id = undefined; - req.session.destroy(function (err) { - if (err) { - console.error(err); - res.send({ error: 'Failed to end session' }) - } - res.send({ success: true }); - }); -}); - -// GET own user info -server.get('/users/me', function (req, res) { - if (!req.session.user_id) { - res.send({ error: 'Must be logged in' }); - return - } - - getUserById(req.session.user_id).then(function (user) { - res.send(user); - }).catch(function (error) { - res.send(error); - }); -}); - -// POST generate API key -server.post('/api/key', function (req, res) { - if (!req.session.user_id) { - res.send({ error: 'Must be logged in' }); - return - } - - getNewUserAPIKey(req.session.user_id).then(function (apiKey) { - res.send(apiKey); - }).catch(function (error) { - res.send(error); - }); -}) - -// GET search -server.get('/search', function (req, res) { - const searchTerm = req.query.q; - if (!searchTerm) { - res.send({ - error: 'Please provide a search term' - }) - return - } - queryLocation(searchTerm).then((results) => { - if (typeof (results) === 'undefined') { - res.send({ - error: 'Database error' - }) - return - } - res.send({ - results: results.map(item => { - // map from DB results to GeoJSON Feature objects - const geom = JSON.parse(item.st_asgeojson) - return { - type: 'Feature', - attributes: { - label: item.search_str, - zoom: item.zoom || 9 - }, - geometry: geom - } - }) - }) - }).catch(function (error) { - res.send(error); - }); -}) +server.use('/api', apiRouter); export default server; From 2c9b5ea3d8c76d44f56c2084033f0bf96c7da223 Mon Sep 17 00:00:00 2001 From: Maciej Ziarkowski Date: Wed, 14 Aug 2019 14:05:49 +0100 Subject: [PATCH 2/6] Modify routes, refactor API structure --- app/src/api/api.ts | 157 +----------------- app/src/api/controllers/buildingController.ts | 154 +++++++++++++++++ app/src/api/routes/buildingsRouter.ts | 32 ++++ app/src/frontend/app.tsx | 8 +- app/src/frontend/building-edit.tsx | 4 +- app/src/frontend/login.tsx | 4 +- app/src/frontend/map.tsx | 2 +- app/src/frontend/my-account.tsx | 4 +- app/src/frontend/search-box.tsx | 2 +- app/src/frontend/signup.tsx | 4 +- app/src/server.tsx | 7 +- .../load_conservation_areas.py | 2 +- etl/join_building_data/load_data.py | 2 +- 13 files changed, 211 insertions(+), 171 deletions(-) create mode 100644 app/src/api/controllers/buildingController.ts create mode 100644 app/src/api/routes/buildingsRouter.ts diff --git a/app/src/api/api.ts b/app/src/api/api.ts index 3401bf39..3b799a38 100644 --- a/app/src/api/api.ts +++ b/app/src/api/api.ts @@ -1,161 +1,18 @@ import express from 'express'; +import bodyParser from 'body-parser'; - -import { authUser, createUser, getUserById, authAPIUser, getNewUserAPIKey } from './services/user'; -import { - queryBuildingsAtPoint, - queryBuildingsByReference, - getBuildingById, - getBuildingLikeById, - getBuildingUPRNsById, - saveBuilding, - likeBuilding, - unlikeBuilding -} from './services/building'; +import { authUser, createUser, getUserById, getNewUserAPIKey } from './services/user'; import { queryLocation } from './services/search'; -const server = express.Router(); +import buildingsRouter from './routes/buildingsRouter'; -// GET buildings -// not implemented - may be useful to GET all buildings, paginated -// GET buildings at point -server.get('/buildings/locate', function (req, res) { - const { lng, lat } = req.query; - queryBuildingsAtPoint(lng, lat).then(function (result) { - res.send(result); - }).catch(function (error) { - console.error(error); - res.send({ error: 'Database error' }) - }) -}); +const server = express(); -// GET buildings by reference (UPRN/TOID or other identifier) -server.get('/buildings/reference', function (req, res) { - const { key, id } = req.query; - queryBuildingsByReference(key, id).then(function (result) { - res.send(result); - }).catch(function (error) { - console.error(error); - res.send({ error: 'Database error' }) - }) -}); +// parse POSTed json body +server.use(bodyParser.json()); -// GET individual building, POST building updates -server.route('/building/:building_id.json') - .get(function (req, res) { - const { building_id } = req.params; - getBuildingById(building_id).then(function (result) { - res.send(result); - }).catch(function (error) { - console.error(error); - res.send({ error: 'Database error' }) - }) - }) - .post(function (req, res) { - if (req.session.user_id) { - updateBuilding(req, res, req.session.user_id); - } else if (req.query.api_key) { - authAPIUser(req.query.api_key) - .then(function (user) { - updateBuilding(req, res, user.user_id) - }) - .catch(function (err) { - console.error(err); - res.send({ error: 'Must be logged in' }); - }); - } else { - res.send({ error: 'Must be logged in' }); - } - }) - -function updateBuilding(req, res, userId) { - const { building_id } = req.params; - const building = req.body; - saveBuilding(building_id, building, userId).then(building => { - if (building.error) { - res.send(building) - return - } - if (typeof (building) === 'undefined') { - res.send({ error: 'Database error' }) - return - } - res.send(building) - }).catch( - () => res.send({ error: 'Database error' }) - ) -} - -// GET building UPRNs -server.get('/building/:building_id/uprns.json', function (req, res) { - const { building_id } = req.params; - getBuildingUPRNsById(building_id).then(function (result) { - if (typeof (result) === 'undefined') { - res.send({ error: 'Database error' }) - return - } - res.send({ - uprns: result - }); - }).catch(function (error) { - console.error(error); - res.send({ error: 'Database error' }) - }) -}) - -// GET/POST like building -server.route('/building/:building_id/like.json') - .get(function (req, res) { - if (!req.session.user_id) { - res.send({ like: false }); // not logged in, so cannot have liked - return - } - const { building_id } = req.params; - getBuildingLikeById(building_id, req.session.user_id).then(like => { - // any value returned means like - res.send({ like: like }) - }).catch( - () => res.send({ error: 'Database error' }) - ) - }) - .post(function (req, res) { - if (!req.session.user_id) { - res.send({ error: 'Must be logged in' }); - return - } - const { building_id } = req.params; - const { like } = req.body; - if (like) { - likeBuilding(building_id, req.session.user_id).then(building => { - if (building.error) { - res.send(building) - return - } - if (typeof (building) === 'undefined') { - res.send({ error: 'Database error' }) - return - } - res.send(building) - }).catch( - () => res.send({ error: 'Database error' }) - ) - } else { - unlikeBuilding(building_id, req.session.user_id).then(building => { - if (building.error) { - res.send(building) - return - } - if (typeof (building) === 'undefined') { - res.send({ error: 'Database error' }) - return - } - res.send(building) - }).catch( - () => res.send({ error: 'Database error' }) - ) - } - }) +server.use('/buildings', buildingsRouter); // POST new user server.post('/users', function (req, res) { diff --git a/app/src/api/controllers/buildingController.ts b/app/src/api/controllers/buildingController.ts new file mode 100644 index 00000000..7987293d --- /dev/null +++ b/app/src/api/controllers/buildingController.ts @@ -0,0 +1,154 @@ +import * as buildingService from '../services/building'; +import * as userService from '../services/user'; + + +// GET buildings +// not implemented - may be useful to GET all buildings, paginated + +// GET buildings at point +function getBuildingsByLocation(req, res) { + const { lng, lat } = req.query; + buildingService.queryBuildingsAtPoint(lng, lat).then(function (result) { + res.send(result); + }).catch(function (error) { + console.error(error); + res.send({ error: 'Database error' }) + }) +} + +// GET buildings by reference (UPRN/TOID or other identifier) +function getBuildingsByReference(req, res) { + const { key, id } = req.query; + buildingService.queryBuildingsByReference(key, id).then(function (result) { + res.send(result); + }).catch(function (error) { + console.error(error); + res.send({ error: 'Database error' }) + }) +} + +// GET individual building, POST building updates +function getBuildingById(req, res) { + const { building_id } = req.params; + buildingService.getBuildingById(building_id).then(function (result) { + res.send(result); + }).catch(function (error) { + console.error(error); + res.send({ error: 'Database error' }) + }) +} + +function updateBuildingById(req, res) { + if (req.session.user_id) { + updateBuilding(req, res, req.session.user_id); + } else if (req.query.api_key) { + userService.authAPIUser(req.query.api_key) + .then(function (user) { + updateBuilding(req, res, user.user_id) + }) + .catch(function (err) { + console.error(err); + res.send({ error: 'Must be logged in' }); + }); + } else { + res.send({ error: 'Must be logged in' }); + } +} + +function updateBuilding(req, res, userId) { + const { building_id } = req.params; + const building = req.body; + buildingService.saveBuilding(building_id, building, userId).then(building => { + if (building.error) { + res.send(building) + return + } + if (typeof (building) === 'undefined') { + res.send({ error: 'Database error' }) + return + } + res.send(building) + }).catch( + () => res.send({ error: 'Database error' }) + ) +} + +// GET building UPRNs +function getBuildingUPRNsById(req, res) { + const { building_id } = req.params; + buildingService.getBuildingUPRNsById(building_id).then(function (result) { + if (typeof (result) === 'undefined') { + res.send({ error: 'Database error' }) + return + } + res.send({ + uprns: result + }); + }).catch(function (error) { + console.error(error); + res.send({ error: 'Database error' }) + }) +} + +// GET/POST like building +function getBuildingLikeById(req, res) { + if (!req.session.user_id) { + res.send({ like: false }); // not logged in, so cannot have liked + return + } + const { building_id } = req.params; + buildingService.getBuildingLikeById(building_id, req.session.user_id).then(like => { + // any value returned means like + res.send({ like: like }) + }).catch( + () => res.send({ error: 'Database error' }) + ) +} + +function updateBuildingLikeById(req, res) { + if (!req.session.user_id) { + res.send({ error: 'Must be logged in' }); + return + } + const { building_id } = req.params; + const { like } = req.body; + if (like) { + buildingService.likeBuilding(building_id, req.session.user_id).then(building => { + if (building.error) { + res.send(building) + return + } + if (typeof (building) === 'undefined') { + res.send({ error: 'Database error' }) + return + } + res.send(building) + }).catch( + () => res.send({ error: 'Database error' }) + ) + } else { + buildingService.unlikeBuilding(building_id, req.session.user_id).then(building => { + if (building.error) { + res.send(building) + return + } + if (typeof (building) === 'undefined') { + res.send({ error: 'Database error' }) + return + } + res.send(building) + }).catch( + () => res.send({ error: 'Database error' }) + ) + } +} + +export default { + getBuildingsByLocation, + getBuildingsByReference, + getBuildingById, + updateBuildingById, + getBuildingUPRNsById, + getBuildingLikeById, + updateBuildingLikeById +}; \ No newline at end of file diff --git a/app/src/api/routes/buildingsRouter.ts b/app/src/api/routes/buildingsRouter.ts new file mode 100644 index 00000000..a8921ab6 --- /dev/null +++ b/app/src/api/routes/buildingsRouter.ts @@ -0,0 +1,32 @@ +import express from 'express'; + +import buildingController from '../controllers/buildingController'; + +const router = express.Router(); + + +// GET buildings +// not implemented - may be useful to GET all buildings, paginated + +// GET buildings at point +router.get('/locate', buildingController.getBuildingsByLocation); + +// GET buildings by reference (UPRN/TOID or other identifier) +router.get('/reference', buildingController.getBuildingsByReference); + +router.route('/:building_id.json') + // GET individual building + .get(buildingController.getBuildingById) + // POST building updates + .post(buildingController.updateBuildingById); + + +// GET building UPRNs +router.get('/:building_id/uprns.json', buildingController.getBuildingUPRNsById); + +// GET/POST like building +router.route('/:building_id/like.json') + .get(buildingController.getBuildingLikeById) + .post(buildingController.updateBuildingLikeById); + +export default router; \ No newline at end of file diff --git a/app/src/frontend/app.tsx b/app/src/frontend/app.tsx index 0c824689..c060313b 100644 --- a/app/src/frontend/app.tsx +++ b/app/src/frontend/app.tsx @@ -85,7 +85,7 @@ class App extends React.Component { // TODO: add proper types selectBuilding(building) { this.increaseRevision(building.revision_id); // get UPRNs and update - fetch(`/building/${building.building_id}/uprns.json`, { + fetch(`/api/buildings/${building.building_id}/uprns.json`, { method: 'GET', headers:{ 'Content-Type': 'application/json' @@ -106,7 +106,7 @@ class App extends React.Component { // TODO: add proper types }); // get if liked and update - fetch(`/building/${building.building_id}/like.json`, { + fetch(`/api/buildings/${building.building_id}/like.json`, { method: 'GET', headers:{ 'Content-Type': 'application/json' @@ -147,7 +147,7 @@ class App extends React.Component { // TODO: add proper types } likeBuilding(buildingId) { - fetch(`/building/${buildingId}/like.json`, { + fetch(`/api/buildings/${buildingId}/like.json`, { method: 'POST', headers:{ 'Content-Type': 'application/json' @@ -168,7 +168,7 @@ class App extends React.Component { // TODO: add proper types } updateBuilding(buildingId, data){ - fetch(`/building/${buildingId}.json`, { + fetch(`/api/buildings/${buildingId}.json`, { method: 'POST', body: JSON.stringify(data), headers:{ diff --git a/app/src/frontend/building-edit.tsx b/app/src/frontend/building-edit.tsx index 80da4816..e7cc9eb8 100644 --- a/app/src/frontend/building-edit.tsx +++ b/app/src/frontend/building-edit.tsx @@ -176,7 +176,7 @@ class EditForm extends Component { // TODO: add proper types event.preventDefault(); const like = event.target.checked; - fetch(`/building/${this.props.building_id}/like.json`, { + fetch(`/api/buildings/${this.props.building_id}/like.json`, { method: 'POST', headers:{ 'Content-Type': 'application/json' @@ -203,7 +203,7 @@ class EditForm extends Component { // TODO: add proper types event.preventDefault(); this.setState({error: undefined}) - fetch(`/building/${this.props.building_id}.json`, { + fetch(`/api/buildings/${this.props.building_id}.json`, { method: 'POST', body: JSON.stringify(this.state), headers:{ diff --git a/app/src/frontend/login.tsx b/app/src/frontend/login.tsx index 569ec4c1..bd8c7302 100644 --- a/app/src/frontend/login.tsx +++ b/app/src/frontend/login.tsx @@ -39,7 +39,7 @@ class Login extends Component { // TODO: add proper types event.preventDefault(); this.setState({error: undefined}) - fetch('/login', { + fetch('/api/login', { method: 'POST', body: JSON.stringify(this.state), headers:{ @@ -52,7 +52,7 @@ class Login extends Component { // TODO: add proper types if (res.error) { this.setState({error: res.error}) } else { - fetch('/users/me', { + fetch('/api/users/me', { credentials: 'same-origin' }).then( (res) => res.json() diff --git a/app/src/frontend/map.tsx b/app/src/frontend/map.tsx index 013b04e6..e32c52b2 100644 --- a/app/src/frontend/map.tsx +++ b/app/src/frontend/map.tsx @@ -61,7 +61,7 @@ class ColouringMap extends Component { // TODO: add proper types const newCat = parseCategoryURL(this.props.match.url); const mapCat = newCat || 'age'; fetch( - '/buildings/locate?lat='+lat+'&lng='+lng + '/api/buildings/locate?lat='+lat+'&lng='+lng ).then( (res) => res.json() ).then(function(data){ diff --git a/app/src/frontend/my-account.tsx b/app/src/frontend/my-account.tsx index 3094e5cc..32c4a7ec 100644 --- a/app/src/frontend/my-account.tsx +++ b/app/src/frontend/my-account.tsx @@ -30,7 +30,7 @@ class MyAccountPage extends Component { // TODO: add proper types event.preventDefault(); this.setState({error: undefined}); - fetch('/logout', { + fetch('/api/logout', { method: 'POST', credentials: 'same-origin' }).then( @@ -50,7 +50,7 @@ class MyAccountPage extends Component { // TODO: add proper types event.preventDefault(); this.setState({error: undefined}); - fetch('/api/key', { + fetch('/api/api/key', { method: 'POST', credentials: 'same-origin' }).then( diff --git a/app/src/frontend/search-box.tsx b/app/src/frontend/search-box.tsx index 52b5a988..e33b05bc 100644 --- a/app/src/frontend/search-box.tsx +++ b/app/src/frontend/search-box.tsx @@ -78,7 +78,7 @@ class SearchBox extends Component { // TODO: add proper types }) fetch( - '/search?q='+this.state.q + '/api/search?q='+this.state.q ).then( (res) => res.json() ).then((data) => { diff --git a/app/src/frontend/signup.tsx b/app/src/frontend/signup.tsx index d78263fb..1163d933 100644 --- a/app/src/frontend/signup.tsx +++ b/app/src/frontend/signup.tsx @@ -42,7 +42,7 @@ class SignUp extends Component { // TODO: add proper types event.preventDefault(); this.setState({error: undefined}) - fetch('/users', { + fetch('/api/users', { method: 'POST', body: JSON.stringify(this.state), headers:{ @@ -55,7 +55,7 @@ class SignUp extends Component { // TODO: add proper types if (res.error) { this.setState({error: res.error}) } else { - fetch('/users/me', { + fetch('/api/users/me', { credentials: 'same-origin' }).then( (res) => res.json() diff --git a/app/src/server.tsx b/app/src/server.tsx index 581d772e..ce807088 100644 --- a/app/src/server.tsx +++ b/app/src/server.tsx @@ -10,7 +10,6 @@ import express from 'express'; import { renderToString } from 'react-dom/server'; import serialize from 'serialize-javascript'; -import bodyParser from 'body-parser'; import session from 'express-session'; import pgConnect from 'connect-pg-simple'; @@ -23,7 +22,7 @@ import { getBuildingUPRNsById } from './api/services/building'; import tileserver from './tiles/tileserver'; -import apiRouter from './api/api'; +import apiServer from './api/api'; import { parseBuildingURL } from './parse'; // create server @@ -38,8 +37,6 @@ server.disable('x-powered-by'); // serve static files server.use(express.static(process.env.RAZZLE_PUBLIC_DIR)); -// parse POSTed json body -server.use(bodyParser.json()); // handle user sessions const pgSession = pgConnect(session); @@ -159,6 +156,6 @@ function renderHTML(context, data, req, res) { server.use('/tiles', tileserver); -server.use('/api', apiRouter); +server.use('/api', apiServer); export default server; diff --git a/etl/join_building_data/load_conservation_areas.py b/etl/join_building_data/load_conservation_areas.py index cd84c595..48832423 100644 --- a/etl/join_building_data/load_conservation_areas.py +++ b/etl/join_building_data/load_conservation_areas.py @@ -59,7 +59,7 @@ def save_data(building_id, data, api_key, base_url): """Save data to a building """ r = requests.post( - "{}/building/{}.json?api_key={}".format(base_url, building_id, api_key), + "{}/buildings/{}.json?api_key={}".format(base_url, building_id, api_key), json=data ) diff --git a/etl/join_building_data/load_data.py b/etl/join_building_data/load_data.py index 28ac7eca..4b805844 100644 --- a/etl/join_building_data/load_data.py +++ b/etl/join_building_data/load_data.py @@ -89,7 +89,7 @@ def save_data(building_id, data, api_key, base_url): """Save data to a building """ r = requests.post( - "{}/building/{}.json?api_key={}".format(base_url, building_id, api_key), + "{}/buildings/{}.json?api_key={}".format(base_url, building_id, api_key), json=data ) From be564dcceabcaa7b19012ad46ac7d1f8b5947336 Mon Sep 17 00:00:00 2001 From: Maciej Ziarkowski Date: Wed, 14 Aug 2019 14:13:14 +0100 Subject: [PATCH 3/6] Add JSON API 404 handler --- app/src/api/api.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/api/api.ts b/app/src/api/api.ts index 3b799a38..253a2081 100644 --- a/app/src/api/api.ts +++ b/app/src/api/api.ts @@ -134,4 +134,8 @@ server.get('/search', function (req, res) { }); }) +server.use((req, res) => { + res.status(404).json({ error: 'Resource not found'}); +}) + export default server; \ No newline at end of file From 6f4dfa22270232806afb26c0906938ba1b7c1fa6 Mon Sep 17 00:00:00 2001 From: Maciej Ziarkowski Date: Wed, 14 Aug 2019 14:17:47 +0100 Subject: [PATCH 4/6] Handle invalid URLs with the frontend 404 page --- app/src/server.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/server.tsx b/app/src/server.tsx index ce807088..3d46d6ab 100644 --- a/app/src/server.tsx +++ b/app/src/server.tsx @@ -158,4 +158,7 @@ server.use('/tiles', tileserver); server.use('/api', apiServer); +// use the frontend route for anything else - will presumably show the 404 page +server.use(frontendRoute); + export default server; From 8b78a748744eb8bbc785c5b447b435f35073f278 Mon Sep 17 00:00:00 2001 From: Maciej Ziarkowski Date: Thu, 15 Aug 2019 12:19:43 +0100 Subject: [PATCH 5/6] Remove unnecessary form action/method The forms calling the API had an unnecessary form action and method. This is because the forms have an onSubmit hook calling preventDefault. The attributes were removed to avoid confusion. --- app/src/frontend/my-account.tsx | 4 ++-- app/src/frontend/search-box.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/frontend/my-account.tsx b/app/src/frontend/my-account.tsx index 32c4a7ec..81c77d30 100644 --- a/app/src/frontend/my-account.tsx +++ b/app/src/frontend/my-account.tsx @@ -81,7 +81,7 @@ class MyAccountPage extends Component { // TODO: add proper types

-
+
Start colouring @@ -104,7 +104,7 @@ class MyAccountPage extends Component { // TODO: add proper types

Are you a software developer? If so, you might be interested in these.

API key

{this.props.user.api_key? this.props.user.api_key : '-'}

- + diff --git a/app/src/frontend/search-box.tsx b/app/src/frontend/search-box.tsx index e33b05bc..cbc270b1 100644 --- a/app/src/frontend/search-box.tsx +++ b/app/src/frontend/search-box.tsx @@ -160,7 +160,7 @@ class SearchBox extends Component { // TODO: add proper types : null; return (
-
+
From b5b72e71521525487a0f2cef295de8db3cba406f Mon Sep 17 00:00:00 2001 From: mz8i <36160844+mz8i@users.noreply.github.com> Date: Fri, 16 Aug 2019 10:57:15 +0100 Subject: [PATCH 6/6] Switch from express app to router in API Co-Authored-By: Tom Russell --- app/src/api/api.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/api/api.ts b/app/src/api/api.ts index 253a2081..2ab9ba3b 100644 --- a/app/src/api/api.ts +++ b/app/src/api/api.ts @@ -7,7 +7,7 @@ import { queryLocation } from './services/search'; import buildingsRouter from './routes/buildingsRouter'; -const server = express(); +const server = express.Router() // parse POSTed json body server.use(bodyParser.json()); @@ -138,4 +138,4 @@ server.use((req, res) => { res.status(404).json({ error: 'Resource not found'}); }) -export default server; \ No newline at end of file +export default server;