colouring-montreal/app/src/api/services/building.ts

402 lines
12 KiB
TypeScript
Raw Normal View History

2018-09-30 14:48:42 -04:00
/**
* Building data access
*
*/
2019-08-14 05:54:13 -04:00
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.
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
});
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]
2019-02-24 14:28:11 -05:00
).catch(function (error) {
console.error(error);
2018-09-13 15:41:42 -04:00
return undefined;
});
}
function queryBuildingsByReference(key, id) {
2019-02-24 14:28:11 -05:00
if (key === 'toid') {
return db.manyOrNone(
2018-10-20 09:51:39 -04:00
`SELECT
*
FROM
buildings
WHERE
ref_toid = $1
`,
[id]
2019-02-24 14:28:11 -05:00
).catch(function (error) {
console.error(error);
return undefined;
});
}
if (key === 'uprn') {
return db.manyOrNone(
2018-10-20 09:51:39 -04:00
`SELECT
b.*
FROM
buildings as b, building_properties as p
WHERE
b.building_id = p.building_id
AND
p.uprn = $1
`,
[id]
2019-02-24 14:28:11 -05:00
).catch(function (error) {
console.error(error);
return undefined;
});
}
return Promise.resolve({ error: 'Key must be UPRN or TOID' });
}
2018-09-11 18:30:17 -04:00
function getBuildingById(id) {
return db.one(
2019-05-27 11:31:48 -04:00
'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) {
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
});
}
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 []
});
}
2019-05-27 13:26:29 -04:00
function getBuildingLikeById(buildingId, userId) {
return db.oneOrNone(
2019-05-27 11:31:48 -04:00
'SELECT true as like FROM building_user_likes WHERE building_id = $1 and user_id = $2 LIMIT 1',
2019-05-27 13:26:29 -04:00
[buildingId, userId]
2019-01-22 12:34:46 -05:00
).then(res => {
return res && res.like
2019-02-24 14:28:11 -05:00
}).catch(function (error) {
console.error(error);
return undefined;
2019-01-22 12:02:03 -05:00
});
}
function getBuildingUPRNsById(id) {
return db.any(
2019-05-27 11:31:48 -04:00
'SELECT uprn, parent_uprn FROM building_properties WHERE building_id = $1',
[id]
2019-02-24 14:28:11 -05:00
).catch(function (error) {
console.error(error);
return undefined;
});
}
2019-05-27 13:26:29 -04:00
function saveBuilding(buildingId, building, userId) {
// 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
// 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(
2019-05-27 11:31:48 -04:00
'SELECT * FROM buildings WHERE building_id = $1 FOR UPDATE;',
2019-05-27 13:26:29 -04:00
[buildingId]
).then(oldBuilding => {
const patches = compare(oldBuilding, building, BUILDING_FIELD_WHITELIST);
console.log('Patching', buildingId, patches)
const forward = patches[0];
const reverse = patches[1];
2018-09-30 18:06:42 -04:00
if (Object.keys(forward).length === 0) {
2019-05-27 11:31:48 -04:00
return Promise.reject('No change provided')
2018-09-30 18:06:42 -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
) RETURNING log_id
`,
2019-05-27 13:26:29 -04:00
[forward, reverse, buildingId, userId]
).then(revision => {
const sets = db.$config.pgp.helpers.sets(forward);
2019-05-27 13:26:29 -04:00
console.log('Setting', buildingId, sets)
return t.one(
`UPDATE
buildings
SET
revision_id = $1,
$2:raw
WHERE
2018-10-05 16:44:51 -04:00
building_id = $3
RETURNING
*
`,
2019-05-27 13:26:29 -04:00
[revision.log_id, sets, buildingId]
).then((data) => {
2019-05-27 13:26:29 -04:00
expireBuildingTileCache(buildingId)
return data
})
});
});
2019-02-24 14:28:11 -05:00
}).catch(function (error) {
2018-09-11 18:30:17 -04:00
console.error(error);
2019-02-24 14:28:11 -05:00
return { error: error };
2018-09-11 18:30:17 -04:00
});
}
2019-05-27 13:26:29 -04:00
function likeBuilding(buildingId, userId) {
2018-09-30 14:48:42 -04:00
// 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?)
2019-05-27 16:28:19 -04:00
return db.tx({mode: serializable}, t => {
2018-09-30 14:48:42 -04:00
return t.none(
2019-05-27 11:31:48 -04:00
'INSERT INTO building_user_likes ( building_id, user_id ) VALUES ($1, $2);',
2019-05-27 13:26:29 -04:00
[buildingId, userId]
2018-09-30 14:48:42 -04:00
).then(() => {
return t.one(
2019-05-27 11:31:48 -04:00
'SELECT count(*) as likes FROM building_user_likes WHERE building_id = $1;',
2019-05-27 13:26:29 -04:00
[buildingId]
2018-09-30 14:48:42 -04:00
).then(building => {
return t.one(
`INSERT INTO logs (
forward_patch, building_id, user_id
) VALUES (
$1:json, $2, $3
2018-09-30 14:48:42 -04:00
) RETURNING log_id
`,
2019-05-27 13:26:29 -04:00
[{ likes_total: building.likes }, buildingId, userId]
2018-09-30 14:48:42 -04:00
).then(revision => {
return t.one(
`UPDATE buildings
SET
revision_id = $1,
likes_total = $2
WHERE
building_id = $3
RETURNING
*
`,
2019-05-27 13:26:29 -04:00
[revision.log_id, building.likes, buildingId]
).then((data) => {
2019-05-27 13:26:29 -04:00
expireBuildingTileCache(buildingId)
return data
})
2018-09-30 14:48:42 -04:00
})
});
});
2019-02-24 14:28:11 -05:00
}).catch(function (error) {
2018-09-30 14:48:42 -04:00
console.error(error);
2019-05-27 11:31:48 -04:00
if (error.detail && error.detail.includes('already exists')) {
2019-01-22 11:43:16 -05:00
// 'already exists' is thrown if user already liked it
2019-02-24 14:28:11 -05:00
return { error: 'It looks like you already like that building!' };
2019-01-22 11:43:16 -05:00
} else {
return undefined
}
2018-09-30 14:48:42 -04:00
});
}
2019-05-27 13:26:29 -04:00
function unlikeBuilding(buildingId, userId) {
2019-01-22 12:52:32 -05:00
// 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?)
2019-05-27 16:28:19 -04:00
return db.tx({mode: serializable}, t => {
2019-01-22 12:52:32 -05:00
return t.none(
2019-05-27 11:31:48 -04:00
'DELETE FROM building_user_likes WHERE building_id = $1 AND user_id = $2;',
2019-05-27 13:26:29 -04:00
[buildingId, userId]
2019-01-22 12:52:32 -05:00
).then(() => {
return t.one(
2019-05-27 11:31:48 -04:00
'SELECT count(*) as likes FROM building_user_likes WHERE building_id = $1;',
2019-05-27 13:26:29 -04:00
[buildingId]
2019-01-22 12:52:32 -05:00
).then(building => {
return t.one(
`INSERT INTO logs (
forward_patch, building_id, user_id
) VALUES (
$1:json, $2, $3
) RETURNING log_id
`,
2019-05-27 13:26:29 -04:00
[{ likes_total: building.likes }, buildingId, userId]
2019-01-22 12:52:32 -05:00
).then(revision => {
return t.one(
`UPDATE buildings
SET
revision_id = $1,
likes_total = $2
WHERE
building_id = $3
RETURNING
*
`,
2019-05-27 13:26:29 -04:00
[revision.log_id, building.likes, buildingId]
).then((data) => {
2019-05-27 13:26:29 -04:00
expireBuildingTileCache(buildingId)
return data
})
2019-01-22 12:52:32 -05:00
})
});
});
2019-02-24 14:28:11 -05:00
}).catch(function (error) {
2019-01-22 12:52:32 -05:00
console.error(error);
2019-05-27 11:31:48 -04:00
if (error.detail && error.detail.includes('already exists')) {
2019-01-22 12:52:32 -05:00
// 'already exists' is thrown if user already liked it
2019-02-24 14:28:11 -05:00
return { error: 'It looks like you already like that building!' };
2019-01-22 12:52:32 -05:00
} else {
return undefined
}
});
}
2019-05-27 13:26:29 -04:00
function privateQueryBuildingBBOX(buildingId){
2019-05-27 11:45:24 -04:00
return db.one(
`SELECT
ST_XMin(envelope) as xmin,
ST_YMin(envelope) as ymin,
ST_XMax(envelope) as xmax,
ST_YMax(envelope) as ymax
FROM (
SELECT
ST_Envelope(g.geometry_geom) as envelope
FROM buildings as b, geometries as g
WHERE
b.geometry_id = g.geometry_id
AND
b.building_id = $1
) as envelope`,
2019-05-27 13:26:29 -04:00
[buildingId]
)
}
2019-05-27 13:26:29 -04:00
function expireBuildingTileCache(buildingId) {
privateQueryBuildingBBOX(buildingId).then((bbox) => {
const buildingBbox = [bbox.xmax, bbox.ymax, bbox.xmin, bbox.ymin]
removeAllAtBbox(buildingBbox);
})
}
const BUILDING_FIELD_WHITELIST = new Set([
'ref_osm_id',
// 'location_name',
'location_number',
// 'location_street',
// 'location_line_two',
2019-01-22 16:43:36 -05:00
'location_town',
'location_postcode',
'location_latitude',
'location_longitude',
'date_year',
'date_lower',
'date_upper',
'date_source',
2018-10-21 15:47:59 -04:00
'date_source_detail',
'date_link',
'facade_year',
'facade_upper',
'facade_lower',
'facade_source',
2018-10-21 15:47:59 -04:00
'facade_source_detail',
'size_storeys_attic',
'size_storeys_core',
'size_storeys_basement',
'size_height_apex',
'size_floor_area_ground',
'size_floor_area_total',
'size_width_frontage',
'planning_portal_link',
'planning_in_conservation_area',
'planning_conservation_area_name',
'planning_in_list',
'planning_list_id',
'planning_list_cat',
'planning_list_grade',
'planning_heritage_at_risk_id',
'planning_world_list_id',
'planning_in_glher',
'planning_glher_url',
'planning_in_apa',
'planning_apa_name',
'planning_apa_tier',
'planning_in_local_list',
'planning_local_list_url',
'planning_in_historic_area_assessment',
'planning_historic_area_assessment_url',
]);
/**
* 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}
*
2019-05-27 13:26:29 -04:00
* @param {object} oldObj
* @param {object} newObj
* @param {Set} whitelist
* @returns {[object, object]}
*/
2019-05-27 13:26:29 -04:00
function compare(oldObj, newObj, whitelist) {
const reverse = {}
const forward = {}
for (const [key, value] of Object.entries(newObj)) {
if (oldObj[key] !== value && whitelist.has(key)) {
reverse[key] = oldObj[key];
forward[key] = value;
}
}
2019-05-27 13:26:29 -04:00
return [forward, reverse]
}
2019-01-22 12:02:03 -05:00
export {
queryBuildingsAtPoint,
queryBuildingsByReference,
getBuildingById,
getBuildingLikeById,
getBuildingUPRNsById,
saveBuilding,
2019-01-22 12:52:32 -05:00
likeBuilding,
unlikeBuilding
2019-01-22 12:02:03 -05:00
};