Refactor types and await for user/building backend (#476)
* Refactor buildings API for async/await, types * Return building data after update * Refactor users API for await, TS types * Refactor building service to remove repetition As part of this refactor, these changes in functionality were made: - tx isolation lvl for save/like/unlike building is always serializable - both reverse and forward patch updated for like/unlike - comparing old and new data uses == instead of === (this is because the new data even for numbers comes in as string) - the checking of no data change in case of building unlike was fixed (didn't work because it re-used code for like which is different) * Improve param order, docs for updateBuildingData
This commit is contained in:
parent
921fcd16e4
commit
c63f42f921
@ -1,147 +1,139 @@
|
||||
import express from 'express';
|
||||
import * as buildingService from '../services/building';
|
||||
import * as userService from '../services/user';
|
||||
import asyncController from '../routes/asyncController';
|
||||
|
||||
|
||||
// GET buildings
|
||||
// not implemented - may be useful to GET all buildings, paginated
|
||||
|
||||
// GET buildings at point
|
||||
function getBuildingsByLocation(req, res) {
|
||||
const getBuildingsByLocation = asyncController(async (req: express.Request, res: express.Response) => {
|
||||
const { lng, lat } = req.query;
|
||||
buildingService.queryBuildingsAtPoint(lng, lat).then(function (result) {
|
||||
try {
|
||||
const result = await buildingService.queryBuildingsAtPoint(lng, lat);
|
||||
res.send(result);
|
||||
}).catch(function (error) {
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.send({ error: 'Database error' })
|
||||
})
|
||||
}
|
||||
res.send({ error: 'Database error' });
|
||||
}
|
||||
});
|
||||
|
||||
// GET buildings by reference (UPRN/TOID or other identifier)
|
||||
function getBuildingsByReference(req, res) {
|
||||
const getBuildingsByReference = asyncController(async (req: express.Request, res: express.Response) => {
|
||||
const { key, id } = req.query;
|
||||
buildingService.queryBuildingsByReference(key, id).then(function (result) {
|
||||
try {
|
||||
const result = await buildingService.queryBuildingsByReference(key, id);
|
||||
res.send(result);
|
||||
}).catch(function (error) {
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
res.send({ error: 'Database error' })
|
||||
})
|
||||
}
|
||||
res.send({ error: 'Database error' });
|
||||
}
|
||||
});
|
||||
|
||||
// GET individual building, POST building updates
|
||||
function getBuildingById(req, res) {
|
||||
const getBuildingById = asyncController(async (req: express.Request, res: express.Response) => {
|
||||
const { building_id } = req.params;
|
||||
buildingService.getBuildingById(building_id).then(function (result) {
|
||||
try {
|
||||
const result = await buildingService.getBuildingById(building_id);
|
||||
res.send(result);
|
||||
}).catch(function (error) {
|
||||
} catch(error) {
|
||||
console.error(error);
|
||||
res.send({ error: 'Database error' })
|
||||
})
|
||||
}
|
||||
res.send({ error: 'Database error' });
|
||||
}
|
||||
});
|
||||
|
||||
function updateBuildingById(req, res) {
|
||||
const updateBuildingById = asyncController(async (req: express.Request, res: express.Response) => {
|
||||
if (req.session.user_id) {
|
||||
updateBuilding(req, res, req.session.user_id);
|
||||
await 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' });
|
||||
});
|
||||
try {
|
||||
const user = await userService.authAPIUser(req.query.api_key);
|
||||
await updateBuilding(req, res, user.user_id);
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
res.send({ error: 'Must be logged in' });
|
||||
}
|
||||
} else {
|
||||
res.send({ error: 'Must be logged in' });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function updateBuilding(req, res, userId) {
|
||||
async function updateBuilding(req: express.Request, res: express.Response, userId: string) {
|
||||
const { building_id } = req.params;
|
||||
const building = req.body;
|
||||
buildingService.saveBuilding(building_id, building, userId).then(building => {
|
||||
if (building.error) {
|
||||
res.send(building)
|
||||
return
|
||||
}
|
||||
const buildingUpdate = req.body;
|
||||
|
||||
try {
|
||||
const building = await buildingService.saveBuilding(building_id, buildingUpdate, userId);
|
||||
|
||||
if (typeof (building) === 'undefined') {
|
||||
res.send({ error: 'Database error' })
|
||||
return
|
||||
return res.send({ error: 'Database error' });
|
||||
}
|
||||
res.send(building)
|
||||
}).catch(
|
||||
() => res.send({ error: 'Database error' })
|
||||
)
|
||||
if (building.error) {
|
||||
return res.send(building);
|
||||
}
|
||||
res.send(building);
|
||||
} catch(err) {
|
||||
res.send({ error: 'Database error' });
|
||||
}
|
||||
}
|
||||
|
||||
// GET building UPRNs
|
||||
function getBuildingUPRNsById(req, res) {
|
||||
const getBuildingUPRNsById = asyncController(async (req: express.Request, res: express.Response) => {
|
||||
const { building_id } = req.params;
|
||||
buildingService.getBuildingUPRNsById(building_id).then(function (result) {
|
||||
try {
|
||||
const result = await buildingService.getBuildingUPRNsById(building_id);
|
||||
|
||||
if (typeof (result) === 'undefined') {
|
||||
res.send({ error: 'Database error' })
|
||||
return
|
||||
return res.send({ error: 'Database error' });
|
||||
}
|
||||
res.send({
|
||||
uprns: result
|
||||
});
|
||||
}).catch(function (error) {
|
||||
res.send({uprns: result});
|
||||
} catch(error) {
|
||||
console.error(error);
|
||||
res.send({ error: 'Database error' })
|
||||
})
|
||||
}
|
||||
res.send({ error: 'Database error' });
|
||||
}
|
||||
});
|
||||
|
||||
// GET/POST like building
|
||||
function getBuildingLikeById(req, res) {
|
||||
const getBuildingLikeById = asyncController(async (req: express.Request, res: express.Response) => {
|
||||
if (!req.session.user_id) {
|
||||
res.send({ like: false }); // not logged in, so cannot have liked
|
||||
return
|
||||
return res.send({ like: false }); // not logged in, so cannot have liked
|
||||
}
|
||||
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' })
|
||||
)
|
||||
}
|
||||
try {
|
||||
const like = await buildingService.getBuildingLikeById(building_id, req.session.user_id);
|
||||
|
||||
function updateBuildingLikeById(req, res) {
|
||||
if (!req.session.user_id) {
|
||||
res.send({ error: 'Must be logged in' });
|
||||
return
|
||||
// any value returned means like
|
||||
res.send({ like: like });
|
||||
} catch(error) {
|
||||
res.send({ error: 'Database error' })
|
||||
}
|
||||
});
|
||||
|
||||
const updateBuildingLikeById = asyncController(async (req: express.Request, res: express.Response) => {
|
||||
if (!req.session.user_id) {
|
||||
return res.send({ error: 'Must be logged in' });
|
||||
}
|
||||
|
||||
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' })
|
||||
)
|
||||
|
||||
try {
|
||||
const building = like ?
|
||||
await buildingService.likeBuilding(building_id, req.session.user_id) :
|
||||
await buildingService.unlikeBuilding(building_id, req.session.user_id);
|
||||
|
||||
if (building.error) {
|
||||
return res.send(building);
|
||||
}
|
||||
if (typeof (building) === 'undefined') {
|
||||
return res.send({ error: 'Database error' });
|
||||
}
|
||||
res.send(building);
|
||||
} catch(error) {
|
||||
res.send({ error: 'Database error' });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default {
|
||||
getBuildingsByLocation,
|
||||
|
@ -8,23 +8,22 @@ import { TokenVerificationError } from '../services/passwordReset';
|
||||
import asyncController from '../routes/asyncController';
|
||||
import { ValidationError } from '../validation';
|
||||
|
||||
function createUser(req, res) {
|
||||
const createUser = asyncController(async (req: express.Request, res: express.Response) => {
|
||||
const user = req.body;
|
||||
if (req.session.user_id) {
|
||||
res.send({ error: 'Already signed in' });
|
||||
return;
|
||||
return res.send({ error: 'Already signed in' });
|
||||
}
|
||||
|
||||
if (user.email) {
|
||||
if (user.email != user.confirm_email) {
|
||||
res.send({ error: 'Email did not match confirmation.' });
|
||||
return;
|
||||
return res.send({ error: 'Email did not match confirmation.' });
|
||||
}
|
||||
} else {
|
||||
user.email = null;
|
||||
}
|
||||
|
||||
userService.createUser(user).then(function (result) {
|
||||
try {
|
||||
const result = await userService.createUser(user);
|
||||
if (result.user_id) {
|
||||
req.session.user_id = result.user_id;
|
||||
res.send({ user_id: result.user_id });
|
||||
@ -32,39 +31,40 @@ function createUser(req, res) {
|
||||
req.session.user_id = undefined;
|
||||
res.send({ error: result.error });
|
||||
}
|
||||
}).catch(function (err) {
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
res.send(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function getCurrentUser(req, res) {
|
||||
const getCurrentUser = asyncController(async (req: express.Request, res: express.Response) => {
|
||||
if (!req.session.user_id) {
|
||||
res.send({ error: 'Must be logged in' });
|
||||
return;
|
||||
return res.send({ error: 'Must be logged in' });
|
||||
}
|
||||
|
||||
userService.getUserById(req.session.user_id).then(function (user) {
|
||||
try {
|
||||
const user = await userService.getUserById(req.session.user_id);
|
||||
res.send(user);
|
||||
}).catch(function (error) {
|
||||
} catch(error) {
|
||||
res.send(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function deleteCurrentUser(req, res) {
|
||||
const deleteCurrentUser = asyncController(async (req: express.Request, res: express.Response) => {
|
||||
if (!req.session.user_id) {
|
||||
return res.send({ error: 'Must be logged in' });
|
||||
}
|
||||
console.log(`Deleting user ${req.session.user_id}`);
|
||||
|
||||
userService.deleteUser(req.session.user_id).then(
|
||||
() => userService.logout(req.session)
|
||||
).then(() => {
|
||||
try {
|
||||
await userService.deleteUser(req.session.user_id);
|
||||
await userService.logout(req.session);
|
||||
|
||||
res.send({ success: true });
|
||||
}).catch(err => {
|
||||
} catch(err) {
|
||||
res.send({ error: err });
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const resetPassword = asyncController(async function(req: express.Request, res: express.Response) {
|
||||
if(req.body == undefined || (req.body.email == undefined && req.body.token == undefined)) {
|
||||
|
@ -5,6 +5,7 @@
|
||||
import db from '../../db';
|
||||
import { tileCache } from '../../tiles/rendererDefinition';
|
||||
import { BoundingBox } from '../../tiles/types';
|
||||
import { ITask } from 'pg-promise';
|
||||
|
||||
// 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.
|
||||
@ -18,282 +19,289 @@ const serializable = new TransactionMode({
|
||||
readOnly: false
|
||||
});
|
||||
|
||||
function queryBuildingsAtPoint(lng, lat) {
|
||||
return db.manyOrNone(
|
||||
`SELECT b.*
|
||||
FROM buildings as b, geometries as g
|
||||
WHERE
|
||||
b.geometry_id = g.geometry_id
|
||||
AND
|
||||
ST_Intersects(
|
||||
ST_Transform(
|
||||
ST_SetSRID(ST_Point($1, $2), 4326),
|
||||
3857
|
||||
),
|
||||
geometry_geom
|
||||
)
|
||||
`,
|
||||
[lng, lat]
|
||||
).catch(function (error) {
|
||||
console.error(error);
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
function queryBuildingsByReference(key, id) {
|
||||
if (key === 'toid') {
|
||||
return db.manyOrNone(
|
||||
`SELECT
|
||||
*
|
||||
FROM
|
||||
buildings
|
||||
async function queryBuildingsAtPoint(lng: number, lat: number) {
|
||||
try {
|
||||
return await db.manyOrNone(
|
||||
`SELECT b.*
|
||||
FROM buildings as b, geometries as g
|
||||
WHERE
|
||||
ref_toid = $1
|
||||
`,
|
||||
[id]
|
||||
).catch(function (error) {
|
||||
console.error(error);
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
if (key === 'uprn') {
|
||||
return db.manyOrNone(
|
||||
`SELECT
|
||||
b.*
|
||||
FROM
|
||||
buildings as b, building_properties as p
|
||||
WHERE
|
||||
b.building_id = p.building_id
|
||||
b.geometry_id = g.geometry_id
|
||||
AND
|
||||
p.uprn = $1
|
||||
ST_Intersects(
|
||||
ST_Transform(
|
||||
ST_SetSRID(ST_Point($1, $2), 4326),
|
||||
3857
|
||||
),
|
||||
geometry_geom
|
||||
)
|
||||
`,
|
||||
[id]
|
||||
).catch(function (error) {
|
||||
console.error(error);
|
||||
return undefined;
|
||||
});
|
||||
[lng, lat]
|
||||
);
|
||||
} catch(error) {
|
||||
console.error(error);
|
||||
return undefined;
|
||||
}
|
||||
return Promise.resolve({ error: 'Key must be UPRN or TOID' });
|
||||
}
|
||||
|
||||
function getBuildingById(id) {
|
||||
return db.one(
|
||||
'SELECT * FROM buildings WHERE building_id = $1',
|
||||
[id]
|
||||
).then((building) => {
|
||||
return getBuildingEditHistory(id).then((edit_history) => {
|
||||
building.edit_history = edit_history
|
||||
return building
|
||||
})
|
||||
}).catch(function (error) {
|
||||
console.error(error);
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
function getBuildingEditHistory(id) {
|
||||
return db.manyOrNone(
|
||||
`SELECT log_id as revision_id, forward_patch, reverse_patch, date_trunc('minute', log_timestamp), username
|
||||
FROM logs, users
|
||||
WHERE building_id = $1 AND logs.user_id = users.user_id`,
|
||||
[id]
|
||||
).then((data) => {
|
||||
return data
|
||||
}).catch(function (error) {
|
||||
console.error(error);
|
||||
return []
|
||||
});
|
||||
}
|
||||
|
||||
function getBuildingLikeById(buildingId, userId) {
|
||||
return db.oneOrNone(
|
||||
'SELECT true as like FROM building_user_likes WHERE building_id = $1 and user_id = $2 LIMIT 1',
|
||||
[buildingId, userId]
|
||||
).then(res => {
|
||||
return res && res.like
|
||||
}).catch(function (error) {
|
||||
console.error(error);
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
function getBuildingUPRNsById(id) {
|
||||
return db.any(
|
||||
'SELECT uprn, parent_uprn FROM building_properties WHERE building_id = $1',
|
||||
[id]
|
||||
).catch(function (error) {
|
||||
console.error(error);
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
function saveBuilding(buildingId, building, userId) {
|
||||
// remove read-only fields from consideration
|
||||
delete building.building_id;
|
||||
delete building.revision_id;
|
||||
delete building.geometry_id;
|
||||
|
||||
// start transaction around save operation
|
||||
// - select and compare to identify changeset
|
||||
// - insert changeset
|
||||
// - update to latest state
|
||||
// commit or rollback (repeated-read sufficient? or serializable?)
|
||||
return db.tx(t => {
|
||||
return t.one(
|
||||
'SELECT * FROM buildings WHERE building_id = $1 FOR UPDATE;',
|
||||
[buildingId]
|
||||
).then(oldBuilding => {
|
||||
const patches = compare(oldBuilding, building, BUILDING_FIELD_WHITELIST);
|
||||
console.log('Patching', buildingId, patches)
|
||||
const forward = patches[0];
|
||||
const reverse = patches[1];
|
||||
if (Object.keys(forward).length === 0) {
|
||||
return Promise.reject('No change provided')
|
||||
}
|
||||
return t.one(
|
||||
`INSERT INTO logs (
|
||||
forward_patch, reverse_patch, building_id, user_id
|
||||
) VALUES (
|
||||
$1:json, $2:json, $3, $4
|
||||
) RETURNING log_id
|
||||
async function queryBuildingsByReference(key: string, ref: string) {
|
||||
try {
|
||||
if (key === 'toid') {
|
||||
return await db.manyOrNone(
|
||||
`SELECT
|
||||
*
|
||||
FROM
|
||||
buildings
|
||||
WHERE
|
||||
ref_toid = $1
|
||||
`,
|
||||
[forward, reverse, buildingId, userId]
|
||||
).then(revision => {
|
||||
const sets = db.$config.pgp.helpers.sets(forward);
|
||||
console.log('Setting', buildingId, sets)
|
||||
return t.one(
|
||||
`UPDATE
|
||||
buildings
|
||||
SET
|
||||
revision_id = $1,
|
||||
$2:raw
|
||||
WHERE
|
||||
building_id = $3
|
||||
RETURNING
|
||||
*
|
||||
`,
|
||||
[revision.log_id, sets, buildingId]
|
||||
).then((data) => {
|
||||
expireBuildingTileCache(buildingId)
|
||||
return data
|
||||
})
|
||||
});
|
||||
[ref]
|
||||
);
|
||||
} else if (key === 'uprn') {
|
||||
return await db.manyOrNone(
|
||||
`SELECT
|
||||
b.*
|
||||
FROM
|
||||
buildings as b, building_properties as p
|
||||
WHERE
|
||||
b.building_id = p.building_id
|
||||
AND
|
||||
p.uprn = $1
|
||||
`,
|
||||
[ref]
|
||||
);
|
||||
} else {
|
||||
return { error: 'Key must be UPRN or TOID' };
|
||||
}
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function getBuildingById(id: number) {
|
||||
try {
|
||||
const building = await db.one(
|
||||
'SELECT * FROM buildings WHERE building_id = $1',
|
||||
[id]
|
||||
);
|
||||
|
||||
building.edit_history = await getBuildingEditHistory(id);
|
||||
|
||||
return building;
|
||||
} catch(error) {
|
||||
console.error(error);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function getBuildingEditHistory(id: number) {
|
||||
try {
|
||||
return await db.manyOrNone(
|
||||
`SELECT log_id as revision_id, forward_patch, reverse_patch, date_trunc('minute', log_timestamp), username
|
||||
FROM logs, users
|
||||
WHERE building_id = $1 AND logs.user_id = users.user_id`,
|
||||
[id]
|
||||
);
|
||||
} catch(error) {
|
||||
console.error(error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function getBuildingLikeById(buildingId: number, userId: string) {
|
||||
try {
|
||||
const res = await db.oneOrNone(
|
||||
'SELECT true as like FROM building_user_likes WHERE building_id = $1 and user_id = $2 LIMIT 1',
|
||||
[buildingId, userId]
|
||||
);
|
||||
return res && res.like;
|
||||
} catch(error) {
|
||||
console.error(error);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function getBuildingUPRNsById(id: number) {
|
||||
try {
|
||||
return await db.any(
|
||||
'SELECT uprn, parent_uprn FROM building_properties WHERE building_id = $1',
|
||||
[id]
|
||||
);
|
||||
} catch(error) {
|
||||
console.error(error);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveBuilding(buildingId: number, building: any, userId: string) { // TODO add proper building type
|
||||
try {
|
||||
return await updateBuildingData(buildingId, userId, async () => {
|
||||
// remove read-only fields from consideration
|
||||
delete building.building_id;
|
||||
delete building.revision_id;
|
||||
delete building.geometry_id;
|
||||
|
||||
// return whitelisted fields to update
|
||||
return pickAttributesToUpdate(building, BUILDING_FIELD_WHITELIST);
|
||||
});
|
||||
}).catch(function (error) {
|
||||
} catch(error) {
|
||||
console.error(error);
|
||||
return { error: error };
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function likeBuilding(buildingId, userId) {
|
||||
// start transaction around save operation
|
||||
// - insert building-user like
|
||||
// - count total likes
|
||||
// - insert changeset
|
||||
// - update building to latest state
|
||||
// commit or rollback (serializable - could be more compact?)
|
||||
return db.tx({mode: serializable}, t => {
|
||||
return t.none(
|
||||
'INSERT INTO building_user_likes ( building_id, user_id ) VALUES ($1, $2);',
|
||||
[buildingId, userId]
|
||||
).then(() => {
|
||||
return t.one(
|
||||
'SELECT count(*) as likes FROM building_user_likes WHERE building_id = $1;',
|
||||
[buildingId]
|
||||
).then(building => {
|
||||
return t.one(
|
||||
`INSERT INTO logs (
|
||||
forward_patch, building_id, user_id
|
||||
) VALUES (
|
||||
$1:json, $2, $3
|
||||
) RETURNING log_id
|
||||
`,
|
||||
[{ likes_total: building.likes }, buildingId, userId]
|
||||
).then(revision => {
|
||||
return t.one(
|
||||
`UPDATE buildings
|
||||
SET
|
||||
revision_id = $1,
|
||||
likes_total = $2
|
||||
WHERE
|
||||
building_id = $3
|
||||
RETURNING
|
||||
*
|
||||
`,
|
||||
[revision.log_id, building.likes, buildingId]
|
||||
).then((data) => {
|
||||
expireBuildingTileCache(buildingId)
|
||||
return data
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
}).catch(function (error) {
|
||||
async function likeBuilding(buildingId: number, userId: string) {
|
||||
try {
|
||||
return await updateBuildingData(
|
||||
buildingId,
|
||||
userId,
|
||||
async (t) => {
|
||||
// return total like count after update
|
||||
return getBuildingLikeCount(buildingId, t);
|
||||
},
|
||||
async (t) => {
|
||||
// insert building-user like
|
||||
await t.none(
|
||||
'INSERT INTO building_user_likes ( building_id, user_id ) VALUES ($1, $2);',
|
||||
[buildingId, userId]
|
||||
);
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (error.detail && error.detail.includes('already exists')) {
|
||||
// 'already exists' is thrown if user already liked it
|
||||
return { error: 'It looks like you already like that building!' };
|
||||
} else {
|
||||
return undefined
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function unlikeBuilding(buildingId, userId) {
|
||||
// start transaction around save operation
|
||||
// - insert building-user like
|
||||
// - count total likes
|
||||
// - insert changeset
|
||||
// - update building to latest state
|
||||
// commit or rollback (serializable - could be more compact?)
|
||||
return db.tx({mode: serializable}, t => {
|
||||
return t.none(
|
||||
'DELETE FROM building_user_likes WHERE building_id = $1 AND user_id = $2;',
|
||||
[buildingId, userId]
|
||||
).then(() => {
|
||||
return t.one(
|
||||
'SELECT count(*) as likes FROM building_user_likes WHERE building_id = $1;',
|
||||
[buildingId]
|
||||
).then(building => {
|
||||
return t.one(
|
||||
`INSERT INTO logs (
|
||||
forward_patch, building_id, user_id
|
||||
async function unlikeBuilding(buildingId: number, userId: string) {
|
||||
try {
|
||||
return await updateBuildingData(
|
||||
buildingId,
|
||||
userId,
|
||||
async (t) => {
|
||||
// return total like count after update
|
||||
return getBuildingLikeCount(buildingId, t);
|
||||
},
|
||||
async (t) => {
|
||||
// remove building-user like
|
||||
const result = await t.result(
|
||||
'DELETE FROM building_user_likes WHERE building_id = $1 AND user_id = $2;',
|
||||
[buildingId, userId]
|
||||
);
|
||||
|
||||
if (result.rowCount === 0) {
|
||||
throw new Error('No change');
|
||||
}
|
||||
},
|
||||
);
|
||||
} catch(error) {
|
||||
console.error(error);
|
||||
if (error.message === 'No change') {
|
||||
// 'No change' is thrown if user doesn't like this building
|
||||
return { error: 'It looks like you have already revoked your like for that building!' };
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// === Utility functions ===
|
||||
|
||||
function pickAttributesToUpdate(obj: any, fieldWhitelist: Set<string>) {
|
||||
const subObject = {};
|
||||
for (let [key, value] of Object.entries(obj)) {
|
||||
if(fieldWhitelist.has(key)) {
|
||||
subObject[key] = value;
|
||||
}
|
||||
}
|
||||
return subObject;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param buildingId ID of the building to count likes for
|
||||
* @param t The database context inside which the count should happen
|
||||
*/
|
||||
function getBuildingLikeCount(buildingId: number, t: ITask<unknown>) {
|
||||
return t.one(
|
||||
'SELECT count(*) as likes_total FROM building_user_likes WHERE building_id = $1;',
|
||||
[buildingId]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Carry out an update of the buildings data. Allows for running any custom database operations before the main update.
|
||||
* All db hooks get passed a transaction.
|
||||
* @param buildingId The ID of the building to update
|
||||
* @param userId The ID of the user updating the data
|
||||
* @param getUpdateValue Function returning the set of attribute to update for the building
|
||||
* @param preUpdateDbAction Any db operations to carry out before updating the buildings table (mostly intended for updating the user likes table)
|
||||
*/
|
||||
async function updateBuildingData(
|
||||
buildingId: number,
|
||||
userId: string,
|
||||
getUpdateValue: (t: ITask<any>) => Promise<object>,
|
||||
preUpdateDbAction?: (t: ITask<any>) => Promise<void>,
|
||||
) {
|
||||
return await db.tx({mode: serializable}, async t => {
|
||||
if (preUpdateDbAction != undefined) {
|
||||
await preUpdateDbAction(t);
|
||||
}
|
||||
|
||||
const update = await getUpdateValue(t);
|
||||
|
||||
const oldBuilding = await t.one(
|
||||
'SELECT * FROM buildings WHERE building_id = $1 FOR UPDATE;',
|
||||
[buildingId]
|
||||
);
|
||||
|
||||
console.log(update);
|
||||
const patches = compare(oldBuilding, update);
|
||||
console.log('Patching', buildingId, patches)
|
||||
const [forward, reverse] = patches;
|
||||
if (Object.keys(forward).length === 0) {
|
||||
throw 'No change provided';
|
||||
}
|
||||
|
||||
const revision = await t.one(
|
||||
`INSERT INTO logs (
|
||||
forward_patch, reverse_patch, building_id, user_id
|
||||
) VALUES (
|
||||
$1:json, $2, $3
|
||||
$1:json, $2:json, $3, $4
|
||||
) RETURNING log_id
|
||||
`,
|
||||
[{ likes_total: building.likes }, buildingId, userId]
|
||||
).then(revision => {
|
||||
return t.one(
|
||||
`UPDATE buildings
|
||||
SET
|
||||
revision_id = $1,
|
||||
likes_total = $2
|
||||
WHERE
|
||||
building_id = $3
|
||||
RETURNING
|
||||
*
|
||||
`,
|
||||
[revision.log_id, building.likes, buildingId]
|
||||
).then((data) => {
|
||||
expireBuildingTileCache(buildingId)
|
||||
return data
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
}).catch(function (error) {
|
||||
console.error(error);
|
||||
if (error.detail && error.detail.includes('already exists')) {
|
||||
// 'already exists' is thrown if user already liked it
|
||||
return { error: 'It looks like you already like that building!' };
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
[forward, reverse, buildingId, userId]
|
||||
);
|
||||
|
||||
const sets = db.$config.pgp.helpers.sets(forward);
|
||||
console.log('Setting', buildingId, sets);
|
||||
|
||||
const data = await t.one(
|
||||
`UPDATE
|
||||
buildings
|
||||
SET
|
||||
revision_id = $1,
|
||||
$2:raw
|
||||
WHERE
|
||||
building_id = $3
|
||||
RETURNING
|
||||
*
|
||||
`,
|
||||
[revision.log_id, sets, buildingId]
|
||||
);
|
||||
|
||||
expireBuildingTileCache(buildingId);
|
||||
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
function privateQueryBuildingBBOX(buildingId){
|
||||
function privateQueryBuildingBBOX(buildingId: number){
|
||||
return db.one(
|
||||
`SELECT
|
||||
ST_XMin(envelope) as xmin,
|
||||
@ -310,14 +318,13 @@ function privateQueryBuildingBBOX(buildingId){
|
||||
b.building_id = $1
|
||||
) as envelope`,
|
||||
[buildingId]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function expireBuildingTileCache(buildingId) {
|
||||
privateQueryBuildingBBOX(buildingId).then((bbox) => {
|
||||
const buildingBbox: BoundingBox = [bbox.xmax, bbox.ymax, bbox.xmin, bbox.ymin];
|
||||
tileCache.removeAllAtBbox(buildingBbox);
|
||||
})
|
||||
async function expireBuildingTileCache(buildingId: number) {
|
||||
const bbox = await privateQueryBuildingBBOX(buildingId)
|
||||
const buildingBbox: BoundingBox = [bbox.xmax, bbox.ymax, bbox.xmin, bbox.ymin];
|
||||
tileCache.removeAllAtBbox(buildingBbox);
|
||||
}
|
||||
|
||||
const BUILDING_FIELD_WHITELIST = new Set([
|
||||
@ -385,16 +392,16 @@ const BUILDING_FIELD_WHITELIST = new Set([
|
||||
* @param {Set} whitelist
|
||||
* @returns {[object, object]}
|
||||
*/
|
||||
function compare(oldObj, newObj, whitelist) {
|
||||
const reverse = {}
|
||||
const forward = {}
|
||||
function compare(oldObj: object, newObj: object): [object, object] {
|
||||
const reverse = {};
|
||||
const forward = {};
|
||||
for (const [key, value] of Object.entries(newObj)) {
|
||||
if (oldObj[key] !== value && whitelist.has(key)) {
|
||||
if (oldObj[key] != value) {
|
||||
reverse[key] = oldObj[key];
|
||||
forward[key] = value;
|
||||
}
|
||||
}
|
||||
return [forward, reverse]
|
||||
return [forward, reverse];
|
||||
}
|
||||
|
||||
export {
|
||||
|
@ -6,173 +6,185 @@ import { errors } from 'pg-promise';
|
||||
|
||||
import db from '../../db';
|
||||
import { validateUsername, ValidationError, validatePassword } from '../validation';
|
||||
import { promisify } from 'util';
|
||||
|
||||
function createUser(user) {
|
||||
|
||||
async function createUser(user) {
|
||||
try {
|
||||
validateUsername(user.username);
|
||||
validatePassword(user.password);
|
||||
} catch(err) {
|
||||
if (err instanceof ValidationError) {
|
||||
return Promise.reject({ error: err.message });
|
||||
throw { error: err.message };
|
||||
} else throw err;
|
||||
}
|
||||
|
||||
return db.one(
|
||||
`INSERT
|
||||
INTO users (
|
||||
user_id,
|
||||
username,
|
||||
email,
|
||||
pass
|
||||
) VALUES (
|
||||
gen_random_uuid(),
|
||||
$1,
|
||||
$2,
|
||||
crypt($3, gen_salt('bf'))
|
||||
) RETURNING user_id
|
||||
`, [
|
||||
user.username,
|
||||
user.email,
|
||||
user.password
|
||||
]
|
||||
).catch(function (error) {
|
||||
console.error('Error:', error)
|
||||
try {
|
||||
return await db.one(
|
||||
`INSERT
|
||||
INTO users (
|
||||
user_id,
|
||||
username,
|
||||
email,
|
||||
pass
|
||||
) VALUES (
|
||||
gen_random_uuid(),
|
||||
$1,
|
||||
$2,
|
||||
crypt($3, gen_salt('bf'))
|
||||
) RETURNING user_id
|
||||
`, [
|
||||
user.username,
|
||||
user.email,
|
||||
user.password
|
||||
]
|
||||
);
|
||||
} catch(error) {
|
||||
console.error('Error:', error);
|
||||
|
||||
if (error.detail.indexOf('already exists') !== -1) {
|
||||
if (error.detail.indexOf('username') !== -1) {
|
||||
if (error.detail.includes('already exists')) {
|
||||
if (error.detail.includes('username')) {
|
||||
return { error: 'Username already registered' };
|
||||
} else if (error.detail.indexOf('email') !== -1) {
|
||||
} else if (error.detail.includes('email')) {
|
||||
return { error: 'Email already registered' };
|
||||
}
|
||||
}
|
||||
return { error: 'Database error' }
|
||||
});
|
||||
return { error: 'Database error' };
|
||||
}
|
||||
}
|
||||
|
||||
function authUser(username, password) {
|
||||
return db.one(
|
||||
`SELECT
|
||||
user_id,
|
||||
(
|
||||
pass = crypt($2, pass)
|
||||
) AS auth_ok
|
||||
FROM users
|
||||
WHERE
|
||||
username = $1
|
||||
`, [
|
||||
username,
|
||||
password
|
||||
]
|
||||
).then(function (user) {
|
||||
async function authUser(username: string, password: string) {
|
||||
try {
|
||||
const user = await db.one(
|
||||
`SELECT
|
||||
user_id,
|
||||
(
|
||||
pass = crypt($2, pass)
|
||||
) AS auth_ok
|
||||
FROM users
|
||||
WHERE
|
||||
username = $1
|
||||
`, [
|
||||
username,
|
||||
password
|
||||
]
|
||||
);
|
||||
|
||||
if (user && user.auth_ok) {
|
||||
return { user_id: user.user_id }
|
||||
} else {
|
||||
return { error: 'Username or password not recognised' }
|
||||
}
|
||||
}).catch(function (err) {
|
||||
} catch(err) {
|
||||
if (err instanceof errors.QueryResultError) {
|
||||
console.error(`Authentication failed for user ${username}`);
|
||||
return { error: 'Username or password not recognised' };
|
||||
}
|
||||
console.error('Error:', err);
|
||||
return { error: 'Database error' };
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function getUserById(id) {
|
||||
return db.one(
|
||||
`SELECT
|
||||
username, email, registered, api_key
|
||||
FROM
|
||||
users
|
||||
WHERE
|
||||
user_id = $1
|
||||
`, [
|
||||
id
|
||||
]
|
||||
).catch(function (error) {
|
||||
async function getUserById(id: string) {
|
||||
try {
|
||||
return await db.one(
|
||||
`SELECT
|
||||
username, email, registered, api_key
|
||||
FROM
|
||||
users
|
||||
WHERE
|
||||
user_id = $1
|
||||
`, [
|
||||
id
|
||||
]
|
||||
);
|
||||
} catch(error) {
|
||||
console.error('Error:', error)
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getUserByEmail(email: string) {
|
||||
return db.one(
|
||||
`SELECT
|
||||
user_id, username, email
|
||||
FROM
|
||||
users
|
||||
WHERE
|
||||
email = $1
|
||||
`, [email]
|
||||
).catch(function(error) {
|
||||
async function getUserByEmail(email: string) {
|
||||
try {
|
||||
return db.one(
|
||||
`SELECT
|
||||
user_id, username, email
|
||||
FROM
|
||||
users
|
||||
WHERE
|
||||
email = $1
|
||||
`, [email]
|
||||
);
|
||||
} catch(error) {
|
||||
console.error('Error:', error);
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function getNewUserAPIKey(id) {
|
||||
return db.one(
|
||||
`UPDATE
|
||||
users
|
||||
SET
|
||||
api_key = gen_random_uuid()
|
||||
WHERE
|
||||
user_id = $1
|
||||
RETURNING
|
||||
api_key
|
||||
`, [
|
||||
id
|
||||
]
|
||||
).catch(function (error) {
|
||||
async function getNewUserAPIKey(id: string) {
|
||||
try{
|
||||
return db.one(
|
||||
`UPDATE
|
||||
users
|
||||
SET
|
||||
api_key = gen_random_uuid()
|
||||
WHERE
|
||||
user_id = $1
|
||||
RETURNING
|
||||
api_key
|
||||
`, [
|
||||
id
|
||||
]
|
||||
);
|
||||
} catch(error) {
|
||||
console.error('Error:', error)
|
||||
return { error: 'Failed to generate new API key.' };
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function authAPIUser(key) {
|
||||
return db.one(
|
||||
`SELECT
|
||||
user_id
|
||||
FROM
|
||||
users
|
||||
WHERE
|
||||
api_key = $1
|
||||
`, [
|
||||
key
|
||||
]
|
||||
).catch(function (error) {
|
||||
async function authAPIUser(key: string) {
|
||||
try {
|
||||
return await db.one(
|
||||
`SELECT
|
||||
user_id
|
||||
FROM
|
||||
users
|
||||
WHERE
|
||||
api_key = $1
|
||||
`, [
|
||||
key
|
||||
]
|
||||
);
|
||||
} catch(error) {
|
||||
console.error('Error:', error)
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function deleteUser(id) {
|
||||
return db.none(
|
||||
`UPDATE users
|
||||
SET
|
||||
email = null,
|
||||
pass = null,
|
||||
api_key = null,
|
||||
username = concat('deleted_', cast(user_id as char(13))),
|
||||
is_deleted = true,
|
||||
deleted_on = now() at time zone 'utc'
|
||||
WHERE user_id = $1
|
||||
`, [id]
|
||||
).catch((error) => {
|
||||
async function deleteUser(id: string) {
|
||||
try {
|
||||
return await db.none(
|
||||
`UPDATE users
|
||||
SET
|
||||
email = null,
|
||||
pass = null,
|
||||
api_key = null,
|
||||
username = concat('deleted_', cast(user_id as char(13))),
|
||||
is_deleted = true,
|
||||
deleted_on = now() at time zone 'utc'
|
||||
WHERE user_id = $1
|
||||
`, [id]
|
||||
);
|
||||
} catch(error) {
|
||||
console.error('Error:', error);
|
||||
return {error: 'Database error'};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function logout(session: Express.Session) {
|
||||
return new Promise((resolve, reject) => {
|
||||
session.user_id = undefined;
|
||||
session.destroy(err => {
|
||||
if (err) return reject(err);
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
function logout(session: Express.Session): Promise<void> {
|
||||
session.user_id = undefined;
|
||||
|
||||
return promisify(session.destroy.bind(session))();
|
||||
}
|
||||
|
||||
export {
|
||||
|
Loading…
Reference in New Issue
Block a user