2018-09-30 14:48:42 -04:00
|
|
|
/**
|
|
|
|
* Building data access
|
|
|
|
*
|
|
|
|
*/
|
2018-09-30 11:25:53 -04:00
|
|
|
import db from './db';
|
|
|
|
// 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.
|
2018-09-09 17:22:44 -04:00
|
|
|
|
2018-09-30 14:48:42 -04:00
|
|
|
const TransactionMode = db.$config.pgp.txMode.TransactionMode;
|
|
|
|
const isolationLevel = db.$config.pgp.txMode.isolationLevel;
|
|
|
|
|
|
|
|
// Create a transaction mode (serializable, read-write):
|
|
|
|
const serializable = new TransactionMode({
|
|
|
|
tiLevel: isolationLevel.serializable,
|
|
|
|
readOnly: false
|
|
|
|
});
|
|
|
|
|
2018-09-29 14:09:48 -04:00
|
|
|
function queryBuildingsAtPoint(lng, lat) {
|
2018-09-30 11:25:53 -04:00
|
|
|
return db.manyOrNone(
|
|
|
|
`SELECT b.*
|
2018-09-09 17:22:44 -04:00
|
|
|
FROM buildings as b, geometries as g
|
|
|
|
WHERE
|
2018-09-30 11:25:53 -04:00
|
|
|
b.geometry_id = g.geometry_id
|
2018-09-09 17:22:44 -04:00
|
|
|
AND
|
2018-09-30 11:25:53 -04:00
|
|
|
ST_Intersects(
|
|
|
|
ST_Transform(
|
|
|
|
ST_SetSRID(ST_Point($1, $2), 4326),
|
|
|
|
3857
|
|
|
|
),
|
|
|
|
geometry_geom
|
|
|
|
)
|
2018-09-09 17:22:44 -04:00
|
|
|
`,
|
|
|
|
[lng, lat]
|
2018-09-30 11:25:53 -04:00
|
|
|
).catch(function(error){
|
2018-09-09 17:22:44 -04:00
|
|
|
console.error(error);
|
2018-09-13 15:41:42 -04:00
|
|
|
return undefined;
|
2018-09-09 17:22:44 -04:00
|
|
|
});
|
|
|
|
}
|
2018-09-30 13:58:41 -04:00
|
|
|
|
2018-09-29 14:09:48 -04:00
|
|
|
function queryBuildingsByReference(key, id) {
|
|
|
|
if (key === 'toid'){
|
2018-09-30 11:25:53 -04:00
|
|
|
return db.manyOrNone(
|
|
|
|
"SELECT * FROM buildings WHERE b.ref_toid = $1",
|
2018-09-29 14:09:48 -04:00
|
|
|
[id]
|
2018-09-30 11:25:53 -04:00
|
|
|
).catch(function(error){
|
2018-09-29 14:09:48 -04:00
|
|
|
console.error(error);
|
|
|
|
return undefined;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
if (key === 'uprn') {
|
2018-09-30 11:25:53 -04:00
|
|
|
return db.manyOrNone(
|
|
|
|
`SELECT b.*
|
|
|
|
FROM buildings as b, building_properties as p
|
2018-09-29 14:09:48 -04:00
|
|
|
WHERE
|
2018-09-30 11:25:53 -04:00
|
|
|
b.building_id = p.building_id
|
2018-09-29 14:09:48 -04:00
|
|
|
AND
|
2018-09-30 11:25:53 -04:00
|
|
|
p.uprn = $1
|
2018-09-29 14:09:48 -04:00
|
|
|
`,
|
|
|
|
[id]
|
2018-09-30 11:25:53 -04:00
|
|
|
).catch(function(error){
|
2018-09-29 14:09:48 -04:00
|
|
|
console.error(error);
|
|
|
|
return undefined;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return {error: 'Key must be UPRN or TOID'};
|
|
|
|
}
|
2018-09-09 17:22:44 -04:00
|
|
|
|
2018-09-11 18:30:17 -04:00
|
|
|
function getBuildingById(id) {
|
2018-09-30 11:25:53 -04:00
|
|
|
return db.one(
|
|
|
|
"SELECT * FROM buildings WHERE building_id = $1",
|
|
|
|
[id]
|
|
|
|
).catch(function(error){
|
2018-09-11 18:30:17 -04:00
|
|
|
console.error(error);
|
2018-09-13 15:41:42 -04:00
|
|
|
return undefined;
|
2018-09-11 18:30:17 -04:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-09-30 11:25:53 -04:00
|
|
|
function saveBuilding(building_id, building, user_id) {
|
|
|
|
// save building must fail if the revision seen by the user != the latest revision
|
|
|
|
// - any 'intuitive' retries to be handled by clients of this code
|
|
|
|
// revision id allows for a long user 'think time' between view-building, update-building
|
|
|
|
// (optimistic locking implemented using field-based row versioning)
|
|
|
|
const previous_revision_id = building.revision_id;
|
2018-09-29 14:09:48 -04:00
|
|
|
|
2018-09-30 11:25:53 -04:00
|
|
|
// remove read-only fields from consideration
|
|
|
|
delete building.building_id;
|
|
|
|
delete building.revision_id;
|
|
|
|
delete building.geometry_id;
|
2018-09-11 18:30:17 -04:00
|
|
|
|
2018-09-30 11:25:53 -04:00
|
|
|
// 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 => {
|
2018-09-30 17:23:13 -04:00
|
|
|
const check_revision = (previous_revision_id)? "and revision_id = $2" : "";
|
2018-09-30 11:25:53 -04:00
|
|
|
return t.one(
|
2018-09-30 17:23:13 -04:00
|
|
|
`SELECT * FROM buildings WHERE building_id = $1 ${check_revision} FOR UPDATE;`,
|
2018-09-30 11:25:53 -04:00
|
|
|
[building_id, previous_revision_id]
|
|
|
|
).then(old_building => {
|
2018-09-30 13:58:41 -04:00
|
|
|
const patches = compare(old_building, building, BUILDING_FIELD_WHITELIST);
|
|
|
|
const forward = patches[0];
|
|
|
|
const reverse = patches[1];
|
2018-09-30 11:25:53 -04:00
|
|
|
return t.one(
|
|
|
|
`INSERT INTO logs (
|
|
|
|
forward_patch, reverse_patch, building_id, user_id
|
|
|
|
) VALUES (
|
2018-09-30 17:23:13 -04:00
|
|
|
$1:json, $2:json, $3, $4
|
2018-09-30 11:25:53 -04:00
|
|
|
) RETURNING log_id
|
|
|
|
`,
|
2018-09-30 13:58:41 -04:00
|
|
|
[forward, reverse, building_id, user_id]
|
2018-09-30 11:25:53 -04:00
|
|
|
).then(revision => {
|
2018-09-30 13:58:41 -04:00
|
|
|
const sets = db.$config.pgp.helpers.sets(forward);
|
2018-09-30 17:23:13 -04:00
|
|
|
const check_revision = (previous_revision_id)? "AND revision_id = $4" : "";
|
2018-09-30 11:25:53 -04:00
|
|
|
return t.one(
|
|
|
|
`UPDATE
|
|
|
|
buildings
|
|
|
|
SET
|
|
|
|
revision_id = $1,
|
|
|
|
$2:raw
|
|
|
|
WHERE
|
2018-09-30 17:23:13 -04:00
|
|
|
building_id = $3 ${check_revision}
|
2018-09-30 11:25:53 -04:00
|
|
|
RETURNING
|
|
|
|
*
|
|
|
|
`,
|
|
|
|
[revision.log_id, sets, building_id, previous_revision_id]
|
|
|
|
)
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}).catch(function(error){
|
|
|
|
// TODO report transaction error as 'Need to re-fetch building before update'
|
2018-09-11 18:30:17 -04:00
|
|
|
console.error(error);
|
2018-09-13 15:41:42 -04:00
|
|
|
return undefined;
|
2018-09-11 18:30:17 -04:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-09-30 14:48:42 -04:00
|
|
|
|
|
|
|
function likeBuilding(building_id, user_id) {
|
|
|
|
// 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({serializable}, t => {
|
|
|
|
return t.none(
|
|
|
|
"INSERT INTO building_user_likes ( building_id, user_id ) VALUES ($1, $2);",
|
|
|
|
[building_id, user_id]
|
|
|
|
).then(() => {
|
|
|
|
return t.one(
|
|
|
|
"SELECT count(*) as likes FROM building_user_likes WHERE building_id = $1;",
|
|
|
|
[building_id]
|
|
|
|
).then(building => {
|
|
|
|
return t.one(
|
|
|
|
`INSERT INTO logs (
|
|
|
|
forward_patch, building_id, user_id
|
|
|
|
) VALUES (
|
|
|
|
$1:jsonb, $2, $3
|
|
|
|
) RETURNING log_id
|
|
|
|
`,
|
|
|
|
[{likes_total: building.likes}, building_id, user_id]
|
|
|
|
).then(revision => {
|
|
|
|
return t.one(
|
|
|
|
`UPDATE buildings
|
|
|
|
SET
|
|
|
|
revision_id = $1,
|
|
|
|
likes_total = $2
|
|
|
|
WHERE
|
|
|
|
building_id = $3
|
|
|
|
RETURNING
|
|
|
|
*
|
|
|
|
`,
|
|
|
|
[revision.log_id, building.likes, building_id]
|
|
|
|
)
|
|
|
|
})
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}).catch(function(error){
|
|
|
|
// TODO report transaction error as 'Need to re-fetch building before update'
|
|
|
|
console.error(error);
|
|
|
|
return undefined;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-09-30 13:58:41 -04:00
|
|
|
const BUILDING_FIELD_WHITELIST = new Set([
|
|
|
|
'ref_toid',
|
|
|
|
'ref_osm_id',
|
|
|
|
'location_name',
|
|
|
|
'location_number',
|
|
|
|
'location_street',
|
|
|
|
'location_line_two',
|
|
|
|
'location_town',
|
|
|
|
'location_postcode',
|
|
|
|
'location_latitude',
|
|
|
|
'location_longitude',
|
|
|
|
'date_year',
|
|
|
|
'date_lower',
|
|
|
|
'date_upper',
|
|
|
|
'date_source',
|
|
|
|
'facade_year',
|
|
|
|
'facade_upper',
|
|
|
|
'facade_lower',
|
|
|
|
'facade_source',
|
|
|
|
'size_storeys_attic',
|
|
|
|
'size_storeys_core',
|
|
|
|
'size_storeys_basement',
|
|
|
|
'size_height_apex',
|
|
|
|
'size_floor_area_ground',
|
|
|
|
'size_floor_area_total',
|
|
|
|
'size_width_frontage',
|
|
|
|
]);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Compare old and new data objects, generate shallow merge patch of changed fields
|
|
|
|
* - forward patch is object with {keys: new_values}
|
|
|
|
* - reverse patch is object with {keys: old_values}
|
|
|
|
*
|
|
|
|
* @param {object} old_obj
|
|
|
|
* @param {object} new_obj
|
|
|
|
* @param {Set} whitelist
|
|
|
|
* @returns {[object, object]}
|
|
|
|
*/
|
|
|
|
function compare(old_obj, new_obj, whitelist){
|
2018-09-30 17:23:13 -04:00
|
|
|
const reverse_patch = {}
|
|
|
|
const forward_patch = {}
|
2018-09-30 13:58:41 -04:00
|
|
|
for (const [key, value] of Object.entries(new_obj)) {
|
|
|
|
if (old_obj[key] !== value && whitelist.has(key)) {
|
|
|
|
reverse_patch[key] = old_obj[key];
|
|
|
|
forward_patch[key] = value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return [forward_patch, reverse_patch]
|
|
|
|
}
|
|
|
|
|
2018-09-30 14:48:42 -04:00
|
|
|
export { queryBuildingsAtPoint, queryBuildingsByReference, getBuildingById, saveBuilding,
|
|
|
|
likeBuilding };
|