1 | var config = require('../lib/config')();
|
2 | var pg = require('pg');
|
3 | require('../validators');
|
4 | var validate = require('validate.js');
|
5 | var errors = require('../errors');
|
6 | var squel = require('squel').useFlavour('postgres');
|
7 | var helpers = require('../helpers');
|
8 | var moment = require('moment');
|
9 | var union = require('lodash.union');
|
10 | var changes = {};
|
11 |
|
12 | module.exports = changes;
|
13 | var pgURL = config.PostgresURL;
|
14 |
|
15 | function pgConnect(url, query) {
|
16 | return new Promise(function (res, rej) {
|
17 | pg.connect(url, function (err, client, done) {
|
18 | if (err) {
|
19 | return rej(err);
|
20 | }
|
21 | client.query(query, function (err, result) {
|
22 | done();
|
23 | if (err) return rej(err);
|
24 | return res(result);
|
25 | });
|
26 | });
|
27 | });
|
28 | }
|
29 |
|
30 | changes.get = function(from, to, users, tags, bbox) {
|
31 | var parseError = validateParams({'from': from, 'to': to});
|
32 | var usersData;
|
33 | if (parseError) {
|
34 | return Promise.reject(new errors.ParseError(parseError));
|
35 | }
|
36 |
|
37 | return getQueryAndUserData(from, to, users, tags, bbox)
|
38 | .then(function (obj) {
|
39 |
|
40 | usersData = obj.usersData;
|
41 | return pgConnect(pgURL, obj.query);
|
42 | })
|
43 | .then(function (res) {
|
44 | var userLookup = {};
|
45 | if (usersData) {
|
46 | usersData.rows.forEach(function (u) {
|
47 | userLookup[u.id] = u.name;
|
48 | });
|
49 | res.rows.forEach(function (r) {
|
50 | r.username = userLookup[r.uid];
|
51 | });
|
52 | }
|
53 | return res.rows.reduce(reducerFn, {});
|
54 | });
|
55 | };
|
56 |
|
57 | function reducerFn(memo, row) {
|
58 | var hour = moment.utc(row.change_at).startOf('hour').toISOString();
|
59 | if (!memo.hasOwnProperty(hour)) {
|
60 | memo[hour] = {};
|
61 | }
|
62 | var username = row.username;
|
63 | if (!memo[hour].hasOwnProperty(username)) {
|
64 | memo[hour][username] = {};
|
65 | }
|
66 | var thisUserMemo = memo[hour][username];
|
67 | ['nodes', 'ways', 'relations'].forEach(function (thing) {
|
68 | if (!thisUserMemo.hasOwnProperty(thing)) {
|
69 | thisUserMemo[thing] = {
|
70 | 'c': 0,
|
71 | 'm': 0,
|
72 | 'd': 0
|
73 | };
|
74 | }
|
75 | ['c', 'm', 'd'].forEach(function (type) {
|
76 |
|
77 | thisUserMemo[thing][type] += row[thing][type];
|
78 | });
|
79 | });
|
80 | ['tags_created', 'tags_modified', 'tags_deleted'].forEach(function (thing) {
|
81 | if (!thisUserMemo.hasOwnProperty(thing)) {
|
82 | thisUserMemo[thing] = {};
|
83 | }
|
84 | Object.keys(row[thing]).forEach(function (tag) {
|
85 | if (!thisUserMemo[thing].hasOwnProperty(tag)) {
|
86 | thisUserMemo[thing][tag] = {};
|
87 | }
|
88 | Object.keys(row[thing][tag]).forEach(function (value) {
|
89 | if (!thisUserMemo[thing][tag].hasOwnProperty(value)) {
|
90 | thisUserMemo[thing][tag][value] = 0;
|
91 | }
|
92 | thisUserMemo[thing][tag][value] += row[thing][tag][value];
|
93 | });
|
94 |
|
95 | });
|
96 | });
|
97 |
|
98 | thisUserMemo.changesets = union(thisUserMemo.changesets, row.changesets);
|
99 | return memo;
|
100 | }
|
101 |
|
102 | function validateParams(params) {
|
103 | var constraints = {
|
104 | 'from': {
|
105 | 'presence': true,
|
106 | 'datetime': true
|
107 | },
|
108 | 'to': {
|
109 | 'presence': true,
|
110 | 'datetime': true
|
111 | }
|
112 | };
|
113 | var errs = validate(params, constraints);
|
114 | if (errs) {
|
115 | var errMsg = Object.keys(errs).map(function(key) {
|
116 | return errs[key][0];
|
117 | }).join(', ');
|
118 | return errMsg;
|
119 | }
|
120 | return null;
|
121 | }
|
122 |
|
123 |
|
124 | function getQueryAndUserData(from, to, users, tags, bbox) {
|
125 |
|
126 | var sql = squel.select({'parameterCharacter': '!!'})
|
127 | .from('stats')
|
128 | .where('change_at > !!', from)
|
129 | .where('change_at < !!', to);
|
130 |
|
131 | if (bbox) {
|
132 | var polygonGeojson = JSON.stringify(helpers.getPolygon(bbox).geometry);
|
133 | var changesetSql = squel.select({'parameterCharacter': '!!'})
|
134 | .field('array_agg(id)')
|
135 | .from('changesets')
|
136 | .where('created_at > !!', from)
|
137 | .where('created_at < !!', to)
|
138 | .where('ST_Intersects(changesets.bbox, ST_SetSRID(ST_GeomFromGeoJSON(!!), 4326))', polygonGeojson);
|
139 | sql = sql.where('changesets <@ !!', changesetSql);
|
140 | }
|
141 |
|
142 | if (tags) {
|
143 | var tagsArray = tags.split(',').map(function(tag) {
|
144 | return tag;
|
145 | });
|
146 | var tagSql = prepareTagQuery(tagsArray, sql);
|
147 | sql = sql.where(tagSql);
|
148 | }
|
149 |
|
150 | if (users) {
|
151 | var usersArray = users.split(',').map(function(user) {
|
152 | return user;
|
153 | });
|
154 | return getUserIds(usersArray)
|
155 | .then(function(usersData) {
|
156 | var userIds = [];
|
157 | usersData.rows.forEach(function (r) {
|
158 | userIds.push(r.id);
|
159 | });
|
160 | sql.where('uid in !!', userIds);
|
161 | return {
|
162 | query: sql.toParam(),
|
163 | usersData: usersData
|
164 | };
|
165 | });
|
166 | }
|
167 |
|
168 | sql.field('u.name', 'username')
|
169 | .field('stats.id', 'id')
|
170 | .field('uid')
|
171 | .field('change_at')
|
172 | .field('nodes')
|
173 | .field('ways')
|
174 | .field('relations')
|
175 | .field('tags_created')
|
176 | .field('tags_modified')
|
177 | .field('tags_deleted')
|
178 | .field('changesets')
|
179 | .join('users', 'u', 'u.id = stats.uid');
|
180 | console.log('#sql', sql.toString());
|
181 | return Promise.resolve({
|
182 | query: sql.toParam()
|
183 | });
|
184 | }
|
185 |
|
186 |
|
187 | function getUserIds(users) {
|
188 | var userSql = squel.select({'parameterCharacter': '!!'})
|
189 | .from('users')
|
190 | .field('id')
|
191 | .field('name')
|
192 | .where('name in !!', users);
|
193 |
|
194 | return new Promise(function (res, rej) {
|
195 | pg.connect(pgURL, function (err, client, done) {
|
196 | if (err) {
|
197 | return rej(err);
|
198 | }
|
199 | client.query(userSql.toParam(), function (err, result) {
|
200 | done();
|
201 | if (err) {
|
202 | return rej(err);
|
203 | }
|
204 |
|
205 | if (result.rows.length === 0) {
|
206 | return rej(new errors.NotFoundError('No such users'));
|
207 | }
|
208 | res(result);
|
209 | });
|
210 | });
|
211 | });
|
212 | }
|
213 |
|
214 | function prepareTagQuery(tags) {
|
215 |
|
216 |
|
217 |
|
218 |
|
219 | var tagsSql = squel.expr();
|
220 | tagsSql.options.parameterCharacter = '!!';
|
221 |
|
222 | tags.forEach(function (tag) {
|
223 | var key = tag.split('=')[0];
|
224 | var value = tag.split('=')[1];
|
225 | if (value !== '*') {
|
226 | tagsSql.or('tags_created -> !! ? !!', key, value);
|
227 | tagsSql.or('tags_modified -> !! ? !!', key, value);
|
228 | tagsSql.or('tags_deleted -> !! ? !!', key, value);
|
229 | } else {
|
230 | tagsSql.or('tags_created ? !!', key);
|
231 | tagsSql.or('tags_modified ? !!', key);
|
232 | tagsSql.or('tags_deleted ? !!', key);
|
233 | }
|
234 | });
|
235 | return tagsSql;
|
236 | }
|