Add backend service to GET/POST user verifications, and include verification counts in building data
This commit is contained in:
parent
672a1efde4
commit
b30e882669
@ -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 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) => {
|
||||
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) => {
|
||||
if (!req.session.user_id) {
|
||||
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 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) => {
|
||||
if (!req.session.user_id) {
|
||||
return res.send({ error: 'Must be logged in' });
|
||||
@ -159,6 +162,48 @@ const updateBuildingLikeById = asyncController(async (req: express.Request, res:
|
||||
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) => {
|
||||
try {
|
||||
const revisionId = await buildingService.getLatestRevisionId();
|
||||
@ -176,6 +221,8 @@ export default {
|
||||
getBuildingUPRNsById,
|
||||
getBuildingLikeById,
|
||||
updateBuildingLikeById,
|
||||
getUserVerifiedAttributes,
|
||||
verifyBuildingAttributes,
|
||||
getBuildingEditHistoryById,
|
||||
getLatestRevisionId
|
||||
};
|
||||
|
88
app/src/api/dataAccess/verify.ts
Normal file
88
app/src/api/dataAccess/verify.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -31,6 +31,11 @@ router.route('/:building_id/like.json')
|
||||
.get(buildingController.getBuildingLikeById)
|
||||
.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')
|
||||
.get(buildingController.getBuildingEditHistoryById);
|
||||
|
||||
|
@ -10,7 +10,8 @@ import { tileCache } from '../../tiles/rendererDefinition';
|
||||
import { BoundingBox } from '../../tiles/types';
|
||||
import * as buildingDataAccess from '../dataAccess/building';
|
||||
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';
|
||||
|
||||
@ -110,6 +111,7 @@ async function getBuildingById(id: number) {
|
||||
const building = await getCurrentBuildingDataById(id);
|
||||
|
||||
building.edit_history = await getBuildingEditHistory(id);
|
||||
building.verified = await getBuildingVerifications(building);
|
||||
|
||||
return building;
|
||||
} catch(error) {
|
||||
@ -159,9 +161,10 @@ async function getBuildingUPRNsById(id: number) {
|
||||
}
|
||||
|
||||
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 () => {
|
||||
const processedBuilding = await processBuildingUpdate(buildingId, building);
|
||||
|
||||
|
||||
// remove read-only fields from consideration
|
||||
delete processedBuilding.building_id;
|
||||
delete processedBuilding.revision_id;
|
||||
@ -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 ===
|
||||
|
||||
function pickAttributesToUpdate(obj: any, fieldWhitelist: Set<string>) {
|
||||
@ -378,5 +443,7 @@ export {
|
||||
saveBuilding,
|
||||
likeBuilding,
|
||||
unlikeBuilding,
|
||||
getLatestRevisionId
|
||||
getLatestRevisionId,
|
||||
verifyBuildingAttributes,
|
||||
getUserVerifiedAttributes
|
||||
};
|
||||
|
@ -18,7 +18,7 @@ async function createUser(user) {
|
||||
throw { error: err.message };
|
||||
} else throw err;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
return await db.one(
|
||||
`INSERT
|
||||
@ -89,7 +89,7 @@ async function getUserById(id: string) {
|
||||
try {
|
||||
return await db.one(
|
||||
`SELECT
|
||||
username, email, registered, api_key
|
||||
username, email, date_trunc('minute', registered) as registered, api_key
|
||||
FROM
|
||||
users
|
||||
WHERE
|
||||
|
Loading…
Reference in New Issue
Block a user