Add backend service to GET/POST user verifications, and include verification counts in building data

This commit is contained in:
Tom Russell 2020-08-04 15:54:12 +01:00
parent 672a1efde4
commit b30e882669
5 changed files with 214 additions and 7 deletions

View File

@ -35,7 +35,7 @@ const getBuildingsByReference = asyncController(async (req: express.Request, res
} }
}); });
// GET individual building, POST building updates // GET individual building
const getBuildingById = asyncController(async (req: express.Request, res: express.Response) => { const getBuildingById = asyncController(async (req: express.Request, res: express.Response) => {
const buildingId = processParam(req.params, 'building_id', parsePositiveIntParam, true); const buildingId = processParam(req.params, 'building_id', parsePositiveIntParam, true);
@ -48,6 +48,7 @@ const getBuildingById = asyncController(async (req: express.Request, res: expres
} }
}); });
// POST building attribute updates
const updateBuildingById = asyncController(async (req: express.Request, res: express.Response) => { const updateBuildingById = asyncController(async (req: express.Request, res: express.Response) => {
let user_id; let user_id;
@ -105,7 +106,7 @@ const getBuildingUPRNsById = asyncController(async (req: express.Request, res: e
} }
}); });
// GET/POST like building // GET whether the user likes a building
const getBuildingLikeById = asyncController(async (req: express.Request, res: express.Response) => { const getBuildingLikeById = asyncController(async (req: express.Request, res: express.Response) => {
if (!req.session.user_id) { if (!req.session.user_id) {
return res.send({ like: false }); // not logged in, so cannot have liked return res.send({ like: false }); // not logged in, so cannot have liked
@ -123,6 +124,7 @@ const getBuildingLikeById = asyncController(async (req: express.Request, res: ex
} }
}); });
// GET building edit history
const getBuildingEditHistoryById = asyncController(async (req: express.Request, res: express.Response) => { const getBuildingEditHistoryById = asyncController(async (req: express.Request, res: express.Response) => {
const buildingId = processParam(req.params, 'building_id', parsePositiveIntParam, true); const buildingId = processParam(req.params, 'building_id', parsePositiveIntParam, true);
@ -135,6 +137,7 @@ const getBuildingEditHistoryById = asyncController(async (req: express.Request,
} }
}); });
// POST update to like/unlike building
const updateBuildingLikeById = asyncController(async (req: express.Request, res: express.Response) => { const updateBuildingLikeById = asyncController(async (req: express.Request, res: express.Response) => {
if (!req.session.user_id) { if (!req.session.user_id) {
return res.send({ error: 'Must be logged in' }); return res.send({ error: 'Must be logged in' });
@ -159,6 +162,48 @@ const updateBuildingLikeById = asyncController(async (req: express.Request, res:
res.send(updatedBuilding); res.send(updatedBuilding);
}); });
// GET building attributes (and values) as verified by user
const getUserVerifiedAttributes = asyncController(async (req: express.Request, res: express.Response) => {
if (!req.session.user_id) {
return res.send({error: "Not logged in"}); // not logged in, so send empty object as no attributes verified
}
const buildingId = processParam(req.params, 'building_id', parsePositiveIntParam, true);
try {
const verifiedAttributes = await buildingService.getUserVerifiedAttributes(buildingId, req.session.user_id);
res.send(verifiedAttributes);
} catch (error) {
if(error instanceof UserError) {
throw new ApiUserError(error.message, error);
}
throw error;
}
});
// POST update to verify building attribute
const verifyBuildingAttributes = asyncController(async (req: express.Request, res: express.Response) => {
if (!req.session.user_id) {
return res.send({ error: 'Must be logged in' });
}
const buildingId = processParam(req.params, 'building_id', parsePositiveIntParam, true);
const verifiedAttributes = req.body;
try {
const success = await buildingService.verifyBuildingAttributes(buildingId, req.session.user_id, verifiedAttributes);
res.send(success);
} catch (error) {
if(error instanceof UserError) {
throw new ApiUserError(error.message, error);
}
throw error;
}
});
// GET latest revision id of any building
const getLatestRevisionId = asyncController(async (req: express.Request, res: express.Response) => { const getLatestRevisionId = asyncController(async (req: express.Request, res: express.Response) => {
try { try {
const revisionId = await buildingService.getLatestRevisionId(); const revisionId = await buildingService.getLatestRevisionId();
@ -176,6 +221,8 @@ export default {
getBuildingUPRNsById, getBuildingUPRNsById,
getBuildingLikeById, getBuildingLikeById,
updateBuildingLikeById, updateBuildingLikeById,
getUserVerifiedAttributes,
verifyBuildingAttributes,
getBuildingEditHistoryById, getBuildingEditHistoryById,
getLatestRevisionId getLatestRevisionId
}; };

View File

@ -0,0 +1,88 @@
import db from '../../db';
import { DatabaseError, InvalidOperationError } from '../errors/general';
export async function getBuildingVerifiedAttributes(buildingId: number): Promise<any[]> {
try {
return await (db).manyOrNone(
`SELECT
attribute,
verified_value
FROM
building_verification
WHERE
building_id = $1;
`,
[buildingId]
);
} catch(error) {
throw new DatabaseError(error.detail);
}
}
export async function getBuildingUserVerifiedAttributes(buildingId: number, userId: string): Promise<any> {
try {
const verifications = await (db).manyOrNone(
`SELECT
attribute,
verified_value,
date_trunc('minute', verification_timestamp) as verification_timestamp
FROM
building_verification
WHERE
building_id = $1
AND user_id = $2;
`,
[buildingId, userId]
);
return asVerified(verifications)
} catch(error) {
throw new DatabaseError(error.detail);
}
}
function asVerified(verifications){
const user_verified = {};
for (const item of verifications) {
user_verified[item.attribute] = item.verified_value
}
return user_verified;
}
export async function updateBuildingUserVerifiedAttribute(buildingId: number, userId: string, attribute: string, value: any): Promise<void> {
try {
await (db).none(
`INSERT INTO
building_verification
( building_id, user_id, attribute, verified_value )
VALUES
($1, $2, $3, to_jsonb($4));
`,
[buildingId, userId, attribute, value]
);
} catch(error) {
if(error.detail?.includes('already exists')) {
const msg = 'User already verified that attribute for this building'
throw new InvalidOperationError(msg);
} else {
throw new DatabaseError(error.detail);
}
}
}
export async function removeBuildingUserVerifiedAttribute(buildingId: number, userId: string, attribute: string) : Promise<null> {
try {
return await (db).none(
`DELETE FROM
building_verification
WHERE
building_id = $1
AND user_id = $2
AND attribute = $3;
`,
[buildingId, userId, attribute]
);
} catch(error) {
throw new DatabaseError(error.detail);
}
}

View File

@ -31,6 +31,11 @@ router.route('/:building_id/like.json')
.get(buildingController.getBuildingLikeById) .get(buildingController.getBuildingLikeById)
.post(buildingController.updateBuildingLikeById); .post(buildingController.updateBuildingLikeById);
// POST verify building attribute
router.route('/:building_id/verify.json')
.get(buildingController.getUserVerifiedAttributes)
.post(buildingController.verifyBuildingAttributes);
router.route('/:building_id/history.json') router.route('/:building_id/history.json')
.get(buildingController.getBuildingEditHistoryById); .get(buildingController.getBuildingEditHistoryById);

View File

@ -10,7 +10,8 @@ import { tileCache } from '../../tiles/rendererDefinition';
import { BoundingBox } from '../../tiles/types'; import { BoundingBox } from '../../tiles/types';
import * as buildingDataAccess from '../dataAccess/building'; import * as buildingDataAccess from '../dataAccess/building';
import * as likeDataAccess from '../dataAccess/like'; import * as likeDataAccess from '../dataAccess/like';
import { UserError } from '../errors/general'; import * as verifyDataAccess from '../dataAccess/verify';
import { UserError, DatabaseError } from '../errors/general';
import { processBuildingUpdate } from './domainLogic/processBuildingUpdate'; import { processBuildingUpdate } from './domainLogic/processBuildingUpdate';
@ -110,6 +111,7 @@ async function getBuildingById(id: number) {
const building = await getCurrentBuildingDataById(id); const building = await getCurrentBuildingDataById(id);
building.edit_history = await getBuildingEditHistory(id); building.edit_history = await getBuildingEditHistory(id);
building.verified = await getBuildingVerifications(building);
return building; return building;
} catch(error) { } catch(error) {
@ -159,6 +161,7 @@ async function getBuildingUPRNsById(id: number) {
} }
async function saveBuilding(buildingId: number, building: any, userId: string): Promise<object> { // TODO add proper building type async function saveBuilding(buildingId: number, building: any, userId: string): Promise<object> { // TODO add proper building type
console.log("SAVE")
return await updateBuildingData(buildingId, userId, async () => { return await updateBuildingData(buildingId, userId, async () => {
const processedBuilding = await processBuildingUpdate(buildingId, building); const processedBuilding = await processBuildingUpdate(buildingId, building);
@ -204,6 +207,68 @@ async function unlikeBuilding(buildingId: number, userId: string) {
); );
} }
async function verifyBuildingAttributes(buildingId: number, userId: string, patch: object) {
// get current building attribute values for comparison
const building = await getCurrentBuildingDataById(buildingId);
// keep track of attributes and values verified
const verified = {}
// loop through attribute => value pairs to mark as verified
for (let [key, value] of Object.entries(patch)) {
// check key in whitelist
if(BUILDING_FIELD_WHITELIST.has(key)) {
// check value against current from database
if (value == building[key]) {
try {
await verifyDataAccess.updateBuildingUserVerifiedAttribute(buildingId, userId, key, building[key]);
verified[key] = building[key];
} catch (error) {
// possible reasons:
// - not a building
// - not a user
// - user already verified this attribute for this building
throw new DatabaseError(error);
}
} else {
if (value === null) {
try {
await verifyDataAccess.removeBuildingUserVerifiedAttribute(buildingId, userId, key);
}
} else {
// not verifying current value
const msg = `Attribute "${key}" with value "${value}" did not match latest saved value "${building[key]}"`;
throw new DatabaseError(msg);
}
}
} else {
// not a valid attribute
const msg = `Attribute ${key} not recognised.`;
throw new DatabaseError(msg);
}
}
return verified;
}
async function getUserVerifiedAttributes(buildingId: number, userId: string) {
return await verifyDataAccess.getBuildingUserVerifiedAttributes(buildingId, userId);
}
async function getBuildingVerifications(building) {
const verifications = await verifyDataAccess.getBuildingVerifiedAttributes(building.building_id);
const verified = {};
for (const element of BUILDING_FIELD_WHITELIST) {
verified[element] = 0;
}
for (const item of verifications) {
if (building[item.attribute] == item.verified_value) {
verified[item.attribute] += 1
}
}
return verified;
}
// === Utility functions === // === Utility functions ===
function pickAttributesToUpdate(obj: any, fieldWhitelist: Set<string>) { function pickAttributesToUpdate(obj: any, fieldWhitelist: Set<string>) {
@ -378,5 +443,7 @@ export {
saveBuilding, saveBuilding,
likeBuilding, likeBuilding,
unlikeBuilding, unlikeBuilding,
getLatestRevisionId getLatestRevisionId,
verifyBuildingAttributes,
getUserVerifiedAttributes
}; };

View File

@ -89,7 +89,7 @@ async function getUserById(id: string) {
try { try {
return await db.one( return await db.one(
`SELECT `SELECT
username, email, registered, api_key username, email, date_trunc('minute', registered) as registered, api_key
FROM FROM
users users
WHERE WHERE