Compare commits
970 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6dd3d8354 | ||
|
|
028d17f149 | ||
|
|
1cba4a1a9b | ||
|
|
7243e4a0e2 | ||
|
|
fcfa763890 | ||
|
|
942b7ef923 | ||
|
|
bc61f79b3a | ||
|
|
d649d7eb1d | ||
|
|
cf20728711 | ||
|
|
cd31f998dd | ||
|
|
51c3215137 | ||
|
|
3e0cb0ed37 | ||
|
|
7d5469ed1c | ||
|
|
d6b081255c | ||
|
|
f9b59d8549 | ||
|
|
1534c26050 | ||
|
|
81eb849aff | ||
|
|
f72ec17c5f | ||
|
|
9f9b933607 | ||
|
|
e5d4369203 | ||
|
|
e1ec38446d | ||
|
|
2d102c4810 | ||
|
|
e59d0f520a | ||
|
|
2063ac15ee | ||
|
|
7b597e0223 | ||
|
|
716f983e71 | ||
|
|
31feb58e1f | ||
|
|
92ef0a60fc | ||
|
|
e21ab12e4c | ||
|
|
b5a0c6505a | ||
|
|
dfb4e20219 | ||
|
|
9cbcd43fda | ||
|
|
7cadbcc533 | ||
|
|
d3a3a7353a | ||
|
|
b8365e9f6e | ||
|
|
e17dd4b5fa | ||
|
|
8dcab568bd | ||
|
|
a0020804c9 | ||
|
|
ea2126a301 | ||
|
|
8442a9a711 | ||
|
|
c07b3de43d | ||
|
|
4f81f402f5 | ||
|
|
10d21a4a0f | ||
|
|
42bcae0e4a | ||
|
|
04c5b2aa36 | ||
|
|
b30f7264f1 | ||
|
|
2800a50f19 | ||
|
|
287ecf5ce2 | ||
|
|
53b7969753 | ||
|
|
9530f17194 | ||
|
|
0829d5bc7d | ||
|
|
b59712ee10 | ||
|
|
e247e45f96 | ||
|
|
df71d93dd9 | ||
|
|
928f10b420 | ||
|
|
a1807fd0c3 | ||
|
|
d937ce4982 | ||
|
|
6411556a97 | ||
|
|
2c334570c3 | ||
|
|
c1671abaa4 | ||
|
|
d82cc98b75 | ||
|
|
5d7af03228 | ||
|
|
529368e858 | ||
|
|
fdc061b7ee | ||
|
|
923b23871f | ||
|
|
409a103990 | ||
|
|
6a6815d893 | ||
|
|
37737e7941 | ||
|
|
af0bc09d52 | ||
|
|
a8d791e2d3 | ||
|
|
c874a734fd | ||
|
|
23e5cefdf1 | ||
|
|
9226b67ab7 | ||
|
|
ee25585f06 | ||
|
|
028c3f9aec | ||
|
|
cc048c41d8 | ||
|
|
ef39a76371 | ||
|
|
1329f1f535 | ||
|
|
66f38e8ecd | ||
|
|
e45b41f55a | ||
|
|
995ff52cf3 | ||
|
|
960eaa5724 | ||
|
|
37f6ac0c87 | ||
|
|
fcc36ddc83 | ||
|
|
79fdd07d8f | ||
|
|
32ce033c06 | ||
|
|
abb194ca9c | ||
|
|
3161cb0322 | ||
|
|
f36cadb809 | ||
|
|
863e42691a | ||
|
|
762dce7853 | ||
|
|
f0d190d157 | ||
|
|
6103d3b8bd | ||
|
|
7a89f303db | ||
|
|
b05d9a0a75 | ||
|
|
e89503e1fa | ||
|
|
e0cd1aba29 | ||
|
|
a5f985257c | ||
|
|
90efe14bfb | ||
|
|
fc9ae9ca20 | ||
|
|
649297df83 | ||
|
|
ae7e7578db | ||
|
|
0cf6605b8d | ||
|
|
4a52620d83 | ||
|
|
251570b638 | ||
|
|
7660a694cb | ||
|
|
7e7b268a66 | ||
|
|
660c1777e3 | ||
|
|
5407df03fa | ||
|
|
84c34361a0 | ||
|
|
50cf5e5c7a | ||
|
|
aba737c61b | ||
|
|
9bbbe9e7c1 | ||
|
|
163f494b8a | ||
|
|
7f7eb78d8c | ||
|
|
462d25bebc | ||
|
|
2bf8caf7fc | ||
|
|
c6a74b66ce | ||
|
|
d22619c1f9 | ||
|
|
d8b59a18ed | ||
|
|
fae6bdff05 | ||
|
|
37182f5138 | ||
|
|
70e8ab8349 | ||
|
|
102244f467 | ||
|
|
db946b93ec | ||
|
|
5b637577c8 | ||
|
|
0258051f06 | ||
|
|
2ab22882d6 | ||
|
|
55f6241769 | ||
|
|
9ec3325cd0 | ||
|
|
e0ab901600 | ||
|
|
fcaab30fe7 | ||
|
|
e8ecd9b2e0 | ||
|
|
22da5a1ff0 | ||
|
|
6d0c38371a | ||
|
|
b10cf4bebb | ||
|
|
6e9871ba4e | ||
|
|
13b23d9ec9 | ||
|
|
1491f29f96 | ||
|
|
9f4b6d5f43 | ||
|
|
a883514c8a | ||
|
|
9ee6d7fc91 | ||
|
|
7df1a19da4 | ||
|
|
2ee6c8487d | ||
|
|
de3dbb8c1e | ||
|
|
f851423b68 | ||
|
|
8ec2b35557 | ||
|
|
ae4b233458 | ||
|
|
6872d57581 | ||
|
|
79962a7566 | ||
|
|
d4c6282455 | ||
|
|
db3370cd21 | ||
|
|
4213e3163a | ||
|
|
3850bbb68e | ||
|
|
ea95050d43 | ||
|
|
c32dba1ecb | ||
|
|
94a5020faf | ||
|
|
c0e6bf1299 | ||
|
|
9fe8958e8c | ||
|
|
7c51895b0f | ||
|
|
16bca85438 | ||
|
|
9d415d0dbe | ||
|
|
1d0210a372 | ||
|
|
2fb6c08702 | ||
|
|
f7c712f6eb | ||
|
|
eabd25ee6a | ||
|
|
5fc49ab3c2 | ||
|
|
1b6a722c0c | ||
|
|
6414cd52c0 | ||
|
|
f732ed970b | ||
|
|
5cfffcfa83 | ||
|
|
4e28f7bb4e | ||
|
|
532d50ad7a | ||
|
|
d8b0d338c0 | ||
|
|
cd0be5f79d | ||
|
|
3820894454 | ||
|
|
9a60ab07a8 | ||
|
|
12b91e7671 | ||
|
|
fe79ee0315 | ||
|
|
9c3b3e698e | ||
|
|
ca4eeb332a | ||
|
|
794c3efb7d | ||
|
|
20fe9c45cf | ||
|
|
26da872704 | ||
|
|
8a1d5d3a48 | ||
|
|
0392a2a343 | ||
|
|
34a2f3b32b | ||
|
|
4bb8914d9a | ||
|
|
f7c80a0101 | ||
|
|
2d417b4a37 | ||
|
|
46bb400ffd | ||
|
|
8b05d75f97 | ||
|
|
4861d35628 | ||
|
|
4a8bfcf647 | ||
|
|
b311f0e091 | ||
|
|
4e4399b727 | ||
|
|
bbab9c1a6b | ||
|
|
5938836ff5 | ||
|
|
d003790e4d | ||
|
|
1d0fa9a5f6 | ||
|
|
8fe192267d | ||
|
|
92255797d9 | ||
|
|
f404285140 | ||
|
|
ee38c717a5 | ||
|
|
1668392296 | ||
|
|
c1feaecbcb | ||
|
|
dea1c74fcc | ||
|
|
22a0f2c14e | ||
|
|
94c34eeb23 | ||
|
|
d63d976916 | ||
|
|
54601db44a | ||
|
|
4701decfcd | ||
|
|
cc81c8ff4c | ||
|
|
8febc78d0e | ||
|
|
87838bd4ce | ||
|
|
dfc354550c | ||
|
|
ef36466b3b | ||
|
|
7041039572 | ||
|
|
97b4e19777 | ||
|
|
55bf6e86f7 | ||
|
|
683ec662b5 | ||
|
|
87c5844704 | ||
|
|
094a0ea76d | ||
|
|
ebc086106f | ||
|
|
6384f5538c | ||
|
|
befedfd80a | ||
|
|
d828a92ea3 | ||
|
|
4711b28c25 | ||
|
|
d7a90e6be4 | ||
|
|
267e770c63 | ||
|
|
b233f18a0f | ||
|
|
32092d212e | ||
|
|
b7b52eee80 | ||
|
|
a8da7e60c3 | ||
|
|
83e944c985 | ||
|
|
7f841d49b2 | ||
|
|
bdc52204e4 | ||
|
|
c95b080267 | ||
|
|
11cdcc65ad | ||
|
|
1e9ec9e053 | ||
|
|
7bafc54280 | ||
|
|
69d5aef59b | ||
|
|
985e61b3c6 | ||
|
|
15f512a3c7 | ||
|
|
1f3f7b4560 | ||
|
|
fecd63e582 | ||
|
|
a32613c854 | ||
|
|
38e55367b1 | ||
|
|
73b837f4d9 | ||
|
|
9b971aa124 | ||
|
|
8479198268 | ||
|
|
9daaf5bb6a | ||
|
|
4e99ff1c39 | ||
|
|
8e8458e557 | ||
|
|
391ac51f0f | ||
|
|
4bc8fb207a | ||
|
|
42cd36afb7 | ||
|
|
dd0436e68e | ||
|
|
d76a0d9f22 | ||
|
|
d9e047e20e | ||
|
|
af35ff7419 | ||
|
|
81bace1dca | ||
|
|
38c69de01b | ||
|
|
213ca07c38 | ||
|
|
012fa91e83 | ||
|
|
3af1182206 | ||
|
|
04e00bb834 | ||
|
|
df089cb0a5 | ||
|
|
56aa1b39f0 | ||
|
|
d940ab36e1 | ||
|
|
0f48d51062 | ||
|
|
1c5344ba6e | ||
|
|
0d4654122c | ||
|
|
63a9d58c67 | ||
|
|
5a397afd06 | ||
|
|
b8109401d1 | ||
|
|
5e09c80b71 | ||
|
|
b906f88a44 | ||
|
|
24b1b53ba0 | ||
|
|
53fae9fbbd | ||
|
|
ad4ed7a06b | ||
|
|
7f5e655730 | ||
|
|
e96a9f0b46 | ||
|
|
db7b4fa937 | ||
|
|
7112341c51 | ||
|
|
f4d60f963d | ||
|
|
c6babc7dc4 | ||
|
|
3905ed796e | ||
|
|
595d006d5b | ||
|
|
3bcf6d7ca0 | ||
|
|
68f5ee7bde | ||
|
|
9db6e2161b | ||
|
|
6eeb75a35e | ||
|
|
1f717617b0 | ||
|
|
3d7231929c | ||
|
|
3b4668cc19 | ||
|
|
34ad3fcfe8 | ||
|
|
242224396d | ||
|
|
68b3cb8a34 | ||
|
|
1cfeda8fe5 | ||
|
|
33af2d37b3 | ||
|
|
69505974fe | ||
|
|
a77dd9a11f | ||
|
|
1bc017eac9 | ||
|
|
07dec2e641 | ||
|
|
d86a839265 | ||
|
|
72d8a26ede | ||
|
|
cae4dd81c9 | ||
|
|
eba68c56ef | ||
|
|
99516f5a75 | ||
|
|
37a2e89c81 | ||
|
|
ed837fbf22 | ||
|
|
913b29070f | ||
|
|
e5ddd57d65 | ||
|
|
db35ec682a | ||
|
|
111889565a | ||
|
|
c68ece96cd | ||
|
|
0741881959 | ||
|
|
f7745928ab | ||
|
|
976ee35a35 | ||
|
|
c51e254287 | ||
|
|
9feea66550 | ||
|
|
ee7bd5fb8a | ||
|
|
fff5b3d85a | ||
|
|
d706d0eb22 | ||
|
|
944ce80c1e | ||
|
|
d8ef8cb12f | ||
|
|
eea7bed2f3 | ||
|
|
741bcd1a80 | ||
|
|
9c9cfd015d | ||
|
|
39a1b0742f | ||
|
|
03bcf573c3 | ||
|
|
97d1b4fafa | ||
|
|
d79d3fd4dc | ||
|
|
f60993b042 | ||
|
|
1005126a5f | ||
|
|
fa647a915c | ||
|
|
2cf0b9d097 | ||
|
|
7d68a2967f | ||
|
|
b96be69a5c | ||
|
|
636cd8cd50 | ||
|
|
0536d0abcb | ||
|
|
c647f852d6 | ||
|
|
ebab879aca | ||
|
|
7561635b24 | ||
|
|
7ed819e84a | ||
|
|
407a83e81d | ||
|
|
3c3731252d | ||
|
|
8b64328087 | ||
|
|
f1bb5b3d1d | ||
|
|
7aeab47df4 | ||
|
|
e1b848afd0 | ||
|
|
f111ddf449 | ||
|
|
534c827904 | ||
|
|
efa765a9fc | ||
|
|
910f5693d8 | ||
|
|
7c5ed7e8f8 | ||
|
|
066626d928 | ||
|
|
6bb8c9c271 | ||
|
|
e76850fce6 | ||
|
|
8d3503e0fc | ||
|
|
9788af4273 | ||
|
|
4442200d6b | ||
|
|
02b12f370a | ||
|
|
dce6349fcb | ||
|
|
862c79ee16 | ||
|
|
29c1d74202 | ||
|
|
d83679c895 | ||
|
|
62bdbed1d5 | ||
|
|
390a2b573f | ||
|
|
bbbd7ab41c | ||
|
|
85eb82a175 | ||
|
|
82189d35e0 | ||
|
|
f880e86cd1 | ||
|
|
87f30a3633 | ||
|
|
47b029a871 | ||
|
|
b16e2cad19 | ||
|
|
f8b347a03a | ||
|
|
12da3a58fc | ||
|
|
73d7ee37f8 | ||
|
|
7fdee0ebe5 | ||
|
|
9437b35c8c | ||
|
|
78326629f3 | ||
|
|
db4c3b70bb | ||
|
|
09d937f533 | ||
|
|
d3615e8d2b | ||
|
|
aeaf325e2a | ||
|
|
d629dc24ae | ||
|
|
d8c356e350 | ||
|
|
55d2dcca04 | ||
|
|
9bd9503e9b | ||
|
|
56495522b8 | ||
|
|
9d1d5c439b | ||
|
|
be5e419288 | ||
|
|
8afe6c5228 | ||
|
|
20b46a33cf | ||
|
|
1694b4b3a6 | ||
|
|
5b1b78d386 | ||
|
|
9c249596c0 | ||
|
|
36394520ff | ||
|
|
45adaf5dc2 | ||
|
|
be0680675b | ||
|
|
6c466d13ff | ||
|
|
9905b20448 | ||
|
|
2ff410946a | ||
|
|
94be7e5294 | ||
|
|
da3939239f | ||
|
|
adefbb3365 | ||
|
|
8000a51918 | ||
|
|
7012c6e77a | ||
|
|
13843772d4 | ||
|
|
ad4cd2067b | ||
|
|
ff3efd7d56 | ||
|
|
8d6a406779 | ||
|
|
98d15e2e34 | ||
|
|
d5c591317b | ||
|
|
817afb13d1 | ||
|
|
98f29f945b | ||
|
|
09fdf5b990 | ||
|
|
b760aa8c63 | ||
|
|
a83e6b6929 | ||
|
|
64fa18220f | ||
|
|
b11a766c28 | ||
|
|
c2c3993887 | ||
|
|
35459b7332 | ||
|
|
1b8e37a62c | ||
|
|
89cc9ad27d | ||
|
|
d8a5dc586d | ||
|
|
837b2f7558 | ||
|
|
1df7df21d5 | ||
|
|
53a40de2e7 | ||
|
|
940cafacac | ||
|
|
d1ba4a1759 | ||
|
|
6437e2ec67 | ||
|
|
31e3b9953f | ||
|
|
f5bdb8b15b | ||
|
|
fbcf312071 | ||
|
|
9a7a8a3243 | ||
|
|
fc36950c1d | ||
|
|
b4ca44f096 | ||
|
|
4978dd86ac | ||
|
|
730b29c9cc | ||
|
|
4d631d1b6a | ||
|
|
2d2629c088 | ||
|
|
9a52edacb2 | ||
|
|
67411e32ff | ||
|
|
e06d3200c3 | ||
|
|
9a3eb3e0fd | ||
|
|
a7c96acc81 | ||
|
|
f2ab33b498 | ||
|
|
6a7c9e34a0 | ||
|
|
6c8b3d2f8f | ||
|
|
c0943a7c58 | ||
|
|
5d230c444c | ||
|
|
ac615b4a25 | ||
|
|
bc45b50290 | ||
|
|
1bbd84b37a | ||
|
|
aa3fb4807b | ||
|
|
d1a4057a8d | ||
|
|
8519d2724b | ||
|
|
ba36a47228 | ||
|
|
e0d8dc0334 | ||
|
|
8dec4814a9 | ||
|
|
6167562758 | ||
|
|
7e68f5270d | ||
|
|
a24b7d4c8f | ||
|
|
25aa967146 | ||
|
|
3bfc7d3d23 | ||
|
|
430a3e3fc9 | ||
|
|
c94d782037 | ||
|
|
233f9698f3 | ||
|
|
61fc15cec0 | ||
|
|
4e754e0d86 | ||
|
|
125ab7d95e | ||
|
|
26c5ff1f93 | ||
|
|
ffa3a96f1a | ||
|
|
9818d8bb6c | ||
|
|
f7fad736c3 | ||
|
|
62cc99c1c9 | ||
|
|
44424583f0 | ||
|
|
98cb0878d9 | ||
|
|
5aa63d35ce | ||
|
|
11099c88dc | ||
|
|
ef22c46199 | ||
|
|
e8cd6856b5 | ||
|
|
3d36802686 | ||
|
|
dc706aeb43 | ||
|
|
071b6816e3 | ||
|
|
cfdff61d08 | ||
|
|
2132960d7c | ||
|
|
fefb0b23af | ||
|
|
c1da1a8a16 | ||
|
|
93bdbb1c50 | ||
|
|
f1e421db05 | ||
|
|
462ba62656 | ||
|
|
5cd073c96f | ||
|
|
40b8d865a9 | ||
|
|
a21d7db390 | ||
|
|
cc61a89c68 | ||
|
|
3316c2ded3 | ||
|
|
b6989ac82a | ||
|
|
6bf06116df | ||
|
|
5bb5bc42ee | ||
|
|
57e10a8d2b | ||
|
|
51fade6bd3 | ||
|
|
48ffe5e660 | ||
|
|
22fdc3d1bf | ||
|
|
04b65c7c0d | ||
|
|
3576eb8081 | ||
|
|
9377b73aa3 | ||
|
|
e5aff3f366 | ||
|
|
78356ab298 | ||
|
|
947a367865 | ||
|
|
e79d9ec2f9 | ||
|
|
16e8451166 | ||
|
|
1d54a8dccd | ||
|
|
b68d2d9115 | ||
|
|
64d540f23b | ||
|
|
d8d681e8bc | ||
|
|
5b9f608667 | ||
|
|
7660046720 | ||
|
|
0315b32d2b | ||
|
|
5f906e54e4 | ||
|
|
143f0ea67b | ||
|
|
0aa2cffb5e | ||
|
|
f2a7953d9d | ||
|
|
f231dc13cf | ||
|
|
a107ee67fa | ||
|
|
cb488cbde8 | ||
|
|
18d3da66f3 | ||
|
|
61dd92129a | ||
|
|
489c0f3108 | ||
|
|
e327580a2f | ||
|
|
bd9c28e29c | ||
|
|
f4d7148f66 | ||
|
|
1dd5bc8f14 | ||
|
|
59db640d0d | ||
|
|
4bb35f5fab | ||
|
|
967a0c31fd | ||
|
|
c5c8dd7ad7 | ||
|
|
d3e2707fce | ||
|
|
4cba4c7a1f | ||
|
|
3b1fd05940 | ||
|
|
5bc5c0ae86 | ||
|
|
5fc801f8a6 | ||
|
|
f7a23c094c | ||
|
|
516b1f765e | ||
|
|
d28c915635 | ||
|
|
f76606bc26 | ||
|
|
7ba3394508 | ||
|
|
f19eeff899 | ||
|
|
d3c9da6d5f | ||
|
|
1ce908177e | ||
|
|
609bf13765 | ||
|
|
97a49fab2f | ||
|
|
10ead27676 | ||
|
|
8be7ea5cc1 | ||
|
|
ebefba9e32 | ||
|
|
fb784d6a91 | ||
|
|
c31639ebbd | ||
|
|
4ff8d6fbc3 | ||
|
|
d029f81992 | ||
|
|
6b7c2675f1 | ||
|
|
4f8c184bc0 | ||
|
|
afc608fc5d | ||
|
|
8523875349 | ||
|
|
79955c7fac | ||
|
|
d3cbd70054 | ||
|
|
81706b8726 | ||
|
|
2812a54210 | ||
|
|
258d768887 | ||
|
|
1059066c05 | ||
|
|
875f3c07b3 | ||
|
|
8ce72ea842 | ||
|
|
e542d38ec7 | ||
|
|
771eaf97c8 | ||
|
|
b40ed13f47 | ||
|
|
b9de49d5ab | ||
|
|
ead6fa5f1f | ||
|
|
6ada8ba6a2 | ||
|
|
672b19b106 | ||
|
|
4a2580c9ea | ||
|
|
52c8c9341a | ||
|
|
72c4a7abd6 | ||
|
|
d022a1fa5e | ||
|
|
a142620b70 | ||
|
|
5603336253 | ||
|
|
838a3464c1 | ||
|
|
0906ae3c93 | ||
|
|
af8ed99ae7 | ||
|
|
f8d1e159f4 | ||
|
|
df999e040c | ||
|
|
2e13bc42a1 | ||
|
|
9fd2519c12 | ||
|
|
325bdfe92f | ||
|
|
9211fa065b | ||
|
|
bb170ee208 | ||
|
|
8333b39928 | ||
|
|
fefff3b788 | ||
|
|
d5e985fde5 | ||
|
|
ed27f980c2 | ||
|
|
b6afad1787 | ||
|
|
7807ea5f8c | ||
|
|
d463d35906 | ||
|
|
5cde325d9a | ||
|
|
b038763b7b | ||
|
|
aa448a8c2e | ||
|
|
a830eb4ea0 | ||
|
|
73397ab500 | ||
|
|
a7f6eafd5c | ||
|
|
ed9083de24 | ||
|
|
a2fa92abf1 | ||
|
|
fa1e1fd779 | ||
|
|
adde66bc57 | ||
|
|
db08fc3da2 | ||
|
|
97e603b215 | ||
|
|
29936d76b1 | ||
|
|
3f88aaae64 | ||
|
|
fccf46c67d | ||
|
|
970aca1c9d | ||
|
|
5e494f0982 | ||
|
|
91a7dc8cf0 | ||
|
|
e52cd28f1e | ||
|
|
7bdbd4cb03 | ||
|
|
95d694f6c5 | ||
|
|
313fc75ec8 | ||
|
|
639a69a639 | ||
|
|
a4ec3ad6da | ||
|
|
b7c10c95d3 | ||
|
|
67d2d2fe95 | ||
|
|
0aa8d63a6e | ||
|
|
7b11cdcb74 | ||
|
|
071a5a4bdf | ||
|
|
5ede6c3021 | ||
|
|
7ff7b0c2d1 | ||
|
|
30dab7df9f | ||
|
|
afff06c7e6 | ||
|
|
05ef43c342 | ||
|
|
c235754df2 | ||
|
|
70dab149ba | ||
|
|
ed4b44a78a | ||
|
|
54f113ab5f | ||
|
|
60bf81d950 | ||
|
|
bdfd58f468 | ||
|
|
de1aaf3808 | ||
|
|
769aee1107 | ||
|
|
3663e6d12a | ||
|
|
a6d9984453 | ||
|
|
4e8cf136c8 | ||
|
|
f49d7478d7 | ||
|
|
b4a1c9d648 | ||
|
|
692246ec44 | ||
|
|
48a7d28aa6 | ||
|
|
04146f897d | ||
|
|
a34658c97f | ||
|
|
cbfeb0158e | ||
|
|
8d37e00869 | ||
|
|
584d6ae9cf | ||
|
|
421e611356 | ||
|
|
f078713d28 | ||
|
|
a8d31d52cf | ||
|
|
d9213b2fe2 | ||
|
|
091efe52fc | ||
|
|
a5c508733a | ||
|
|
ce944d9a7d | ||
|
|
8321b5adba | ||
|
|
667c972308 | ||
|
|
3dbe05be3a | ||
|
|
2d4ce19250 | ||
|
|
3b3e0c0acd | ||
|
|
c3ddb933bb | ||
|
|
6aae60ece7 | ||
|
|
6b3dc8ece0 | ||
|
|
7dd231a8c9 | ||
|
|
35a3219012 | ||
|
|
7598e6ab4b | ||
|
|
bbedc5f41b | ||
|
|
e1a2c45b19 | ||
|
|
8fa801e032 | ||
|
|
75870dc6c1 | ||
|
|
42900b5d0e | ||
|
|
c1423d77ff | ||
|
|
0e43c54214 | ||
|
|
2cc4161239 | ||
|
|
fc8f3fdf27 | ||
|
|
24b76208ac | ||
|
|
0de272b195 | ||
|
|
7faf40004c | ||
|
|
88ae2d473a | ||
|
|
337b47685c | ||
|
|
ed3f9be655 | ||
|
|
248c6d5f22 | ||
|
|
dd4aa09d21 | ||
|
|
132f2226ca | ||
|
|
2eb6e95fed | ||
|
|
db8130be4f | ||
|
|
379b649e95 | ||
|
|
e5619492ef | ||
|
|
cc76ccc626 | ||
|
|
3d6512dd11 | ||
|
|
b19d97e01f | ||
|
|
25931a618b | ||
|
|
ffab576399 | ||
|
|
e7067ab9cf | ||
|
|
7cfcf6d579 | ||
|
|
a4b586055a | ||
|
|
5ad1e1b645 | ||
|
|
01ed513a79 | ||
|
|
504f68b8aa | ||
|
|
cbb08f5642 | ||
|
|
a4b5d681ce | ||
|
|
02f93f3a14 | ||
|
|
ad2f4573f8 | ||
|
|
06604cd738 | ||
|
|
089be35b5d | ||
|
|
bbcb335d60 | ||
|
|
6ef2e0bb5f | ||
|
|
d8202d881d | ||
|
|
aae814a156 | ||
|
|
49bcc5368d | ||
|
|
555e04f9e7 | ||
|
|
3f6f2e4e23 | ||
|
|
abffc4b067 | ||
|
|
363cb0b679 | ||
|
|
d26910ba9c | ||
|
|
74b2f305ea | ||
|
|
6c2f893651 | ||
|
|
faaf121eb6 | ||
|
|
83ab65163d | ||
|
|
9dcd5ff332 | ||
|
|
c6635f63c1 | ||
|
|
56213219e4 | ||
|
|
7c2dc20dbe | ||
|
|
c8e8317ea4 | ||
|
|
8509796743 | ||
|
|
90aaed0f2c | ||
|
|
48be15b742 | ||
|
|
a95b3f2f99 | ||
|
|
b2cc7ab84f | ||
|
|
eb3414f07f | ||
|
|
292dad130d | ||
|
|
ec41cddb19 | ||
|
|
5871f8290d | ||
|
|
33089be2cd | ||
|
|
d351c8d14c | ||
|
|
82446e5ffa | ||
|
|
b786164e8a | ||
|
|
f9cbb3aac8 | ||
|
|
a66c19c6c7 | ||
|
|
94d1667d70 | ||
|
|
3399db1cff | ||
|
|
874ea99d19 | ||
|
|
7022fb87b4 | ||
|
|
7c1e2a6af0 | ||
|
|
2f011c3266 | ||
|
|
4762aa0897 | ||
|
|
f30f83331f | ||
|
|
3695e1e3e5 | ||
|
|
585b5929aa | ||
|
|
0185cdf785 | ||
|
|
8d22ca66ba | ||
|
|
b0eacb2a79 | ||
|
|
9b40370794 | ||
|
|
95f3d58383 | ||
|
|
0f0cde1093 | ||
|
|
e679366dac | ||
|
|
ca56df5cfe | ||
|
|
d8a4209768 | ||
|
|
40712a2e62 | ||
|
|
acb9ce33b1 | ||
|
|
5e43a7145a | ||
|
|
39bd6694f2 | ||
|
|
5de8c4f9c3 | ||
|
|
31a554d94f | ||
|
|
9bc9fc46ff | ||
|
|
4274e06795 | ||
|
|
a2bf235553 | ||
|
|
1b18b2b188 | ||
|
|
9c27447b17 | ||
|
|
f03d98cd0d | ||
|
|
6331bebb30 | ||
|
|
fdd4c4aaa0 | ||
|
|
4dd404771e | ||
|
|
bf267e9c95 | ||
|
|
843f70cdba | ||
|
|
42e0e07c14 | ||
|
|
dfdd2b9043 | ||
|
|
8656fcd8d1 | ||
|
|
f2f6b9d49c | ||
|
|
82f1e6753b | ||
|
|
7ed717607a | ||
|
|
0ec9491d21 | ||
|
|
416970c819 | ||
|
|
ccc28f3617 | ||
|
|
5bac36b30f | ||
|
|
ef3ffddec7 | ||
|
|
e6ba467d98 | ||
|
|
314508bcd8 | ||
|
|
da18506e41 | ||
|
|
5eaee0b71e | ||
|
|
bd93e7dc7e | ||
|
|
2c762813ba | ||
|
|
136c6fa70b | ||
|
|
67b2343571 | ||
|
|
3caa1d9c4a | ||
|
|
b0c924ca03 | ||
|
|
f6f59023b4 | ||
|
|
9dc4e7c955 | ||
|
|
faa44e54ae | ||
|
|
bfb743b851 | ||
|
|
dad2e92dd3 | ||
|
|
59c312ea40 | ||
|
|
48c5a458f3 | ||
|
|
c0830862c8 | ||
|
|
42deb7abbe | ||
|
|
62deda6470 | ||
|
|
3e0981978a | ||
|
|
35e5170907 | ||
|
|
8eba5dcc01 | ||
|
|
5c2248d419 | ||
|
|
335d91b42d | ||
|
|
102b11b1b5 | ||
|
|
26df09b13f | ||
|
|
254991c56c | ||
|
|
a492ab0143 | ||
|
|
b0d63b2ec0 | ||
|
|
85b0c63eb0 | ||
|
|
98a92f51e6 | ||
|
|
ae50dbd47c | ||
|
|
a97e628520 | ||
|
|
b48dcc1418 | ||
|
|
8867cdbc02 | ||
|
|
f03ee4b836 | ||
|
|
90418b204e | ||
|
|
918674e01a | ||
|
|
7b44b7d559 | ||
|
|
612b11cbe8 | ||
|
|
b34f05690c | ||
|
|
9b01a05727 | ||
|
|
d0024409df | ||
|
|
91856372f0 | ||
|
|
2937b6a804 | ||
|
|
b76a8249fa | ||
|
|
db09476137 | ||
|
|
08a5e57180 | ||
|
|
7464d827fe | ||
|
|
ae0ec159e1 | ||
|
|
2a1c08da65 | ||
|
|
faab174a79 | ||
|
|
521b441da5 | ||
|
|
59ca00b33b | ||
|
|
6564ed69d8 | ||
|
|
fadd9032c6 | ||
|
|
80918f5b9b | ||
|
|
e061b3e631 | ||
|
|
06ec3f80b9 | ||
|
|
e6011287f4 | ||
|
|
a0f560ca1a | ||
|
|
7c7d606aa7 | ||
|
|
46587e3cf1 | ||
|
|
603ef4044c | ||
|
|
2e3abfb2cd | ||
|
|
47ccb7ded8 | ||
|
|
98907a886c | ||
|
|
7e14247ea9 | ||
|
|
e103427750 | ||
|
|
95f55b00b3 | ||
|
|
e519984790 | ||
|
|
fa3223777f | ||
|
|
eeb4966294 | ||
|
|
5823859b2a | ||
|
|
7b21bd26d0 | ||
|
|
4ac224688c | ||
|
|
4742e7f64f | ||
|
|
a66f127828 | ||
|
|
e84d88b7a3 | ||
|
|
cda2616a8a | ||
|
|
11aa4d12bd | ||
|
|
18dbeea003 | ||
|
|
3e916c6054 | ||
|
|
fc420c2c0f | ||
|
|
7b9d653c46 | ||
|
|
140441b777 | ||
|
|
5db0e9c8d8 | ||
|
|
63d1c19263 | ||
|
|
018cd25593 | ||
|
|
963737d3fb | ||
|
|
c059f44bf1 | ||
|
|
890f0d1ef6 | ||
|
|
5fca005a3f | ||
|
|
86d4f8e219 | ||
|
|
6b0ab45e63 | ||
|
|
0b475ab5e2 | ||
|
|
97972ac73f | ||
|
|
c3b38b2f60 | ||
|
|
b4e06ec1ac | ||
|
|
d0a8bd428f | ||
|
|
251fe96509 | ||
|
|
15bf74f770 | ||
|
|
32986e3ebd | ||
|
|
f106f27df4 | ||
|
|
3539b658fb | ||
|
|
abf33a1c68 | ||
|
|
fc6790ea1e | ||
|
|
ac6f0e1c67 | ||
|
|
041cd40ec2 | ||
|
|
8721f56269 | ||
|
|
1d3045c799 | ||
|
|
52a1ed869c | ||
|
|
04f60baec5 | ||
|
|
a8de436424 | ||
|
|
ee7917676b | ||
|
|
c7780e9f42 | ||
|
|
3edd7b8b01 | ||
|
|
455202cd1a | ||
|
|
8bdb82c7be | ||
|
|
fa503ee66a | ||
|
|
e1a2ee2381 | ||
|
|
b82d26527a | ||
|
|
1c50dd6b48 | ||
|
|
b0e9df1400 | ||
|
|
6ebf51ce45 | ||
|
|
d9a34f3384 | ||
|
|
8136a1e136 | ||
|
|
41f3606572 | ||
|
|
ea0542dcb1 | ||
|
|
a4dbc1bac2 | ||
|
|
065f56e161 | ||
|
|
6b5d6648de | ||
|
|
95538707c9 | ||
|
|
4c76a921b1 | ||
|
|
85c1c987af | ||
|
|
bde86323fd | ||
|
|
880e3f388d | ||
|
|
c1535b1a12 | ||
|
|
232ff1ba33 | ||
|
|
1b63dcd4e5 | ||
|
|
b32a0a6547 | ||
|
|
d634be0c30 | ||
|
|
f9fe3ace37 | ||
|
|
6cd8131888 | ||
|
|
0ea76f7d15 | ||
|
|
51e5b5c255 | ||
|
|
cedcc094e6 | ||
|
|
bbe8d4e820 | ||
|
|
5aa98c4ab2 | ||
|
|
d6a9103779 | ||
|
|
2e7784ddf2 | ||
|
|
086be461b2 | ||
|
|
a7157532f1 | ||
|
|
55fd660d69 | ||
|
|
80604b739a | ||
|
|
d88fbbaa87 | ||
|
|
7db0744f67 | ||
|
|
d1fcd797a3 | ||
|
|
150c6ee4be | ||
|
|
d0df8b1533 | ||
|
|
43e1de31fa | ||
|
|
33ed9ab47d | ||
|
|
749a08336a | ||
|
|
467097b3cc | ||
|
|
487aca52d0 | ||
|
|
781d2d3a28 | ||
|
|
2a767cdb83 | ||
|
|
e3cf69ac1a | ||
|
|
2c703e5c16 | ||
|
|
e68ba95fed | ||
|
|
83d00a8aca | ||
|
|
479e8970a1 | ||
|
|
cf5e797f90 |
85
.travis.yml
85
.travis.yml
@@ -1,14 +1,81 @@
|
||||
sudo: required
|
||||
dist: trusty
|
||||
jobs:
|
||||
include:
|
||||
- sudo: required
|
||||
services:
|
||||
- docker
|
||||
language: generic
|
||||
before_install: docker pull carto/nodejs6-xenial-pg101
|
||||
script: npm run docker-test
|
||||
- dist: precise
|
||||
addons:
|
||||
postgresql: "9.5"
|
||||
apt:
|
||||
sources:
|
||||
- ubuntu-toolchain-r-test
|
||||
packages:
|
||||
- pkg-config
|
||||
- libcairo2-dev
|
||||
- libjpeg8-dev
|
||||
- libgif-dev
|
||||
- libpango1.0-dev
|
||||
- g++-4.9
|
||||
- wget
|
||||
|
||||
services:
|
||||
- docker
|
||||
before_install:
|
||||
# Add custom PPAs from cartodb
|
||||
- sudo add-apt-repository -y ppa:cartodb/postgresql-9.5
|
||||
- sudo add-apt-repository -y ppa:cartodb/gis
|
||||
- sudo add-apt-repository -y ppa:cartodb/gis-testing
|
||||
|
||||
before_install:
|
||||
- docker pull cartoimages/windshaft-testing
|
||||
- sudo apt-get update
|
||||
|
||||
# Force instalation of libgeos-3.5.0 (presumably needed because of existing version of postgis)
|
||||
- sudo apt-get -y install libgeos-3.5.0=3.5.0-1cdb2
|
||||
|
||||
script:
|
||||
- docker run -e POSTGIS_VERSION=2.4 -v `pwd`:/srv cartoimages/windshaft-testing bash docker-test.sh
|
||||
# Install postgres db and build deps
|
||||
- sudo /etc/init.d/postgresql stop # stop travis default instance
|
||||
- sudo apt-get -y remove --purge postgresql-9.1
|
||||
- sudo apt-get -y remove --purge postgresql-9.2
|
||||
- sudo apt-get -y remove --purge postgresql-9.3
|
||||
- sudo apt-get -y remove --purge postgresql-9.4
|
||||
- sudo apt-get -y remove --purge postgresql-9.5
|
||||
- sudo apt-get -y remove --purge postgresql-9.6
|
||||
- sudo rm -rf /var/lib/postgresql/
|
||||
- sudo rm -rf /var/log/postgresql/
|
||||
- sudo rm -rf /etc/postgresql/
|
||||
- sudo apt-get -y remove --purge postgis-2.2
|
||||
- sudo apt-get -y autoremove
|
||||
- sudo apt-get -y install postgresql-9.5=9.5.2-3cdb3
|
||||
- sudo apt-get -y install postgresql-server-dev-9.5=9.5.2-3cdb3
|
||||
- sudo apt-get -y install postgresql-plpython-9.5=9.5.2-3cdb3
|
||||
- sudo apt-get -y install postgresql-9.5-postgis-scripts=2.2.2.0-cdb2
|
||||
- sudo apt-get -y install postgresql-9.5-postgis-2.2=2.2.2.0-cdb2
|
||||
|
||||
language: generic
|
||||
# configure it to accept local connections from postgres
|
||||
- echo -e "# TYPE DATABASE USER ADDRESS METHOD \nlocal all postgres trust\nlocal all all trust\nhost all all 127.0.0.1/32 trust" \
|
||||
| sudo tee /etc/postgresql/9.5/main/pg_hba.conf
|
||||
- sudo /etc/init.d/postgresql restart 9.5
|
||||
|
||||
- createdb template_postgis
|
||||
- createuser publicuser
|
||||
- psql -c "CREATE EXTENSION postgis" template_postgis
|
||||
|
||||
# install yarn 0.27.5
|
||||
- curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 0.27.5
|
||||
- export PATH="$HOME/.yarn/bin:$PATH"
|
||||
|
||||
# instal redis 4
|
||||
- wget http://download.redis.io/releases/redis-4.0.8.tar.gz
|
||||
- tar xvzf redis-4.0.8.tar.gz
|
||||
- cd redis-4.0.8
|
||||
- make
|
||||
- sudo make install
|
||||
- cd ..
|
||||
- rm redis-4.0.8.tar.gz
|
||||
|
||||
env:
|
||||
- NPROCS=1 JOBS=1 PGUSER=postgres CXX=g++-4.9
|
||||
|
||||
language: node_js
|
||||
node_js:
|
||||
- "6"
|
||||
|
||||
133
NEWS.md
133
NEWS.md
@@ -1,7 +1,138 @@
|
||||
# Changelog
|
||||
|
||||
## 6.2.0
|
||||
Released 2018-07-20
|
||||
|
||||
Notice:
|
||||
- This release changes the way that authentication works internally. You'll need to run `bundle exec rake carto:api_key:create_default` in your development environment to keep working.
|
||||
|
||||
New features:
|
||||
- CI tests with Ubuntu Xenial + PostgreSQL 10.1 and Ubuntu Precise + PostgreSQL 9.5
|
||||
- Upgrades Windshaft to [4.8.3](https://github.com/CartoDB/Windshaft/blob/4.8.3/NEWS.md#version-483) which includes:
|
||||
- Update internal deps.
|
||||
- A fix in mapnik-vector-tile to avoid grouping together properties with the same value but a different type.
|
||||
- Performance improvements in the marker symbolizer (local cache, avoid building the collision matrix when possible).
|
||||
- MVT: Disable simplify_distance to avoid multiple simplifications.
|
||||
- Fix a bug with zero length lines not being rendered when using the marker symbolizer.
|
||||
- Reduce size of npm package
|
||||
- Omit attributes validation in layers with aggregation to avoid potentially long instantiation times
|
||||
- Upgrades Camshaft to [0.61.11](https://github.com/CartoDB/camshaft/releases/tag/0.61.11):
|
||||
- Use Dollar-Quoted String Constants to avoid Syntax Error while running moran analyses. [0.61.10](https://github.com/CartoDB/camshaft/releases/tag/0.61.10)
|
||||
- Quote name columns when performing trade area analysis to avoid Syntax Errors. [0.61.11](https://github.com/CartoDB/camshaft/releases/tag/0.61.11)
|
||||
- Update other deps:
|
||||
- body-parser: 1.18.3
|
||||
- cartodb-psql: 0.11.0
|
||||
- cartodb-redis: 2.0.1
|
||||
- dot: 1.1.2
|
||||
- express: 4.16.3
|
||||
- lru-cache: 4.1.3
|
||||
- node-statsd: 0.1.1,
|
||||
- queue-async: 1.1.0
|
||||
- request: 2.87.0
|
||||
- semver: 5.5.0
|
||||
- step: 1.0.0
|
||||
- turbo-carto: 0.20.4
|
||||
- yargs: 11.1.0
|
||||
- Update devel deps:
|
||||
- istanbul: 0.4.5
|
||||
- jshint: 2.9.5
|
||||
- mocha: 3.5.3
|
||||
- moment: 2.22.1
|
||||
- nock: 9.2.6
|
||||
- strftime: 0.10.0
|
||||
- Optional instantiation metadata stats (https://github.com/CartoDB/Windshaft-cartodb/pull/952)
|
||||
- Experimental dates_as_numbers support
|
||||
- Tiles base urls with api key
|
||||
|
||||
Bug Fixes:
|
||||
- Validates tile coordinates (z/x/y) from request params to be a valid integer value.
|
||||
- Static maps fails for unsupported formats
|
||||
- Handling errors extracting the column type on dataviews
|
||||
- Fix `meta.stats.estimatedFeatureCount` for aggregations and queries with tokens
|
||||
- Fix numeric histogram bounds when `start` and `end` are specified (#991)
|
||||
- Static maps filters correctly if `layer` option is passed in the url.
|
||||
- Aggregation doesn't return out-of-tile, partially aggregated clusters
|
||||
- Aggregation was not accurate for high zoom, far away from the origin tiles
|
||||
|
||||
Announcements:
|
||||
* Improve error message when the DB query is over the user's limits
|
||||
|
||||
## 6.1.0
|
||||
Released 2018-04-16
|
||||
|
||||
New features:
|
||||
- Aggreation filters
|
||||
- Upgrades Windshaft to 4.7.0, which includes @carto/mapnik v3.6.2-carto.7 with improvements to metrics and markers caching. It also adds an option to disable the markers symbolizer caches in mapnik.
|
||||
|
||||
Bug Fixes:
|
||||
- Non-default aggregation selected the wrong columns (e.g. for vector tiles)
|
||||
- Aggregation dimensions with alias where broken
|
||||
- cartodb_id was not unique accross aggregated vector tiles
|
||||
|
||||
## 6.0.0
|
||||
Released 2018-03-19
|
||||
Backward incompatible changes:
|
||||
- Needs Redis v4
|
||||
|
||||
New features:
|
||||
- Upgrades camshaft to 0.61.8
|
||||
- Upgrades cartodb-redis to 1.0.0
|
||||
- Rate limit feature (disabled by default)
|
||||
- Fixes for tests with PG11
|
||||
|
||||
## 5.4.0
|
||||
Released 2018-03-15
|
||||
- Upgrades Windshaft to 4.5.7 ([Mapnik top metrics](https://github.com/CartoDB/Windshaft/pull/597), [AttributesBackend allows multiple features if all the attributes are the same](https://github.com/CartoDB/Windshaft/pull/602))
|
||||
- Implemented middleware to authorize users via new Api Key system
|
||||
- Keep the old authorization system as fallback
|
||||
- Aggregation widget: Remove NULL categories in 'count' aggregations too
|
||||
- Update request to 2.85.0
|
||||
- Update camshaft to 0.61.4 (Fixes for AOI and Merge analyses)
|
||||
- Update windshaft to 4.6.0, which in turn updates @carto/mapnik to 3.6.2-carto.4 and related dependencies. It brings in a cache for rasterized symbols. See https://github.com/CartoDB/node-mapnik/blob/v3.6.2-carto/CHANGELOG.carto.md#362-carto4
|
||||
- PostGIS: Variables in postgis SQL queries must now additionally be wrapped in `!` (refs [#29](https://github.com/CartoDB/mapnik/issues/29), [mapnik/#3618](https://github.com/mapnik/mapnik/pull/3618)):
|
||||
```sql
|
||||
-- Before
|
||||
SELECT ... WHERE trait = @variable
|
||||
|
||||
-- Now
|
||||
SELECT ... WHERE trait = !@variable!
|
||||
```
|
||||
|
||||
## 5.3.1
|
||||
Released 2018-02-13
|
||||
- Improve the speed of the aggregation dataview #865
|
||||
|
||||
## 5.3.0
|
||||
Released 2018-02-12
|
||||
- Upgrades redis-mpool to 0.5.0
|
||||
- Upgrades windshaft to 4.5.2
|
||||
- Upgrades cartodb-redis to 0.15.0
|
||||
- Adds metrics option to the Mapnik renderer
|
||||
- Upgrades camshadft to 0.61.2
|
||||
|
||||
## 5.2.1
|
||||
Released 2018-02-01
|
||||
|
||||
Bug Fixes:
|
||||
- Allow use of aggregation with attributes #861
|
||||
|
||||
## 5.2.0
|
||||
Released 2018-02-01
|
||||
|
||||
Announcements:
|
||||
- Upgrade windshaft to [4.3.3](https://github.com/CartoDB/windshaft/releases/tag/4.3.2) adding support for cache-features' in Mapnik/CartoDB layers.
|
||||
|
||||
## 5.1.0
|
||||
Released 2018-01-30
|
||||
New features:
|
||||
- Now mapnik has support for fine-grained metrics.
|
||||
- Variables can be passed for later substitution in postgis datasource.
|
||||
|
||||
Announcements:
|
||||
- Upgrade windshaft to [4.3.1](https://github.com/CartoDB/windshaft/releases/tag/4.3.1). Underneath it upgrades mapnik and all the related dependencies.
|
||||
|
||||
## 5.0.1
|
||||
Released 2018-01-20
|
||||
Released 2018-01-29
|
||||
|
||||
Bug Fixes:
|
||||
- Allow aggregation for queries with no the_geom (only the_geom_webmercator) #856
|
||||
|
||||
2
app.js
2
app.js
@@ -100,8 +100,6 @@ if ( global.environment.log_filename ) {
|
||||
global.log4js.configure(log4jsConfig);
|
||||
global.logger = global.log4js.getLogger();
|
||||
|
||||
global.environment.api_hostname = require('os').hostname().split('.')[0];
|
||||
|
||||
// Include cartodb_windshaft only _after_ the "global" variable is set
|
||||
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/28
|
||||
var cartodbWindshaft = require('./lib/cartodb/server');
|
||||
|
||||
@@ -15,26 +15,59 @@ var config = {
|
||||
|
||||
// Base URLs for the APIs
|
||||
//
|
||||
// See http://github.com/CartoDB/Windshaft-cartodb/wiki/Unified-Map-API
|
||||
//
|
||||
// Base url for the Templated Maps API
|
||||
// "/api/v1/map/named" is the new API,
|
||||
// "/tiles/template" is for compatibility with versions up to 1.6.x
|
||||
,base_url_templated: '(?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)'
|
||||
// Base url for the Detached Maps API
|
||||
// "maps" is the the new API,
|
||||
// "tiles/layergroup" is for compatibility with versions up to 1.6.x
|
||||
,base_url_detached: '(?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)'
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/wiki/Unified-Map-API
|
||||
,routes: {
|
||||
v1: {
|
||||
paths: [
|
||||
'/api/v1',
|
||||
'/user/:user/api/v1',
|
||||
],
|
||||
// Base url for the Detached Maps API
|
||||
// "/api/v1/map" is the new API,
|
||||
map: {
|
||||
paths: [
|
||||
'/map',
|
||||
]
|
||||
},
|
||||
// Base url for the Templated Maps API
|
||||
// "/api/v1/map/named" is the new API,
|
||||
template: {
|
||||
paths: [
|
||||
'/map/named'
|
||||
]
|
||||
}
|
||||
},
|
||||
// For compatibility with versions up to 1.6.x
|
||||
v0: {
|
||||
paths: [
|
||||
'/tiles'
|
||||
],
|
||||
// Base url for the Detached Maps API
|
||||
// "/tiles/layergroup" is for compatibility with versions up to 1.6.x
|
||||
map: {
|
||||
paths: [
|
||||
'/layergroup'
|
||||
]
|
||||
},
|
||||
// Base url for the Templated Maps API
|
||||
// "/tiles/template" is for compatibility with versions up to 1.6.x
|
||||
template: {
|
||||
paths: [
|
||||
'/template'
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Resource URLs expose endpoints to request/retrieve metadata associated to Maps: dataviews, analysis node status.
|
||||
//
|
||||
// This URLs depend on how `base_url_detached` and `user_from_host` are configured: the application can be
|
||||
// This URLs depend on how `routes` and `user_from_host` are configured: the application can be
|
||||
// configured to accept request with the {user} in the header host or in the request path.
|
||||
// It also might depend on the configured cdn_url via `serverMetadata.cdn_url`.
|
||||
//
|
||||
// This template allows to make the endpoints generation more flexible, the template exposes the following params:
|
||||
// 1. {{=it.cdn_url}}: will be used when `serverMetadata.cdn_url` exists.
|
||||
// 2. {{=it.user}}: will use the username as extraced from `user_from_host` or `base_url_detached`.
|
||||
// 2. {{=it.user}}: will use the username as extraced from `user_from_host` or `routes`.
|
||||
// 3. {{=it.port}}: will use the `port` from this very same configuration file.
|
||||
,resources_url_templates: {
|
||||
http: 'http://{{=it.user}}.localhost.lan:{{=it.port}}/api/v1/map',
|
||||
@@ -54,12 +87,12 @@ var config = {
|
||||
// idle socket timeout, in milliseconds
|
||||
,socket_timeout: 600000
|
||||
,enable_cors: true
|
||||
,cache_enabled: false
|
||||
,cache_enabled: true
|
||||
,log_format: ':req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler]) (:res[X-Tiler-Errors])'
|
||||
// If log_filename is given logs will be written
|
||||
// there, in append mode. Otherwise stdout is used (default).
|
||||
// Log file will be re-opened on receiving the HUP signal
|
||||
,log_filename: undefined
|
||||
,log_filename: 'logs/node-windshaft.log'
|
||||
// Templated database username for authorized user
|
||||
// Supported labels: 'user_id' (read from redis)
|
||||
,postgres_auth_user: 'development_cartodb_user_<%= user_id %>'
|
||||
@@ -67,39 +100,25 @@ var config = {
|
||||
// Supported labels: 'user_id', 'user_password' (both read from redis)
|
||||
,postgres_auth_pass: '<%= user_password %>'
|
||||
,postgres: {
|
||||
// Parameters to pass to datasource plugin of mapnik
|
||||
// See http://github.com/mapnik/mapnik/wiki/PostGIS
|
||||
type: "postgis",
|
||||
user: "publicuser",
|
||||
password: "public",
|
||||
host: '127.0.0.1',
|
||||
port: 5432,
|
||||
extent: "-20037508.3,-20037508.3,20037508.3,20037508.3",
|
||||
/* experimental
|
||||
geometry_field: "the_geom",
|
||||
extent: "-180,-90,180,90",
|
||||
srid: 4326,
|
||||
*/
|
||||
// max number of rows to return when querying data, 0 means no limit
|
||||
row_limit: 65535,
|
||||
simplify_geometries: true,
|
||||
use_overviews: true, // use overviews to retrieve raster
|
||||
/*
|
||||
* Set persist_connection to false if you want
|
||||
* database connections to be closed on renderer
|
||||
* expiration (1 minute after last use).
|
||||
* Setting to true (the default) would never
|
||||
* close any connection for the server's lifetime
|
||||
*/
|
||||
persist_connection: false,
|
||||
max_size: 500
|
||||
pool: {
|
||||
// maximum number of resources to create at any given time
|
||||
size: 16,
|
||||
// max milliseconds a resource can go unused before it should be destroyed
|
||||
idleTimeout: 3000,
|
||||
// frequency to check for idle resources
|
||||
reapInterval: 1000
|
||||
}
|
||||
}
|
||||
,mapnik_version: undefined
|
||||
,mapnik_tile_format: 'png8:m=h'
|
||||
,statsd: {
|
||||
host: 'localhost',
|
||||
port: 8125,
|
||||
prefix: 'dev.',
|
||||
prefix: 'dev.', // could be hostname, better not containing dots
|
||||
cacheDns: true
|
||||
// support all allowed node-statsd options
|
||||
}
|
||||
@@ -111,15 +130,7 @@ var config = {
|
||||
//If enabled, MVTs will be generated with PostGIS directly, instead of using Mapnik,
|
||||
//PostGIS 2.4 is required for this to work
|
||||
//If disabled it will use Mapnik MVT generation
|
||||
usePostGIS: false,
|
||||
dbPoolParams: {
|
||||
// maximum number of resources to create at any given time
|
||||
size: 16,
|
||||
// max milliseconds a resource can go unused before it should be destroyed
|
||||
idleTimeout: 3000,
|
||||
// frequency to check for idle resources
|
||||
reapInterval: 1000
|
||||
}
|
||||
usePostGIS: false
|
||||
},
|
||||
mapnik: {
|
||||
// The size of the pool of internal mapnik backend
|
||||
@@ -173,6 +184,30 @@ var config = {
|
||||
// It will only work if snapToGrid is enabled
|
||||
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
|
||||
|
||||
postgis: {
|
||||
// Parameters to pass to datasource plugin of mapnik
|
||||
// See http://github.com/mapnik/mapnik/wiki/PostGIS
|
||||
user: "publicuser",
|
||||
password: "public",
|
||||
host: '127.0.0.1',
|
||||
port: 5432,
|
||||
extent: "-20037508.3,-20037508.3,20037508.3,20037508.3",
|
||||
// max number of rows to return when querying data, 0 means no limit
|
||||
row_limit: 65535,
|
||||
/*
|
||||
* Set persist_connection to false if you want
|
||||
* database connections to be closed on renderer
|
||||
* expiration (1 minute after last use).
|
||||
* Setting to true (the default) would never
|
||||
* close any connection for the server's lifetime
|
||||
*/
|
||||
persist_connection: false,
|
||||
simplify_geometries: true,
|
||||
use_overviews: true, // use overviews to retrieve raster
|
||||
max_size: 500,
|
||||
twkb_encoding: true
|
||||
},
|
||||
|
||||
limits: {
|
||||
// Time in milliseconds a render request can take before it fails, some notes:
|
||||
// - 0 means no render limit
|
||||
@@ -186,26 +221,17 @@ var config = {
|
||||
cacheOnTimeout: true
|
||||
},
|
||||
|
||||
geojson: {
|
||||
dbPoolParams: {
|
||||
// maximum number of resources to create at any given time
|
||||
size: 16,
|
||||
// max milliseconds a resource can go unused before it should be destroyed
|
||||
idleTimeout: 3000,
|
||||
// frequency to check for idle resources
|
||||
reapInterval: 1000
|
||||
},
|
||||
// If enabled Mapnik will reuse the features retrieved from the database
|
||||
// instead of requesting them once per style inside a layer
|
||||
'cache-features': true,
|
||||
|
||||
// SQL queries will be wrapped with ST_ClipByBox2D
|
||||
// Returning the portion of a geometry falling within a rectangle
|
||||
// It will only work if snapToGrid is enabled
|
||||
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
|
||||
// geometries will be simplified using ST_RemoveRepeatedPoints
|
||||
// which cost is no more expensive than snapping and results are
|
||||
// much closer to the original geometry
|
||||
removeRepeatedPoints: false // this requires postgis >=2.2
|
||||
// Require metrics to the renderer
|
||||
metrics: false,
|
||||
|
||||
// Options for markers attributes, ellipses and images caches
|
||||
markers_symbolizer_caches: {
|
||||
disabled: false
|
||||
}
|
||||
|
||||
},
|
||||
http: {
|
||||
timeout: 2000, // the timeout in ms for a http tile request
|
||||
@@ -221,16 +247,7 @@ var config = {
|
||||
src: __dirname + '/../../assets/default-placeholder.png'
|
||||
}
|
||||
},
|
||||
torque: {
|
||||
dbPoolParams: {
|
||||
// maximum number of resources to create at any given time
|
||||
size: 16,
|
||||
// max milliseconds a resource can go unused before it should be destroyed
|
||||
idleTimeout: 3000,
|
||||
// frequency to check for idle resources
|
||||
reapInterval: 1000
|
||||
}
|
||||
}
|
||||
torque: {}
|
||||
}
|
||||
// anything analyses related
|
||||
,analysis: {
|
||||
@@ -251,7 +268,7 @@ var config = {
|
||||
// If filename is given logs comming from analysis client will be written
|
||||
// there, in append mode. Otherwise 'log_filename' is used. Otherwise stdout is used (default).
|
||||
// Log file will be re-opened on receiving the HUP signal
|
||||
filename: '/tmp/analysis.log'
|
||||
filename: 'logs/node-windshaft-analysis.log'
|
||||
},
|
||||
// Define max execution time in ms for analyses or tags
|
||||
// If analysis or tag are not found in redis this values will be used as default.
|
||||
@@ -321,6 +338,12 @@ var config = {
|
||||
// X-Tiler-Profile header containing elapsed timing for various
|
||||
// steps taken for producing the response.
|
||||
,useProfiler:true
|
||||
,serverMetadata: {
|
||||
cdn_url: {
|
||||
http: undefined,
|
||||
https: undefined
|
||||
}
|
||||
}
|
||||
// Settings for the health check available at /health
|
||||
,health: {
|
||||
enabled: false,
|
||||
@@ -338,7 +361,28 @@ var config = {
|
||||
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API
|
||||
cdbQueryTablesFromPostgres: true,
|
||||
// whether in mapconfig is available stats & metadata for each layer
|
||||
layerStats: true
|
||||
layerStats: true,
|
||||
// whether it should rate limit endpoints (global configuration)
|
||||
rateLimitsEnabled: false,
|
||||
// whether it should rate limit one or more endpoints (only if rateLimitsEnabled = true)
|
||||
rateLimitsByEndpoint: {
|
||||
anonymous: false,
|
||||
static: false,
|
||||
static_named: false,
|
||||
dataview: false,
|
||||
dataview_search: false,
|
||||
analysis: false,
|
||||
analysis_catalog: false,
|
||||
tile: false,
|
||||
attributes: false,
|
||||
named_list: false,
|
||||
named_create: false,
|
||||
named_get: false,
|
||||
named: false,
|
||||
named_update: false,
|
||||
named_delete: false,
|
||||
named_tiles: false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -15,26 +15,59 @@ var config = {
|
||||
|
||||
// Base URLs for the APIs
|
||||
//
|
||||
// See http://github.com/CartoDB/Windshaft-cartodb/wiki/Unified-Map-API
|
||||
//
|
||||
// Base url for the Templated Maps API
|
||||
// "/api/v1/map/named" is the new API,
|
||||
// "/tiles/template" is for compatibility with versions up to 1.6.x
|
||||
,base_url_templated: '(?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)'
|
||||
// Base url for the Detached Maps API
|
||||
// "maps" is the the new API,
|
||||
// "tiles/layergroup" is for compatibility with versions up to 1.6.x
|
||||
,base_url_detached: '(?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)'
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/wiki/Unified-Map-API
|
||||
,routes: {
|
||||
v1: {
|
||||
paths: [
|
||||
'/api/v1',
|
||||
'/user/:user/api/v1',
|
||||
],
|
||||
// Base url for the Detached Maps API
|
||||
// "/api/v1/map" is the new API,
|
||||
map: {
|
||||
paths: [
|
||||
'/map',
|
||||
]
|
||||
},
|
||||
// Base url for the Templated Maps API
|
||||
// "/api/v1/map/named" is the new API,
|
||||
template: {
|
||||
paths: [
|
||||
'/map/named'
|
||||
]
|
||||
}
|
||||
},
|
||||
// For compatibility with versions up to 1.6.x
|
||||
v0: {
|
||||
paths: [
|
||||
'/tiles'
|
||||
],
|
||||
// Base url for the Detached Maps API
|
||||
// "/tiles/layergroup" is for compatibility with versions up to 1.6.x
|
||||
map: {
|
||||
paths: [
|
||||
'/layergroup'
|
||||
]
|
||||
},
|
||||
// Base url for the Templated Maps API
|
||||
// "/tiles/template" is for compatibility with versions up to 1.6.x
|
||||
template: {
|
||||
paths: [
|
||||
'/template'
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Resource URLs expose endpoints to request/retrieve metadata associated to Maps: dataviews, analysis node status.
|
||||
//
|
||||
// This URLs depend on how `base_url_detached` and `user_from_host` are configured: the application can be
|
||||
// This URLs depend on how `routes` and `user_from_host` are configured: the application can be
|
||||
// configured to accept request with the {user} in the header host or in the request path.
|
||||
// It also might depend on the configured cdn_url via `serverMetadata.cdn_url`.
|
||||
//
|
||||
// This template allows to make the endpoints generation more flexible, the template exposes the following params:
|
||||
// 1. {{=it.cdn_url}}: will be used when `serverMetadata.cdn_url` exists.
|
||||
// 2. {{=it.user}}: will use the username as extraced from `user_from_host` or `base_url_detached`.
|
||||
// 2. {{=it.user}}: will use the username as extraced from `user_from_host` or `routes`.
|
||||
// 3. {{=it.port}}: will use the `port` from this very same configuration file.
|
||||
,resources_url_templates: {
|
||||
http: 'http://{{=it.cdn_url}}/{{=it.user}}/api/v1/map',
|
||||
@@ -67,26 +100,18 @@ var config = {
|
||||
// Supported labels: 'user_id', 'user_password' (both read from redis)
|
||||
,postgres_auth_pass: '<%= user_password %>'
|
||||
,postgres: {
|
||||
// Parameters to pass to datasource plugin of mapnik
|
||||
// See http://github.com/mapnik/mapnik/wiki/PostGIS
|
||||
user: "publicuser",
|
||||
password: "public",
|
||||
host: '127.0.0.1',
|
||||
port: 6432,
|
||||
extent: "-20037508.3,-20037508.3,20037508.3,20037508.3",
|
||||
// max number of rows to return when querying data, 0 means no limit
|
||||
row_limit: 65535,
|
||||
/*
|
||||
* Set persist_connection to false if you want
|
||||
* database connections to be closed on renderer
|
||||
* expiration (1 minute after last use).
|
||||
* Setting to true (the default) would never
|
||||
* close any connection for the server's lifetime
|
||||
*/
|
||||
persist_connection: false,
|
||||
simplify_geometries: true,
|
||||
use_overviews: true, // use overviews to retrieve raster
|
||||
max_size: 500
|
||||
port: 5432,
|
||||
pool: {
|
||||
// maximum number of resources to create at any given time
|
||||
size: 16,
|
||||
// max milliseconds a resource can go unused before it should be destroyed
|
||||
idleTimeout: 3000,
|
||||
// frequency to check for idle resources
|
||||
reapInterval: 1000
|
||||
}
|
||||
}
|
||||
,mapnik_version: undefined
|
||||
,mapnik_tile_format: 'png8:m=h'
|
||||
@@ -105,15 +130,7 @@ var config = {
|
||||
//If enabled, MVTs will be generated with PostGIS directly, instead of using Mapnik,
|
||||
//PostGIS 2.4 is required for this to work
|
||||
//If disabled it will use Mapnik MVT generation
|
||||
usePostGIS: false,
|
||||
dbPoolParams: {
|
||||
// maximum number of resources to create at any given time
|
||||
size: 16,
|
||||
// max milliseconds a resource can go unused before it should be destroyed
|
||||
idleTimeout: 3000,
|
||||
// frequency to check for idle resources
|
||||
reapInterval: 1000
|
||||
}
|
||||
usePostGIS: false
|
||||
},
|
||||
mapnik: {
|
||||
// The size of the pool of internal mapnik backend
|
||||
@@ -167,6 +184,30 @@ var config = {
|
||||
// It will only work if snapToGrid is enabled
|
||||
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
|
||||
|
||||
postgis: {
|
||||
// Parameters to pass to datasource plugin of mapnik
|
||||
// See http://github.com/mapnik/mapnik/wiki/PostGIS
|
||||
user: "publicuser",
|
||||
password: "public",
|
||||
host: '127.0.0.1',
|
||||
port: 5432,
|
||||
extent: "-20037508.3,-20037508.3,20037508.3,20037508.3",
|
||||
// max number of rows to return when querying data, 0 means no limit
|
||||
row_limit: 65535,
|
||||
/*
|
||||
* Set persist_connection to false if you want
|
||||
* database connections to be closed on renderer
|
||||
* expiration (1 minute after last use).
|
||||
* Setting to true (the default) would never
|
||||
* close any connection for the server's lifetime
|
||||
*/
|
||||
persist_connection: false,
|
||||
simplify_geometries: true,
|
||||
use_overviews: true, // use overviews to retrieve raster
|
||||
max_size: 500,
|
||||
twkb_encoding: true
|
||||
},
|
||||
|
||||
limits: {
|
||||
// Time in milliseconds a render request can take before it fails, some notes:
|
||||
// - 0 means no render limit
|
||||
@@ -180,26 +221,17 @@ var config = {
|
||||
cacheOnTimeout: true
|
||||
},
|
||||
|
||||
geojson: {
|
||||
dbPoolParams: {
|
||||
// maximum number of resources to create at any given time
|
||||
size: 16,
|
||||
// max milliseconds a resource can go unused before it should be destroyed
|
||||
idleTimeout: 3000,
|
||||
// frequency to check for idle resources
|
||||
reapInterval: 1000
|
||||
},
|
||||
// If enabled Mapnik will reuse the features retrieved from the database
|
||||
// instead of requesting them once per style inside a layer
|
||||
'cache-features': true,
|
||||
|
||||
// SQL queries will be wrapped with ST_ClipByBox2D
|
||||
// Returning the portion of a geometry falling within a rectangle
|
||||
// It will only work if snapToGrid is enabled
|
||||
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
|
||||
// geometries will be simplified using ST_RemoveRepeatedPoints
|
||||
// which cost is no more expensive than snapping and results are
|
||||
// much closer to the original geometry
|
||||
removeRepeatedPoints: false // this requires postgis >=2.2
|
||||
// Require metrics to the renderer
|
||||
metrics: false,
|
||||
|
||||
// Options for markers attributes, ellipses and images caches
|
||||
markers_symbolizer_caches: {
|
||||
disabled: false
|
||||
}
|
||||
|
||||
},
|
||||
http: {
|
||||
timeout: 2000, // the timeout in ms for a http tile request
|
||||
@@ -215,16 +247,7 @@ var config = {
|
||||
src: __dirname + '/../../assets/default-placeholder.png'
|
||||
}
|
||||
},
|
||||
torque: {
|
||||
dbPoolParams: {
|
||||
// maximum number of resources to create at any given time
|
||||
size: 16,
|
||||
// max milliseconds a resource can go unused before it should be destroyed
|
||||
idleTimeout: 3000,
|
||||
// frequency to check for idle resources
|
||||
reapInterval: 1000
|
||||
}
|
||||
}
|
||||
torque: {}
|
||||
}
|
||||
// anything analyses related
|
||||
,analysis: {
|
||||
@@ -245,7 +268,7 @@ var config = {
|
||||
// If filename is given logs comming from analysis client will be written
|
||||
// there, in append mode. Otherwise 'log_filename' is used. Otherwise stdout is used (default).
|
||||
// Log file will be re-opened on receiving the HUP signal
|
||||
filename: 'logs/analysis.log'
|
||||
filename: 'logs/node-windshaft-analysis.log'
|
||||
},
|
||||
// Define max execution time in ms for analyses or tags
|
||||
// If analysis or tag are not found in redis this values will be used as default.
|
||||
@@ -338,7 +361,28 @@ var config = {
|
||||
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API
|
||||
cdbQueryTablesFromPostgres: true,
|
||||
// whether in mapconfig is available stats & metadata for each layer
|
||||
layerStats: false
|
||||
layerStats: false,
|
||||
// whether it should rate limit endpoints (global configuration)
|
||||
rateLimitsEnabled: false,
|
||||
// whether it should rate limit one or more endpoints (only if rateLimitsEnabled = true)
|
||||
rateLimitsByEndpoint: {
|
||||
anonymous: false,
|
||||
static: false,
|
||||
static_named: false,
|
||||
dataview: false,
|
||||
dataview_search: false,
|
||||
analysis: false,
|
||||
analysis_catalog: false,
|
||||
tile: false,
|
||||
attributes: false,
|
||||
named_list: false,
|
||||
named_create: false,
|
||||
named_get: false,
|
||||
named: false,
|
||||
named_update: false,
|
||||
named_delete: false,
|
||||
named_tiles: false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -15,29 +15,62 @@ var config = {
|
||||
|
||||
// Base URLs for the APIs
|
||||
//
|
||||
// See http://github.com/CartoDB/Windshaft-cartodb/wiki/Unified-Map-API
|
||||
//
|
||||
// Base url for the Templated Maps API
|
||||
// "/api/v1/maps/named" is the new API,
|
||||
// "/tiles/template" is for compatibility with versions up to 1.6.x
|
||||
,base_url_templated: '(?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)'
|
||||
// Base url for the Detached Maps API
|
||||
// "/api/v1/maps" is the the new API,
|
||||
// "/tiles/layergroup" is for compatibility with versions up to 1.6.x
|
||||
,base_url_detached: '(?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)'
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/wiki/Unified-Map-API
|
||||
,routes: {
|
||||
v1: {
|
||||
paths: [
|
||||
'/api/v1',
|
||||
'/user/:user/api/v1',
|
||||
],
|
||||
// Base url for the Detached Maps API
|
||||
// "/api/v1/map" is the new API,
|
||||
map: {
|
||||
paths: [
|
||||
'/map',
|
||||
]
|
||||
},
|
||||
// Base url for the Templated Maps API
|
||||
// "/api/v1/map/named" is the new API,
|
||||
template: {
|
||||
paths: [
|
||||
'/map/named'
|
||||
]
|
||||
}
|
||||
},
|
||||
// For compatibility with versions up to 1.6.x
|
||||
v0: {
|
||||
paths: [
|
||||
'/tiles'
|
||||
],
|
||||
// Base url for the Detached Maps API
|
||||
// "/tiles/layergroup" is for compatibility with versions up to 1.6.x
|
||||
map: {
|
||||
paths: [
|
||||
'/layergroup'
|
||||
]
|
||||
},
|
||||
// Base url for the Templated Maps API
|
||||
// "/tiles/template" is for compatibility with versions up to 1.6.x
|
||||
template: {
|
||||
paths: [
|
||||
'/template'
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Resource URLs expose endpoints to request/retrieve metadata associated to Maps: dataviews, analysis node status.
|
||||
//
|
||||
// This URLs depend on how `base_url_detached` and `user_from_host` are configured: the application can be
|
||||
// This URLs depend on how `routes` and `user_from_host` are configured: the application can be
|
||||
// configured to accept request with the {user} in the header host or in the request path.
|
||||
// It also might depend on the configured cdn_url via `serverMetadata.cdn_url`.
|
||||
//
|
||||
// This template allows to make the endpoints generation more flexible, the template exposes the following params:
|
||||
// 1. {{=it.cdn_url}}: will be used when `serverMetadata.cdn_url` exists.
|
||||
// 2. {{=it.user}}: will use the username as extraced from `user_from_host` or `base_url_detached`.
|
||||
// 2. {{=it.user}}: will use the username as extraced from `user_from_host` or `routes`.
|
||||
// 3. {{=it.port}}: will use the `port` from this very same configuration file.
|
||||
,resources_url_templates: {
|
||||
http: 'http://{{=it.user}}.localhost.lan:{{=it.port}}/api/v1/map',
|
||||
http: 'http://{{=it.cdn_url}}/{{=it.user}}/api/v1/map',
|
||||
https: 'https://{{=it.cdn_url}}/{{=it.user}}/api/v1/map'
|
||||
}
|
||||
|
||||
@@ -55,7 +88,7 @@ var config = {
|
||||
,socket_timeout: 600000
|
||||
,enable_cors: true
|
||||
,cache_enabled: true
|
||||
,log_format: ':req[X-Real-IP] :method :req[Host]:url :status :response-time ms (:res[X-Tiler-Profiler]) -> :res[Content-Type] (:res[X-Tiler-Errors])'
|
||||
,log_format: ':req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler]) (:res[X-Tiler-Errors])'
|
||||
// If log_filename is given logs will be written
|
||||
// there, in append mode. Otherwise stdout is used (default).
|
||||
// Log file will be re-opened on receiving the HUP signal
|
||||
@@ -67,33 +100,25 @@ var config = {
|
||||
// Supported labels: 'user_id', 'user_password' (both read from redis)
|
||||
,postgres_auth_pass: '<%= user_password %>'
|
||||
,postgres: {
|
||||
// Parameters to pass to datasource plugin of mapnik
|
||||
// See http://github.com/mapnik/mapnik/wiki/PostGIS
|
||||
user: "publicuser",
|
||||
password: "public",
|
||||
host: '127.0.0.1',
|
||||
port: 6432,
|
||||
extent: "-20037508.3,-20037508.3,20037508.3,20037508.3",
|
||||
// max number of rows to return when querying data, 0 means no limit
|
||||
row_limit: 65535,
|
||||
simplify_geometries: true,
|
||||
use_overviews: true, // use overviews to retrieve raster
|
||||
/*
|
||||
* Set persist_connection to false if you want
|
||||
* database connections to be closed on renderer
|
||||
* expiration (1 minute after last use).
|
||||
* Setting to true (the default) would never
|
||||
* close any connection for the server's lifetime
|
||||
*/
|
||||
persist_connection: false,
|
||||
max_size: 500
|
||||
port: 5432,
|
||||
pool: {
|
||||
// maximum number of resources to create at any given time
|
||||
size: 16,
|
||||
// max milliseconds a resource can go unused before it should be destroyed
|
||||
idleTimeout: 3000,
|
||||
// frequency to check for idle resources
|
||||
reapInterval: 1000
|
||||
}
|
||||
}
|
||||
,mapnik_version: undefined
|
||||
,mapnik_tile_format: 'png8:m=h'
|
||||
,statsd: {
|
||||
host: 'localhost',
|
||||
port: 8125,
|
||||
prefix: 'stage.:host.',
|
||||
prefix: 'stage.:host.', // could be hostname, better not containing dots
|
||||
cacheDns: true
|
||||
// support all allowed node-statsd options
|
||||
}
|
||||
@@ -105,15 +130,7 @@ var config = {
|
||||
//If enabled, MVTs will be generated with PostGIS directly, instead of using Mapnik,
|
||||
//PostGIS 2.4 is required for this to work
|
||||
//If disabled it will use Mapnik MVT generation
|
||||
usePostGIS: false,
|
||||
dbPoolParams: {
|
||||
// maximum number of resources to create at any given time
|
||||
size: 16,
|
||||
// max milliseconds a resource can go unused before it should be destroyed
|
||||
idleTimeout: 3000,
|
||||
// frequency to check for idle resources
|
||||
reapInterval: 1000
|
||||
}
|
||||
usePostGIS: false
|
||||
},
|
||||
mapnik: {
|
||||
// The size of the pool of internal mapnik backend
|
||||
@@ -167,6 +184,30 @@ var config = {
|
||||
// It will only work if snapToGrid is enabled
|
||||
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
|
||||
|
||||
postgis: {
|
||||
// Parameters to pass to datasource plugin of mapnik
|
||||
// See http://github.com/mapnik/mapnik/wiki/PostGIS
|
||||
user: "publicuser",
|
||||
password: "public",
|
||||
host: '127.0.0.1',
|
||||
port: 5432,
|
||||
extent: "-20037508.3,-20037508.3,20037508.3,20037508.3",
|
||||
// max number of rows to return when querying data, 0 means no limit
|
||||
row_limit: 65535,
|
||||
/*
|
||||
* Set persist_connection to false if you want
|
||||
* database connections to be closed on renderer
|
||||
* expiration (1 minute after last use).
|
||||
* Setting to true (the default) would never
|
||||
* close any connection for the server's lifetime
|
||||
*/
|
||||
persist_connection: false,
|
||||
simplify_geometries: true,
|
||||
use_overviews: true, // use overviews to retrieve raster
|
||||
max_size: 500,
|
||||
twkb_encoding: true
|
||||
},
|
||||
|
||||
limits: {
|
||||
// Time in milliseconds a render request can take before it fails, some notes:
|
||||
// - 0 means no render limit
|
||||
@@ -180,26 +221,17 @@ var config = {
|
||||
cacheOnTimeout: true
|
||||
},
|
||||
|
||||
geojson: {
|
||||
dbPoolParams: {
|
||||
// maximum number of resources to create at any given time
|
||||
size: 16,
|
||||
// max milliseconds a resource can go unused before it should be destroyed
|
||||
idleTimeout: 3000,
|
||||
// frequency to check for idle resources
|
||||
reapInterval: 1000
|
||||
},
|
||||
// If enabled Mapnik will reuse the features retrieved from the database
|
||||
// instead of requesting them once per style inside a layer
|
||||
'cache-features': true,
|
||||
|
||||
// SQL queries will be wrapped with ST_ClipByBox2D
|
||||
// Returning the portion of a geometry falling within a rectangle
|
||||
// It will only work if snapToGrid is enabled
|
||||
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
|
||||
// geometries will be simplified using ST_RemoveRepeatedPoints
|
||||
// which cost is no more expensive than snapping and results are
|
||||
// much closer to the original geometry
|
||||
removeRepeatedPoints: false // this requires postgis >=2.2
|
||||
// Require metrics to the renderer
|
||||
metrics: false,
|
||||
|
||||
// Options for markers attributes, ellipses and images caches
|
||||
markers_symbolizer_caches: {
|
||||
disabled: false
|
||||
}
|
||||
|
||||
},
|
||||
http: {
|
||||
timeout: 2000, // the timeout in ms for a http tile request
|
||||
@@ -215,16 +247,7 @@ var config = {
|
||||
src: __dirname + '/../../assets/default-placeholder.png'
|
||||
}
|
||||
},
|
||||
torque: {
|
||||
dbPoolParams: {
|
||||
// maximum number of resources to create at any given time
|
||||
size: 16,
|
||||
// max milliseconds a resource can go unused before it should be destroyed
|
||||
idleTimeout: 3000,
|
||||
// frequency to check for idle resources
|
||||
reapInterval: 1000
|
||||
}
|
||||
}
|
||||
torque: {}
|
||||
}
|
||||
// anything analyses related
|
||||
,analysis: {
|
||||
@@ -245,7 +268,7 @@ var config = {
|
||||
// If filename is given logs comming from analysis client will be written
|
||||
// there, in append mode. Otherwise 'log_filename' is used. Otherwise stdout is used (default).
|
||||
// Log file will be re-opened on receiving the HUP signal
|
||||
filename: 'logs/analysis.log'
|
||||
filename: 'logs/node-windshaft-analysis.log'
|
||||
},
|
||||
// Define max execution time in ms for analyses or tags
|
||||
// If analysis or tag are not found in redis this values will be used as default.
|
||||
@@ -323,7 +346,7 @@ var config = {
|
||||
}
|
||||
// Settings for the health check available at /health
|
||||
,health: {
|
||||
enabled: false,
|
||||
enabled: true,
|
||||
username: 'localhost',
|
||||
z: 0,
|
||||
x: 0,
|
||||
@@ -338,7 +361,28 @@ var config = {
|
||||
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API
|
||||
cdbQueryTablesFromPostgres: true,
|
||||
// whether in mapconfig is available stats & metadata for each layer
|
||||
layerStats: true
|
||||
layerStats: true,
|
||||
// whether it should rate limit endpoints (global configuration)
|
||||
rateLimitsEnabled: false,
|
||||
// whether it should rate limit one or more endpoints (only if rateLimitsEnabled = true)
|
||||
rateLimitsByEndpoint: {
|
||||
anonymous: false,
|
||||
static: false,
|
||||
static_named: false,
|
||||
dataview: false,
|
||||
dataview_search: false,
|
||||
analysis: false,
|
||||
analysis_catalog: false,
|
||||
tile: false,
|
||||
attributes: false,
|
||||
named_list: false,
|
||||
named_create: false,
|
||||
named_get: false,
|
||||
named: false,
|
||||
named_update: false,
|
||||
named_delete: false,
|
||||
named_tiles: false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -16,28 +16,62 @@ var config = {
|
||||
// Base URLs for the APIs
|
||||
//
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/wiki/Unified-Map-API
|
||||
//
|
||||
// Base url for the Templated Maps API
|
||||
// "/api/v1/map/named" is the new API,
|
||||
// "/tiles/template" is for compatibility with versions up to 1.6.x
|
||||
,base_url_templated: '(?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)'
|
||||
// Base url for the Detached Maps API
|
||||
// "maps" is the the new API,
|
||||
// "tiles/layergroup" is for compatibility with versions up to 1.6.x
|
||||
,base_url_detached: '(?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)'
|
||||
,routes: {
|
||||
v1: {
|
||||
paths: [
|
||||
'/api/v1',
|
||||
'/user/:user/api/v1',
|
||||
],
|
||||
// Base url for the Detached Maps API
|
||||
// "/api/v1/map" is the new API,
|
||||
map: {
|
||||
paths: [
|
||||
'/map',
|
||||
]
|
||||
},
|
||||
// Base url for the Templated Maps API
|
||||
// "/api/v1/map/named" is the new API,
|
||||
template: {
|
||||
paths: [
|
||||
'/map/named'
|
||||
]
|
||||
}
|
||||
},
|
||||
// For compatibility with versions up to 1.6.x
|
||||
v0: {
|
||||
paths: [
|
||||
'/tiles'
|
||||
],
|
||||
// Base url for the Detached Maps API
|
||||
// "/tiles/layergroup" is for compatibility with versions up to 1.6.x
|
||||
map: {
|
||||
paths: [
|
||||
'/layergroup'
|
||||
]
|
||||
},
|
||||
// Base url for the Templated Maps API
|
||||
// "/tiles/template" is for compatibility with versions up to 1.6.x
|
||||
template: {
|
||||
paths: [
|
||||
'/template'
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Resource URLs expose endpoints to request/retrieve metadata associated to Maps: dataviews, analysis node status.
|
||||
//
|
||||
// This URLs depend on how `base_url_detached` and `user_from_host` are configured: the application can be
|
||||
// This URLs depend on how `routes` and `user_from_host` are configured: the application can be
|
||||
// configured to accept request with the {user} in the header host or in the request path.
|
||||
// It also might depend on the configured cdn_url via `serverMetadata.cdn_url`.
|
||||
//
|
||||
// This template allows to make the endpoints generation more flexible, the template exposes the following params:
|
||||
// 1. {{=it.cdn_url}}: will be used when `serverMetadata.cdn_url` exists.
|
||||
// 2. {{=it.user}}: will use the username as extraced from `user_from_host` or `base_url_detached`.
|
||||
// 2. {{=it.user}}: will use the username as extraced from `user_from_host` or `routes`.
|
||||
// 3. {{=it.port}}: will use the `port` from this very same configuration file.
|
||||
,resources_url_templates: {
|
||||
http: 'http://{{=it.user}}.localhost.lan:{{=it.port}}/api/v1/map'
|
||||
http: 'http://{{=it.user}}.localhost.lan:{{=it.port}}/api/v1/map',
|
||||
https: 'https://{{=it.user}}.localhost.lan:{{=it.port}}/api/v1/map'
|
||||
}
|
||||
|
||||
// Maximum number of connections for one process
|
||||
@@ -54,11 +88,11 @@ var config = {
|
||||
,socket_timeout: 600000
|
||||
,enable_cors: true
|
||||
,cache_enabled: false
|
||||
,log_format: '[:date] :req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler]) (:res[X-Tiler-Errors])'
|
||||
,log_format: ':req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler]) (:res[X-Tiler-Errors])'
|
||||
// If log_filename is given logs will be written
|
||||
// there, in append mode. Otherwise stdout is used (default).
|
||||
// Log file will be re-opened on receiving the HUP signal
|
||||
//,log_filename: 'logs/node-windshaft.log'
|
||||
,log_filename: '/tmp/node-windshaft.log'
|
||||
// Templated database username for authorized user
|
||||
// Supported labels: 'user_id' (read from redis)
|
||||
,postgres_auth_user: 'test_windshaft_cartodb_user_<%= user_id %>'
|
||||
@@ -66,33 +100,25 @@ var config = {
|
||||
// Supported labels: 'user_id', 'user_password' (both read from redis)
|
||||
,postgres_auth_pass: 'test_windshaft_cartodb_user_<%= user_id %>_pass'
|
||||
,postgres: {
|
||||
// Parameters to pass to datasource plugin of mapnik
|
||||
// See http://github.com/mapnik/mapnik/wiki/PostGIS
|
||||
user: "test_windshaft_publicuser",
|
||||
password: "public",
|
||||
host: '127.0.0.1',
|
||||
port: 5432,
|
||||
extent: "-20037508.3,-20037508.3,20037508.3,20037508.3",
|
||||
// max number of rows to return when querying data, 0 means no limit
|
||||
row_limit: 65535,
|
||||
simplify_geometries: true,
|
||||
use_overviews: true, // use overviews to retrieve raster
|
||||
/*
|
||||
* Set persist_connection to false if you want
|
||||
* database connections to be closed on renderer
|
||||
* expiration (1 minute after last use).
|
||||
* Setting to true (the default) would never
|
||||
* close any connection for the server's lifetime
|
||||
*/
|
||||
persist_connection: false,
|
||||
max_size: 500
|
||||
pool: {
|
||||
// maximum number of resources to create at any given time
|
||||
size: 16,
|
||||
// max milliseconds a resource can go unused before it should be destroyed
|
||||
idleTimeout: 3000,
|
||||
// frequency to check for idle resources
|
||||
reapInterval: 1000
|
||||
}
|
||||
}
|
||||
,mapnik_version: ''
|
||||
,mapnik_version: undefined
|
||||
,mapnik_tile_format: 'png8:m=h'
|
||||
,statsd: {
|
||||
host: 'localhost',
|
||||
port: 8125,
|
||||
prefix: 'test.:host.',
|
||||
prefix: 'test.:host.', // could be hostname, better not containing dots
|
||||
cacheDns: true
|
||||
// support all allowed node-statsd options
|
||||
}
|
||||
@@ -104,15 +130,7 @@ var config = {
|
||||
//If enabled, MVTs will be generated with PostGIS directly, instead of using Mapnik,
|
||||
//PostGIS 2.4 is required for this to work
|
||||
//If disabled it will use Mapnik MVT generation
|
||||
usePostGIS: false,
|
||||
dbPoolParams: {
|
||||
// maximum number of resources to create at any given time
|
||||
size: 16,
|
||||
// max milliseconds a resource can go unused before it should be destroyed
|
||||
idleTimeout: 3000,
|
||||
// frequency to check for idle resources
|
||||
reapInterval: 1000
|
||||
}
|
||||
usePostGIS: false
|
||||
},
|
||||
mapnik: {
|
||||
// The size of the pool of internal mapnik backend
|
||||
@@ -166,6 +184,30 @@ var config = {
|
||||
// It will only work if snapToGrid is enabled
|
||||
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
|
||||
|
||||
postgis: {
|
||||
// Parameters to pass to datasource plugin of mapnik
|
||||
// See http://github.com/mapnik/mapnik/wiki/PostGIS
|
||||
user: "publicuser",
|
||||
password: "public",
|
||||
host: '127.0.0.1',
|
||||
port: 5432,
|
||||
extent: "-20037508.3,-20037508.3,20037508.3,20037508.3",
|
||||
// max number of rows to return when querying data, 0 means no limit
|
||||
row_limit: 65535,
|
||||
/*
|
||||
* Set persist_connection to false if you want
|
||||
* database connections to be closed on renderer
|
||||
* expiration (1 minute after last use).
|
||||
* Setting to true (the default) would never
|
||||
* close any connection for the server's lifetime
|
||||
*/
|
||||
persist_connection: false,
|
||||
simplify_geometries: true,
|
||||
use_overviews: true, // use overviews to retrieve raster
|
||||
max_size: 500,
|
||||
twkb_encoding: false
|
||||
},
|
||||
|
||||
limits: {
|
||||
// Time in milliseconds a render request can take before it fails, some notes:
|
||||
// - 0 means no render limit
|
||||
@@ -179,24 +221,16 @@ var config = {
|
||||
cacheOnTimeout: true
|
||||
},
|
||||
|
||||
geojson: {
|
||||
dbPoolParams: {
|
||||
// maximum number of resources to create at any given time
|
||||
size: 16,
|
||||
// max milliseconds a resource can go unused before it should be destroyed
|
||||
idleTimeout: 3000,
|
||||
// frequency to check for idle resources
|
||||
reapInterval: 1000
|
||||
},
|
||||
// If enabled Mapnik will reuse the features retrieved from the database
|
||||
// instead of requesting them once per style inside a layer
|
||||
'cache-features': false,
|
||||
|
||||
// SQL queries will be wrapped with ST_ClipByBox2D
|
||||
// Returning the portion of a geometry falling within a rectangle
|
||||
// It will only work if snapToGrid is enabled
|
||||
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
|
||||
// geometries will be simplified using ST_RemoveRepeatedPoints
|
||||
// which cost is no more expensive than snapping and results are
|
||||
// much closer to the original geometry
|
||||
removeRepeatedPoints: false // this requires postgis >=2.2
|
||||
// Require metrics to the renderer
|
||||
metrics: false,
|
||||
|
||||
// Options for markers attributes, ellipses and images caches
|
||||
markers_symbolizer_caches: {
|
||||
disabled: false
|
||||
}
|
||||
},
|
||||
http: {
|
||||
@@ -215,16 +249,7 @@ var config = {
|
||||
src: __dirname + '/../../assets/default-placeholder.png'
|
||||
}
|
||||
},
|
||||
torque: {
|
||||
dbPoolParams: {
|
||||
// maximum number of resources to create at any given time
|
||||
size: 16,
|
||||
// max milliseconds a resource can go unused before it should be destroyed
|
||||
idleTimeout: 3000,
|
||||
// frequency to check for idle resources
|
||||
reapInterval: 1000
|
||||
}
|
||||
}
|
||||
torque: {}
|
||||
}
|
||||
// anything analyses related
|
||||
,analysis: {
|
||||
@@ -245,7 +270,7 @@ var config = {
|
||||
// If filename is given logs comming from analysis client will be written
|
||||
// there, in append mode. Otherwise 'log_filename' is used. Otherwise stdout is used (default).
|
||||
// Log file will be re-opened on receiving the HUP signal
|
||||
filename: 'node-windshaft.log'
|
||||
filename: '/tmp/node-windshaft-analysis.log'
|
||||
},
|
||||
// Define max execution time in ms for analyses or tags
|
||||
// If analysis or tag are not found in redis this values will be used as default.
|
||||
@@ -315,6 +340,12 @@ var config = {
|
||||
// X-Tiler-Profile header containing elapsed timing for various
|
||||
// steps taken for producing the response.
|
||||
,useProfiler:true
|
||||
,serverMetadata: {
|
||||
cdn_url: {
|
||||
http: undefined,
|
||||
https: undefined
|
||||
}
|
||||
}
|
||||
// Settings for the health check available at /health
|
||||
,health: {
|
||||
enabled: false,
|
||||
@@ -332,7 +363,28 @@ var config = {
|
||||
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API
|
||||
cdbQueryTablesFromPostgres: true,
|
||||
// whether in mapconfig is available stats & metadata for each layer
|
||||
layerStats: true
|
||||
layerStats: true,
|
||||
// whether it should rate limit endpoints (global configuration)
|
||||
rateLimitsEnabled: false,
|
||||
// whether it should rate limit one or more endpoints (only if rateLimitsEnabled = true)
|
||||
rateLimitsByEndpoint: {
|
||||
anonymous: false,
|
||||
static: false,
|
||||
static_named: false,
|
||||
dataview: false,
|
||||
dataview_search: false,
|
||||
analysis: false,
|
||||
analysis_catalog: false,
|
||||
tile: false,
|
||||
attributes: false,
|
||||
named_list: false,
|
||||
named_create: false,
|
||||
named_get: false,
|
||||
named: false,
|
||||
named_update: false,
|
||||
named_delete: false,
|
||||
named_tiles: false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
export NPROCS=1 && export JOBS=1 && export CXX=g++-4.9 && export PGUSER=postgres
|
||||
|
||||
npm install -g yarn@0.27.5
|
||||
yarn
|
||||
|
||||
/etc/init.d/postgresql start
|
||||
|
||||
createdb template_postgis && createuser publicuser
|
||||
psql -c "CREATE EXTENSION postgis" template_postgis
|
||||
|
||||
POSTGIS_VERSION=2.4 npm test
|
||||
89
docker/Dockerfile-nodejs6-xenial-pg101
Normal file
89
docker/Dockerfile-nodejs6-xenial-pg101
Normal file
@@ -0,0 +1,89 @@
|
||||
FROM ubuntu:xenial
|
||||
|
||||
# Use UTF8 to avoid encoding problems with pgsql
|
||||
ENV LANG C.UTF-8
|
||||
ENV NPROCS 1
|
||||
ENV JOBS 1
|
||||
ENV CXX g++-4.9
|
||||
ENV PGUSER postgres
|
||||
|
||||
# Add external repos
|
||||
RUN set -ex \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y \
|
||||
curl \
|
||||
software-properties-common \
|
||||
locales \
|
||||
&& add-apt-repository -y ppa:ubuntu-toolchain-r/test \
|
||||
&& add-apt-repository -y ppa:cartodb/postgresql-10 \
|
||||
&& add-apt-repository -y ppa:cartodb/gis \
|
||||
&& curl -sL https://deb.nodesource.com/setup_6.x | bash \
|
||||
&& locale-gen en_US.UTF-8 \
|
||||
&& update-locale LANG=en_US.UTF-8
|
||||
|
||||
# Install dependencies and PostGIS 2.4 from sources
|
||||
RUN set -ex \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y \
|
||||
g++-4.9 \
|
||||
gcc-4.9 \
|
||||
git \
|
||||
libcairo2-dev \
|
||||
libgdal-dev \
|
||||
libgdal1i \
|
||||
libgdal20 \
|
||||
libgeos-dev \
|
||||
libgif-dev \
|
||||
libjpeg8-dev \
|
||||
libjson-c-dev \
|
||||
libpango1.0-dev \
|
||||
libpixman-1-dev \
|
||||
libproj-dev \
|
||||
libprotobuf-c-dev \
|
||||
libxml2-dev \
|
||||
gdal-bin \
|
||||
make \
|
||||
nodejs \
|
||||
protobuf-c-compiler \
|
||||
pkg-config \
|
||||
wget \
|
||||
zip \
|
||||
postgresql-10 \
|
||||
postgresql-10-plproxy \
|
||||
postgresql-10-postgis-2.4 \
|
||||
postgresql-10-postgis-2.4-scripts \
|
||||
postgresql-10-postgis-scripts \
|
||||
postgresql-client-10 \
|
||||
postgresql-client-common \
|
||||
postgresql-common \
|
||||
postgresql-contrib \
|
||||
postgresql-plpython-10 \
|
||||
postgresql-server-dev-10 \
|
||||
postgis \
|
||||
&& wget http://download.redis.io/releases/redis-4.0.8.tar.gz \
|
||||
&& tar xvzf redis-4.0.8.tar.gz \
|
||||
&& cd redis-4.0.8 \
|
||||
&& make \
|
||||
&& make install \
|
||||
&& cd .. \
|
||||
&& rm redis-4.0.8.tar.gz \
|
||||
&& rm -R redis-4.0.8 \
|
||||
&& apt-get purge -y wget protobuf-c-compiler \
|
||||
&& apt-get autoremove -y
|
||||
|
||||
# Configure PostgreSQL
|
||||
RUN set -ex \
|
||||
&& echo "listen_addresses='*'" >> /etc/postgresql/10/main/postgresql.conf \
|
||||
&& echo "local all all trust" > /etc/postgresql/10/main/pg_hba.conf \
|
||||
&& echo "host all all 0.0.0.0/0 trust" >> /etc/postgresql/10/main/pg_hba.conf \
|
||||
&& echo "host all all ::1/128 trust" >> /etc/postgresql/10/main/pg_hba.conf \
|
||||
&& /etc/init.d/postgresql start \
|
||||
&& createdb template_postgis \
|
||||
&& createuser publicuser \
|
||||
&& psql -c "CREATE EXTENSION postgis" template_postgis \
|
||||
&& /etc/init.d/postgresql stop
|
||||
|
||||
WORKDIR /srv
|
||||
EXPOSE 5858
|
||||
|
||||
CMD /etc/init.d/postgresql start
|
||||
23
docker/reference.md
Normal file
23
docker/reference.md
Normal file
@@ -0,0 +1,23 @@
|
||||
After running the tests with docker, you will need Docker installed and the docker image downloaded.
|
||||
|
||||
## Install docker
|
||||
`sudo apt install docker.io && sudo usermod -aG docker $(whoami)`
|
||||
|
||||
## Download image
|
||||
`docker pull carto/IMAGE`
|
||||
|
||||
## Carto account
|
||||
https://hub.docker.com/r/carto/
|
||||
|
||||
## Update image
|
||||
- Edit the docker image file with your desired changes
|
||||
- Build image:
|
||||
- `docker build -t carto/IMAGE -f docker/DOCKER_FILE docker/`
|
||||
|
||||
- Upload to docker hub:
|
||||
- Login into docker hub:
|
||||
- `docker login`
|
||||
- Create tag:
|
||||
- `docker tag carto/IMAGE carto/IMAGE`
|
||||
- Upload:
|
||||
- `docker push carto/IMAGE`
|
||||
@@ -29,7 +29,7 @@ The value of this attribute can be `false` to explicitly disable aggregation for
|
||||
// object, defines the columns of the aggregated datasets. Each property corresponds to a columns name and
|
||||
// should contain an object with two properties: "aggregate_function" (one of "sum", "max", "min", "avg", "mode" or "count"),
|
||||
// and "aggregated_column" (the name of a column of the original layer query or "*")
|
||||
// A column defined as `"_cdb_features_count": {"aggregate_function": "count", aggregated_column: "*"}`
|
||||
// A column defined as `"_cdb_feature_count": {"aggregate_function": "count", aggregated_column: "*"}`
|
||||
// is always generated in addition to the defined columns.
|
||||
// The column names `cartodb_id`, `the_geom`, `the_geom_webmercator` and `_cdb_feature_count` cannot be used
|
||||
// for aggregated columns, as they correspond to columns always present in the result.
|
||||
|
||||
@@ -10,7 +10,7 @@ Aggregation is available only for point geometries. During aggregation the point
|
||||
|
||||
When no placement or columns are specified a special default aggregation is performed.
|
||||
|
||||
This special mode performs only spatial aggregation (using a grid defined by the requested tile and the resolution, parameter, as all the other cases), and returns a _random_ record from each group (grid cell) with all its columns and an additional `_cdb_features_count` with the number of features in the group.
|
||||
This special mode performs only spatial aggregation (using a grid defined by the requested tile and the resolution, parameter, as all the other cases), and returns a _random_ record from each group (grid cell) with all its columns and an additional `_cdb_feature_count` with the number of features in the group.
|
||||
|
||||
Regarding the randomness of the sample: currently we use the row with the minimum `cartodb_id` value in each group.
|
||||
|
||||
@@ -18,7 +18,7 @@ The rationale behind having this special aggregation with all the original colum
|
||||
|
||||
### User defined aggregations
|
||||
|
||||
When either a explicit placement or columns are requested we no longer use the special, query; we use one determined by the placement (which will default to "centroid"), and it will have as columns only the aggregated columns specified, in addition to `_cdb_features_count`, which is always present.
|
||||
When either a explicit placement or columns are requested we no longer use the special, query; we use one determined by the placement (which will default to "centroid"), and it will have as columns only the aggregated columns specified, in addition to `_cdb_feature_count`, which is always present.
|
||||
|
||||
We might decide in the future to allow sampling column values for any of the different placement modes.
|
||||
|
||||
@@ -185,3 +185,80 @@ This is the minimum number of (estimated) rows in the dataset (query results) fo
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `filters`
|
||||
|
||||
Aggregated data can be filtered by imposing filtering conditions on the aggregated columns.
|
||||
|
||||
Each condition is represented by one or more parameters:
|
||||
|
||||
* `{ "equal": V }` selects an specific value of the aggregated column.
|
||||
* `{ "not_equal": V }` selects values different from the one specified.
|
||||
* `{ "in": [v1, v2, v3] }` selects any value from a list.
|
||||
* `{ "not_in": [v1, v2, v3] }` selects any value not in a list.
|
||||
* `{ "less_than": v }` selects values strictly less than the one given.
|
||||
* `{ "less_than_or_equal_to": v }` selects values less than or equal to the one given.
|
||||
* `{ "greater_than": v }` selects values strictly greater than the one given.
|
||||
* `{ "greater_than_or_equal_to": v }` selects values greater than or equal to the one given.
|
||||
|
||||
One of the *less* conditions can be combined with one of the *greater* conditions to select a range of values, for example:
|
||||
* `{ "greater_than": v1, "less_than": v2 }`
|
||||
* `{ "greater_than_or_equal_to": v1, "less_than": v2 }`
|
||||
* `{ "greater_than": v1, "less_than_or_equal_to": v2 }`
|
||||
* `{ "greater_than_or_equal_to": v1, "less_than_or_equal_to": v2 }`
|
||||
|
||||
For a given column, multiple conditions can be passed in an array; the conditions will logically ORed (any of the conditions have to be verifid for the value to be selected):
|
||||
|
||||
* `"myvalue": [ { "equal": 10 }, { "less_than": 0 }]` will select values of the column `myvalue` which are equal to 10 **or** less than 0.
|
||||
|
||||
In addition, the filters applied to different columns are logically combined with AND (all the conditions have to be satisfied for an element to be selected); for example with the following `filters` parameter we'll select aggregated records which have a `total_value` > 100 **and** a category equal to "a".
|
||||
|
||||
```json
|
||||
{
|
||||
"total_value": { "greater_than": 100 },
|
||||
"category": { "equal": "a" }
|
||||
}
|
||||
```
|
||||
|
||||
Note that the filtered columns have to be defined with the `columns` parameter, except for `_cdb_feature_count`, which is always implicitly defined and can be filtered too.
|
||||
|
||||
#### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "1.7.0",
|
||||
"extent": [-20037508.5, -20037508.5, 20037508.5, 20037508.5],
|
||||
"srid": 3857,
|
||||
"maxzoom": 18,
|
||||
"minzoom": 3,
|
||||
"layers": [
|
||||
{
|
||||
"type": "mapnik",
|
||||
"options": {
|
||||
"sql": "select * from table",
|
||||
"cartocss": "#table { marker-width: [total]; marker-fill: ramp(value, (red, green, blue), jenks); }",
|
||||
"cartocss_version": "2.3.0",
|
||||
"aggregation": {
|
||||
"placement": "centroid",
|
||||
"columns": {
|
||||
"total_value": {
|
||||
"aggregate_function": "sum",
|
||||
"aggregated_column": "value"
|
||||
},
|
||||
"category": {
|
||||
"aggregate_function": "mode",
|
||||
"aggregated_column": "category"
|
||||
}
|
||||
},
|
||||
"filters" : {
|
||||
"total_value": { "greater_than": 100 },
|
||||
"category": { "equal": "a" }
|
||||
},
|
||||
"resolution": 2,
|
||||
"threshold": 500000
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -11,7 +11,7 @@ Begin by instantiating either a Named or Anonymous Map using the `layergroupid t
|
||||
#### Definition
|
||||
|
||||
```bash
|
||||
GET /api/v1/map/static/center/{token}/{z}/{lat}/{lng}/{width}/{height}.{format}
|
||||
GET /api/v1/map/static/center/{token}/{z}/{lat}/{lng}/{width}/{height}.{format}{{?}extra_options}
|
||||
```
|
||||
|
||||
#### Params
|
||||
@@ -58,6 +58,9 @@ Note: you can see this endpoint as
|
||||
GET /api/v1/map/static/bbox/{token}/{west},{south},{east},{north}/{width}/{height}.{format}`
|
||||
```
|
||||
|
||||
#### Extra options
|
||||
* Layer: List of layers to be shown in the image (by default `all`), for example `?layer=0,1`.
|
||||
|
||||
### Named Map
|
||||
|
||||
#### Definition
|
||||
|
||||
321
lib/cartodb/api/api-router.js
Normal file
321
lib/cartodb/api/api-router.js
Normal file
@@ -0,0 +1,321 @@
|
||||
const { Router: router } = require('express');
|
||||
|
||||
const RedisPool = require('redis-mpool');
|
||||
const cartodbRedis = require('cartodb-redis');
|
||||
|
||||
const windshaft = require('windshaft');
|
||||
|
||||
const PgConnection = require('../backends/pg_connection');
|
||||
const AnalysisBackend = require('../backends/analysis');
|
||||
const AnalysisStatusBackend = require('../backends/analysis-status');
|
||||
const DataviewBackend = require('../backends/dataview');
|
||||
const TemplateMaps = require('../backends/template_maps.js');
|
||||
const PgQueryRunner = require('../backends/pg_query_runner');
|
||||
const StatsBackend = require('../backends/stats');
|
||||
const AuthBackend = require('../backends/auth');
|
||||
|
||||
const UserLimitsBackend = require('../backends/user-limits');
|
||||
const OverviewsMetadataBackend = require('../backends/overviews-metadata');
|
||||
const FilterStatsApi = require('../backends/filter-stats');
|
||||
const TablesExtentBackend = require('../backends/tables-extent');
|
||||
|
||||
const LayergroupAffectedTablesCache = require('../cache/layergroup_affected_tables');
|
||||
const SurrogateKeysCache = require('../cache/surrogate_keys_cache');
|
||||
const VarnishHttpCacheBackend = require('../cache/backend/varnish_http');
|
||||
const FastlyCacheBackend = require('../cache/backend/fastly');
|
||||
const NamedMapProviderCache = require('../cache/named_map_provider_cache');
|
||||
const NamedMapsCacheEntry = require('../cache/model/named_maps_entry');
|
||||
|
||||
const SqlWrapMapConfigAdapter = require('../models/mapconfig/adapter/sql-wrap-mapconfig-adapter');
|
||||
const MapConfigNamedLayersAdapter = require('../models/mapconfig/adapter/mapconfig-named-layers-adapter');
|
||||
const MapConfigBufferSizeAdapter = require('../models/mapconfig/adapter/mapconfig-buffer-size-adapter');
|
||||
const AnalysisMapConfigAdapter = require('../models/mapconfig/adapter/analysis-mapconfig-adapter');
|
||||
const MapConfigOverviewsAdapter = require('../models/mapconfig/adapter/mapconfig-overviews-adapter');
|
||||
const TurboCartoAdapter = require('../models/mapconfig/adapter/turbo-carto-adapter');
|
||||
const DataviewsWidgetsAdapter = require('../models/mapconfig/adapter/dataviews-widgets-adapter');
|
||||
const AggregationMapConfigAdapter = require('../models/mapconfig/adapter/aggregation-mapconfig-adapter');
|
||||
const MapConfigAdapter = require('../models/mapconfig/adapter');
|
||||
const VectorMapConfigAdapter = require('../models/mapconfig/adapter/vector-mapconfig-adapter');
|
||||
|
||||
const ResourceLocator = require('../models/resource-locator');
|
||||
const LayergroupMetadata = require('../utils/layergroup-metadata');
|
||||
const RendererStatsReporter = require('../stats/reporter/renderer');
|
||||
|
||||
const initializeStatusCode = require('./middlewares/initialize-status-code');
|
||||
const logger = require('./middlewares/logger');
|
||||
const bodyParser = require('body-parser');
|
||||
const servedByHostHeader = require('./middlewares/served-by-host-header');
|
||||
const stats = require('./middlewares/stats');
|
||||
const lzmaMiddleware = require('./middlewares/lzma');
|
||||
const cors = require('./middlewares/cors');
|
||||
const user = require('./middlewares/user');
|
||||
const sendResponse = require('./middlewares/send-response');
|
||||
const syntaxError = require('./middlewares/syntax-error');
|
||||
const errorMiddleware = require('./middlewares/error-middleware');
|
||||
|
||||
const MapRouter = require('./map/map-router');
|
||||
const TemplateRouter = require('./template/template-router');
|
||||
|
||||
module.exports = class ApiRouter {
|
||||
constructor ({ serverOptions, environmentOptions }) {
|
||||
this.serverOptions = serverOptions;
|
||||
|
||||
const redisOptions = Object.assign({
|
||||
name: 'windshaft-server',
|
||||
unwatchOnRelease: false,
|
||||
noReadyCheck: true
|
||||
}, environmentOptions.redis);
|
||||
|
||||
const redisPool = new RedisPool(redisOptions);
|
||||
|
||||
redisPool.on('status', function (status) {
|
||||
var keyPrefix = 'windshaft.redis-pool.' + status.name + '.db' + status.db + '.';
|
||||
global.statsClient.gauge(keyPrefix + 'count', status.count);
|
||||
global.statsClient.gauge(keyPrefix + 'unused', status.unused);
|
||||
global.statsClient.gauge(keyPrefix + 'waiting', status.waiting);
|
||||
});
|
||||
|
||||
const metadataBackend = cartodbRedis({ pool: redisPool });
|
||||
const pgConnection = new PgConnection(metadataBackend);
|
||||
|
||||
const mapStore = new windshaft.storage.MapStore({
|
||||
pool: redisPool,
|
||||
expire_time: serverOptions.grainstore.default_layergroup_ttl
|
||||
});
|
||||
|
||||
const rendererFactory = createRendererFactory({ redisPool, serverOptions, environmentOptions });
|
||||
|
||||
const rendererCacheOpts = Object.assign({
|
||||
ttl: 60000, // 60 seconds TTL by default
|
||||
statsInterval: 60000 // reports stats every milliseconds defined here
|
||||
}, serverOptions.renderCache || {});
|
||||
|
||||
const rendererCache = new windshaft.cache.RendererCache(rendererFactory, rendererCacheOpts);
|
||||
const rendererStatsReporter = new RendererStatsReporter(rendererCache, rendererCacheOpts.statsInterval);
|
||||
rendererStatsReporter.start();
|
||||
|
||||
const tileBackend = new windshaft.backend.Tile(rendererCache);
|
||||
const attributesBackend = new windshaft.backend.Attributes();
|
||||
const previewBackend = new windshaft.backend.Preview(rendererCache);
|
||||
const mapValidatorBackend = new windshaft.backend.MapValidator(tileBackend, attributesBackend);
|
||||
const mapBackend = new windshaft.backend.Map(rendererCache, mapStore, mapValidatorBackend);
|
||||
|
||||
const surrogateKeysCacheBackends = createSurrogateKeysCacheBackends(serverOptions);
|
||||
const surrogateKeysCache = new SurrogateKeysCache(surrogateKeysCacheBackends);
|
||||
const templateMaps = createTemplateMaps({ redisPool, surrogateKeysCache });
|
||||
|
||||
const analysisStatusBackend = new AnalysisStatusBackend();
|
||||
const analysisBackend = new AnalysisBackend(metadataBackend, serverOptions.analysis);
|
||||
const dataviewBackend = new DataviewBackend(analysisBackend);
|
||||
const statsBackend = new StatsBackend();
|
||||
|
||||
const userLimitsBackend = new UserLimitsBackend(metadataBackend, {
|
||||
limits: {
|
||||
cacheOnTimeout: serverOptions.renderer.mapnik.limits.cacheOnTimeout || false,
|
||||
render: serverOptions.renderer.mapnik.limits.render || 0,
|
||||
rateLimitsEnabled: global.environment.enabledFeatures.rateLimitsEnabled
|
||||
}
|
||||
});
|
||||
const authBackend = new AuthBackend(pgConnection, metadataBackend, mapStore, templateMaps);
|
||||
|
||||
const layergroupAffectedTablesCache = new LayergroupAffectedTablesCache();
|
||||
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
this.layergroupAffectedTablesCache = layergroupAffectedTablesCache;
|
||||
}
|
||||
|
||||
const pgQueryRunner = new PgQueryRunner(pgConnection);
|
||||
const overviewsMetadataBackend = new OverviewsMetadataBackend(pgQueryRunner);
|
||||
|
||||
const filterStatsBackend = new FilterStatsApi(pgQueryRunner);
|
||||
const tablesExtentBackend = new TablesExtentBackend(pgQueryRunner);
|
||||
|
||||
const mapConfigAdapter = new MapConfigAdapter(
|
||||
new MapConfigNamedLayersAdapter(templateMaps, pgConnection),
|
||||
new MapConfigBufferSizeAdapter(),
|
||||
new SqlWrapMapConfigAdapter(),
|
||||
new DataviewsWidgetsAdapter(),
|
||||
new AnalysisMapConfigAdapter(analysisBackend),
|
||||
new VectorMapConfigAdapter(pgConnection),
|
||||
new AggregationMapConfigAdapter(pgConnection),
|
||||
new MapConfigOverviewsAdapter(overviewsMetadataBackend, filterStatsBackend),
|
||||
new TurboCartoAdapter()
|
||||
);
|
||||
|
||||
const resourceLocator = new ResourceLocator(global.environment);
|
||||
const layergroupMetadata = new LayergroupMetadata(resourceLocator);
|
||||
|
||||
const namedMapProviderCache = new NamedMapProviderCache(
|
||||
templateMaps,
|
||||
pgConnection,
|
||||
metadataBackend,
|
||||
userLimitsBackend,
|
||||
mapConfigAdapter,
|
||||
layergroupAffectedTablesCache
|
||||
);
|
||||
|
||||
['update', 'delete'].forEach(function(eventType) {
|
||||
templateMaps.on(eventType, namedMapProviderCache.invalidate.bind(namedMapProviderCache));
|
||||
});
|
||||
|
||||
const collaborators = {
|
||||
analysisStatusBackend,
|
||||
attributesBackend,
|
||||
dataviewBackend,
|
||||
previewBackend,
|
||||
tileBackend,
|
||||
pgConnection,
|
||||
mapStore,
|
||||
userLimitsBackend,
|
||||
layergroupAffectedTablesCache,
|
||||
authBackend,
|
||||
surrogateKeysCache,
|
||||
templateMaps,
|
||||
mapBackend,
|
||||
metadataBackend,
|
||||
mapConfigAdapter,
|
||||
statsBackend,
|
||||
layergroupMetadata,
|
||||
namedMapProviderCache,
|
||||
tablesExtentBackend
|
||||
};
|
||||
|
||||
this.mapRouter = new MapRouter({ collaborators });
|
||||
this.templateRouter = new TemplateRouter({ collaborators });
|
||||
}
|
||||
|
||||
register (app) {
|
||||
// FIXME: we need a better way to reset cache while running tests
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
app.layergroupAffectedTablesCache = this.layergroupAffectedTablesCache;
|
||||
}
|
||||
|
||||
Object.keys(this.serverOptions.routes).forEach(apiVersion => {
|
||||
const routes = this.serverOptions.routes[apiVersion];
|
||||
|
||||
const apiRouter = router({ mergeParams: true });
|
||||
|
||||
apiRouter.use(logger(this.serverOptions));
|
||||
apiRouter.use(initializeStatusCode());
|
||||
apiRouter.use(bodyParser.json());
|
||||
apiRouter.use(servedByHostHeader());
|
||||
apiRouter.use(stats({
|
||||
enabled: this.serverOptions.useProfiler,
|
||||
statsClient: global.statsClient
|
||||
}));
|
||||
apiRouter.use(lzmaMiddleware());
|
||||
apiRouter.use(cors());
|
||||
apiRouter.use(user());
|
||||
|
||||
this.templateRouter.register(apiRouter, routes.template.paths);
|
||||
this.mapRouter.register(apiRouter, routes.map.paths);
|
||||
|
||||
apiRouter.use(sendResponse());
|
||||
apiRouter.use(syntaxError());
|
||||
apiRouter.use(errorMiddleware());
|
||||
|
||||
const apiPaths = routes.paths;
|
||||
|
||||
apiPaths.forEach(path => app.use(path, apiRouter));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
function createTemplateMaps ({ redisPool, surrogateKeysCache }) {
|
||||
const templateMaps = new TemplateMaps(redisPool, {
|
||||
max_user_templates: global.environment.maxUserTemplates
|
||||
});
|
||||
|
||||
function invalidateNamedMap (owner, templateName) {
|
||||
var startTime = Date.now();
|
||||
surrogateKeysCache.invalidate(new NamedMapsCacheEntry(owner, templateName), function(err) {
|
||||
var logMessage = JSON.stringify({
|
||||
username: owner,
|
||||
type: 'named_map_invalidation',
|
||||
elapsed: Date.now() - startTime,
|
||||
error: !!err ? JSON.stringify(err.message) : undefined
|
||||
});
|
||||
if (err) {
|
||||
global.logger.warn(logMessage);
|
||||
} else {
|
||||
global.logger.info(logMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
['update', 'delete'].forEach(function(eventType) {
|
||||
templateMaps.on(eventType, invalidateNamedMap);
|
||||
});
|
||||
|
||||
return templateMaps;
|
||||
}
|
||||
|
||||
function createSurrogateKeysCacheBackends(serverOptions) {
|
||||
var cacheBackends = [];
|
||||
|
||||
if (serverOptions.varnish_purge_enabled) {
|
||||
cacheBackends.push(
|
||||
new VarnishHttpCacheBackend(serverOptions.varnish_host, serverOptions.varnish_http_port)
|
||||
);
|
||||
}
|
||||
|
||||
if (serverOptions.fastly &&
|
||||
!!serverOptions.fastly.enabled && !!serverOptions.fastly.apiKey && !!serverOptions.fastly.serviceId) {
|
||||
cacheBackends.push(
|
||||
new FastlyCacheBackend(serverOptions.fastly.apiKey, serverOptions.fastly.serviceId)
|
||||
);
|
||||
}
|
||||
|
||||
return cacheBackends;
|
||||
}
|
||||
|
||||
const timeoutErrorTilePath = __dirname + '/../../../assets/render-timeout-fallback.png';
|
||||
const timeoutErrorTile = require('fs').readFileSync(timeoutErrorTilePath, {encoding: null});
|
||||
|
||||
function createRendererFactory ({ redisPool, serverOptions, environmentOptions }) {
|
||||
var onTileErrorStrategy;
|
||||
if (environmentOptions.enabledFeatures.onTileErrorStrategy !== false) {
|
||||
onTileErrorStrategy = function onTileErrorStrategy$TimeoutTile(err, tile, headers, stats, format, callback) {
|
||||
|
||||
function isRenderTimeoutError (err) {
|
||||
return err.message === 'Render timed out';
|
||||
}
|
||||
|
||||
function isDatasourceTimeoutError (err) {
|
||||
return err.message && err.message.match(/canceling statement due to statement timeout/i);
|
||||
}
|
||||
|
||||
function isTimeoutError (err) {
|
||||
return isRenderTimeoutError(err) || isDatasourceTimeoutError(err);
|
||||
}
|
||||
|
||||
function isRasterFormat (format) {
|
||||
return format === 'png' || format === 'jpg';
|
||||
}
|
||||
|
||||
if (isTimeoutError(err) && isRasterFormat(format)) {
|
||||
return callback(null, timeoutErrorTile, {
|
||||
'Content-Type': 'image/png',
|
||||
}, {});
|
||||
} else {
|
||||
return callback(err, tile, headers, stats);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const rendererFactory = new windshaft.renderer.Factory({
|
||||
onTileErrorStrategy: onTileErrorStrategy,
|
||||
mapnik: {
|
||||
redisPool: redisPool,
|
||||
grainstore: serverOptions.grainstore,
|
||||
mapnik: serverOptions.renderer.mapnik
|
||||
},
|
||||
http: serverOptions.renderer.http,
|
||||
mvt: serverOptions.renderer.mvt,
|
||||
torque: serverOptions.renderer.torque
|
||||
});
|
||||
|
||||
return rendererFactory;
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
var assert = require('assert');
|
||||
var step = require('step');
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {PgConnection} pgConnection
|
||||
* @param metadataBackend
|
||||
* @param {MapStore} mapStore
|
||||
* @param {TemplateMaps} templateMaps
|
||||
* @constructor
|
||||
* @type {AuthApi}
|
||||
*/
|
||||
function AuthApi(pgConnection, metadataBackend, mapStore, templateMaps) {
|
||||
this.pgConnection = pgConnection;
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.mapStore = mapStore;
|
||||
this.templateMaps = templateMaps;
|
||||
}
|
||||
|
||||
module.exports = AuthApi;
|
||||
|
||||
// Check if the user is authorized by a signer
|
||||
//
|
||||
// @param res express response object
|
||||
// @param callback function(err, signed_by) signed_by will be
|
||||
// null if the request is not signed by anyone
|
||||
// or will be a string cartodb username otherwise.
|
||||
//
|
||||
AuthApi.prototype.authorizedBySigner = function(res, callback) {
|
||||
if ( ! res.locals.token || ! res.locals.signer ) {
|
||||
return callback(null, false); // no signer requested
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
var layergroup_id = res.locals.token;
|
||||
var auth_token = res.locals.auth_token;
|
||||
|
||||
this.mapStore.load(layergroup_id, function(err, mapConfig) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var authorized = self.templateMaps.isAuthorized(mapConfig.obj().template, auth_token);
|
||||
|
||||
return callback(null, authorized);
|
||||
});
|
||||
};
|
||||
|
||||
// Check if a request is authorized by api_key
|
||||
//
|
||||
// @param user
|
||||
// @param req express request object
|
||||
// @param callback function(err, authorized)
|
||||
// NOTE: authorized is expected to be 0 or 1 (integer)
|
||||
//
|
||||
AuthApi.prototype.authorizedByAPIKey = function(user, req, callback) {
|
||||
var givenKey = req.query.api_key || req.query.map_key;
|
||||
if ( ! givenKey && req.body ) {
|
||||
// check also in request body
|
||||
givenKey = req.body.api_key || req.body.map_key;
|
||||
}
|
||||
if ( ! givenKey ) {
|
||||
return callback(null, 0); // no api key, no authorization...
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
step(
|
||||
function () {
|
||||
self.metadataBackend.getUserMapKey(user, this);
|
||||
},
|
||||
function checkApiKey(err, val){
|
||||
assert.ifError(err);
|
||||
return val && givenKey === val;
|
||||
},
|
||||
function finish(err, authorized) {
|
||||
callback(err, authorized);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check access authorization
|
||||
*
|
||||
* @param req - standard req object. Importantly contains table and host information
|
||||
* @param res - standard res object. Contains the auth parameters in locals
|
||||
* @param callback function(err, allowed) is access allowed not?
|
||||
*/
|
||||
AuthApi.prototype.authorize = function(req, res, callback) {
|
||||
var self = this;
|
||||
var user = res.locals.user;
|
||||
|
||||
step(
|
||||
function () {
|
||||
self.authorizedByAPIKey(user, req, this);
|
||||
},
|
||||
function checkApiKey(err, authorized){
|
||||
req.profiler.done('authorizedByAPIKey');
|
||||
assert.ifError(err);
|
||||
|
||||
// if not authorized by api_key, continue
|
||||
if (!authorized) {
|
||||
// not authorized by api_key, check if authorized by signer
|
||||
return self.authorizedBySigner(res, this);
|
||||
}
|
||||
|
||||
// authorized by api key, login as the given username and stop
|
||||
self.pgConnection.setDBAuth(user, res.locals, function(err) {
|
||||
callback(err, true); // authorized (or error)
|
||||
});
|
||||
},
|
||||
function checkSignAuthorized(err, authorized) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if ( ! authorized ) {
|
||||
// request not authorized by signer.
|
||||
|
||||
// if no signer name was given, let dbparams and
|
||||
// PostgreSQL do the rest.
|
||||
//
|
||||
if ( ! res.locals.signer ) {
|
||||
return callback(null, true); // authorized so far
|
||||
}
|
||||
|
||||
// if signer name was given, return no authorization
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
self.pgConnection.setDBAuth(user, res.locals, function(err) {
|
||||
req.profiler.done('setDBAuth');
|
||||
callback(err, true); // authorized (or error)
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -1,37 +1,52 @@
|
||||
var PSQL = require('cartodb-psql');
|
||||
var cors = require('../middleware/cors');
|
||||
var userMiddleware = require('../middleware/user');
|
||||
const PSQL = require('cartodb-psql');
|
||||
const cleanUpQueryParams = require('../middlewares/clean-up-query-params');
|
||||
const credentials = require('../middlewares/credentials');
|
||||
const authorize = require('../middlewares/authorize');
|
||||
const dbConnSetup = require('../middlewares/db-conn-setup');
|
||||
const rateLimit = require('../middlewares/rate-limit');
|
||||
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit;
|
||||
const cacheControlHeader = require('../middlewares/cache-control-header');
|
||||
const dbParamsFromResLocals = require('../../utils/database-params');
|
||||
|
||||
function AnalysesController(prepareContext) {
|
||||
this.prepareContext = prepareContext;
|
||||
}
|
||||
module.exports = class AnalysesController {
|
||||
constructor (pgConnection, authBackend, userLimitsBackend) {
|
||||
this.pgConnection = pgConnection;
|
||||
this.authBackend = authBackend;
|
||||
this.userLimitsBackend = userLimitsBackend;
|
||||
}
|
||||
|
||||
module.exports = AnalysesController;
|
||||
register (mapRouter) {
|
||||
mapRouter.get('/analyses/catalog', this.middlewares());
|
||||
}
|
||||
|
||||
AnalysesController.prototype.register = function (app) {
|
||||
app.get(
|
||||
`${app.base_url_mapconfig}/analyses/catalog`,
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.prepareContext,
|
||||
this.createPGClient(),
|
||||
this.getDataFromQuery({ queryTemplate: catalogQueryTpl, key: 'catalog' }),
|
||||
this.getDataFromQuery({ queryTemplate: tablesQueryTpl, key: 'tables' }),
|
||||
this.prepareResponse(),
|
||||
this.setCacheControlHeader(),
|
||||
this.sendResponse(),
|
||||
this.unathorizedError()
|
||||
);
|
||||
middlewares () {
|
||||
return [
|
||||
credentials(),
|
||||
authorize(this.authBackend),
|
||||
dbConnSetup(this.pgConnection),
|
||||
rateLimit(this.userLimitsBackend, RATE_LIMIT_ENDPOINTS_GROUPS.ANALYSIS_CATALOG),
|
||||
cleanUpQueryParams(),
|
||||
createPGClient(),
|
||||
getDataFromQuery({ queryTemplate: catalogQueryTpl, key: 'catalog' }),
|
||||
getDataFromQuery({ queryTemplate: tablesQueryTpl, key: 'tables' }),
|
||||
prepareResponse(),
|
||||
cacheControlHeader({ ttl: 10, revalidate: true }),
|
||||
unauthorizedError()
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
AnalysesController.prototype.createPGClient = function () {
|
||||
function createPGClient () {
|
||||
return function createPGClientMiddleware (req, res, next) {
|
||||
res.locals.pg = new PSQL(dbParamsFromReqParams(res.locals));
|
||||
const dbParams = dbParamsFromResLocals(res.locals);
|
||||
|
||||
res.locals.pg = new PSQL(dbParams);
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
AnalysesController.prototype.getDataFromQuery = function ({ queryTemplate, key }) {
|
||||
function getDataFromQuery({ queryTemplate, key }) {
|
||||
const readOnlyTransactionOn = true;
|
||||
|
||||
return function getCatalogMiddleware(req, res, next) {
|
||||
@@ -48,9 +63,9 @@ AnalysesController.prototype.getDataFromQuery = function ({ queryTemplate, key }
|
||||
next();
|
||||
}, readOnlyTransactionOn);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
AnalysesController.prototype.prepareResponse = function () {
|
||||
function prepareResponse () {
|
||||
return function prepareResponseMiddleware (req, res, next) {
|
||||
const { catalog, tables } = res.locals;
|
||||
|
||||
@@ -87,32 +102,14 @@ AnalysesController.prototype.prepareResponse = function () {
|
||||
return -1;
|
||||
});
|
||||
|
||||
res.statusCode = 200;
|
||||
res.body = { catalog: analysisCatalog };
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
AnalysesController.prototype.setCacheControlHeader = function () {
|
||||
return function setCacheControlHeaderMiddleware (req, res, next) {
|
||||
res.set('Cache-Control', 'public,max-age=10,must-revalidate');
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
AnalysesController.prototype.sendResponse = function() {
|
||||
return function sendResponseMiddleware (req, res) {
|
||||
res.status(200);
|
||||
|
||||
if (req.query && req.query.callback) {
|
||||
res.jsonp(res.body);
|
||||
} else {
|
||||
res.json(res.body);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
AnalysesController.prototype.unathorizedError = function () {
|
||||
function unauthorizedError () {
|
||||
return function unathorizedErrorMiddleware(err, req, res, next) {
|
||||
if (err.message.match(/permission\sdenied/)) {
|
||||
err = new Error('Unauthorized');
|
||||
@@ -121,7 +118,7 @@ AnalysesController.prototype.unathorizedError = function () {
|
||||
|
||||
next(err);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const catalogQueryTpl = ctx => `
|
||||
SELECT analysis_def->>'type' as type, * FROM cdb_analysis_catalog WHERE username = '${ctx._username}'
|
||||
@@ -145,23 +142,3 @@ var tablesQueryTpl = ctx => `
|
||||
FROM analysis_tables
|
||||
ORDER BY size DESC
|
||||
`;
|
||||
|
||||
function dbParamsFromReqParams(params) {
|
||||
var dbParams = {};
|
||||
if ( params.dbuser ) {
|
||||
dbParams.user = params.dbuser;
|
||||
}
|
||||
if ( params.dbpassword ) {
|
||||
dbParams.pass = params.dbpassword;
|
||||
}
|
||||
if ( params.dbhost ) {
|
||||
dbParams.host = params.dbhost;
|
||||
}
|
||||
if ( params.dbport ) {
|
||||
dbParams.port = params.dbport;
|
||||
}
|
||||
if ( params.dbname ) {
|
||||
dbParams.dbname = params.dbname;
|
||||
}
|
||||
return dbParams;
|
||||
}
|
||||
59
lib/cartodb/api/map/analysis-layergroup-controller.js
Normal file
59
lib/cartodb/api/map/analysis-layergroup-controller.js
Normal file
@@ -0,0 +1,59 @@
|
||||
const layergroupToken = require('../middlewares/layergroup-token');
|
||||
const cleanUpQueryParams = require('../middlewares/clean-up-query-params');
|
||||
const credentials = require('../middlewares/credentials');
|
||||
const dbConnSetup = require('../middlewares/db-conn-setup');
|
||||
const authorize = require('../middlewares/authorize');
|
||||
const rateLimit = require('../middlewares/rate-limit');
|
||||
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit;
|
||||
const dbParamsFromResLocals = require('../../utils/database-params');
|
||||
|
||||
module.exports = class AnalysisLayergroupController {
|
||||
constructor (analysisStatusBackend, pgConnection, userLimitsBackend, authBackend) {
|
||||
this.analysisStatusBackend = analysisStatusBackend;
|
||||
this.pgConnection = pgConnection;
|
||||
this.userLimitsBackend = userLimitsBackend;
|
||||
this.authBackend = authBackend;
|
||||
}
|
||||
|
||||
register (mapRouter) {
|
||||
mapRouter.get('/:token/analysis/node/:nodeId', this.middlewares());
|
||||
}
|
||||
|
||||
middlewares () {
|
||||
return [
|
||||
layergroupToken(),
|
||||
credentials(),
|
||||
authorize(this.authBackend),
|
||||
dbConnSetup(this.pgConnection),
|
||||
rateLimit(this.userLimitsBackend, RATE_LIMIT_ENDPOINTS_GROUPS.ANALYSIS),
|
||||
cleanUpQueryParams(),
|
||||
analysisNodeStatus(this.analysisStatusBackend)
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
function analysisNodeStatus (analysisStatusBackend) {
|
||||
return function analysisNodeStatusMiddleware(req, res, next) {
|
||||
const { nodeId } = req.params;
|
||||
const dbParams = dbParamsFromResLocals(res.locals);
|
||||
|
||||
analysisStatusBackend.getNodeStatus(nodeId, dbParams, (err, nodeStatus, stats = {}) => {
|
||||
req.profiler.add(stats);
|
||||
|
||||
if (err) {
|
||||
err.label = 'GET NODE STATUS';
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.set({
|
||||
'Cache-Control': 'public,max-age=5',
|
||||
'Last-Modified': new Date().toUTCString()
|
||||
});
|
||||
|
||||
res.statusCode = 200;
|
||||
res.body = nodeStatus;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
213
lib/cartodb/api/map/anonymous-map-controller.js
Normal file
213
lib/cartodb/api/map/anonymous-map-controller.js
Normal file
@@ -0,0 +1,213 @@
|
||||
const windshaft = require('windshaft');
|
||||
const MapConfig = windshaft.model.MapConfig;
|
||||
const Datasource = windshaft.model.Datasource;
|
||||
const cleanUpQueryParams = require('../middlewares/clean-up-query-params');
|
||||
const credentials = require('../middlewares/credentials');
|
||||
const dbConnSetup = require('../middlewares/db-conn-setup');
|
||||
const authorize = require('../middlewares/authorize');
|
||||
const initProfiler = require('../middlewares/init-profiler');
|
||||
const checkJsonContentType = require('../middlewares/check-json-content-type');
|
||||
const incrementMapViewCount = require('../middlewares/increment-map-view-count');
|
||||
const augmentLayergroupData = require('../middlewares/augment-layergroup-data');
|
||||
const cacheControlHeader = require('../middlewares/cache-control-header');
|
||||
const cacheChannelHeader = require('../middlewares/cache-channel-header');
|
||||
const surrogateKeyHeader = require('../middlewares/surrogate-key-header');
|
||||
const lastModifiedHeader = require('../middlewares/last-modified-header');
|
||||
const lastUpdatedTimeLayergroup = require('../middlewares/last-updated-time-layergroup');
|
||||
const layerStats = require('../middlewares/layer-stats');
|
||||
const layergroupIdHeader = require('../middlewares/layergroup-id-header');
|
||||
const layergroupMetadata = require('../middlewares/layergroup-metadata');
|
||||
const mapError = require('../middlewares/map-error');
|
||||
const CreateLayergroupMapConfigProvider = require('../../models/mapconfig/provider/create-layergroup-provider');
|
||||
const rateLimit = require('../middlewares/rate-limit');
|
||||
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit;
|
||||
|
||||
module.exports = class AnonymousMapController {
|
||||
/**
|
||||
* @param {AuthBackend} authBackend
|
||||
* @param {PgConnection} pgConnection
|
||||
* @param {TemplateMaps} templateMaps
|
||||
* @param {MapBackend} mapBackend
|
||||
* @param metadataBackend
|
||||
* @param {SurrogateKeysCache} surrogateKeysCache
|
||||
* @param {UserLimitsBackend} userLimitsBackend
|
||||
* @param {LayergroupAffectedTables} layergroupAffectedTables
|
||||
* @param {MapConfigAdapter} mapConfigAdapter
|
||||
* @param {StatsBackend} statsBackend
|
||||
* @constructor
|
||||
*/
|
||||
constructor (
|
||||
pgConnection,
|
||||
templateMaps,
|
||||
mapBackend,
|
||||
metadataBackend,
|
||||
surrogateKeysCache,
|
||||
userLimitsBackend,
|
||||
layergroupAffectedTables,
|
||||
mapConfigAdapter,
|
||||
statsBackend,
|
||||
authBackend,
|
||||
layergroupMetadata
|
||||
) {
|
||||
this.pgConnection = pgConnection;
|
||||
this.templateMaps = templateMaps;
|
||||
this.mapBackend = mapBackend;
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.surrogateKeysCache = surrogateKeysCache;
|
||||
this.userLimitsBackend = userLimitsBackend;
|
||||
this.layergroupAffectedTables = layergroupAffectedTables;
|
||||
this.mapConfigAdapter = mapConfigAdapter;
|
||||
this.statsBackend = statsBackend;
|
||||
this.authBackend = authBackend;
|
||||
this.layergroupMetadata = layergroupMetadata;
|
||||
}
|
||||
|
||||
register (mapRouter) {
|
||||
mapRouter.options('/');
|
||||
mapRouter.get('/', this.middlewares());
|
||||
mapRouter.post('/', this.middlewares());
|
||||
}
|
||||
|
||||
middlewares () {
|
||||
const isTemplateInstantiation = false;
|
||||
const useTemplateHash = false;
|
||||
const includeQuery = true;
|
||||
const label = 'ANONYMOUS LAYERGROUP';
|
||||
const addContext = true;
|
||||
|
||||
return [
|
||||
credentials(),
|
||||
authorize(this.authBackend),
|
||||
dbConnSetup(this.pgConnection),
|
||||
rateLimit(this.userLimitsBackend, RATE_LIMIT_ENDPOINTS_GROUPS.ANONYMOUS),
|
||||
cleanUpQueryParams(['aggregation']),
|
||||
initProfiler(isTemplateInstantiation),
|
||||
checkJsonContentType(),
|
||||
checkCreateLayergroup(),
|
||||
prepareAdapterMapConfig(this.mapConfigAdapter),
|
||||
createLayergroup (
|
||||
this.mapBackend,
|
||||
this.userLimitsBackend,
|
||||
this.pgConnection,
|
||||
this.layergroupAffectedTables
|
||||
),
|
||||
incrementMapViewCount(this.metadataBackend),
|
||||
augmentLayergroupData(),
|
||||
cacheControlHeader({ ttl: global.environment.varnish.layergroupTtl || 86400, revalidate: true }),
|
||||
cacheChannelHeader(),
|
||||
surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
|
||||
lastModifiedHeader(),
|
||||
lastUpdatedTimeLayergroup(),
|
||||
layerStats(this.pgConnection, this.statsBackend),
|
||||
layergroupIdHeader(this.templateMaps, useTemplateHash),
|
||||
layergroupMetadata(this.layergroupMetadata, includeQuery),
|
||||
mapError({ label, addContext })
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
function checkCreateLayergroup () {
|
||||
return function checkCreateLayergroupMiddleware (req, res, next) {
|
||||
if (req.method === 'GET') {
|
||||
const { config } = req.query;
|
||||
|
||||
if (!config) {
|
||||
return next(new Error('layergroup GET needs a "config" parameter'));
|
||||
}
|
||||
|
||||
try {
|
||||
req.body = JSON.parse(config);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
}
|
||||
|
||||
req.profiler.done('checkCreateLayergroup');
|
||||
return next();
|
||||
};
|
||||
}
|
||||
|
||||
function prepareAdapterMapConfig (mapConfigAdapter) {
|
||||
return function prepareAdapterMapConfigMiddleware(req, res, next) {
|
||||
const requestMapConfig = req.body;
|
||||
|
||||
const { user, api_key } = res.locals;
|
||||
const { dbuser, dbname, dbpassword, dbhost, dbport } = res.locals;
|
||||
const params = Object.assign({ dbuser, dbname, dbpassword, dbhost, dbport }, req.query);
|
||||
|
||||
const context = {
|
||||
analysisConfiguration: {
|
||||
user,
|
||||
db: {
|
||||
host: dbhost,
|
||||
port: dbport,
|
||||
dbname: dbname,
|
||||
user: dbuser,
|
||||
pass: dbpassword
|
||||
},
|
||||
batch: {
|
||||
username: user,
|
||||
apiKey: api_key
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
mapConfigAdapter.getMapConfig(user, requestMapConfig, params, context, (err, requestMapConfig) => {
|
||||
req.profiler.done('anonymous.getMapConfig');
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
req.body = requestMapConfig;
|
||||
res.locals.context = context;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function createLayergroup (mapBackend, userLimitsBackend, pgConnection, affectedTablesCache) {
|
||||
return function createLayergroupMiddleware (req, res, next) {
|
||||
const requestMapConfig = req.body;
|
||||
|
||||
const { context } = res.locals;
|
||||
const { user, cache_buster, api_key } = res.locals;
|
||||
const { dbuser, dbname, dbpassword, dbhost, dbport } = res.locals;
|
||||
|
||||
const params = {
|
||||
cache_buster, api_key,
|
||||
dbuser, dbname, dbpassword, dbhost, dbport
|
||||
};
|
||||
|
||||
const datasource = context.datasource || Datasource.EmptyDatasource();
|
||||
const mapConfig = new MapConfig(requestMapConfig, datasource);
|
||||
|
||||
const mapConfigProvider = new CreateLayergroupMapConfigProvider(
|
||||
mapConfig,
|
||||
user,
|
||||
userLimitsBackend,
|
||||
pgConnection,
|
||||
affectedTablesCache,
|
||||
params
|
||||
);
|
||||
|
||||
res.locals.mapConfig = mapConfig;
|
||||
res.locals.analysesResults = context.analysesResults;
|
||||
|
||||
const mapParams = { dbuser, dbname, dbpassword, dbhost, dbport };
|
||||
|
||||
mapBackend.createLayergroup(mapConfig, mapParams, mapConfigProvider, (err, layergroup, stats = {}) => {
|
||||
req.profiler.add(stats);
|
||||
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.statusCode = 200;
|
||||
res.body = layergroup;
|
||||
res.locals.mapConfigProvider = mapConfigProvider;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
89
lib/cartodb/api/map/attributes-layergroup-controller.js
Normal file
89
lib/cartodb/api/map/attributes-layergroup-controller.js
Normal file
@@ -0,0 +1,89 @@
|
||||
const layergroupToken = require('../middlewares/layergroup-token');
|
||||
const cleanUpQueryParams = require('../middlewares/clean-up-query-params');
|
||||
const credentials = require('../middlewares/credentials');
|
||||
const dbConnSetup = require('../middlewares/db-conn-setup');
|
||||
const authorize = require('../middlewares/authorize');
|
||||
const rateLimit = require('../middlewares/rate-limit');
|
||||
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit;
|
||||
const createMapStoreMapConfigProvider = require('../middlewares/map-store-map-config-provider');
|
||||
const cacheControlHeader = require('../middlewares/cache-control-header');
|
||||
const cacheChannelHeader = require('../middlewares/cache-channel-header');
|
||||
const surrogateKeyHeader = require('../middlewares/surrogate-key-header');
|
||||
const lastModifiedHeader = require('../middlewares/last-modified-header');
|
||||
|
||||
module.exports = class AttributesLayergroupController {
|
||||
constructor (
|
||||
attributesBackend,
|
||||
pgConnection,
|
||||
mapStore,
|
||||
userLimitsBackend,
|
||||
layergroupAffectedTablesCache,
|
||||
authBackend,
|
||||
surrogateKeysCache
|
||||
) {
|
||||
this.attributesBackend = attributesBackend;
|
||||
this.pgConnection = pgConnection;
|
||||
this.mapStore = mapStore;
|
||||
this.userLimitsBackend = userLimitsBackend;
|
||||
this.layergroupAffectedTablesCache = layergroupAffectedTablesCache;
|
||||
this.authBackend = authBackend;
|
||||
this.surrogateKeysCache = surrogateKeysCache;
|
||||
}
|
||||
|
||||
register (mapRouter) {
|
||||
mapRouter.get('/:token/:layer/attributes/:fid', this.middlewares());
|
||||
}
|
||||
|
||||
middlewares () {
|
||||
return [
|
||||
layergroupToken(),
|
||||
credentials(),
|
||||
authorize(this.authBackend),
|
||||
dbConnSetup(this.pgConnection),
|
||||
rateLimit(this.userLimitsBackend, RATE_LIMIT_ENDPOINTS_GROUPS.ATTRIBUTES),
|
||||
cleanUpQueryParams(),
|
||||
createMapStoreMapConfigProvider(
|
||||
this.mapStore,
|
||||
this.userLimitsBackend,
|
||||
this.pgConnection,
|
||||
this.layergroupAffectedTablesCache
|
||||
),
|
||||
getFeatureAttributes(this.attributesBackend),
|
||||
cacheControlHeader(),
|
||||
cacheChannelHeader(),
|
||||
surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
|
||||
lastModifiedHeader()
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
function getFeatureAttributes (attributesBackend) {
|
||||
return function getFeatureAttributesMiddleware (req, res, next) {
|
||||
req.profiler.start('windshaft.maplayer_attribute');
|
||||
|
||||
const { mapConfigProvider } = res.locals;
|
||||
const { token } = res.locals;
|
||||
const { dbuser, dbname, dbpassword, dbhost, dbport } = res.locals;
|
||||
const { layer, fid } = req.params;
|
||||
|
||||
const params = {
|
||||
token,
|
||||
dbuser, dbname, dbpassword, dbhost, dbport,
|
||||
layer, fid
|
||||
};
|
||||
|
||||
attributesBackend.getFeatureAttributes(mapConfigProvider, params, false, (err, tile, stats = {}) => {
|
||||
req.profiler.add(stats);
|
||||
|
||||
if (err) {
|
||||
err.label = 'GET ATTRIBUTES';
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.statusCode = 200;
|
||||
res.body = tile;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
142
lib/cartodb/api/map/dataview-layergroup-controller.js
Normal file
142
lib/cartodb/api/map/dataview-layergroup-controller.js
Normal file
@@ -0,0 +1,142 @@
|
||||
const layergroupToken = require('../middlewares/layergroup-token');
|
||||
const cleanUpQueryParams = require('../middlewares/clean-up-query-params');
|
||||
const credentials = require('../middlewares/credentials');
|
||||
const dbConnSetup = require('../middlewares/db-conn-setup');
|
||||
const authorize = require('../middlewares/authorize');
|
||||
const rateLimit = require('../middlewares/rate-limit');
|
||||
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit;
|
||||
const createMapStoreMapConfigProvider = require('../middlewares/map-store-map-config-provider');
|
||||
const cacheControlHeader = require('../middlewares/cache-control-header');
|
||||
const cacheChannelHeader = require('../middlewares/cache-channel-header');
|
||||
const surrogateKeyHeader = require('../middlewares/surrogate-key-header');
|
||||
const lastModifiedHeader = require('../middlewares/last-modified-header');
|
||||
|
||||
const ALLOWED_DATAVIEW_QUERY_PARAMS = [
|
||||
'filters', // json
|
||||
'own_filter', // 0, 1
|
||||
'no_filters', // 0, 1
|
||||
'bbox', // w,s,e,n
|
||||
'start', // number
|
||||
'end', // number
|
||||
'column_type', // string
|
||||
'bins', // number
|
||||
'aggregation', //string
|
||||
'offset', // number
|
||||
'q', // widgets search
|
||||
'categories', // number
|
||||
];
|
||||
|
||||
module.exports = class DataviewLayergroupController {
|
||||
constructor (
|
||||
dataviewBackend,
|
||||
pgConnection,
|
||||
mapStore,
|
||||
userLimitsBackend,
|
||||
layergroupAffectedTablesCache,
|
||||
authBackend,
|
||||
surrogateKeysCache
|
||||
) {
|
||||
this.dataviewBackend = dataviewBackend;
|
||||
this.pgConnection = pgConnection;
|
||||
this.mapStore = mapStore;
|
||||
this.userLimitsBackend = userLimitsBackend;
|
||||
this.layergroupAffectedTablesCache = layergroupAffectedTablesCache;
|
||||
this.authBackend = authBackend;
|
||||
this.surrogateKeysCache = surrogateKeysCache;
|
||||
}
|
||||
|
||||
register (mapRouter) {
|
||||
// Undocumented/non-supported API endpoint methods.
|
||||
// Use at your own peril.
|
||||
|
||||
mapRouter.get('/:token/dataview/:dataviewName', this.middlewares({
|
||||
action: 'get',
|
||||
rateLimitGroup: RATE_LIMIT_ENDPOINTS_GROUPS.DATAVIEW
|
||||
}));
|
||||
|
||||
mapRouter.get('/:token/:layer/widget/:dataviewName', this.middlewares({
|
||||
action: 'get',
|
||||
rateLimitGroup: RATE_LIMIT_ENDPOINTS_GROUPS.DATAVIEW
|
||||
}));
|
||||
|
||||
mapRouter.get('/:token/dataview/:dataviewName/search', this.middlewares({
|
||||
action: 'search',
|
||||
rateLimitGroup: RATE_LIMIT_ENDPOINTS_GROUPS.DATAVIEW_SEARCH
|
||||
}));
|
||||
|
||||
mapRouter.get('/:token/:layer/widget/:dataviewName/search', this.middlewares({
|
||||
action: 'search',
|
||||
rateLimitGroup: RATE_LIMIT_ENDPOINTS_GROUPS.DATAVIEW_SEARCH
|
||||
}));
|
||||
}
|
||||
|
||||
middlewares ({ action, rateLimitGroup }) {
|
||||
return [
|
||||
layergroupToken(),
|
||||
credentials(),
|
||||
authorize(this.authBackend),
|
||||
dbConnSetup(this.pgConnection),
|
||||
rateLimit(this.userLimitsBackend, rateLimitGroup),
|
||||
cleanUpQueryParams(ALLOWED_DATAVIEW_QUERY_PARAMS),
|
||||
createMapStoreMapConfigProvider(
|
||||
this.mapStore,
|
||||
this.userLimitsBackend,
|
||||
this.pgConnection,
|
||||
this.layergroupAffectedTablesCache
|
||||
),
|
||||
action === 'search' ? dataviewSearch(this.dataviewBackend) : getDataview(this.dataviewBackend),
|
||||
cacheControlHeader(),
|
||||
cacheChannelHeader(),
|
||||
surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
|
||||
lastModifiedHeader()
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
function getDataview (dataviewBackend) {
|
||||
return function getDataviewMiddleware (req, res, next) {
|
||||
const { user, mapConfigProvider } = res.locals;
|
||||
const { dataviewName } = req.params;
|
||||
const { dbuser, dbname, dbpassword, dbhost, dbport } = res.locals;
|
||||
|
||||
const params = Object.assign({ dataviewName, dbuser, dbname, dbpassword, dbhost, dbport }, req.query);
|
||||
|
||||
dataviewBackend.getDataview(mapConfigProvider, user, params, (err, dataview, stats = {}) => {
|
||||
req.profiler.add(stats);
|
||||
|
||||
if (err) {
|
||||
err.label = 'GET DATAVIEW';
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.statusCode = 200;
|
||||
res.body = dataview;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function dataviewSearch (dataviewBackend) {
|
||||
return function dataviewSearchMiddleware (req, res, next) {
|
||||
const { user, mapConfigProvider } = res.locals;
|
||||
const { dataviewName } = req.params;
|
||||
const { dbuser, dbname, dbpassword, dbhost, dbport } = res.locals;
|
||||
|
||||
const params = Object.assign({ dbuser, dbname, dbpassword, dbhost, dbport }, req.query);
|
||||
|
||||
dataviewBackend.search(mapConfigProvider, user, dataviewName, params, (err, searchResult, stats = {}) => {
|
||||
req.profiler.add(stats);
|
||||
|
||||
if (err) {
|
||||
err.label = 'GET DATAVIEW SEARCH';
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.statusCode = 200;
|
||||
res.body = searchResult;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
129
lib/cartodb/api/map/map-router.js
Normal file
129
lib/cartodb/api/map/map-router.js
Normal file
@@ -0,0 +1,129 @@
|
||||
const { Router: router } = require('express');
|
||||
|
||||
const AnalysisLayergroupController = require('./analysis-layergroup-controller');
|
||||
const AttributesLayergroupController = require('./attributes-layergroup-controller');
|
||||
const DataviewLayergroupController = require('./dataview-layergroup-controller');
|
||||
const PreviewLayergroupController = require('./preview-layergroup-controller');
|
||||
const TileLayergroupController = require('./tile-layergroup-controller');
|
||||
const AnonymousMapController = require('./anonymous-map-controller');
|
||||
const PreviewTemplateController = require('./preview-template-controller');
|
||||
const AnalysesCatalogController = require('./analyses-catalog-controller');
|
||||
|
||||
module.exports = class MapRouter {
|
||||
constructor ({ collaborators }) {
|
||||
const {
|
||||
analysisStatusBackend,
|
||||
attributesBackend,
|
||||
dataviewBackend,
|
||||
previewBackend,
|
||||
tileBackend,
|
||||
pgConnection,
|
||||
mapStore,
|
||||
userLimitsBackend,
|
||||
layergroupAffectedTablesCache,
|
||||
authBackend,
|
||||
surrogateKeysCache,
|
||||
templateMaps,
|
||||
mapBackend,
|
||||
metadataBackend,
|
||||
mapConfigAdapter,
|
||||
statsBackend,
|
||||
layergroupMetadata,
|
||||
namedMapProviderCache,
|
||||
tablesExtentBackend
|
||||
} = collaborators;
|
||||
|
||||
this.analysisLayergroupController = new AnalysisLayergroupController(
|
||||
analysisStatusBackend,
|
||||
pgConnection,
|
||||
userLimitsBackend,
|
||||
authBackend
|
||||
);
|
||||
|
||||
this.attributesLayergroupController = new AttributesLayergroupController(
|
||||
attributesBackend,
|
||||
pgConnection,
|
||||
mapStore,
|
||||
userLimitsBackend,
|
||||
layergroupAffectedTablesCache,
|
||||
authBackend,
|
||||
surrogateKeysCache
|
||||
);
|
||||
|
||||
this.dataviewLayergroupController = new DataviewLayergroupController(
|
||||
dataviewBackend,
|
||||
pgConnection,
|
||||
mapStore,
|
||||
userLimitsBackend,
|
||||
layergroupAffectedTablesCache,
|
||||
authBackend,
|
||||
surrogateKeysCache
|
||||
);
|
||||
|
||||
this.previewLayergroupController = new PreviewLayergroupController(
|
||||
previewBackend,
|
||||
pgConnection,
|
||||
mapStore,
|
||||
userLimitsBackend,
|
||||
layergroupAffectedTablesCache,
|
||||
authBackend,
|
||||
surrogateKeysCache
|
||||
);
|
||||
|
||||
this.tileLayergroupController = new TileLayergroupController(
|
||||
tileBackend,
|
||||
pgConnection,
|
||||
mapStore,
|
||||
userLimitsBackend,
|
||||
layergroupAffectedTablesCache,
|
||||
authBackend,
|
||||
surrogateKeysCache
|
||||
);
|
||||
|
||||
this.anonymousMapController = new AnonymousMapController(
|
||||
pgConnection,
|
||||
templateMaps,
|
||||
mapBackend,
|
||||
metadataBackend,
|
||||
surrogateKeysCache,
|
||||
userLimitsBackend,
|
||||
layergroupAffectedTablesCache,
|
||||
mapConfigAdapter,
|
||||
statsBackend,
|
||||
authBackend,
|
||||
layergroupMetadata
|
||||
);
|
||||
|
||||
this.previewTemplateController = new PreviewTemplateController(
|
||||
namedMapProviderCache,
|
||||
previewBackend,
|
||||
surrogateKeysCache,
|
||||
tablesExtentBackend,
|
||||
metadataBackend,
|
||||
pgConnection,
|
||||
authBackend,
|
||||
userLimitsBackend
|
||||
);
|
||||
|
||||
this.analysesController = new AnalysesCatalogController(
|
||||
pgConnection,
|
||||
authBackend,
|
||||
userLimitsBackend
|
||||
);
|
||||
}
|
||||
|
||||
register (apiRouter, mapPaths) {
|
||||
const mapRouter = router({ mergeParams: true });
|
||||
|
||||
this.analysisLayergroupController.register(mapRouter);
|
||||
this.attributesLayergroupController.register(mapRouter);
|
||||
this.dataviewLayergroupController.register(mapRouter);
|
||||
this.previewLayergroupController.register(mapRouter);
|
||||
this.tileLayergroupController.register(mapRouter);
|
||||
this.anonymousMapController.register(mapRouter);
|
||||
this.previewTemplateController.register(mapRouter);
|
||||
this.analysesController.register(mapRouter);
|
||||
|
||||
mapPaths.forEach(path => apiRouter.use(path, mapRouter));
|
||||
}
|
||||
};
|
||||
156
lib/cartodb/api/map/preview-layergroup-controller.js
Normal file
156
lib/cartodb/api/map/preview-layergroup-controller.js
Normal file
@@ -0,0 +1,156 @@
|
||||
const layergroupToken = require('../middlewares/layergroup-token');
|
||||
const coordinates = require('../middlewares/coordinates');
|
||||
const cleanUpQueryParams = require('../middlewares/clean-up-query-params');
|
||||
const credentials = require('../middlewares/credentials');
|
||||
const noop = require('../middlewares/noop');
|
||||
const dbConnSetup = require('../middlewares/db-conn-setup');
|
||||
const authorize = require('../middlewares/authorize');
|
||||
const rateLimit = require('../middlewares/rate-limit');
|
||||
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit;
|
||||
const createMapStoreMapConfigProvider = require('../middlewares/map-store-map-config-provider');
|
||||
const cacheControlHeader = require('../middlewares/cache-control-header');
|
||||
const cacheChannelHeader = require('../middlewares/cache-channel-header');
|
||||
const surrogateKeyHeader = require('../middlewares/surrogate-key-header');
|
||||
const lastModifiedHeader = require('../middlewares/last-modified-header');
|
||||
const checkStaticImageFormat = require('../middlewares/check-static-image-format');
|
||||
|
||||
module.exports = class PreviewLayergroupController {
|
||||
constructor (
|
||||
previewBackend,
|
||||
pgConnection,
|
||||
mapStore,
|
||||
userLimitsBackend,
|
||||
layergroupAffectedTablesCache,
|
||||
authBackend,
|
||||
surrogateKeysCache
|
||||
) {
|
||||
this.previewBackend = previewBackend;
|
||||
this.pgConnection = pgConnection;
|
||||
this.mapStore = mapStore;
|
||||
this.userLimitsBackend = userLimitsBackend;
|
||||
this.layergroupAffectedTablesCache = layergroupAffectedTablesCache;
|
||||
this.authBackend = authBackend;
|
||||
this.surrogateKeysCache = surrogateKeysCache;
|
||||
}
|
||||
|
||||
register (mapRouter) {
|
||||
mapRouter.get('/static/center/:token/:z/:lat/:lng/:width/:height.:format', this.middlewares({
|
||||
validateZoom: true,
|
||||
previewType: 'centered'
|
||||
}));
|
||||
|
||||
mapRouter.get('/static/bbox/:token/:west,:south,:east,:north/:width/:height.:format', this.middlewares({
|
||||
validateZoom: false,
|
||||
previewType: 'bbox'
|
||||
}));
|
||||
}
|
||||
|
||||
middlewares ({ validateZoom, previewType }) {
|
||||
const forcedFormat = 'png';
|
||||
|
||||
let getPreviewImage;
|
||||
|
||||
if (previewType === 'centered') {
|
||||
getPreviewImage = getPreviewImageByCenter;
|
||||
}
|
||||
|
||||
if (previewType === 'bbox') {
|
||||
getPreviewImage = getPreviewImageByBoundingBox;
|
||||
}
|
||||
|
||||
return [
|
||||
layergroupToken(),
|
||||
validateZoom ? coordinates({ z: true, x: false, y: false }) : noop(),
|
||||
credentials(),
|
||||
authorize(this.authBackend),
|
||||
dbConnSetup(this.pgConnection),
|
||||
rateLimit(this.userLimitsBackend, RATE_LIMIT_ENDPOINTS_GROUPS.STATIC),
|
||||
cleanUpQueryParams(['layer']),
|
||||
checkStaticImageFormat(),
|
||||
createMapStoreMapConfigProvider(
|
||||
this.mapStore,
|
||||
this.userLimitsBackend,
|
||||
this.pgConnection,
|
||||
this.layergroupAffectedTablesCache,
|
||||
forcedFormat
|
||||
),
|
||||
getPreviewImage(this.previewBackend),
|
||||
cacheControlHeader(),
|
||||
cacheChannelHeader(),
|
||||
surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
|
||||
lastModifiedHeader()
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
function getPreviewImageByCenter (previewBackend) {
|
||||
return function getPreviewImageByCenterMiddleware (req, res, next) {
|
||||
const width = +req.params.width;
|
||||
const height = +req.params.height;
|
||||
const zoom = +req.params.z;
|
||||
const center = {
|
||||
lng: +req.params.lng,
|
||||
lat: +req.params.lat
|
||||
};
|
||||
|
||||
const format = req.params.format === 'jpg' ? 'jpeg' : 'png';
|
||||
const { mapConfigProvider: provider } = res.locals;
|
||||
|
||||
previewBackend.getImage(provider, format, width, height, zoom, center, (err, image, headers, stats = {}) => {
|
||||
req.profiler.done(`render-${format}`);
|
||||
req.profiler.add(stats);
|
||||
|
||||
if (err) {
|
||||
err.label = 'STATIC_MAP';
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (headers) {
|
||||
res.set(headers);
|
||||
}
|
||||
|
||||
res.set('Content-Type', headers['Content-Type'] || `image/${format}`);
|
||||
|
||||
res.statusCode = 200;
|
||||
res.body = image;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function getPreviewImageByBoundingBox (previewBackend) {
|
||||
return function getPreviewImageByBoundingBoxMiddleware (req, res, next) {
|
||||
const width = +req.params.width;
|
||||
const height = +req.params.height;
|
||||
const bounds = {
|
||||
west: +req.params.west,
|
||||
north: +req.params.north,
|
||||
east: +req.params.east,
|
||||
south: +req.params.south
|
||||
};
|
||||
const format = req.params.format === 'jpg' ? 'jpeg' : 'png';
|
||||
const { mapConfigProvider: provider } = res.locals;
|
||||
|
||||
previewBackend.getImage(provider, format, width, height, bounds, (err, image, headers, stats = {}) => {
|
||||
req.profiler.done(`render-${format}`);
|
||||
req.profiler.add(stats);
|
||||
|
||||
if (err) {
|
||||
err.label = 'STATIC_MAP';
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (headers) {
|
||||
res.set(headers);
|
||||
}
|
||||
|
||||
res.set('Content-Type', headers['Content-Type'] || `image/${format}`);
|
||||
|
||||
res.statusCode = 200;
|
||||
res.body = image;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
367
lib/cartodb/api/map/preview-template-controller.js
Normal file
367
lib/cartodb/api/map/preview-template-controller.js
Normal file
@@ -0,0 +1,367 @@
|
||||
const cleanUpQueryParams = require('../middlewares/clean-up-query-params');
|
||||
const credentials = require('../middlewares/credentials');
|
||||
const dbConnSetup = require('../middlewares/db-conn-setup');
|
||||
const authorize = require('../middlewares/authorize');
|
||||
const namedMapProvider = require('../middlewares/named-map-provider');
|
||||
const cacheControlHeader = require('../middlewares/cache-control-header');
|
||||
const cacheChannelHeader = require('../middlewares/cache-channel-header');
|
||||
const surrogateKeyHeader = require('../middlewares/surrogate-key-header');
|
||||
const lastModifiedHeader = require('../middlewares/last-modified-header');
|
||||
const checkStaticImageFormat = require('../middlewares/check-static-image-format');
|
||||
const rateLimit = require('../middlewares/rate-limit');
|
||||
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit;
|
||||
|
||||
const DEFAULT_ZOOM_CENTER = {
|
||||
zoom: 1,
|
||||
center: {
|
||||
lng: 0,
|
||||
lat: 0
|
||||
}
|
||||
};
|
||||
|
||||
function numMapper(n) {
|
||||
return +n;
|
||||
}
|
||||
|
||||
module.exports = class PreviewTemplateController {
|
||||
constructor (
|
||||
namedMapProviderCache,
|
||||
previewBackend,
|
||||
surrogateKeysCache,
|
||||
tablesExtentBackend,
|
||||
metadataBackend,
|
||||
pgConnection,
|
||||
authBackend,
|
||||
userLimitsBackend
|
||||
) {
|
||||
this.namedMapProviderCache = namedMapProviderCache;
|
||||
this.previewBackend = previewBackend;
|
||||
this.surrogateKeysCache = surrogateKeysCache;
|
||||
this.tablesExtentBackend = tablesExtentBackend;
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.pgConnection = pgConnection;
|
||||
this.authBackend = authBackend;
|
||||
this.userLimitsBackend = userLimitsBackend;
|
||||
}
|
||||
|
||||
register (mapRouter) {
|
||||
mapRouter.get('/static/named/:template_id/:width/:height.:format', this.middlewares());
|
||||
}
|
||||
|
||||
middlewares () {
|
||||
return [
|
||||
credentials(),
|
||||
authorize(this.authBackend),
|
||||
dbConnSetup(this.pgConnection),
|
||||
rateLimit(this.userLimitsBackend, RATE_LIMIT_ENDPOINTS_GROUPS.STATIC_NAMED),
|
||||
cleanUpQueryParams(['layer', 'zoom', 'lon', 'lat', 'bbox']),
|
||||
checkStaticImageFormat(),
|
||||
namedMapProvider({
|
||||
namedMapProviderCache: this.namedMapProviderCache,
|
||||
label: 'STATIC_VIZ_MAP', forcedFormat: 'png'
|
||||
}),
|
||||
getTemplate({ label: 'STATIC_VIZ_MAP' }),
|
||||
prepareLayerFilterFromPreviewLayers({
|
||||
namedMapProviderCache: this.namedMapProviderCache,
|
||||
label: 'STATIC_VIZ_MAP'
|
||||
}),
|
||||
getStaticImageOptions({ tablesExtentBackend: this.tablesExtentBackend }),
|
||||
getImage({ previewBackend: this.previewBackend, label: 'STATIC_VIZ_MAP' }),
|
||||
setContentTypeHeader(),
|
||||
incrementMapViews({ metadataBackend: this.metadataBackend }),
|
||||
cacheControlHeader(),
|
||||
cacheChannelHeader(),
|
||||
surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
|
||||
lastModifiedHeader()
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
function getTemplate ({ label }) {
|
||||
return function getTemplateMiddleware (req, res, next) {
|
||||
const { mapConfigProvider } = res.locals;
|
||||
|
||||
mapConfigProvider.getTemplate((err, template) => {
|
||||
if (err) {
|
||||
err.label = label;
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.locals.template = template;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function prepareLayerFilterFromPreviewLayers ({ namedMapProviderCache, label }) {
|
||||
return function prepareLayerFilterFromPreviewLayersMiddleware (req, res, next) {
|
||||
const { template } = res.locals;
|
||||
const { config, auth_token } = req.query;
|
||||
|
||||
if (!template || !template.view || !template.view.preview_layers) {
|
||||
return next();
|
||||
}
|
||||
|
||||
var previewLayers = template.view.preview_layers;
|
||||
var layerVisibilityFilter = [];
|
||||
|
||||
template.layergroup.layers.forEach((layer, index) => {
|
||||
if (previewLayers[''+index] !== false && previewLayers[layer.id] !== false) {
|
||||
layerVisibilityFilter.push(''+index);
|
||||
}
|
||||
});
|
||||
|
||||
if (!layerVisibilityFilter.length) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const { user, token, cache_buster, api_key } = res.locals;
|
||||
const { dbuser, dbname, dbpassword, dbhost, dbport } = res.locals;
|
||||
const { template_id, format } = req.params;
|
||||
|
||||
const params = {
|
||||
user, token, cache_buster, api_key,
|
||||
dbuser, dbname, dbpassword, dbhost, dbport,
|
||||
template_id, format
|
||||
};
|
||||
|
||||
// overwrites 'all' default filter
|
||||
params.layer = layerVisibilityFilter.join(',');
|
||||
|
||||
// recreates the provider
|
||||
namedMapProviderCache.get(user, template_id, config, auth_token, params, (err, provider) => {
|
||||
if (err) {
|
||||
err.label = label;
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.locals.mapConfigProvider = provider;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function getStaticImageOptions ({ tablesExtentBackend }) {
|
||||
return function getStaticImageOptionsMiddleware(req, res, next) {
|
||||
const { user, mapConfigProvider, template } = res.locals;
|
||||
const { zoom, lon, lat, bbox } = req.query;
|
||||
const params = { zoom, lon, lat, bbox };
|
||||
|
||||
const imageOpts = getImageOptions(params, template);
|
||||
|
||||
if (imageOpts) {
|
||||
res.locals.imageOpts = imageOpts;
|
||||
return next();
|
||||
}
|
||||
|
||||
res.locals.imageOpts = DEFAULT_ZOOM_CENTER;
|
||||
|
||||
mapConfigProvider.createAffectedTables((err, affectedTables) => {
|
||||
if (err) {
|
||||
return next();
|
||||
}
|
||||
|
||||
var tables = affectedTables.tables || [];
|
||||
|
||||
if (tables.length === 0) {
|
||||
return next();
|
||||
}
|
||||
|
||||
tablesExtentBackend.getBounds(user, tables, (err, bounds) => {
|
||||
if (err) {
|
||||
return next();
|
||||
}
|
||||
|
||||
res.locals.imageOpts = bounds;
|
||||
|
||||
return next();
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function getImageOptions (params, template) {
|
||||
const { zoom, lon, lat, bbox } = params;
|
||||
|
||||
let imageOpts = getImageOptionsFromCoordinates(zoom, lon, lat);
|
||||
if (imageOpts) {
|
||||
return imageOpts;
|
||||
}
|
||||
|
||||
imageOpts = getImageOptionsFromBoundingBox(bbox);
|
||||
if (imageOpts) {
|
||||
return imageOpts;
|
||||
}
|
||||
|
||||
imageOpts = getImageOptionsFromTemplate(template, zoom);
|
||||
if (imageOpts) {
|
||||
return imageOpts;
|
||||
}
|
||||
}
|
||||
|
||||
function getImageOptionsFromCoordinates (zoom, lon, lat) {
|
||||
if ([zoom, lon, lat].map(numMapper).every(Number.isFinite)) {
|
||||
return {
|
||||
zoom: zoom,
|
||||
center: {
|
||||
lng: lon,
|
||||
lat: lat
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function getImageOptionsFromTemplate (template, zoom) {
|
||||
if (template.view) {
|
||||
var zoomCenter = templateZoomCenter(template.view);
|
||||
if (zoomCenter) {
|
||||
if (Number.isFinite(+zoom)) {
|
||||
zoomCenter.zoom = +zoom;
|
||||
}
|
||||
|
||||
return zoomCenter;
|
||||
}
|
||||
|
||||
var bounds = templateBounds(template.view);
|
||||
if (bounds) {
|
||||
return bounds;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getImageOptionsFromBoundingBox (bbox = '') {
|
||||
var _bbox = bbox.split(',').map(numMapper);
|
||||
|
||||
if (_bbox.length === 4 && _bbox.every(Number.isFinite)) {
|
||||
return {
|
||||
bounds: {
|
||||
west: _bbox[0],
|
||||
south: _bbox[1],
|
||||
east: _bbox[2],
|
||||
north: _bbox[3]
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getImage({ previewBackend, label }) {
|
||||
return function getImageMiddleware (req, res, next) {
|
||||
const { imageOpts, mapConfigProvider } = res.locals;
|
||||
const { zoom, center, bounds } = imageOpts;
|
||||
|
||||
let { width, height } = req.params;
|
||||
|
||||
width = +width;
|
||||
height = +height;
|
||||
|
||||
const format = req.params.format === 'jpg' ? 'jpeg' : 'png';
|
||||
|
||||
if (zoom !== undefined && center) {
|
||||
return previewBackend.getImage(mapConfigProvider, format, width, height, zoom, center,
|
||||
(err, image, headers, stats) => {
|
||||
req.profiler.add(stats);
|
||||
|
||||
if (err) {
|
||||
err.label = label;
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (headers) {
|
||||
res.set(headers);
|
||||
}
|
||||
|
||||
res.statusCode = 200;
|
||||
res.body = image;
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
previewBackend.getImage(mapConfigProvider, format, width, height, bounds, (err, image, headers, stats) => {
|
||||
req.profiler.add(stats);
|
||||
req.profiler.done('render-' + format);
|
||||
|
||||
if (err) {
|
||||
err.label = label;
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (headers) {
|
||||
res.set(headers);
|
||||
}
|
||||
|
||||
res.statusCode = 200;
|
||||
res.body = image;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function setContentTypeHeader () {
|
||||
return function setContentTypeHeaderMiddleware(req, res, next) {
|
||||
res.set('Content-Type', res.get('content-type') || res.get('Content-Type') || 'image/png');
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
function incrementMapViewsError (ctx) {
|
||||
return `ERROR: failed to increment mapview count for user '${ctx.user}': ${ctx.err}`;
|
||||
}
|
||||
|
||||
function incrementMapViews ({ metadataBackend }) {
|
||||
return function incrementMapViewsMiddleware(req, res, next) {
|
||||
const { user, mapConfigProvider } = res.locals;
|
||||
|
||||
mapConfigProvider.getMapConfig((err, mapConfig) => {
|
||||
if (err) {
|
||||
global.logger.log(incrementMapViewsError({ user, err }));
|
||||
return next();
|
||||
}
|
||||
|
||||
const statTag = mapConfig.obj().stat_tag;
|
||||
|
||||
metadataBackend.incMapviewCount(user, statTag, (err) => {
|
||||
if (err) {
|
||||
global.logger.log(incrementMapViewsError({ user, err }));
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function templateZoomCenter(view) {
|
||||
if (view.zoom !== undefined && view.center) {
|
||||
return {
|
||||
zoom: view.zoom,
|
||||
center: view.center
|
||||
};
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function templateBounds(view) {
|
||||
if (view.bounds) {
|
||||
var hasAllBounds = ['west', 'south', 'east', 'north'].every(prop => Number.isFinite(view.bounds[prop]));
|
||||
|
||||
if (hasAllBounds) {
|
||||
return {
|
||||
bounds: {
|
||||
west: view.bounds.west,
|
||||
south: view.bounds.south,
|
||||
east: view.bounds.east,
|
||||
north: view.bounds.north
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
170
lib/cartodb/api/map/tile-layergroup-controller.js
Normal file
170
lib/cartodb/api/map/tile-layergroup-controller.js
Normal file
@@ -0,0 +1,170 @@
|
||||
const layergroupToken = require('../middlewares/layergroup-token');
|
||||
const coordinates = require('../middlewares/coordinates');
|
||||
const cleanUpQueryParams = require('../middlewares/clean-up-query-params');
|
||||
const credentials = require('../middlewares/credentials');
|
||||
const dbConnSetup = require('../middlewares/db-conn-setup');
|
||||
const authorize = require('../middlewares/authorize');
|
||||
const rateLimit = require('../middlewares/rate-limit');
|
||||
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit;
|
||||
const createMapStoreMapConfigProvider = require('../middlewares/map-store-map-config-provider');
|
||||
const cacheControlHeader = require('../middlewares/cache-control-header');
|
||||
const cacheChannelHeader = require('../middlewares/cache-channel-header');
|
||||
const surrogateKeyHeader = require('../middlewares/surrogate-key-header');
|
||||
const lastModifiedHeader = require('../middlewares/last-modified-header');
|
||||
const vectorError = require('../middlewares/vector-error');
|
||||
|
||||
const SUPPORTED_FORMATS = {
|
||||
grid_json: true,
|
||||
json_torque: true,
|
||||
torque_json: true,
|
||||
png: true,
|
||||
png32: true,
|
||||
mvt: true
|
||||
};
|
||||
|
||||
module.exports = class TileLayergroupController {
|
||||
constructor (
|
||||
tileBackend,
|
||||
pgConnection,
|
||||
mapStore,
|
||||
userLimitsBackend,
|
||||
layergroupAffectedTablesCache,
|
||||
authBackend,
|
||||
surrogateKeysCache
|
||||
) {
|
||||
this.tileBackend = tileBackend;
|
||||
this.pgConnection = pgConnection;
|
||||
this.mapStore = mapStore;
|
||||
this.userLimitsBackend = userLimitsBackend;
|
||||
this.layergroupAffectedTablesCache = layergroupAffectedTablesCache;
|
||||
this.authBackend = authBackend;
|
||||
this.surrogateKeysCache = surrogateKeysCache;
|
||||
}
|
||||
|
||||
register (mapRouter) {
|
||||
// REGEXP: doesn't match with `val`
|
||||
const not = (val) => `(?!${val})([^\/]+?)`;
|
||||
|
||||
// Sadly the path that matches 1 also matches with 2 so we need to tell to express
|
||||
// that performs only the middlewares of the first path that matches
|
||||
// for that we use one array to group all paths.
|
||||
mapRouter.get([
|
||||
`/:token/:z/:x/:y@:scale_factor?x.:format`, // 1
|
||||
`/:token/:z/:x/:y.:format`, // 2
|
||||
`/:token${not('static')}/:layer/:z/:x/:y.(:format)`
|
||||
], this.middlewares());
|
||||
}
|
||||
|
||||
middlewares () {
|
||||
return [
|
||||
layergroupToken(),
|
||||
coordinates(),
|
||||
credentials(),
|
||||
authorize(this.authBackend),
|
||||
dbConnSetup(this.pgConnection),
|
||||
rateLimit(this.userLimitsBackend, RATE_LIMIT_ENDPOINTS_GROUPS.TILE),
|
||||
cleanUpQueryParams(),
|
||||
createMapStoreMapConfigProvider(
|
||||
this.mapStore,
|
||||
this.userLimitsBackend,
|
||||
this.pgConnection,
|
||||
this.layergroupAffectedTablesCache
|
||||
),
|
||||
getTile(this.tileBackend),
|
||||
cacheControlHeader(),
|
||||
cacheChannelHeader(),
|
||||
surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
|
||||
lastModifiedHeader(),
|
||||
incrementSuccessMetrics(global.statsClient),
|
||||
incrementErrorMetrics(global.statsClient),
|
||||
tileError(),
|
||||
vectorError()
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
function parseFormat (format = '') {
|
||||
const prettyFormat = format.replace('.', '_');
|
||||
return SUPPORTED_FORMATS[prettyFormat] ? prettyFormat : 'invalid';
|
||||
}
|
||||
|
||||
function getStatusCode(tile, format){
|
||||
return tile.length === 0 && format === 'mvt' ? 204 : 200;
|
||||
}
|
||||
|
||||
function getTile (tileBackend) {
|
||||
return function getTileMiddleware (req, res, next) {
|
||||
req.profiler.start(`windshaft.${req.params.layer ? 'maplayer_tile' : 'map_tile'}`);
|
||||
|
||||
const { mapConfigProvider } = res.locals;
|
||||
const { token } = res.locals;
|
||||
const { layer, z, x, y, format } = req.params;
|
||||
|
||||
const params = { token, layer, z, x, y, format };
|
||||
|
||||
tileBackend.getTile(mapConfigProvider, params, (err, tile, headers, stats = {}) => {
|
||||
req.profiler.add(stats);
|
||||
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (headers) {
|
||||
res.set(headers);
|
||||
}
|
||||
|
||||
const formatStat = parseFormat(req.params.format);
|
||||
|
||||
res.statusCode = getStatusCode(tile, formatStat);
|
||||
res.body = tile;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function incrementSuccessMetrics (statsClient) {
|
||||
return function incrementSuccessMetricsMiddleware (req, res, next) {
|
||||
const formatStat = parseFormat(req.params.format);
|
||||
|
||||
statsClient.increment('windshaft.tiles.success');
|
||||
statsClient.increment(`windshaft.tiles.${formatStat}.success`);
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
function incrementErrorMetrics (statsClient) {
|
||||
return function incrementErrorMetricsMiddleware (err, req, res, next) {
|
||||
const formatStat = parseFormat(req.params.format);
|
||||
|
||||
statsClient.increment('windshaft.tiles.error');
|
||||
statsClient.increment(`windshaft.tiles.${formatStat}.error`);
|
||||
|
||||
next(err);
|
||||
};
|
||||
}
|
||||
|
||||
function tileError () {
|
||||
return function tileErrorMiddleware (err, req, res, next) {
|
||||
if (err.message === 'Tile does not exist' && req.params.format === 'mvt') {
|
||||
res.statusCode = 204;
|
||||
return next();
|
||||
}
|
||||
|
||||
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
|
||||
let errMsg = err.message ? ( '' + err.message ) : ( '' + err );
|
||||
|
||||
// Rewrite mapnik parsing errors to start with layer number
|
||||
const matches = errMsg.match("(.*) in style 'layer([0-9]+)'");
|
||||
|
||||
if (matches) {
|
||||
errMsg = `style${matches[2]}: ${matches[1]}`;
|
||||
}
|
||||
|
||||
err.message = errMsg;
|
||||
err.label = 'TILE RENDER';
|
||||
|
||||
next(err);
|
||||
};
|
||||
}
|
||||
14
lib/cartodb/api/middlewares/augment-layergroup-data.js
Normal file
14
lib/cartodb/api/middlewares/augment-layergroup-data.js
Normal file
@@ -0,0 +1,14 @@
|
||||
const _ = require('underscore');
|
||||
|
||||
module.exports = function augmentLayergroupData () {
|
||||
return function augmentLayergroupDataMiddleware (req, res, next) {
|
||||
const layergroup = res.body;
|
||||
|
||||
// include in layergroup response the variables in serverMedata
|
||||
// those variables are useful to send to the client information
|
||||
// about how to reach this server or information about it
|
||||
_.extend(layergroup, global.environment.serverMetadata);
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
@@ -1,9 +1,8 @@
|
||||
module.exports = function authorizeMiddleware (authApi) {
|
||||
return function (req, res, next) {
|
||||
req.profiler.done('req2params.setup');
|
||||
|
||||
authApi.authorize(req, res, (err, authorized) => {
|
||||
module.exports = function authorize (authBackend) {
|
||||
return function authorizeMiddleware (req, res, next) {
|
||||
authBackend.authorize(req, res, (err, authorized) => {
|
||||
req.profiler.done('authorize');
|
||||
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
24
lib/cartodb/api/middlewares/cache-channel-header.js
Normal file
24
lib/cartodb/api/middlewares/cache-channel-header.js
Normal file
@@ -0,0 +1,24 @@
|
||||
module.exports = function setCacheChannelHeader () {
|
||||
return function setCacheChannelHeaderMiddleware (req, res, next) {
|
||||
if (req.method !== 'GET') {
|
||||
return next();
|
||||
}
|
||||
|
||||
const { mapConfigProvider } = res.locals;
|
||||
|
||||
mapConfigProvider.getAffectedTables((err, affectedTables) => {
|
||||
if (err) {
|
||||
global.logger.warn('ERROR generating Cache Channel Header:', err);
|
||||
return next();
|
||||
}
|
||||
|
||||
if (!affectedTables) {
|
||||
return next();
|
||||
}
|
||||
|
||||
res.set('X-Cache-Channel', affectedTables.getCacheChannel());
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
};
|
||||
19
lib/cartodb/api/middlewares/cache-control-header.js
Normal file
19
lib/cartodb/api/middlewares/cache-control-header.js
Normal file
@@ -0,0 +1,19 @@
|
||||
const ONE_YEAR_IN_SECONDS = 60 * 60 * 24 * 365;
|
||||
|
||||
module.exports = function setCacheControlHeader ({ ttl = ONE_YEAR_IN_SECONDS, revalidate = false } = {}) {
|
||||
return function setCacheControlHeaderMiddleware (req, res, next) {
|
||||
if (req.method !== 'GET') {
|
||||
return next();
|
||||
}
|
||||
|
||||
const directives = [ 'public', `max-age=${ttl}` ];
|
||||
|
||||
if (revalidate) {
|
||||
directives.push('must-revalidate');
|
||||
}
|
||||
|
||||
res.set('Cache-Control', directives.join(','));
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
11
lib/cartodb/api/middlewares/check-json-content-type.js
Normal file
11
lib/cartodb/api/middlewares/check-json-content-type.js
Normal file
@@ -0,0 +1,11 @@
|
||||
module.exports = function checkJsonContentType () {
|
||||
return function checkJsonContentTypeMiddleware(req, res, next) {
|
||||
if (req.method === 'POST' && !req.is('application/json')) {
|
||||
return next(new Error('POST data must be of type application/json'));
|
||||
}
|
||||
|
||||
req.profiler.done('checkJsonContentTypeMiddleware');
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
11
lib/cartodb/api/middlewares/check-static-image-format.js
Normal file
11
lib/cartodb/api/middlewares/check-static-image-format.js
Normal file
@@ -0,0 +1,11 @@
|
||||
const VALID_IMAGE_FORMATS = ['png', 'jpg'];
|
||||
|
||||
module.exports = function checkStaticImageFormat () {
|
||||
return function checkStaticImageFormatMiddleware (req, res, next) {
|
||||
if(!VALID_IMAGE_FORMATS.includes(req.params.format)) {
|
||||
return next(new Error(`Unsupported image format "${req.params.format}"`));
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
@@ -14,19 +14,16 @@ const REQUEST_QUERY_PARAMS_WHITELIST = [
|
||||
'filters' // json
|
||||
];
|
||||
|
||||
module.exports = function cleanUpQueryParamsMiddleware () {
|
||||
return function cleanUpQueryParams (req, res, next) {
|
||||
var allowedQueryParams = REQUEST_QUERY_PARAMS_WHITELIST;
|
||||
module.exports = function cleanUpQueryParamsMiddleware (customQueryParams = []) {
|
||||
if (!Array.isArray(customQueryParams)) {
|
||||
throw new Error('customQueryParams must receive an Array of params');
|
||||
}
|
||||
|
||||
if (Array.isArray(res.locals.allowedQueryParams)) {
|
||||
allowedQueryParams = allowedQueryParams.concat(res.locals.allowedQueryParams);
|
||||
}
|
||||
return function cleanUpQueryParams (req, res, next) {
|
||||
const allowedQueryParams = [...REQUEST_QUERY_PARAMS_WHITELIST, ...customQueryParams];
|
||||
|
||||
req.query = _.pick(req.query, allowedQueryParams);
|
||||
|
||||
// bring all query values onto res.locals object
|
||||
_.extend(res.locals, req.query);
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
41
lib/cartodb/api/middlewares/coordinates.js
Normal file
41
lib/cartodb/api/middlewares/coordinates.js
Normal file
@@ -0,0 +1,41 @@
|
||||
const positiveIntegerNumberRegExp = /^\d+$/;
|
||||
const integerNumberRegExp = /^-?\d+$/;
|
||||
const invalidZoomMessage = function (zoom) {
|
||||
return `Invalid zoom value (${zoom}). It should be an integer number greather than or equal to 0`;
|
||||
};
|
||||
const invalidCoordXMessage = function (x) {
|
||||
return `Invalid coodinate 'x' value (${x}). It should be an integer number`;
|
||||
};
|
||||
const invalidCoordYMessage = function (y) {
|
||||
return `Invalid coodinate 'y' value (${y}). It should be an integer number greather than or equal to 0`;
|
||||
};
|
||||
|
||||
module.exports = function coordinates (validate = { z: true, x: true, y: true }) {
|
||||
return function coordinatesMiddleware (req, res, next) {
|
||||
const { z, x, y } = req.params;
|
||||
|
||||
if (validate.z && !positiveIntegerNumberRegExp.test(z)) {
|
||||
const err = new Error(invalidZoomMessage(z));
|
||||
err.http_status = 400;
|
||||
|
||||
return next(err);
|
||||
}
|
||||
|
||||
// Negative values for x param are valid. The x param is wrapped
|
||||
if (validate.x && !integerNumberRegExp.test(x)) {
|
||||
const err = new Error(invalidCoordXMessage(x));
|
||||
err.http_status = 400;
|
||||
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (validate.y && !positiveIntegerNumberRegExp.test(y)) {
|
||||
const err = new Error(invalidCoordYMessage(y));
|
||||
err.http_status = 400;
|
||||
|
||||
return next(err);
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
18
lib/cartodb/api/middlewares/cors.js
Normal file
18
lib/cartodb/api/middlewares/cors.js
Normal file
@@ -0,0 +1,18 @@
|
||||
module.exports = function cors () {
|
||||
return function corsMiddleware (req, res, next) {
|
||||
const headers = [
|
||||
'X-Requested-With',
|
||||
'X-Prototype-Version',
|
||||
'X-CSRF-Token'
|
||||
];
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
headers.push('Content-Type');
|
||||
}
|
||||
|
||||
res.set("Access-Control-Allow-Origin", "*");
|
||||
res.set("Access-Control-Allow-Headers", headers.join(', '));
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
85
lib/cartodb/api/middlewares/credentials.js
Normal file
85
lib/cartodb/api/middlewares/credentials.js
Normal file
@@ -0,0 +1,85 @@
|
||||
const basicAuth = require('basic-auth');
|
||||
|
||||
module.exports = function credentials () {
|
||||
return function credentialsMiddleware(req, res, next) {
|
||||
const apikeyCredentials = getApikeyCredentialsFromRequest(req);
|
||||
|
||||
res.locals.api_key = apikeyCredentials.token;
|
||||
res.locals.basicAuthUsername = apikeyCredentials.username;
|
||||
res.set('vary', 'Authorization'); //Honor Authorization header when caching.
|
||||
|
||||
return next();
|
||||
};
|
||||
};
|
||||
|
||||
function getApikeyCredentialsFromRequest(req) {
|
||||
let apikeyCredentials = {
|
||||
token: null,
|
||||
username: null,
|
||||
};
|
||||
|
||||
for (let getter of apikeyGetters) {
|
||||
apikeyCredentials = getter(req);
|
||||
if (apikeyTokenFound(apikeyCredentials)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return apikeyCredentials;
|
||||
}
|
||||
|
||||
const apikeyGetters = [
|
||||
getApikeyTokenFromHeaderAuthorization,
|
||||
getApikeyTokenFromRequestQueryString,
|
||||
getApikeyTokenFromRequestBody,
|
||||
];
|
||||
|
||||
function getApikeyTokenFromHeaderAuthorization(req) {
|
||||
const credentials = basicAuth(req);
|
||||
|
||||
if (credentials) {
|
||||
return {
|
||||
username: credentials.username,
|
||||
token: credentials.pass
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
username: null,
|
||||
token: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getApikeyTokenFromRequestQueryString(req) {
|
||||
let token = null;
|
||||
|
||||
if (req.query && req.query.api_key) {
|
||||
token = req.query.api_key;
|
||||
} else if (req.query && req.query.map_key) {
|
||||
token = req.query.map_key;
|
||||
}
|
||||
|
||||
return {
|
||||
username: null,
|
||||
token: token,
|
||||
};
|
||||
}
|
||||
|
||||
function getApikeyTokenFromRequestBody(req) {
|
||||
let token = null;
|
||||
|
||||
if (req.body && req.body.api_key) {
|
||||
token = req.body.api_key;
|
||||
} else if (req.body && req.body.map_key) {
|
||||
token = req.body.map_key;
|
||||
}
|
||||
|
||||
return {
|
||||
username: null,
|
||||
token: token,
|
||||
};
|
||||
}
|
||||
|
||||
function apikeyTokenFound(apikey) {
|
||||
return !!apikey && !!apikey.token;
|
||||
}
|
||||
@@ -1,31 +1,30 @@
|
||||
const _ = require('underscore');
|
||||
|
||||
module.exports = function dbConnSetupMiddleware(pgConnection) {
|
||||
return function dbConnSetup(req, res, next) {
|
||||
const user = res.locals.user;
|
||||
module.exports = function dbConnSetup (pgConnection) {
|
||||
return function dbConnSetupMiddleware (req, res, next) {
|
||||
const { user } = res.locals;
|
||||
|
||||
pgConnection.setDBConn(user, res.locals, (err) => {
|
||||
req.profiler.done('dbConnSetup');
|
||||
|
||||
if (err) {
|
||||
if (err.message && -1 !== err.message.indexOf('name not found')) {
|
||||
err.http_status = 404;
|
||||
}
|
||||
req.profiler.done('req2params');
|
||||
|
||||
return next(err);
|
||||
}
|
||||
|
||||
// Add default database connection parameters
|
||||
// if none given
|
||||
_.defaults(res.locals, {
|
||||
dbuser: global.environment.postgres.user,
|
||||
dbpassword: global.environment.postgres.password,
|
||||
dbhost: global.environment.postgres.host,
|
||||
dbport: global.environment.postgres.port
|
||||
});
|
||||
|
||||
|
||||
res.set('X-Served-By-DB-Host', res.locals.dbhost);
|
||||
|
||||
req.profiler.done('req2params');
|
||||
|
||||
next(null);
|
||||
next();
|
||||
});
|
||||
};
|
||||
};
|
||||
@@ -15,10 +15,6 @@ module.exports = function errorMiddleware (/* options */) {
|
||||
|
||||
var statusCode = findStatusCode(err);
|
||||
|
||||
if (err.message === 'Tile does not exist' && res.locals.format === 'mvt') {
|
||||
statusCode = 204;
|
||||
}
|
||||
|
||||
setErrorHeader(allErrors, statusCode, res);
|
||||
debug('[%s ERROR] -- %d: %s, %s', label, statusCode, err, err.stack);
|
||||
|
||||
@@ -50,26 +46,39 @@ function isDatasourceTimeoutError (err) {
|
||||
return err.message && err.message.match(/canceling statement due to statement timeout/i);
|
||||
}
|
||||
|
||||
function isTimeoutError (err) {
|
||||
return isRenderTimeoutError(err) || isDatasourceTimeoutError(err);
|
||||
function isTimeoutError (errorTypes) {
|
||||
return errorTypes.renderTimeoutError || errorTypes.datasourceTimeoutError;
|
||||
}
|
||||
|
||||
function getErrorTypes(error) {
|
||||
return {
|
||||
renderTimeoutError: isRenderTimeoutError(error),
|
||||
datasourceTimeoutError: isDatasourceTimeoutError(error),
|
||||
};
|
||||
}
|
||||
|
||||
function populateTimeoutErrors (errors) {
|
||||
return errors.map(function (error) {
|
||||
if (isRenderTimeoutError(error)) {
|
||||
error.subtype = 'render';
|
||||
}
|
||||
const errorTypes = getErrorTypes(error);
|
||||
|
||||
if (isDatasourceTimeoutError(error)) {
|
||||
error.subtype = 'datasource';
|
||||
}
|
||||
|
||||
if (isTimeoutError(error)) {
|
||||
if (isTimeoutError(errorTypes)) {
|
||||
error.message = 'You are over platform\'s limits. Please contact us to know more details';
|
||||
error.type = 'limit';
|
||||
error.http_status = 429;
|
||||
}
|
||||
|
||||
if (errorTypes.datasourceTimeoutError) {
|
||||
error.subtype = 'datasource';
|
||||
error.message = 'You are over platform\'s limits: SQL query timeout error.' +
|
||||
' Refactor your query before running again or contact CARTO support for more details.';
|
||||
}
|
||||
|
||||
if (errorTypes.renderTimeoutError) {
|
||||
error.subtype = 'render';
|
||||
error.message = 'You are over platform\'s limits: Render timeout error.' +
|
||||
' Contact CARTO support for more details.';
|
||||
}
|
||||
|
||||
return error;
|
||||
});
|
||||
}
|
||||
@@ -186,15 +195,15 @@ function setErrorHeader(errors, statusCode, res) {
|
||||
subtype: error.subtype
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
res.set('X-Tiler-Errors', stringifyForLogs(errorsLog));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove problematic nested characters
|
||||
* Remove problematic nested characters
|
||||
* from object for logs RegEx
|
||||
*
|
||||
* @param {Object} object
|
||||
*
|
||||
* @param {Object} object
|
||||
*/
|
||||
function stringifyForLogs(object) {
|
||||
Object.keys(object).map(key => {
|
||||
16
lib/cartodb/api/middlewares/increment-map-view-count.js
Normal file
16
lib/cartodb/api/middlewares/increment-map-view-count.js
Normal file
@@ -0,0 +1,16 @@
|
||||
module.exports = function incrementMapViewCount (metadataBackend) {
|
||||
return function incrementMapViewCountMiddleware(req, res, next) {
|
||||
const { mapConfig, user } = res.locals;
|
||||
|
||||
// Error won't blow up, just be logged.
|
||||
metadataBackend.incMapviewCount(user, mapConfig.obj().stat_tag, (err) => {
|
||||
req.profiler.done('incMapviewCount');
|
||||
|
||||
if (err) {
|
||||
global.logger.log(`ERROR: failed to increment mapview count for user '${user}': ${err.message}`);
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
};
|
||||
9
lib/cartodb/api/middlewares/init-profiler.js
Normal file
9
lib/cartodb/api/middlewares/init-profiler.js
Normal file
@@ -0,0 +1,9 @@
|
||||
module.exports = function initProfiler (isTemplateInstantiation) {
|
||||
const operation = isTemplateInstantiation ? 'instance_template' : 'createmap';
|
||||
|
||||
return function initProfilerMiddleware (req, res, next) {
|
||||
req.profiler.start(`windshaft-cartodb.${operation}_${req.method.toLowerCase()}`);
|
||||
req.profiler.done(`${operation}.initProfilerMiddleware`);
|
||||
next();
|
||||
};
|
||||
};
|
||||
9
lib/cartodb/api/middlewares/initialize-status-code.js
Normal file
9
lib/cartodb/api/middlewares/initialize-status-code.js
Normal file
@@ -0,0 +1,9 @@
|
||||
module.exports = function initializeStatusCode () {
|
||||
return function initializeStatusCodeMiddleware (req, res, next) {
|
||||
if (req.method !== 'OPTIONS') {
|
||||
res.statusCode = 404;
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
38
lib/cartodb/api/middlewares/last-modified-header.js
Normal file
38
lib/cartodb/api/middlewares/last-modified-header.js
Normal file
@@ -0,0 +1,38 @@
|
||||
module.exports = function setLastModifiedHeader () {
|
||||
return function setLastModifiedHeaderMiddleware(req, res, next) {
|
||||
if (req.method !== 'GET') {
|
||||
return next();
|
||||
}
|
||||
|
||||
const { mapConfigProvider, cache_buster } = res.locals;
|
||||
|
||||
if (cache_buster) {
|
||||
const cacheBuster = parseInt(cache_buster, 10);
|
||||
const lastModifiedDate = Number.isFinite(cacheBuster) ? new Date(cacheBuster) : new Date();
|
||||
|
||||
res.set('Last-Modified', lastModifiedDate.toUTCString());
|
||||
|
||||
return next();
|
||||
}
|
||||
|
||||
mapConfigProvider.getAffectedTables((err, affectedTables) => {
|
||||
if (err) {
|
||||
global.logger.warn('ERROR generating Last Modified Header:', err);
|
||||
return next();
|
||||
}
|
||||
|
||||
if (!affectedTables) {
|
||||
res.set('Last-Modified', new Date().toUTCString());
|
||||
|
||||
return next();
|
||||
}
|
||||
|
||||
const lastUpdatedAt = affectedTables.getLastUpdatedAt();
|
||||
const lastModifiedDate = Number.isFinite(lastUpdatedAt) ? new Date(lastUpdatedAt) : new Date();
|
||||
|
||||
res.set('Last-Modified', lastModifiedDate.toUTCString());
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
};
|
||||
39
lib/cartodb/api/middlewares/last-updated-time-layergroup.js
Normal file
39
lib/cartodb/api/middlewares/last-updated-time-layergroup.js
Normal file
@@ -0,0 +1,39 @@
|
||||
module.exports = function setLastUpdatedTimeToLayergroup () {
|
||||
return function setLastUpdatedTimeToLayergroupMiddleware (req, res, next) {
|
||||
const { mapConfigProvider, analysesResults } = res.locals;
|
||||
const layergroup = res.body;
|
||||
|
||||
mapConfigProvider.createAffectedTables((err, affectedTables) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (!affectedTables) {
|
||||
return next();
|
||||
}
|
||||
|
||||
var lastUpdateTime = affectedTables.getLastUpdatedAt();
|
||||
|
||||
lastUpdateTime = getLastUpdatedTime(analysesResults, lastUpdateTime) || lastUpdateTime;
|
||||
|
||||
// last update for layergroup cache buster
|
||||
layergroup.layergroupid = layergroup.layergroupid + ':' + lastUpdateTime;
|
||||
layergroup.last_updated = new Date(lastUpdateTime).toISOString();
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
function getLastUpdatedTime(analysesResults, lastUpdateTime) {
|
||||
if (!Array.isArray(analysesResults)) {
|
||||
return lastUpdateTime;
|
||||
}
|
||||
return analysesResults.reduce(function(lastUpdateTime, analysis) {
|
||||
return analysis.getNodes().reduce(function(lastNodeUpdatedAtTime, node) {
|
||||
var nodeUpdatedAtDate = node.getUpdatedAt();
|
||||
var nodeUpdatedTimeAt = (nodeUpdatedAtDate && nodeUpdatedAtDate.getTime()) || 0;
|
||||
return nodeUpdatedTimeAt > lastNodeUpdatedAtTime ? nodeUpdatedTimeAt : lastNodeUpdatedAtTime;
|
||||
}, lastUpdateTime);
|
||||
}, lastUpdateTime);
|
||||
}
|
||||
26
lib/cartodb/api/middlewares/layer-stats.js
Normal file
26
lib/cartodb/api/middlewares/layer-stats.js
Normal file
@@ -0,0 +1,26 @@
|
||||
module.exports = function setLayerStats (pgConnection, statsBackend) {
|
||||
return function setLayerStatsMiddleware(req, res, next) {
|
||||
const { user, mapConfig } = res.locals;
|
||||
const layergroup = res.body;
|
||||
|
||||
pgConnection.getConnection(user, (err, connection) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
statsBackend.getStats(mapConfig, connection, function(err, layersStats) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (layersStats.length > 0) {
|
||||
layergroup.metadata.layers.forEach(function (layer, index) {
|
||||
layer.meta.stats = layersStats[index];
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
});
|
||||
};
|
||||
};
|
||||
15
lib/cartodb/api/middlewares/layergroup-id-header.js
Normal file
15
lib/cartodb/api/middlewares/layergroup-id-header.js
Normal file
@@ -0,0 +1,15 @@
|
||||
module.exports = function setLayergroupIdHeader (templateMaps, useTemplateHash) {
|
||||
return function setLayergroupIdHeaderMiddleware (req, res, next) {
|
||||
const { user, template } = res.locals;
|
||||
const layergroup = res.body;
|
||||
|
||||
if (useTemplateHash) {
|
||||
var templateHash = templateMaps.fingerPrint(template).substring(0, 8);
|
||||
layergroup.layergroupid = `${user}@${templateHash}@${layergroup.layergroupid}`;
|
||||
}
|
||||
|
||||
res.set('X-Layergroup-Id', layergroup.layergroupid);
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
15
lib/cartodb/api/middlewares/layergroup-metadata.js
Normal file
15
lib/cartodb/api/middlewares/layergroup-metadata.js
Normal file
@@ -0,0 +1,15 @@
|
||||
module.exports = function setMetadataToLayergroup (layergroupMetadata, includeQuery) {
|
||||
return function setMetadataToLayergroupMiddleware (req, res, next) {
|
||||
const { user, mapConfig, analysesResults = [], context, api_key: userApiKey } = res.locals;
|
||||
const layergroup = res.body;
|
||||
|
||||
layergroupMetadata.addDataviewsAndWidgetsUrls(user, layergroup, mapConfig.obj());
|
||||
layergroupMetadata.addAnalysesMetadata(user, layergroup, analysesResults, includeQuery);
|
||||
layergroupMetadata.addTurboCartoContextMetadata(layergroup, mapConfig.obj(), context);
|
||||
layergroupMetadata.addAggregationContextMetadata(layergroup, mapConfig.obj(), context);
|
||||
layergroupMetadata.addDateWrappingMetadata (layergroup, mapConfig.obj());
|
||||
layergroupMetadata.addTileJsonMetadata(layergroup, user, mapConfig, userApiKey);
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
28
lib/cartodb/api/middlewares/layergroup-token.js
Normal file
28
lib/cartodb/api/middlewares/layergroup-token.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const LayergroupToken = require('../../models/layergroup-token');
|
||||
const authErrorMessageTemplate = function (signer, user) {
|
||||
return `Cannot use map signature of user "${signer}" on db of user "${user}"`;
|
||||
};
|
||||
|
||||
module.exports = function layergroupToken () {
|
||||
return function layergroupTokenMiddleware (req, res, next) {
|
||||
const user = res.locals.user;
|
||||
const layergroupToken = LayergroupToken.parse(req.params.token);
|
||||
|
||||
res.locals.token = layergroupToken.token;
|
||||
res.locals.cache_buster = layergroupToken.cacheBuster;
|
||||
|
||||
if (layergroupToken.signer) {
|
||||
res.locals.signer = layergroupToken.signer;
|
||||
|
||||
if (res.locals.signer !== user) {
|
||||
const err = new Error(authErrorMessageTemplate(res.locals.signer, user));
|
||||
err.type = 'auth';
|
||||
err.http_status = (req.query && req.query.callback) ? 200: 403;
|
||||
|
||||
return next(err);
|
||||
}
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
};
|
||||
22
lib/cartodb/api/middlewares/logger.js
Normal file
22
lib/cartodb/api/middlewares/logger.js
Normal file
@@ -0,0 +1,22 @@
|
||||
module.exports = function logger (options) {
|
||||
if (!global.log4js || !options.log_format) {
|
||||
return function dummyLoggerMiddleware (req, res, next) {
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
const opts = {
|
||||
level: 'info',
|
||||
// Allowing for unbuffered logging is mainly
|
||||
// used to avoid hanging during unit testing.
|
||||
// TODO: provide an explicit teardown function instead,
|
||||
// releasing any event handler or timer set by
|
||||
// this component.
|
||||
buffer: !options.unbuffered_logging,
|
||||
// optional log format
|
||||
format: options.log_format
|
||||
};
|
||||
const logger = global.log4js.getLogger();
|
||||
|
||||
return global.log4js.connectLogger(logger, opts);
|
||||
};
|
||||
33
lib/cartodb/api/middlewares/lzma.js
Normal file
33
lib/cartodb/api/middlewares/lzma.js
Normal file
@@ -0,0 +1,33 @@
|
||||
const LZMA = require('lzma').LZMA;
|
||||
|
||||
module.exports = function lzma () {
|
||||
const lzmaWorker = new LZMA();
|
||||
|
||||
return function lzmaMiddleware (req, res, next) {
|
||||
if (!req.query.hasOwnProperty('lzma')) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Decode (from base64)
|
||||
var lzma = new Buffer(req.query.lzma, 'base64')
|
||||
.toString('binary')
|
||||
.split('')
|
||||
.map(function(c) {
|
||||
return c.charCodeAt(0) - 128;
|
||||
});
|
||||
|
||||
// Decompress
|
||||
lzmaWorker.decompress(lzma, function(result) {
|
||||
try {
|
||||
delete req.query.lzma;
|
||||
Object.assign(req.query, JSON.parse(result));
|
||||
|
||||
req.profiler.done('lzma');
|
||||
|
||||
next();
|
||||
} catch (err) {
|
||||
next(new Error('Error parsing lzma as JSON: ' + err));
|
||||
}
|
||||
});
|
||||
};
|
||||
};
|
||||
35
lib/cartodb/api/middlewares/map-error.js
Normal file
35
lib/cartodb/api/middlewares/map-error.js
Normal file
@@ -0,0 +1,35 @@
|
||||
module.exports = function mapError (options) {
|
||||
const { addContext = false, label = 'MAPS CONTROLLER' } = options;
|
||||
|
||||
return function mapErrorMiddleware (err, req, res, next) {
|
||||
req.profiler.done('error');
|
||||
const { mapConfig } = res.locals;
|
||||
|
||||
if (addContext) {
|
||||
err = Number.isFinite(err.layerIndex) ? populateError(err, mapConfig) : err;
|
||||
}
|
||||
|
||||
err.label = label;
|
||||
|
||||
next(err);
|
||||
};
|
||||
};
|
||||
|
||||
function populateError(err, mapConfig) {
|
||||
var error = new Error(err.message);
|
||||
error.http_status = err.http_status;
|
||||
|
||||
if (!err.http_status && err.message.indexOf('column "the_geom_webmercator" does not exist') >= 0) {
|
||||
error.http_status = 400;
|
||||
}
|
||||
|
||||
error.type = 'layer';
|
||||
error.subtype = err.message.indexOf('Postgis Plugin') >= 0 ? 'query' : undefined;
|
||||
error.layer = {
|
||||
id: mapConfig.getLayerId(err.layerIndex),
|
||||
index: err.layerIndex,
|
||||
type: mapConfig.layerType(err.layerIndex)
|
||||
};
|
||||
|
||||
return error;
|
||||
}
|
||||
38
lib/cartodb/api/middlewares/map-store-map-config-provider.js
Normal file
38
lib/cartodb/api/middlewares/map-store-map-config-provider.js
Normal file
@@ -0,0 +1,38 @@
|
||||
const MapStoreMapConfigProvider = require('../../models/mapconfig/provider/map-store-provider');
|
||||
|
||||
module.exports = function createMapStoreMapConfigProvider (
|
||||
mapStore,
|
||||
userLimitsBackend,
|
||||
pgConnection,
|
||||
affectedTablesCache,
|
||||
forcedFormat = null
|
||||
) {
|
||||
return function createMapStoreMapConfigProviderMiddleware (req, res, next) {
|
||||
const { user, token, cache_buster, api_key } = res.locals;
|
||||
const { dbuser, dbname, dbpassword, dbhost, dbport } = res.locals;
|
||||
const { layer: layerFromParams, z, x, y, scale_factor, format } = req.params;
|
||||
const { layer: layerFromQuery } = req.query;
|
||||
|
||||
const params = {
|
||||
user, token, cache_buster, api_key,
|
||||
dbuser, dbname, dbpassword, dbhost, dbport,
|
||||
layer: (layerFromQuery || layerFromParams), z, x, y, scale_factor, format
|
||||
};
|
||||
|
||||
if (forcedFormat) {
|
||||
params.format = forcedFormat;
|
||||
params.layer = params.layer || 'all';
|
||||
}
|
||||
|
||||
res.locals.mapConfigProvider = new MapStoreMapConfigProvider(
|
||||
mapStore,
|
||||
user,
|
||||
userLimitsBackend,
|
||||
pgConnection,
|
||||
affectedTablesCache,
|
||||
params
|
||||
);
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
32
lib/cartodb/api/middlewares/named-map-provider.js
Normal file
32
lib/cartodb/api/middlewares/named-map-provider.js
Normal file
@@ -0,0 +1,32 @@
|
||||
module.exports = function getNamedMapProvider ({ namedMapProviderCache, label, forcedFormat = null }) {
|
||||
return function getNamedMapProviderMiddleware (req, res, next) {
|
||||
const { user, token, cache_buster, api_key } = res.locals;
|
||||
const { dbuser, dbname, dbpassword, dbhost, dbport } = res.locals;
|
||||
const { template_id, layer: layerFromParams, z, x, y, format } = req.params;
|
||||
const { layer: layerFromQuery } = req.query;
|
||||
|
||||
const params = {
|
||||
user, token, cache_buster, api_key,
|
||||
dbuser, dbname, dbpassword, dbhost, dbport,
|
||||
template_id, layer: (layerFromQuery || layerFromParams), z, x, y, format
|
||||
};
|
||||
|
||||
if (forcedFormat) {
|
||||
params.format = forcedFormat;
|
||||
params.layer = params.layer || 'all';
|
||||
}
|
||||
|
||||
const { config, auth_token } = req.query;
|
||||
|
||||
namedMapProviderCache.get(user, template_id, config, auth_token, params, (err, namedMapProvider) => {
|
||||
if (err) {
|
||||
err.label = label;
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.locals.mapConfigProvider = namedMapProvider;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
};
|
||||
5
lib/cartodb/api/middlewares/noop.js
Normal file
5
lib/cartodb/api/middlewares/noop.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = function noop () {
|
||||
return function noopMiddleware (req, res, next) {
|
||||
next();
|
||||
};
|
||||
};
|
||||
72
lib/cartodb/api/middlewares/rate-limit.js
Normal file
72
lib/cartodb/api/middlewares/rate-limit.js
Normal file
@@ -0,0 +1,72 @@
|
||||
'use strict';
|
||||
|
||||
const RATE_LIMIT_ENDPOINTS_GROUPS = {
|
||||
ANONYMOUS: 'anonymous',
|
||||
STATIC: 'static',
|
||||
STATIC_NAMED: 'static_named',
|
||||
DATAVIEW: 'dataview',
|
||||
DATAVIEW_SEARCH: 'dataview_search',
|
||||
ANALYSIS: 'analysis',
|
||||
ANALYSIS_CATALOG: 'analysis_catalog',
|
||||
TILE: 'tile',
|
||||
ATTRIBUTES: 'attributes',
|
||||
NAMED_LIST: 'named_list',
|
||||
NAMED_CREATE: 'named_create',
|
||||
NAMED_GET: 'named_get',
|
||||
NAMED: 'named',
|
||||
NAMED_UPDATE: 'named_update',
|
||||
NAMED_DELETE: 'named_delete',
|
||||
NAMED_TILES: 'named_tiles'
|
||||
};
|
||||
|
||||
function rateLimit(userLimitsBackend, endpointGroup = null) {
|
||||
if (!isRateLimitEnabled(endpointGroup)) {
|
||||
return function rateLimitDisabledMiddleware(req, res, next) { next(); };
|
||||
}
|
||||
|
||||
return function rateLimitMiddleware(req, res, next) {
|
||||
userLimitsBackend.getRateLimit(res.locals.user, endpointGroup, function (err, userRateLimit) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (!userRateLimit) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const [isBlocked, limit, remaining, retry, reset] = userRateLimit;
|
||||
|
||||
res.set({
|
||||
'Carto-Rate-Limit-Limit': limit,
|
||||
'Carto-Rate-Limit-Remaining': remaining,
|
||||
'Carto-Rate-Limit-Reset': reset
|
||||
});
|
||||
|
||||
if (isBlocked) {
|
||||
// retry is floor rounded in seconds by redis-cell
|
||||
res.set('Retry-After', retry + 1);
|
||||
|
||||
let rateLimitError = new Error(
|
||||
'You are over platform\'s limits: too many requests.' +
|
||||
' Please contact us to know more details'
|
||||
);
|
||||
rateLimitError.http_status = 429;
|
||||
rateLimitError.type = 'limit';
|
||||
rateLimitError.subtype = 'rate-limit';
|
||||
return next(rateLimitError);
|
||||
}
|
||||
|
||||
return next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function isRateLimitEnabled(endpointGroup) {
|
||||
return global.environment.enabledFeatures.rateLimitsEnabled &&
|
||||
endpointGroup &&
|
||||
global.environment.enabledFeatures.rateLimitsByEndpoint[endpointGroup];
|
||||
}
|
||||
|
||||
module.exports = rateLimit;
|
||||
module.exports.RATE_LIMIT_ENDPOINTS_GROUPS = RATE_LIMIT_ENDPOINTS_GROUPS;
|
||||
17
lib/cartodb/api/middlewares/send-response.js
Normal file
17
lib/cartodb/api/middlewares/send-response.js
Normal file
@@ -0,0 +1,17 @@
|
||||
module.exports = function sendResponse () {
|
||||
return function sendResponseMiddleware (req, res) {
|
||||
req.profiler.done('res');
|
||||
|
||||
res.status(res.statusCode);
|
||||
|
||||
if (Buffer.isBuffer(res.body)) {
|
||||
return res.send(res.body);
|
||||
}
|
||||
|
||||
if (req.query.callback) {
|
||||
return res.jsonp(res.body);
|
||||
}
|
||||
|
||||
res.json(res.body);
|
||||
};
|
||||
};
|
||||
11
lib/cartodb/api/middlewares/served-by-host-header.js
Normal file
11
lib/cartodb/api/middlewares/served-by-host-header.js
Normal file
@@ -0,0 +1,11 @@
|
||||
const os = require('os');
|
||||
|
||||
module.exports = function servedByHostHeader () {
|
||||
const hostname = os.hostname().split('.')[0];
|
||||
|
||||
return function servedByHostHeaderMiddleware (req, res, next) {
|
||||
res.set('X-Served-By-Host', hostname);
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
@@ -1,11 +1,11 @@
|
||||
const Profiler = require('../stats/profiler_proxy');
|
||||
const Profiler = require('../../stats/profiler_proxy');
|
||||
const debug = require('debug')('windshaft:cartodb:stats');
|
||||
const onHeaders = require('on-headers');
|
||||
|
||||
module.exports = function statsMiddleware(options) {
|
||||
module.exports = function stats (options) {
|
||||
const { enabled = true, statsClient } = options;
|
||||
|
||||
return function stats(req, res, next) {
|
||||
return function statsMiddleware (req, res, next) {
|
||||
req.profiler = new Profiler({
|
||||
statsd_client: statsClient,
|
||||
profile: enabled
|
||||
31
lib/cartodb/api/middlewares/surrogate-key-header.js
Normal file
31
lib/cartodb/api/middlewares/surrogate-key-header.js
Normal file
@@ -0,0 +1,31 @@
|
||||
const NamedMapsCacheEntry = require('../../cache/model/named_maps_entry');
|
||||
const NamedMapMapConfigProvider = require('../../models/mapconfig/provider/named-map-provider');
|
||||
|
||||
module.exports = function setSurrogateKeyHeader ({ surrogateKeysCache }) {
|
||||
return function setSurrogateKeyHeaderMiddleware(req, res, next) {
|
||||
const { user, mapConfigProvider } = res.locals;
|
||||
|
||||
if (mapConfigProvider instanceof NamedMapMapConfigProvider) {
|
||||
surrogateKeysCache.tag(res, new NamedMapsCacheEntry(user, mapConfigProvider.getTemplateName()));
|
||||
}
|
||||
|
||||
if (req.method !== 'GET') {
|
||||
return next();
|
||||
}
|
||||
|
||||
mapConfigProvider.getAffectedTables((err, affectedTables) => {
|
||||
if (err) {
|
||||
global.logger.warn('ERROR generating Surrogate Key Header:', err);
|
||||
return next();
|
||||
}
|
||||
|
||||
if (!affectedTables || !affectedTables.tables || affectedTables.tables.length === 0) {
|
||||
return next();
|
||||
}
|
||||
|
||||
surrogateKeysCache.tag(res, affectedTables);
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
};
|
||||
10
lib/cartodb/api/middlewares/syntax-error.js
Normal file
10
lib/cartodb/api/middlewares/syntax-error.js
Normal file
@@ -0,0 +1,10 @@
|
||||
module.exports = function syntaxError () {
|
||||
return function syntaxErrorMiddleware (err, req, res, next) {
|
||||
if (err.name === 'SyntaxError') {
|
||||
err.http_status = 400;
|
||||
err.message = `${err.name}: ${err.message}`;
|
||||
}
|
||||
|
||||
next(err);
|
||||
};
|
||||
};
|
||||
11
lib/cartodb/api/middlewares/user.js
Normal file
11
lib/cartodb/api/middlewares/user.js
Normal file
@@ -0,0 +1,11 @@
|
||||
const CdbRequest = require('../../models/cdb_request');
|
||||
|
||||
module.exports = function user () {
|
||||
const cdbRequest = new CdbRequest();
|
||||
|
||||
return function userMiddleware(req, res, next) {
|
||||
res.locals.user = cdbRequest.userByReq(req);
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
@@ -1,12 +1,11 @@
|
||||
const fs = require('fs');
|
||||
|
||||
const timeoutErrorVectorTile = fs.readFileSync(__dirname + '/../../../assets/render-timeout-fallback.mvt');
|
||||
const timeoutErrorVectorTile = fs.readFileSync(__dirname + '/../../../../assets/render-timeout-fallback.mvt');
|
||||
|
||||
module.exports = function vectorError() {
|
||||
return function vectorErrorMiddleware(err, req, res, next) {
|
||||
if(req.params.format === 'mvt') {
|
||||
|
||||
if (isTimeoutError(err)) {
|
||||
if (isTimeoutError(err) || isRateLimitError(err)) {
|
||||
res.set('Content-Type', 'application/x-protobuf');
|
||||
return res.status(429).send(timeoutErrorVectorTile);
|
||||
}
|
||||
@@ -28,3 +27,7 @@ function isDatasourceTimeoutError (err) {
|
||||
function isTimeoutError (err) {
|
||||
return isRenderTimeoutError(err) || isDatasourceTimeoutError(err);
|
||||
}
|
||||
|
||||
function isRateLimitError (err) {
|
||||
return err.type === 'limit' && err.subtype === 'rate-limit';
|
||||
}
|
||||
231
lib/cartodb/api/template/admin-template-controller.js
Normal file
231
lib/cartodb/api/template/admin-template-controller.js
Normal file
@@ -0,0 +1,231 @@
|
||||
const { templateName } = require('../../backends/template_maps');
|
||||
const credentials = require('../middlewares/credentials');
|
||||
const rateLimit = require('../middlewares/rate-limit');
|
||||
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit;
|
||||
|
||||
module.exports = class AdminTemplateController {
|
||||
/**
|
||||
* @param {AuthBackend} authBackend
|
||||
* @param {PgConnection} pgConnection
|
||||
* @param {TemplateMaps} templateMaps
|
||||
* @constructor
|
||||
*/
|
||||
constructor (authBackend, templateMaps, userLimitsBackend) {
|
||||
this.authBackend = authBackend;
|
||||
this.templateMaps = templateMaps;
|
||||
this.userLimitsBackend = userLimitsBackend;
|
||||
}
|
||||
|
||||
register (templateRouter) {
|
||||
templateRouter.options(`/:template_id`);
|
||||
|
||||
templateRouter.post('/', this.middlewares({
|
||||
action: 'create',
|
||||
label: 'POST TEMPLATE',
|
||||
rateLimitGroup: RATE_LIMIT_ENDPOINTS_GROUPS.NAMED_CREATE
|
||||
}));
|
||||
|
||||
templateRouter.put('/:template_id', this.middlewares({
|
||||
action: 'update',
|
||||
label: 'PUT TEMPLATE',
|
||||
rateLimitGroup: RATE_LIMIT_ENDPOINTS_GROUPS.NAMED_UPDATE
|
||||
}));
|
||||
|
||||
templateRouter.get('/:template_id', this.middlewares({
|
||||
action: 'get',
|
||||
label: 'GET TEMPLATE',
|
||||
rateLimitGroup: RATE_LIMIT_ENDPOINTS_GROUPS.NAMED_GET
|
||||
}));
|
||||
|
||||
templateRouter.delete('/:template_id', this.middlewares({
|
||||
action: 'delete',
|
||||
label: 'DELETE TEMPLATE',
|
||||
rateLimitGroup: RATE_LIMIT_ENDPOINTS_GROUPS.NAMED_DELETE
|
||||
}));
|
||||
|
||||
templateRouter.get('/', this.middlewares({
|
||||
action: 'list',
|
||||
label: 'GET TEMPLATE LIST',
|
||||
rateLimitGroup: RATE_LIMIT_ENDPOINTS_GROUPS.NAMED_LIST
|
||||
}));
|
||||
}
|
||||
|
||||
middlewares ({ action, label, rateLimitGroup }) {
|
||||
let template;
|
||||
|
||||
if (action === 'create') {
|
||||
template = createTemplate;
|
||||
}
|
||||
|
||||
if (action === 'update') {
|
||||
template = updateTemplate;
|
||||
}
|
||||
|
||||
if (action === 'get') {
|
||||
template = retrieveTemplate;
|
||||
}
|
||||
|
||||
if (action === 'delete') {
|
||||
template = destroyTemplate;
|
||||
}
|
||||
|
||||
if (action === 'list') {
|
||||
template = listTemplates;
|
||||
}
|
||||
|
||||
return [
|
||||
credentials(),
|
||||
authorizedByAPIKey({ authBackend: this.authBackend, action, label }),
|
||||
rateLimit(this.userLimitsBackend, rateLimitGroup),
|
||||
checkContentType({ action: 'POST', label: 'POST TEMPLATE' }),
|
||||
template({ templateMaps: this.templateMaps })
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
function checkContentType ({ label }) {
|
||||
return function checkContentTypeMiddleware (req, res, next) {
|
||||
if ((req.method === 'POST' || req.method === 'PUT') && !req.is('application/json')) {
|
||||
const error = new Error(`${req.method} template data must be of type application/json`);
|
||||
error.label = label;
|
||||
return next(error);
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
function authorizedByAPIKey ({ authBackend, action, label }) {
|
||||
return function authorizedByAPIKeyMiddleware (req, res, next) {
|
||||
const { user } = res.locals;
|
||||
|
||||
authBackend.authorizedByAPIKey(user, res, (err, authenticated, apikey) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (!authenticated) {
|
||||
const error = new Error(`Only authenticated users can ${action} templated maps`);
|
||||
error.http_status = 403;
|
||||
error.label = label;
|
||||
return next(error);
|
||||
}
|
||||
|
||||
if (apikey.type !== 'master') {
|
||||
const error = new Error('Forbidden');
|
||||
error.type = 'auth';
|
||||
error.subtype = 'api-key-does-not-grant-access';
|
||||
error.http_status = 403;
|
||||
|
||||
return next(error);
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function createTemplate ({ templateMaps }) {
|
||||
return function createTemplateMiddleware (req, res, next) {
|
||||
const { user } = res.locals;
|
||||
const template = req.body;
|
||||
|
||||
templateMaps.addTemplate(user, template, (err, templateId) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.statusCode = 200;
|
||||
res.body = { template_id: templateId };
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function updateTemplate ({ templateMaps }) {
|
||||
return function updateTemplateMiddleware (req, res, next) {
|
||||
const { user } = res.locals;
|
||||
const template = req.body;
|
||||
const templateId = templateName(req.params.template_id);
|
||||
|
||||
templateMaps.updTemplate(user, templateId, template, (err) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.statusCode = 200;
|
||||
res.body = { template_id: templateId };
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function retrieveTemplate ({ templateMaps }) {
|
||||
return function retrieveTemplateMiddleware (req, res, next) {
|
||||
req.profiler.start('windshaft-cartodb.get_template');
|
||||
|
||||
const { user } = res.locals;
|
||||
const templateId = templateName(req.params.template_id);
|
||||
|
||||
templateMaps.getTemplate(user, templateId, (err, template) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (!template) {
|
||||
const error = new Error(`Cannot find template '${templateId}' of user '${user}'`);
|
||||
error.http_status = 404;
|
||||
return next(error);
|
||||
}
|
||||
// auth_id was added by ourselves,
|
||||
// so we remove it before returning to the user
|
||||
delete template.auth_id;
|
||||
|
||||
res.statusCode = 200;
|
||||
res.body = { template };
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function destroyTemplate ({ templateMaps }) {
|
||||
return function destroyTemplateMiddleware (req, res, next) {
|
||||
req.profiler.start('windshaft-cartodb.delete_template');
|
||||
|
||||
const { user } = res.locals;
|
||||
const templateId = templateName(req.params.template_id);
|
||||
|
||||
templateMaps.delTemplate(user, templateId, (err/* , tpl_val */) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.statusCode = 204;
|
||||
res.body = '';
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function listTemplates ({ templateMaps }) {
|
||||
return function listTemplatesMiddleware (req, res, next) {
|
||||
req.profiler.start('windshaft-cartodb.get_template_list');
|
||||
|
||||
const { user } = res.locals;
|
||||
|
||||
templateMaps.listTemplates(user, (err, templateIds) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.statusCode = 200;
|
||||
res.body = { template_ids: templateIds };
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
214
lib/cartodb/api/template/named-template-controller.js
Normal file
214
lib/cartodb/api/template/named-template-controller.js
Normal file
@@ -0,0 +1,214 @@
|
||||
const cleanUpQueryParams = require('../middlewares/clean-up-query-params');
|
||||
const credentials = require('../middlewares/credentials');
|
||||
const dbConnSetup = require('../middlewares/db-conn-setup');
|
||||
const authorize = require('../middlewares/authorize');
|
||||
const initProfiler = require('../middlewares/init-profiler');
|
||||
const checkJsonContentType = require('../middlewares/check-json-content-type');
|
||||
const incrementMapViewCount = require('../middlewares/increment-map-view-count');
|
||||
const augmentLayergroupData = require('../middlewares/augment-layergroup-data');
|
||||
const cacheControlHeader = require('../middlewares/cache-control-header');
|
||||
const cacheChannelHeader = require('../middlewares/cache-channel-header');
|
||||
const surrogateKeyHeader = require('../middlewares/surrogate-key-header');
|
||||
const lastModifiedHeader = require('../middlewares/last-modified-header');
|
||||
const lastUpdatedTimeLayergroup = require('../middlewares/last-updated-time-layergroup');
|
||||
const layerStats = require('../middlewares/layer-stats');
|
||||
const layergroupIdHeader = require('../middlewares/layergroup-id-header');
|
||||
const layergroupMetadata = require('../middlewares/layergroup-metadata');
|
||||
const mapError = require('../middlewares/map-error');
|
||||
const NamedMapMapConfigProvider = require('../../models/mapconfig/provider/named-map-provider');
|
||||
const CreateLayergroupMapConfigProvider = require('../../models/mapconfig/provider/create-layergroup-provider');
|
||||
const rateLimit = require('../middlewares/rate-limit');
|
||||
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit;
|
||||
|
||||
module.exports = class NamedMapController {
|
||||
/**
|
||||
* @param {PgConnection} pgConnection
|
||||
* @param {TemplateMaps} templateMaps
|
||||
* @param {MapBackend} mapBackend
|
||||
* @param metadataBackend
|
||||
* @param {SurrogateKeysCache} surrogateKeysCache
|
||||
* @param {UserLimitsBackend} userLimitsBackend
|
||||
* @param {LayergroupAffectedTables} layergroupAffectedTables
|
||||
* @param {MapConfigAdapter} mapConfigAdapter
|
||||
* @param {StatsBackend} statsBackend
|
||||
* @param {AuthBackend} authBackend
|
||||
* @param layergroupMetadata
|
||||
* @constructor
|
||||
*/
|
||||
constructor (
|
||||
pgConnection,
|
||||
templateMaps,
|
||||
mapBackend,
|
||||
metadataBackend,
|
||||
surrogateKeysCache,
|
||||
userLimitsBackend,
|
||||
layergroupAffectedTables,
|
||||
mapConfigAdapter,
|
||||
statsBackend,
|
||||
authBackend,
|
||||
layergroupMetadata
|
||||
) {
|
||||
this.pgConnection = pgConnection;
|
||||
this.templateMaps = templateMaps;
|
||||
this.mapBackend = mapBackend;
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.surrogateKeysCache = surrogateKeysCache;
|
||||
this.userLimitsBackend = userLimitsBackend;
|
||||
this.layergroupAffectedTables = layergroupAffectedTables;
|
||||
this.mapConfigAdapter = mapConfigAdapter;
|
||||
this.statsBackend = statsBackend;
|
||||
this.authBackend = authBackend;
|
||||
this.layergroupMetadata = layergroupMetadata;
|
||||
}
|
||||
|
||||
register (templateRouter) {
|
||||
templateRouter.get('/:template_id/jsonp', this.middlewares());
|
||||
templateRouter.post('/:template_id', this.middlewares());
|
||||
}
|
||||
|
||||
middlewares () {
|
||||
const isTemplateInstantiation = true;
|
||||
const useTemplateHash = true;
|
||||
const includeQuery = false;
|
||||
const label = 'NAMED MAP LAYERGROUP';
|
||||
const addContext = false;
|
||||
|
||||
return [
|
||||
credentials(),
|
||||
authorize(this.authBackend),
|
||||
dbConnSetup(this.pgConnection),
|
||||
rateLimit(this.userLimitsBackend, RATE_LIMIT_ENDPOINTS_GROUPS.NAMED),
|
||||
cleanUpQueryParams(['aggregation']),
|
||||
initProfiler(isTemplateInstantiation),
|
||||
checkJsonContentType(),
|
||||
checkInstantiteLayergroup(),
|
||||
getTemplate(
|
||||
this.templateMaps,
|
||||
this.pgConnection,
|
||||
this.metadataBackend,
|
||||
this.userLimitsBackend,
|
||||
this.mapConfigAdapter,
|
||||
this.layergroupAffectedTables
|
||||
),
|
||||
instantiateLayergroup(
|
||||
this.mapBackend,
|
||||
this.userLimitsBackend,
|
||||
this.pgConnection,
|
||||
this.layergroupAffectedTables
|
||||
),
|
||||
incrementMapViewCount(this.metadataBackend),
|
||||
augmentLayergroupData(),
|
||||
cacheControlHeader({ ttl: global.environment.varnish.layergroupTtl || 86400, revalidate: true }),
|
||||
cacheChannelHeader(),
|
||||
surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
|
||||
lastModifiedHeader(),
|
||||
lastUpdatedTimeLayergroup(),
|
||||
layerStats(this.pgConnection, this.statsBackend),
|
||||
layergroupIdHeader(this.templateMaps ,useTemplateHash),
|
||||
layergroupMetadata(this.layergroupMetadata, includeQuery),
|
||||
mapError({ label, addContext })
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
function checkInstantiteLayergroup () {
|
||||
return function checkInstantiteLayergroupMiddleware(req, res, next) {
|
||||
if (req.method === 'GET') {
|
||||
const { callback, config } = req.query;
|
||||
|
||||
if (callback === undefined || callback.length === 0) {
|
||||
return next(new Error('callback parameter should be present and be a function name'));
|
||||
}
|
||||
|
||||
if (config) {
|
||||
try {
|
||||
req.body = JSON.parse(config);
|
||||
} catch(e) {
|
||||
return next(new Error('Invalid config parameter, should be a valid JSON'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
req.profiler.done('checkInstantiteLayergroup');
|
||||
|
||||
return next();
|
||||
};
|
||||
}
|
||||
|
||||
function getTemplate (
|
||||
templateMaps,
|
||||
pgConnection,
|
||||
metadataBackend,
|
||||
userLimitsBackend,
|
||||
mapConfigAdapter,
|
||||
affectedTablesCache
|
||||
) {
|
||||
return function getTemplateMiddleware (req, res, next) {
|
||||
const templateParams = req.body;
|
||||
const { user, dbuser, dbname, dbpassword, dbhost, dbport } = res.locals;
|
||||
const { template_id } = req.params;
|
||||
const { auth_token } = req.query;
|
||||
|
||||
const params = Object.assign({ dbuser, dbname, dbpassword, dbhost, dbport }, req.query);
|
||||
|
||||
const mapConfigProvider = new NamedMapMapConfigProvider(
|
||||
templateMaps,
|
||||
pgConnection,
|
||||
metadataBackend,
|
||||
userLimitsBackend,
|
||||
mapConfigAdapter,
|
||||
affectedTablesCache,
|
||||
user,
|
||||
template_id,
|
||||
templateParams,
|
||||
auth_token,
|
||||
params
|
||||
);
|
||||
|
||||
mapConfigProvider.getMapConfig((err, mapConfig, rendererParams) => {
|
||||
req.profiler.done('named.getMapConfig');
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.locals.mapConfig = mapConfig;
|
||||
res.locals.rendererParams = rendererParams;
|
||||
res.locals.mapConfigProvider = mapConfigProvider;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function instantiateLayergroup (mapBackend, userLimitsBackend, pgConnection, affectedTablesCache) {
|
||||
return function instantiateLayergroupMiddleware (req, res, next) {
|
||||
const { user, mapConfig, rendererParams } = res.locals;
|
||||
const mapConfigProvider = new CreateLayergroupMapConfigProvider(
|
||||
mapConfig,
|
||||
user,
|
||||
userLimitsBackend,
|
||||
pgConnection,
|
||||
affectedTablesCache,
|
||||
rendererParams
|
||||
);
|
||||
|
||||
mapBackend.createLayergroup(mapConfig, rendererParams, mapConfigProvider, (err, layergroup, stats = {}) => {
|
||||
req.profiler.add(stats);
|
||||
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.statusCode = 200;
|
||||
res.body = layergroup;
|
||||
|
||||
const { mapConfigProvider } = res.locals;
|
||||
|
||||
res.locals.analysesResults = mapConfigProvider.analysesResults;
|
||||
res.locals.template = mapConfigProvider.template;
|
||||
res.locals.context = mapConfigProvider.context;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
64
lib/cartodb/api/template/template-router.js
Normal file
64
lib/cartodb/api/template/template-router.js
Normal file
@@ -0,0 +1,64 @@
|
||||
const { Router: router } = require('express');
|
||||
|
||||
const NamedMapController = require('./named-template-controller');
|
||||
const AdminTemplateController = require('./admin-template-controller');
|
||||
const TileTemplateController = require('./tile-template-controller');
|
||||
|
||||
module.exports = class TemplateRouter {
|
||||
constructor ({ collaborators }) {
|
||||
const {
|
||||
pgConnection,
|
||||
templateMaps,
|
||||
mapBackend,
|
||||
metadataBackend,
|
||||
surrogateKeysCache,
|
||||
userLimitsBackend,
|
||||
layergroupAffectedTablesCache,
|
||||
mapConfigAdapter,
|
||||
statsBackend,
|
||||
authBackend,
|
||||
layergroupMetadata,
|
||||
namedMapProviderCache,
|
||||
tileBackend,
|
||||
} = collaborators;
|
||||
|
||||
this.namedMapController = new NamedMapController(
|
||||
pgConnection,
|
||||
templateMaps,
|
||||
mapBackend,
|
||||
metadataBackend,
|
||||
surrogateKeysCache,
|
||||
userLimitsBackend,
|
||||
layergroupAffectedTablesCache,
|
||||
mapConfigAdapter,
|
||||
statsBackend,
|
||||
authBackend,
|
||||
layergroupMetadata
|
||||
);
|
||||
|
||||
this.tileTemplateController = new TileTemplateController(
|
||||
namedMapProviderCache,
|
||||
tileBackend,
|
||||
surrogateKeysCache,
|
||||
pgConnection,
|
||||
authBackend,
|
||||
userLimitsBackend
|
||||
);
|
||||
|
||||
this.adminTemplateController = new AdminTemplateController(
|
||||
authBackend,
|
||||
templateMaps,
|
||||
userLimitsBackend
|
||||
);
|
||||
}
|
||||
|
||||
register (apiRouter, templatePaths) {
|
||||
const templateRouter = router({ mergeParams: true });
|
||||
|
||||
this.namedMapController.register(templateRouter);
|
||||
this.tileTemplateController.register(templateRouter);
|
||||
this.adminTemplateController.register(templateRouter);
|
||||
|
||||
templatePaths.forEach(path => apiRouter.use(path, templateRouter));
|
||||
}
|
||||
};
|
||||
95
lib/cartodb/api/template/tile-template-controller.js
Normal file
95
lib/cartodb/api/template/tile-template-controller.js
Normal file
@@ -0,0 +1,95 @@
|
||||
const coordinates = require('../middlewares/coordinates');
|
||||
const cleanUpQueryParams = require('../middlewares/clean-up-query-params');
|
||||
const credentials = require('../middlewares/credentials');
|
||||
const dbConnSetup = require('../middlewares/db-conn-setup');
|
||||
const authorize = require('../middlewares/authorize');
|
||||
const namedMapProvider = require('../middlewares/named-map-provider');
|
||||
const cacheControlHeader = require('../middlewares/cache-control-header');
|
||||
const cacheChannelHeader = require('../middlewares/cache-channel-header');
|
||||
const surrogateKeyHeader = require('../middlewares/surrogate-key-header');
|
||||
const lastModifiedHeader = require('../middlewares/last-modified-header');
|
||||
const vectorError = require('../middlewares/vector-error');
|
||||
const rateLimit = require('../middlewares/rate-limit');
|
||||
const { RATE_LIMIT_ENDPOINTS_GROUPS } = rateLimit;
|
||||
|
||||
module.exports = class TileTemplateController {
|
||||
constructor (
|
||||
namedMapProviderCache,
|
||||
tileBackend,
|
||||
surrogateKeysCache,
|
||||
pgConnection,
|
||||
authBackend,
|
||||
userLimitsBackend
|
||||
) {
|
||||
this.namedMapProviderCache = namedMapProviderCache;
|
||||
this.tileBackend = tileBackend;
|
||||
this.surrogateKeysCache = surrogateKeysCache;
|
||||
this.pgConnection = pgConnection;
|
||||
this.authBackend = authBackend;
|
||||
this.userLimitsBackend = userLimitsBackend;
|
||||
}
|
||||
|
||||
register (templateRouter) {
|
||||
templateRouter.get('/:template_id/:layer/:z/:x/:y.(:format)', this.middlewares());
|
||||
}
|
||||
|
||||
middlewares () {
|
||||
return [
|
||||
coordinates(),
|
||||
credentials(),
|
||||
authorize(this.authBackend),
|
||||
dbConnSetup(this.pgConnection),
|
||||
rateLimit(this.userLimitsBackend, RATE_LIMIT_ENDPOINTS_GROUPS.NAMED_TILES),
|
||||
cleanUpQueryParams(),
|
||||
namedMapProvider({
|
||||
namedMapProviderCache: this.namedMapProviderCache,
|
||||
label: 'NAMED_MAP_TILE'
|
||||
}),
|
||||
getTile({
|
||||
tileBackend: this.tileBackend,
|
||||
label: 'NAMED_MAP_TILE'
|
||||
}),
|
||||
setContentTypeHeader(),
|
||||
cacheControlHeader(),
|
||||
cacheChannelHeader(),
|
||||
surrogateKeyHeader({ surrogateKeysCache: this.surrogateKeysCache }),
|
||||
lastModifiedHeader(),
|
||||
vectorError()
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
function getTile ({ tileBackend, label }) {
|
||||
return function getTileMiddleware (req, res, next) {
|
||||
const { mapConfigProvider } = res.locals;
|
||||
const { layer, z, x, y, format } = req.params;
|
||||
const params = { layer, z, x, y, format };
|
||||
|
||||
tileBackend.getTile(mapConfigProvider, params, (err, tile, headers, stats) => {
|
||||
req.profiler.add(stats);
|
||||
req.profiler.done('render-' + format);
|
||||
|
||||
if (err) {
|
||||
err.label = label;
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (headers) {
|
||||
res.set(headers);
|
||||
}
|
||||
|
||||
res.statusCode = 200;
|
||||
res.body = tile;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function setContentTypeHeader () {
|
||||
return function setContentTypeHeaderMiddleware(req, res, next) {
|
||||
res.set('Content-Type', res.get('content-type') || res.get('Content-Type') || 'image/png');
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
@@ -5,16 +5,14 @@ function AnalysisStatusBackend() {
|
||||
|
||||
module.exports = AnalysisStatusBackend;
|
||||
|
||||
|
||||
AnalysisStatusBackend.prototype.getNodeStatus = function (params, callback) {
|
||||
var nodeId = params.nodeId;
|
||||
|
||||
AnalysisStatusBackend.prototype.getNodeStatus = function (nodeId, dbParams, callback) {
|
||||
var statusQuery = [
|
||||
'SELECT node_id, status, updated_at, last_error_message as error_message',
|
||||
'FROM cdb_analysis_catalog where node_id = \'' + nodeId + '\''
|
||||
].join(' ');
|
||||
|
||||
var pg = new PSQL(dbParamsFromReqParams(params));
|
||||
var pg = new PSQL(dbParams);
|
||||
|
||||
pg.query(statusQuery, function(err, result) {
|
||||
if (err) {
|
||||
return callback(err, result);
|
||||
@@ -36,23 +34,3 @@ AnalysisStatusBackend.prototype.getNodeStatus = function (params, callback) {
|
||||
return callback(null, statusResponse);
|
||||
}, true); // use read-only transaction
|
||||
};
|
||||
|
||||
function dbParamsFromReqParams(params) {
|
||||
var dbParams = {};
|
||||
if ( params.dbuser ) {
|
||||
dbParams.user = params.dbuser;
|
||||
}
|
||||
if ( params.dbpassword ) {
|
||||
dbParams.pass = params.dbpassword;
|
||||
}
|
||||
if ( params.dbhost ) {
|
||||
dbParams.host = params.dbhost;
|
||||
}
|
||||
if ( params.dbport ) {
|
||||
dbParams.port = params.dbport;
|
||||
}
|
||||
if ( params.dbname ) {
|
||||
dbParams.dbname = params.dbname;
|
||||
}
|
||||
return dbParams;
|
||||
}
|
||||
|
||||
178
lib/cartodb/backends/auth.js
Normal file
178
lib/cartodb/backends/auth.js
Normal file
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
*
|
||||
* @param {PgConnection} pgConnection
|
||||
* @param metadataBackend
|
||||
* @param {MapStore} mapStore
|
||||
* @param {TemplateMaps} templateMaps
|
||||
* @constructor
|
||||
* @type {AuthBackend}
|
||||
*/
|
||||
function AuthBackend(pgConnection, metadataBackend, mapStore, templateMaps) {
|
||||
this.pgConnection = pgConnection;
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.mapStore = mapStore;
|
||||
this.templateMaps = templateMaps;
|
||||
}
|
||||
|
||||
module.exports = AuthBackend;
|
||||
|
||||
// Check if the user is authorized by a signer
|
||||
//
|
||||
// @param res express response object
|
||||
// @param callback function(err, signed_by) signed_by will be
|
||||
// null if the request is not signed by anyone
|
||||
// or will be a string cartodb username otherwise.
|
||||
//
|
||||
AuthBackend.prototype.authorizedBySigner = function(req, res, callback) {
|
||||
if ( ! res.locals.token || ! res.locals.signer ) {
|
||||
return callback(null, false); // no signer requested
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
var layergroup_id = res.locals.token;
|
||||
var auth_token = req.query.auth_token;
|
||||
|
||||
this.mapStore.load(layergroup_id, function(err, mapConfig) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var authorized = self.templateMaps.isAuthorized(mapConfig.obj().template, auth_token);
|
||||
|
||||
return callback(null, authorized);
|
||||
});
|
||||
};
|
||||
|
||||
function isValidApiKey(apikey) {
|
||||
return apikey.type &&
|
||||
apikey.user &&
|
||||
apikey.databasePassword &&
|
||||
apikey.databaseRole;
|
||||
}
|
||||
|
||||
// Check if a request is authorized by api_key
|
||||
//
|
||||
// @param user
|
||||
// @param res express response object
|
||||
// @param callback function(err, authorized)
|
||||
// NOTE: authorized is expected to be 0 or 1 (integer)
|
||||
//
|
||||
AuthBackend.prototype.authorizedByAPIKey = function(user, res, callback) {
|
||||
const apikeyToken = res.locals.api_key;
|
||||
const basicAuthUsername = res.locals.basicAuthUsername;
|
||||
|
||||
if ( ! apikeyToken ) {
|
||||
return callback(null, false); // no api key, no authorization...
|
||||
}
|
||||
|
||||
this.metadataBackend.getApikey(user, apikeyToken, (err, apikey) => {
|
||||
if (err) {
|
||||
if (isNameNotFoundError(err)) {
|
||||
err.http_status = 404;
|
||||
}
|
||||
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if ( !isValidApiKey(apikey)) {
|
||||
const error = new Error('Unauthorized');
|
||||
error.type = 'auth';
|
||||
error.subtype = 'api-key-not-found';
|
||||
error.http_status = 401;
|
||||
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
if (!usernameMatches(basicAuthUsername, res.locals.user)) {
|
||||
const error = new Error('Forbidden');
|
||||
error.type = 'auth';
|
||||
error.subtype = 'api-key-username-mismatch';
|
||||
error.http_status = 403;
|
||||
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
if (!apikey.grantsMaps) {
|
||||
const error = new Error('Forbidden');
|
||||
error.type = 'auth';
|
||||
error.subtype = 'api-key-does-not-grant-access';
|
||||
error.http_status = 403;
|
||||
|
||||
return callback(error);
|
||||
}
|
||||
|
||||
return callback(null, true, apikey);
|
||||
});
|
||||
};
|
||||
|
||||
function isNameNotFoundError (err) {
|
||||
return err.message && -1 !== err.message.indexOf('name not found');
|
||||
}
|
||||
|
||||
function usernameMatches (basicAuthUsername, requestUsername) {
|
||||
return !(basicAuthUsername && (basicAuthUsername !== requestUsername));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check access authorization
|
||||
*
|
||||
* @param req - standard req object. Importantly contains table and host information
|
||||
* @param res - standard res object. Contains the auth parameters in locals
|
||||
* @param callback function(err, allowed) is access allowed not?
|
||||
*/
|
||||
AuthBackend.prototype.authorize = function(req, res, callback) {
|
||||
var user = res.locals.user;
|
||||
|
||||
this.authorizedByAPIKey(user, res, (err, isAuthorizedByApikey) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (isAuthorizedByApikey) {
|
||||
return this.pgConnection.setDBAuth(user, res.locals, 'regular', function (err) {
|
||||
req.profiler.done('setDBAuth');
|
||||
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(null, true);
|
||||
});
|
||||
}
|
||||
|
||||
this.authorizedBySigner(req, res, (err, isAuthorizedBySigner) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (isAuthorizedBySigner) {
|
||||
return this.pgConnection.setDBAuth(user, res.locals, 'master', function (err) {
|
||||
req.profiler.done('setDBAuth');
|
||||
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(null, true);
|
||||
});
|
||||
}
|
||||
|
||||
// if no signer name was given, use default api key
|
||||
if (!res.locals.signer) {
|
||||
return this.pgConnection.setDBAuth(user, res.locals, 'default', function (err) {
|
||||
req.profiler.done('setDBAuth');
|
||||
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(null, true);
|
||||
});
|
||||
}
|
||||
|
||||
// if signer name was given, return no authorization
|
||||
return callback(null, false);
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -1,13 +1,11 @@
|
||||
var assert = require('assert');
|
||||
|
||||
var _ = require('underscore');
|
||||
var PSQL = require('cartodb-psql');
|
||||
var step = require('step');
|
||||
|
||||
var BBoxFilter = require('../models/filter/bbox');
|
||||
|
||||
var DataviewFactory = require('../models/dataview/factory');
|
||||
var DataviewFactoryWithOverviews = require('../models/dataview/overviews/factory');
|
||||
const dbParamsFromReqParams = require('../utils/database-params');
|
||||
var OverviewsQueryRewriter = require('../utils/overviews_query_rewriter');
|
||||
var overviewsQueryRewriter = new OverviewsQueryRewriter({
|
||||
zoom_level: 'CDB_ZoomFromScale(!scale_denominator!)'
|
||||
@@ -48,7 +46,7 @@ DataviewBackend.prototype.getDataview = function (mapConfigProvider, user, param
|
||||
}
|
||||
|
||||
var pg = new PSQL(dbParamsFromReqParams(params));
|
||||
|
||||
|
||||
var query = getDataviewQuery(dataviewDefinition, ownFilter, noFilters);
|
||||
if (params.bbox) {
|
||||
var bboxFilter = new BBoxFilter({column: 'the_geom_webmercator', srid: 3857}, {bbox: params.bbox});
|
||||
@@ -170,23 +168,3 @@ function getDataviewDefinition(mapConfig, dataviewName) {
|
||||
var dataviews = mapConfig.dataviews || {};
|
||||
return dataviews[dataviewName];
|
||||
}
|
||||
|
||||
function dbParamsFromReqParams(params) {
|
||||
var dbParams = {};
|
||||
if ( params.dbuser ) {
|
||||
dbParams.user = params.dbuser;
|
||||
}
|
||||
if ( params.dbpassword ) {
|
||||
dbParams.pass = params.dbpassword;
|
||||
}
|
||||
if ( params.dbhost ) {
|
||||
dbParams.host = params.dbhost;
|
||||
}
|
||||
if ( params.dbport ) {
|
||||
dbParams.port = params.dbport;
|
||||
}
|
||||
if ( params.dbname ) {
|
||||
dbParams.dbname = params.dbname;
|
||||
}
|
||||
return dbParams;
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ var _ = require('underscore');
|
||||
var step = require('step');
|
||||
var AnalysisFilter = require('../models/filter/analysis');
|
||||
|
||||
function FilterStatsApi(pgQueryRunner) {
|
||||
function FilterStatsBackends(pgQueryRunner) {
|
||||
this.pgQueryRunner = pgQueryRunner;
|
||||
}
|
||||
|
||||
module.exports = FilterStatsApi;
|
||||
module.exports = FilterStatsBackends;
|
||||
|
||||
function getEstimatedRows(pgQueryRunner, username, query, callback) {
|
||||
pgQueryRunner.run(username, "EXPLAIN (FORMAT JSON)"+query, function(err, result_rows) {
|
||||
@@ -23,7 +23,7 @@ function getEstimatedRows(pgQueryRunner, username, query, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
FilterStatsApi.prototype.getFilterStats = function (username, unfiltered_query, filters, callback) {
|
||||
FilterStatsBackends.prototype.getFilterStats = function (username, unfiltered_query, filters, callback) {
|
||||
var stats = {};
|
||||
var self = this;
|
||||
step(
|
||||
@@ -1,4 +1,5 @@
|
||||
var queryUtils = require('../../utils/query-utils');
|
||||
const queryUtils = require('../../utils/query-utils');
|
||||
const AggregationMapConfig = require('../../models/aggregation/aggregation-mapconfig');
|
||||
|
||||
function MapnikLayerStats () {
|
||||
this._types = {
|
||||
@@ -11,17 +12,255 @@ MapnikLayerStats.prototype.is = function (type) {
|
||||
return this._types[type] ? this._types[type] : false;
|
||||
};
|
||||
|
||||
function columnAggregations(field) {
|
||||
if (field.type === 'number') {
|
||||
return ['min', 'max', 'avg', 'sum'];
|
||||
}
|
||||
if (field.type === 'date') { // TODO other types too?
|
||||
return ['min', 'max'];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function _getSQL(ctx, query, type='pre', zoom=0) {
|
||||
let sql;
|
||||
if (type === 'pre') {
|
||||
sql = ctx.preQuery;
|
||||
}
|
||||
else {
|
||||
sql = ctx.aggrQuery;
|
||||
}
|
||||
sql = queryUtils.subsituteTokensForZoom(sql, zoom || 0);
|
||||
return query(sql);
|
||||
}
|
||||
|
||||
function _estimatedFeatureCount(ctx) {
|
||||
return queryUtils.queryPromise(ctx.dbConnection, _getSQL(ctx, queryUtils.getQueryRowEstimation))
|
||||
.then(res => ({ estimatedFeatureCount: res.rows[0].rows }))
|
||||
.catch(() => ({ estimatedFeatureCount: -1 }));
|
||||
}
|
||||
|
||||
function _featureCount(ctx) {
|
||||
if (ctx.metaOptions.featureCount) {
|
||||
// TODO: if ctx.metaOptions.columnStats we can combine this with column stats query
|
||||
return queryUtils.queryPromise(ctx.dbConnection, _getSQL(ctx, queryUtils.getQueryActualRowCount))
|
||||
.then(res => ({ featureCount: res.rows[0].rows }));
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
function _aggrFeatureCount(ctx) {
|
||||
if (ctx.metaOptions.hasOwnProperty('aggrFeatureCount')) {
|
||||
// We expect as zoom level as the value of aggrFeatureCount
|
||||
// TODO: it'd be nice to admit an array of zoom levels to
|
||||
// return metadata for multiple levels.
|
||||
return queryUtils.queryPromise(
|
||||
ctx.dbConnection,
|
||||
_getSQL(ctx, queryUtils.getQueryActualRowCount, 'post', ctx.metaOptions.aggrFeatureCount)
|
||||
).then(res => ({ aggrFeatureCount: res.rows[0].rows }));
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
function _geometryType(ctx) {
|
||||
if (ctx.metaOptions.geometryType) {
|
||||
const geometryColumn = AggregationMapConfig.getAggregationGeometryColumn();
|
||||
const sqlQuery = _getSQL(ctx, sql => queryUtils.getQueryGeometryType(sql, geometryColumn));
|
||||
return queryUtils.queryPromise(ctx.dbConnection, sqlQuery)
|
||||
.then(res => ({ geometryType: res.rows[0].geom_type }));
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
function _columns(ctx) {
|
||||
if (ctx.metaOptions.columns || ctx.metaOptions.columnStats) {
|
||||
// note: post-aggregation columns are in layer.options.columns when aggregation is present
|
||||
return queryUtils.queryPromise(ctx.dbConnection, _getSQL(ctx, sql => queryUtils.getQueryLimited(sql, 0)))
|
||||
.then(res => formatResultFields(ctx.dbConnection, res.fields));
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// combine a list of results merging the properties of all the objects
|
||||
// undefined results are admitted and ignored
|
||||
function mergeResults(results) {
|
||||
if (results) {
|
||||
if (results.length === 0) {
|
||||
return {};
|
||||
}
|
||||
return results.reduce((a, b) => {
|
||||
if (a === undefined) {
|
||||
return b;
|
||||
}
|
||||
if (b === undefined) {
|
||||
return a;
|
||||
}
|
||||
return Object.assign({}, a, b);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// deeper (1 level) combination of a list of objects:
|
||||
// mergeColumns([{ col1: { a: 1 }, col2: { a: 2 } }, { col1: { b: 3 } }]) => { col1: { a: 1, b: 3 }, col2: { a: 2 } }
|
||||
function mergeColumns(results) {
|
||||
if (results) {
|
||||
if (results.length === 0) {
|
||||
return {};
|
||||
}
|
||||
return results.reduce((a, b) => {
|
||||
let c = Object.assign({}, b || {}, a || {});
|
||||
Object.keys(c).forEach(key => {
|
||||
if (b.hasOwnProperty(key)) {
|
||||
c[key] = Object.assign(c[key], b[key]);
|
||||
}
|
||||
});
|
||||
return c;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const SAMPLE_SEED = 0.5;
|
||||
const DEFAULT_SAMPLE_ROWS = 100;
|
||||
|
||||
function _sample(ctx, numRows) {
|
||||
if (ctx.metaOptions.sample) {
|
||||
const sampleProb = Math.min(ctx.metaOptions.sample.num_rows / numRows, 1);
|
||||
// We'll use a safety limit just in case numRows is a bad estimate
|
||||
const requestedRows = ctx.metaOptions.sample.num_rows || DEFAULT_SAMPLE_ROWS;
|
||||
const limit = Math.ceil(requestedRows * 1.5);
|
||||
let columns = ctx.metaOptions.sample.include_columns;
|
||||
return queryUtils.queryPromise(ctx.dbConnection, _getSQL(
|
||||
ctx,
|
||||
sql => queryUtils.getQuerySample(sql, sampleProb, limit, SAMPLE_SEED, columns)
|
||||
)).then(res => ({ sample: res.rows }));
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
function _columnStats(ctx, columns) {
|
||||
if (!columns) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (ctx.metaOptions.columnStats) {
|
||||
let queries = [];
|
||||
let aggr = [];
|
||||
queries.push(new Promise(resolve => resolve(columns))); // add columns as first result
|
||||
Object.keys(columns).forEach(name => {
|
||||
aggr = aggr.concat(
|
||||
columnAggregations(columns[name])
|
||||
.map(fn => `${fn}(${name}) AS ${name}_${fn}`)
|
||||
);
|
||||
if (columns[name].type === 'string') {
|
||||
const topN = ctx.metaOptions.columnStats.topCategories || 1024;
|
||||
const includeNulls = ctx.metaOptions.columnStats.hasOwnProperty('includeNulls') ?
|
||||
ctx.metaOptions.columnStats.includeNulls :
|
||||
true;
|
||||
|
||||
// TODO: ctx.metaOptions.columnStats.maxCategories
|
||||
// => use PG stats to dismiss columns with more distinct values
|
||||
queries.push(
|
||||
queryUtils.queryPromise(
|
||||
ctx.dbConnection,
|
||||
_getSQL(ctx, sql => queryUtils.getQueryTopCategories(sql, name, topN, includeNulls))
|
||||
).then(res => ({ [name]: { categories: res.rows } }))
|
||||
);
|
||||
}
|
||||
});
|
||||
queries.push(
|
||||
queryUtils.queryPromise(
|
||||
ctx.dbConnection,
|
||||
_getSQL(ctx, sql => `SELECT ${aggr.join(',')} FROM (${sql}) AS __cdb_query`)
|
||||
).then(res => {
|
||||
let stats = {};
|
||||
Object.keys(columns).forEach(name => {
|
||||
stats[name] = {};
|
||||
columnAggregations(columns[name]).forEach(fn => {
|
||||
stats[name][fn] = res.rows[0][`${name}_${fn}`];
|
||||
});
|
||||
});
|
||||
return stats;
|
||||
})
|
||||
);
|
||||
return Promise.all(queries).then(results => ({ columns: mergeColumns(results) }));
|
||||
}
|
||||
return Promise.resolve({ columns });
|
||||
}
|
||||
|
||||
// This is adapted from SQL API:
|
||||
function fieldType(cname) {
|
||||
let tname;
|
||||
switch (true) {
|
||||
case /bool/.test(cname):
|
||||
tname = 'boolean';
|
||||
break;
|
||||
case /int|float|numeric/.test(cname):
|
||||
tname = 'number';
|
||||
break;
|
||||
case /text|char|unknown/.test(cname):
|
||||
tname = 'string';
|
||||
break;
|
||||
case /date|time/.test(cname):
|
||||
tname = 'date';
|
||||
break;
|
||||
default:
|
||||
tname = cname;
|
||||
}
|
||||
if ( tname && cname.match(/^_/) ) {
|
||||
tname += '[]';
|
||||
}
|
||||
return tname;
|
||||
}
|
||||
|
||||
// columns are returned as an object { columnName1: { type1: ...}, ..}
|
||||
// for consistency with SQL API
|
||||
function formatResultFields(dbConnection, fields = []) {
|
||||
let nfields = {};
|
||||
for (let field of fields) {
|
||||
const cname = dbConnection.typeName(field.dataTypeID);
|
||||
let tname;
|
||||
if ( ! cname ) {
|
||||
tname = 'unknown(' + field.dataTypeID + ')';
|
||||
} else {
|
||||
tname = fieldType(cname);
|
||||
}
|
||||
nfields[field.name] = { type: tname };
|
||||
}
|
||||
return nfields;
|
||||
}
|
||||
|
||||
MapnikLayerStats.prototype.getStats =
|
||||
function (layer, dbConnection, callback) {
|
||||
var queryRowCountSql = queryUtils.getQueryRowCount(layer.options.sql);
|
||||
// This query would gather stats for postgresql table if not exists
|
||||
dbConnection.query(queryRowCountSql, function (err, res) {
|
||||
if (err) {
|
||||
return callback(null, {estimatedFeatureCount: -1});
|
||||
} else {
|
||||
// We decided that the relation is 1 row == 1 feature
|
||||
return callback(null, {estimatedFeatureCount: res.rows[0].rows});
|
||||
}
|
||||
let aggrQuery = layer.options.sql;
|
||||
let preQuery = layer.options.sql_raw || aggrQuery;
|
||||
|
||||
let ctx = {
|
||||
dbConnection,
|
||||
preQuery,
|
||||
aggrQuery,
|
||||
metaOptions: layer.options.metadata || {}
|
||||
};
|
||||
|
||||
// TODO: could save some queries if queryUtils.getAggregationMetadata() has been used and kept somewhere
|
||||
// we would set queries.results.estimatedFeatureCount and queries.results.geometryType
|
||||
// (if metaOptions.geometryType) from it.
|
||||
|
||||
// TODO: compute _sample with _featureCount when available
|
||||
// TODO: add support for sample.exclude option by, in that case, forcing the columns query and
|
||||
// passing the results to the sample query function.
|
||||
|
||||
Promise.all([
|
||||
_estimatedFeatureCount(ctx).then(
|
||||
({ estimatedFeatureCount }) => _sample(ctx, estimatedFeatureCount)
|
||||
.then(sampleResults => mergeResults([sampleResults, { estimatedFeatureCount }]))
|
||||
),
|
||||
_featureCount(ctx),
|
||||
_aggrFeatureCount(ctx),
|
||||
_geometryType(ctx),
|
||||
_columns(ctx).then(columns => _columnStats(ctx, columns))
|
||||
]).then(results => {
|
||||
callback(null, mergeResults(results));
|
||||
}).catch(error => {
|
||||
callback(error);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,25 +1,20 @@
|
||||
var SubstitutionTokens = require('../utils/substitution-tokens');
|
||||
const queryUtils = require('../utils/query-utils');
|
||||
|
||||
function OverviewsMetadataApi(pgQueryRunner) {
|
||||
function OverviewsMetadataBackend(pgQueryRunner) {
|
||||
this.pgQueryRunner = pgQueryRunner;
|
||||
}
|
||||
|
||||
module.exports = OverviewsMetadataApi;
|
||||
module.exports = OverviewsMetadataBackend;
|
||||
|
||||
function prepareSql(sql) {
|
||||
return sql && SubstitutionTokens.replace(sql, {
|
||||
bbox: 'ST_MakeEnvelope(0,0,0,0)',
|
||||
scale_denominator: '0',
|
||||
pixel_width: '1',
|
||||
pixel_height: '1'
|
||||
});
|
||||
}
|
||||
|
||||
OverviewsMetadataApi.prototype.getOverviewsMetadata = function (username, sql, callback) {
|
||||
OverviewsMetadataBackend.prototype.getOverviewsMetadata = function (username, sql, callback) {
|
||||
// FIXME: Currently using internal function _cdb_schema_name
|
||||
// CDB_Overviews should provide the schema information directly.
|
||||
var query = 'SELECT *, _cdb_schema_name(base_table)' +
|
||||
' FROM CDB_Overviews(CDB_QueryTablesText($windshaft$' + prepareSql(sql) + '$windshaft$))';
|
||||
const query = `
|
||||
SELECT *, _cdb_schema_name(base_table)
|
||||
FROM CDB_Overviews(
|
||||
CDB_QueryTablesText($windshaft$${queryUtils.substituteDummyTokens(sql)}$windshaft$)
|
||||
);
|
||||
`;
|
||||
this.pgQueryRunner.run(username, query, function handleOverviewsRows(err, rows) {
|
||||
if (err){
|
||||
callback(err);
|
||||
@@ -1,7 +1,6 @@
|
||||
var assert = require('assert');
|
||||
var step = require('step');
|
||||
var PSQL = require('cartodb-psql');
|
||||
var _ = require('underscore');
|
||||
const debug = require('debug')('cachechan');
|
||||
|
||||
function PgConnection(metadataBackend) {
|
||||
this.metadataBackend = metadataBackend;
|
||||
@@ -20,45 +19,59 @@ module.exports = PgConnection;
|
||||
//
|
||||
// @param callback function(err)
|
||||
//
|
||||
PgConnection.prototype.setDBAuth = function(username, params, callback) {
|
||||
var self = this;
|
||||
|
||||
var user_params = {};
|
||||
var auth_user = global.environment.postgres_auth_user;
|
||||
var auth_pass = global.environment.postgres_auth_pass;
|
||||
step(
|
||||
function getId() {
|
||||
self.metadataBackend.getUserId(username, this);
|
||||
},
|
||||
function(err, user_id) {
|
||||
assert.ifError(err);
|
||||
user_params.user_id = user_id;
|
||||
var dbuser = _.template(auth_user, user_params);
|
||||
_.extend(params, {dbuser:dbuser});
|
||||
|
||||
// skip looking up user_password if postgres_auth_pass
|
||||
// doesn't contain the "user_password" label
|
||||
if (!auth_pass || ! auth_pass.match(/\buser_password\b/) ) {
|
||||
return null;
|
||||
PgConnection.prototype.setDBAuth = function(username, params, apikeyType, callback) {
|
||||
if (apikeyType === 'master') {
|
||||
this.metadataBackend.getMasterApikey(username, (err, apikey) => {
|
||||
if (err) {
|
||||
if (isNameNotFoundError(err)) {
|
||||
err.http_status = 404;
|
||||
}
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
self.metadataBackend.getUserDBPass(username, this);
|
||||
},
|
||||
function(err, user_password) {
|
||||
assert.ifError(err);
|
||||
user_params.user_password = user_password;
|
||||
if ( auth_pass ) {
|
||||
var dbpass = _.template(auth_pass, user_params);
|
||||
_.extend(params, {dbpassword:dbpass});
|
||||
params.dbuser = apikey.databaseRole;
|
||||
params.dbpassword = apikey.databasePassword;
|
||||
|
||||
return callback();
|
||||
});
|
||||
} else if (apikeyType === 'regular') { //Actually it can be any type of api key
|
||||
this.metadataBackend.getApikey(username, params.api_key, (err, apikey) => {
|
||||
if (err) {
|
||||
if (isNameNotFoundError(err)) {
|
||||
err.http_status = 404;
|
||||
}
|
||||
return callback(err);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
function finish(err) {
|
||||
callback(err);
|
||||
}
|
||||
);
|
||||
|
||||
params.dbuser = apikey.databaseRole;
|
||||
params.dbpassword = apikey.databasePassword;
|
||||
|
||||
return callback();
|
||||
});
|
||||
} else if (apikeyType === 'default') {
|
||||
this.metadataBackend.getApikey(username, 'default_public', (err, apikey) => {
|
||||
if (err) {
|
||||
if (isNameNotFoundError(err)) {
|
||||
err.http_status = 404;
|
||||
}
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
params.dbuser = apikey.databaseRole;
|
||||
params.dbpassword = apikey.databasePassword;
|
||||
|
||||
return callback();
|
||||
});
|
||||
} else {
|
||||
return callback(new Error(`Invalid Apikey type: ${apikeyType}, valid ones: master, regular, default`));
|
||||
}
|
||||
};
|
||||
|
||||
function isNameNotFoundError (err) {
|
||||
return err.message && -1 !== err.message.indexOf('name not found');
|
||||
}
|
||||
|
||||
|
||||
// Set db connection parameters to those for the given username
|
||||
//
|
||||
// @param dbowner cartodb username of database owner,
|
||||
@@ -71,36 +84,30 @@ PgConnection.prototype.setDBAuth = function(username, params, callback) {
|
||||
// @param callback function(err)
|
||||
//
|
||||
PgConnection.prototype.setDBConn = function(dbowner, params, callback) {
|
||||
var self = this;
|
||||
// Add default database connection parameters
|
||||
// if none given
|
||||
_.defaults(params, {
|
||||
dbuser: global.environment.postgres.user,
|
||||
dbpassword: global.environment.postgres.password,
|
||||
// dbuser: global.environment.postgres.user,
|
||||
// dbpassword: global.environment.postgres.password,
|
||||
dbhost: global.environment.postgres.host,
|
||||
dbport: global.environment.postgres.port
|
||||
});
|
||||
step(
|
||||
function getConnectionParams() {
|
||||
self.metadataBackend.getUserDBConnectionParams(dbowner, this);
|
||||
},
|
||||
function extendParams(err, dbParams){
|
||||
assert.ifError(err);
|
||||
// we don't want null values or overwrite a non public user
|
||||
if (params.dbuser !== 'publicuser' || !dbParams.dbuser) {
|
||||
delete dbParams.dbuser;
|
||||
}
|
||||
if ( dbParams ) {
|
||||
_.extend(params, dbParams);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
callback(err);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
this.metadataBackend.getUserDBConnectionParams(dbowner, (err, dbParams) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// we don’t want null values or overwrite a non public user
|
||||
if (params.dbuser !== 'publicuser' || !dbParams.dbuser) {
|
||||
delete dbParams.dbuser;
|
||||
}
|
||||
|
||||
if (dbParams) {
|
||||
_.extend(params, dbParams);
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a `cartodb-psql` object for a given username.
|
||||
@@ -109,28 +116,37 @@ PgConnection.prototype.setDBConn = function(dbowner, params, callback) {
|
||||
*/
|
||||
|
||||
PgConnection.prototype.getConnection = function(username, callback) {
|
||||
var self = this;
|
||||
debug("getConn1");
|
||||
|
||||
var params = {};
|
||||
|
||||
require('debug')('cachechan')("getConn1");
|
||||
step(
|
||||
function setAuth() {
|
||||
self.setDBAuth(username, params, this);
|
||||
},
|
||||
function setConn(err) {
|
||||
assert.ifError(err);
|
||||
self.setDBConn(username, params, this);
|
||||
},
|
||||
function openConnection(err) {
|
||||
assert.ifError(err);
|
||||
return callback(err, new PSQL({
|
||||
user: params.dbuser,
|
||||
pass: params.dbpass,
|
||||
host: params.dbhost,
|
||||
port: params.dbport,
|
||||
dbname: params.dbname
|
||||
}));
|
||||
this.getDatabaseParams(username, (err, databaseParams) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
);
|
||||
return callback(err, new PSQL({
|
||||
user: databaseParams.dbuser,
|
||||
pass: databaseParams.dbpass,
|
||||
host: databaseParams.dbhost,
|
||||
port: databaseParams.dbport,
|
||||
dbname: databaseParams.dbname
|
||||
}));
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
PgConnection.prototype.getDatabaseParams = function(username, callback) {
|
||||
const databaseParams = {};
|
||||
|
||||
this.setDBAuth(username, databaseParams, 'master', err => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
this.setDBConn(username, databaseParams, err => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
callback(null, databaseParams);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
var assert = require('assert');
|
||||
var PSQL = require('cartodb-psql');
|
||||
var step = require('step');
|
||||
|
||||
function PgQueryRunner(pgConnection) {
|
||||
this.pgConnection = pgConnection;
|
||||
@@ -16,31 +14,23 @@ module.exports = PgQueryRunner;
|
||||
* @param {Function} callback function({Error}, {Array}) second argument is guaranteed to be an array
|
||||
*/
|
||||
PgQueryRunner.prototype.run = function(username, query, callback) {
|
||||
var self = this;
|
||||
|
||||
var params = {};
|
||||
|
||||
step(
|
||||
function setAuth() {
|
||||
self.pgConnection.setDBAuth(username, params, this);
|
||||
},
|
||||
function setConn(err) {
|
||||
assert.ifError(err);
|
||||
self.pgConnection.setDBConn(username, params, this);
|
||||
},
|
||||
function executeQuery(err) {
|
||||
assert.ifError(err);
|
||||
var psql = new PSQL({
|
||||
user: params.dbuser,
|
||||
pass: params.dbpass,
|
||||
host: params.dbhost,
|
||||
port: params.dbport,
|
||||
dbname: params.dbname
|
||||
});
|
||||
psql.query(query, function(err, resultSet) {
|
||||
resultSet = resultSet || {};
|
||||
return callback(err, resultSet.rows || []);
|
||||
});
|
||||
this.pgConnection.getDatabaseParams(username, (err, databaseParams) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
);
|
||||
|
||||
const psql = new PSQL({
|
||||
user: databaseParams.dbuser,
|
||||
pass: databaseParams.dbpass,
|
||||
host: databaseParams.dbhost,
|
||||
port: databaseParams.dbport,
|
||||
dbname: databaseParams.dbname
|
||||
});
|
||||
|
||||
psql.query(query, function (err, resultSet) {
|
||||
resultSet = resultSet || {};
|
||||
return callback(err, resultSet.rows || []);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
function TablesExtentApi(pgQueryRunner) {
|
||||
function TablesExtentBackend(pgQueryRunner) {
|
||||
this.pgQueryRunner = pgQueryRunner;
|
||||
}
|
||||
|
||||
module.exports = TablesExtentApi;
|
||||
module.exports = TablesExtentBackend;
|
||||
|
||||
/**
|
||||
* Given a username and a list of tables it will return the estimated extent in SRID 4326 for all the tables based on
|
||||
@@ -13,7 +13,7 @@ module.exports = TablesExtentApi;
|
||||
* `table_name` format as valid input
|
||||
* @param {Function} callback function(err, result) {Object} result with `west`, `south`, `east`, `north`
|
||||
*/
|
||||
TablesExtentApi.prototype.getBounds = function (username, tables, callback) {
|
||||
TablesExtentBackend.prototype.getBounds = function (username, tables, callback) {
|
||||
var estimatedExtentSQLs = tables.map(function(table) {
|
||||
return "ST_EstimatedExtent('" + table.schema_name + "', '" + table.table_name + "', 'the_geom_webmercator')";
|
||||
});
|
||||
@@ -5,17 +5,19 @@ var step = require('step');
|
||||
* @param metadataBackend
|
||||
* @param options
|
||||
* @constructor
|
||||
* @type {UserLimitsApi}
|
||||
* @type {UserLimitsBackend}
|
||||
*/
|
||||
function UserLimitsApi(metadataBackend, options) {
|
||||
function UserLimitsBackend(metadataBackend, options) {
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.options = options || {};
|
||||
this.options.limits = this.options.limits || {};
|
||||
|
||||
this.preprareRateLimit();
|
||||
}
|
||||
|
||||
module.exports = UserLimitsApi;
|
||||
module.exports = UserLimitsBackend;
|
||||
|
||||
UserLimitsApi.prototype.getRenderLimits = function (username, apiKey, callback) {
|
||||
UserLimitsBackend.prototype.getRenderLimits = function (username, apiKey, callback) {
|
||||
var self = this;
|
||||
|
||||
var limits = {
|
||||
@@ -38,7 +40,7 @@ UserLimitsApi.prototype.getRenderLimits = function (username, apiKey, callback)
|
||||
});
|
||||
};
|
||||
|
||||
UserLimitsApi.prototype.getTimeoutRenderLimit = function (username, apiKey, callback) {
|
||||
UserLimitsBackend.prototype.getTimeoutRenderLimit = function (username, apiKey, callback) {
|
||||
var self = this;
|
||||
|
||||
step(
|
||||
@@ -77,3 +79,13 @@ UserLimitsApi.prototype.getTimeoutRenderLimit = function (username, apiKey, call
|
||||
callback
|
||||
);
|
||||
};
|
||||
|
||||
UserLimitsBackend.prototype.preprareRateLimit = function () {
|
||||
if (this.options.limits.rateLimitsEnabled) {
|
||||
this.metadataBackend.loadRateLimitsScript();
|
||||
}
|
||||
};
|
||||
|
||||
UserLimitsBackend.prototype.getRateLimit = function (user, endpointGroup, callback) {
|
||||
this.metadataBackend.getRateLimit(user, 'maps', endpointGroup, callback);
|
||||
};
|
||||
15
lib/cartodb/cache/named_map_provider_cache.js
vendored
15
lib/cartodb/cache/named_map_provider_cache.js
vendored
@@ -6,12 +6,20 @@ var queue = require('queue-async');
|
||||
|
||||
var LruCache = require("lru-cache");
|
||||
|
||||
function NamedMapProviderCache(templateMaps, pgConnection, metadataBackend, userLimitsApi, mapConfigAdapter) {
|
||||
function NamedMapProviderCache(
|
||||
templateMaps,
|
||||
pgConnection,
|
||||
metadataBackend,
|
||||
userLimitsBackend,
|
||||
mapConfigAdapter,
|
||||
affectedTablesCache
|
||||
) {
|
||||
this.templateMaps = templateMaps;
|
||||
this.pgConnection = pgConnection;
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.userLimitsApi = userLimitsApi;
|
||||
this.userLimitsBackend = userLimitsBackend;
|
||||
this.mapConfigAdapter = mapConfigAdapter;
|
||||
this.affectedTablesCache = affectedTablesCache;
|
||||
|
||||
this.providerCache = new LruCache({ max: 2000 });
|
||||
}
|
||||
@@ -28,8 +36,9 @@ NamedMapProviderCache.prototype.get = function(user, templateId, config, authTok
|
||||
this.templateMaps,
|
||||
this.pgConnection,
|
||||
this.metadataBackend,
|
||||
this.userLimitsApi,
|
||||
this.userLimitsBackend,
|
||||
this.mapConfigAdapter,
|
||||
this.affectedTablesCache,
|
||||
user,
|
||||
templateId,
|
||||
config,
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
module.exports = {
|
||||
Analyses: require('./analyses'),
|
||||
Layergroup: require('./layergroup'),
|
||||
Map: require('./map'),
|
||||
NamedMaps: require('./named_maps'),
|
||||
NamedMapsAdmin: require('./named_maps_admin'),
|
||||
ServerInfo: require('./server_info')
|
||||
};
|
||||
@@ -1,513 +0,0 @@
|
||||
var assert = require('assert');
|
||||
var step = require('step');
|
||||
|
||||
var cors = require('../middleware/cors');
|
||||
var userMiddleware = require('../middleware/user');
|
||||
var allowQueryParams = require('../middleware/allow-query-params');
|
||||
var vectorError = require('../middleware/vector-error');
|
||||
|
||||
var DataviewBackend = require('../backends/dataview');
|
||||
var AnalysisStatusBackend = require('../backends/analysis-status');
|
||||
|
||||
var MapStoreMapConfigProvider = require('../models/mapconfig/provider/map-store-provider');
|
||||
|
||||
var QueryTables = require('cartodb-query-tables');
|
||||
|
||||
/**
|
||||
* @param {AuthApi} authApi
|
||||
* @param {PgConnection} pgConnection
|
||||
* @param {MapStore} mapStore
|
||||
* @param {TileBackend} tileBackend
|
||||
* @param {PreviewBackend} previewBackend
|
||||
* @param {AttributesBackend} attributesBackend
|
||||
* @param {SurrogateKeysCache} surrogateKeysCache
|
||||
* @param {UserLimitsApi} userLimitsApi
|
||||
* @param {LayergroupAffectedTables} layergroupAffectedTables
|
||||
* @param {AnalysisBackend} analysisBackend
|
||||
* @constructor
|
||||
*/
|
||||
function LayergroupController(prepareContext, pgConnection, mapStore, tileBackend, previewBackend, attributesBackend,
|
||||
surrogateKeysCache, userLimitsApi, layergroupAffectedTables, analysisBackend) {
|
||||
this.pgConnection = pgConnection;
|
||||
this.mapStore = mapStore;
|
||||
this.tileBackend = tileBackend;
|
||||
this.previewBackend = previewBackend;
|
||||
this.attributesBackend = attributesBackend;
|
||||
this.surrogateKeysCache = surrogateKeysCache;
|
||||
this.userLimitsApi = userLimitsApi;
|
||||
this.layergroupAffectedTables = layergroupAffectedTables;
|
||||
|
||||
this.dataviewBackend = new DataviewBackend(analysisBackend);
|
||||
this.analysisStatusBackend = new AnalysisStatusBackend();
|
||||
|
||||
this.prepareContext = prepareContext;
|
||||
}
|
||||
|
||||
module.exports = LayergroupController;
|
||||
|
||||
LayergroupController.prototype.register = function(app) {
|
||||
app.get(
|
||||
app.base_url_mapconfig + '/:token/:z/:x/:y@:scale_factor?x.:format',
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.prepareContext,
|
||||
this.tile.bind(this),
|
||||
vectorError()
|
||||
);
|
||||
|
||||
app.get(
|
||||
app.base_url_mapconfig + '/:token/:z/:x/:y.:format',
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.prepareContext,
|
||||
this.tile.bind(this),
|
||||
vectorError()
|
||||
);
|
||||
|
||||
app.get(
|
||||
app.base_url_mapconfig + '/:token/:layer/:z/:x/:y.(:format)',
|
||||
cors(),
|
||||
userMiddleware,
|
||||
validateLayerRouteMiddleware,
|
||||
this.prepareContext,
|
||||
this.layer.bind(this),
|
||||
vectorError()
|
||||
);
|
||||
|
||||
app.get(
|
||||
app.base_url_mapconfig + '/:token/:layer/attributes/:fid',
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.prepareContext,
|
||||
this.attributes.bind(this)
|
||||
);
|
||||
|
||||
app.get(
|
||||
app.base_url_mapconfig + '/static/center/:token/:z/:lat/:lng/:width/:height.:format',
|
||||
cors(),
|
||||
userMiddleware,
|
||||
allowQueryParams(['layer']),
|
||||
this.prepareContext,
|
||||
this.center.bind(this)
|
||||
);
|
||||
|
||||
app.get(
|
||||
app.base_url_mapconfig + '/static/bbox/:token/:west,:south,:east,:north/:width/:height.:format',
|
||||
cors(),
|
||||
userMiddleware,
|
||||
allowQueryParams(['layer']),
|
||||
this.prepareContext,
|
||||
this.bbox.bind(this)
|
||||
);
|
||||
|
||||
// Undocumented/non-supported API endpoint methods.
|
||||
// Use at your own peril.
|
||||
|
||||
var allowedDataviewQueryParams = [
|
||||
'filters', // json
|
||||
'own_filter', // 0, 1
|
||||
'no_filters', // 0, 1
|
||||
'bbox', // w,s,e,n
|
||||
'start', // number
|
||||
'end', // number
|
||||
'column_type', // string
|
||||
'bins', // number
|
||||
'aggregation', //string
|
||||
'offset', // number
|
||||
'q', // widgets search
|
||||
'categories', // number
|
||||
];
|
||||
|
||||
app.get(
|
||||
app.base_url_mapconfig + '/:token/dataview/:dataviewName',
|
||||
cors(),
|
||||
userMiddleware,
|
||||
allowQueryParams(allowedDataviewQueryParams),
|
||||
this.prepareContext,
|
||||
this.dataview.bind(this)
|
||||
);
|
||||
|
||||
app.get(
|
||||
app.base_url_mapconfig + '/:token/:layer/widget/:dataviewName',
|
||||
cors(),
|
||||
userMiddleware,
|
||||
allowQueryParams(allowedDataviewQueryParams),
|
||||
this.prepareContext,
|
||||
this.dataview.bind(this)
|
||||
);
|
||||
|
||||
app.get(
|
||||
app.base_url_mapconfig + '/:token/dataview/:dataviewName/search',
|
||||
cors(),
|
||||
userMiddleware,
|
||||
allowQueryParams(allowedDataviewQueryParams),
|
||||
this.prepareContext,
|
||||
this.dataviewSearch.bind(this)
|
||||
);
|
||||
|
||||
app.get(
|
||||
app.base_url_mapconfig + '/:token/:layer/widget/:dataviewName/search',
|
||||
cors(),
|
||||
userMiddleware,
|
||||
allowQueryParams(allowedDataviewQueryParams),
|
||||
this.prepareContext,
|
||||
this.dataviewSearch.bind(this)
|
||||
);
|
||||
|
||||
app.get(
|
||||
app.base_url_mapconfig + '/:token/analysis/node/:nodeId',
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.prepareContext,
|
||||
this.analysisNodeStatus.bind(this)
|
||||
);
|
||||
};
|
||||
|
||||
LayergroupController.prototype.analysisNodeStatus = function(req, res, next) {
|
||||
var self = this;
|
||||
|
||||
step(
|
||||
function retrieveNodeStatus() {
|
||||
self.analysisStatusBackend.getNodeStatus(res.locals, this);
|
||||
},
|
||||
function finish(err, nodeStatus, stats) {
|
||||
req.profiler.add(stats || {});
|
||||
|
||||
if (err) {
|
||||
err.label = 'GET NODE STATUS';
|
||||
next(err);
|
||||
} else {
|
||||
self.sendResponse(req, res, nodeStatus, 200, {
|
||||
'Cache-Control': 'public,max-age=5',
|
||||
'Last-Modified': new Date().toUTCString()
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
LayergroupController.prototype.dataview = function(req, res, next) {
|
||||
var self = this;
|
||||
|
||||
step(
|
||||
function retrieveDataview() {
|
||||
var mapConfigProvider = new MapStoreMapConfigProvider(
|
||||
self.mapStore, res.locals.user, self.userLimitsApi, res.locals
|
||||
);
|
||||
self.dataviewBackend.getDataview(
|
||||
mapConfigProvider,
|
||||
res.locals.user,
|
||||
res.locals,
|
||||
this
|
||||
);
|
||||
},
|
||||
function finish(err, dataview, stats) {
|
||||
req.profiler.add(stats || {});
|
||||
|
||||
if (err) {
|
||||
err.label = 'GET DATAVIEW';
|
||||
next(err);
|
||||
} else {
|
||||
self.sendResponse(req, res, dataview, 200);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
LayergroupController.prototype.dataviewSearch = function(req, res, next) {
|
||||
var self = this;
|
||||
|
||||
step(
|
||||
function searchDataview() {
|
||||
var mapConfigProvider = new MapStoreMapConfigProvider(
|
||||
self.mapStore, res.locals.user, self.userLimitsApi, res.locals
|
||||
);
|
||||
self.dataviewBackend.search(mapConfigProvider, res.locals.user, req.params.dataviewName, res.locals, this);
|
||||
},
|
||||
function finish(err, searchResult, stats) {
|
||||
req.profiler.add(stats || {});
|
||||
|
||||
if (err) {
|
||||
err.label = 'GET DATAVIEW SEARCH';
|
||||
next(err);
|
||||
} else {
|
||||
self.sendResponse(req, res, searchResult, 200);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
LayergroupController.prototype.attributes = function(req, res, next) {
|
||||
var self = this;
|
||||
|
||||
req.profiler.start('windshaft.maplayer_attribute');
|
||||
|
||||
step(
|
||||
function retrieveFeatureAttributes() {
|
||||
var mapConfigProvider = new MapStoreMapConfigProvider(
|
||||
self.mapStore, res.locals.user, self.userLimitsApi, res.locals
|
||||
);
|
||||
self.attributesBackend.getFeatureAttributes(mapConfigProvider, res.locals, false, this);
|
||||
},
|
||||
function finish(err, tile, stats) {
|
||||
req.profiler.add(stats || {});
|
||||
|
||||
if (err) {
|
||||
err.label = 'GET ATTRIBUTES';
|
||||
next(err);
|
||||
} else {
|
||||
self.sendResponse(req, res, tile, 200);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
// Gets a tile for a given token and set of tile ZXY coords. (OSM style)
|
||||
LayergroupController.prototype.tile = function(req, res, next) {
|
||||
req.profiler.start('windshaft.map_tile');
|
||||
this.tileOrLayer(req, res, next);
|
||||
};
|
||||
|
||||
// Gets a tile for a given token, layer set of tile ZXY coords. (OSM style)
|
||||
LayergroupController.prototype.layer = function(req, res, next) {
|
||||
req.profiler.start('windshaft.maplayer_tile');
|
||||
this.tileOrLayer(req, res, next);
|
||||
};
|
||||
|
||||
LayergroupController.prototype.tileOrLayer = function (req, res, next) {
|
||||
var self = this;
|
||||
|
||||
step(
|
||||
function mapController$getTileOrGrid() {
|
||||
self.tileBackend.getTile(
|
||||
new MapStoreMapConfigProvider(self.mapStore, res.locals.user, self.userLimitsApi, res.locals),
|
||||
req.params, this
|
||||
);
|
||||
},
|
||||
function mapController$finalize(err, tile, headers, stats) {
|
||||
req.profiler.add(stats);
|
||||
self.finalizeGetTileOrGrid(err, req, res, tile, headers, next);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
function getStatusCode(tile, format){
|
||||
return tile.length===0 && format==='mvt'? 204:200;
|
||||
}
|
||||
|
||||
// This function is meant for being called as the very last
|
||||
// step by all endpoints serving tiles or grids
|
||||
LayergroupController.prototype.finalizeGetTileOrGrid = function(err, req, res, tile, headers, next) {
|
||||
var supportedFormats = {
|
||||
grid_json: true,
|
||||
json_torque: true,
|
||||
torque_json: true,
|
||||
png: true,
|
||||
png32: true,
|
||||
mvt: true
|
||||
};
|
||||
|
||||
var formatStat = 'invalid';
|
||||
if (req.params.format) {
|
||||
var format = req.params.format.replace('.', '_');
|
||||
if (supportedFormats[format]) {
|
||||
formatStat = format;
|
||||
}
|
||||
}
|
||||
|
||||
if (err) {
|
||||
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
|
||||
var errMsg = err.message ? ( '' + err.message ) : ( '' + err );
|
||||
|
||||
// Rewrite mapnik parsing errors to start with layer number
|
||||
var matches = errMsg.match("(.*) in style 'layer([0-9]+)'");
|
||||
if (matches) {
|
||||
errMsg = 'style'+matches[2]+': ' + matches[1];
|
||||
}
|
||||
err.message = errMsg;
|
||||
|
||||
err.label = 'TILE RENDER';
|
||||
next(err);
|
||||
|
||||
global.statsClient.increment('windshaft.tiles.error');
|
||||
global.statsClient.increment('windshaft.tiles.' + formatStat + '.error');
|
||||
} else {
|
||||
this.sendResponse(req, res, tile, getStatusCode(tile, formatStat), headers);
|
||||
global.statsClient.increment('windshaft.tiles.success');
|
||||
global.statsClient.increment('windshaft.tiles.' + formatStat + '.success');
|
||||
}
|
||||
};
|
||||
|
||||
LayergroupController.prototype.bbox = function(req, res, next) {
|
||||
this.staticMap(req, res, +req.params.width, +req.params.height, {
|
||||
west: +req.params.west,
|
||||
north: +req.params.north,
|
||||
east: +req.params.east,
|
||||
south: +req.params.south
|
||||
}, null, next);
|
||||
};
|
||||
|
||||
LayergroupController.prototype.center = function(req, res, next) {
|
||||
this.staticMap(req, res, +req.params.width, +req.params.height, +req.params.z, {
|
||||
lng: +req.params.lng,
|
||||
lat: +req.params.lat
|
||||
}, next);
|
||||
};
|
||||
|
||||
LayergroupController.prototype.staticMap = function(req, res, width, height, zoom /* bounds */, center, next) {
|
||||
var format = req.params.format === 'jpg' ? 'jpeg' : 'png';
|
||||
// We force always the tile to be generated using PNG because
|
||||
// is the only format we support by now
|
||||
res.locals.format = 'png';
|
||||
res.locals.layer = res.locals.layer || 'all';
|
||||
|
||||
var self = this;
|
||||
|
||||
step(
|
||||
function getImage() {
|
||||
if (center) {
|
||||
self.previewBackend.getImage(
|
||||
new MapStoreMapConfigProvider(self.mapStore, res.locals.user, self.userLimitsApi, res.locals),
|
||||
format, width, height, zoom, center, this);
|
||||
} else {
|
||||
self.previewBackend.getImage(
|
||||
new MapStoreMapConfigProvider(self.mapStore, res.locals.user, self.userLimitsApi, res.locals),
|
||||
format, width, height, zoom /* bounds */, this);
|
||||
}
|
||||
},
|
||||
function handleImage(err, image, headers, stats) {
|
||||
req.profiler.done('render-' + format);
|
||||
req.profiler.add(stats || {});
|
||||
|
||||
if (err) {
|
||||
err.label = 'STATIC_MAP';
|
||||
next(err);
|
||||
} else {
|
||||
res.set('Content-Type', headers['Content-Type'] || 'image/' + format);
|
||||
self.sendResponse(req, res, image, 200);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
LayergroupController.prototype.sendResponse = function(req, res, body, status, headers) {
|
||||
var self = this;
|
||||
|
||||
req.profiler.done('res');
|
||||
|
||||
res.set('Cache-Control', 'public,max-age=31536000');
|
||||
|
||||
// Set Last-Modified header
|
||||
var lastUpdated;
|
||||
if (res.locals.cache_buster) {
|
||||
// Assuming cache_buster is a timestamp
|
||||
lastUpdated = new Date(parseInt(res.locals.cache_buster));
|
||||
} else {
|
||||
lastUpdated = new Date();
|
||||
}
|
||||
res.set('Last-Modified', lastUpdated.toUTCString());
|
||||
|
||||
var dbName = res.locals.dbname;
|
||||
step(
|
||||
function getAffectedTables() {
|
||||
self.getAffectedTables(res.locals.user, dbName, res.locals.token, this);
|
||||
},
|
||||
function sendResponse(err, affectedTables) {
|
||||
req.profiler.done('affectedTables');
|
||||
if (err) {
|
||||
global.logger.warn('ERROR generating cache channel: ' + err);
|
||||
}
|
||||
if (!!affectedTables) {
|
||||
res.set('X-Cache-Channel', affectedTables.getCacheChannel());
|
||||
self.surrogateKeysCache.tag(res, affectedTables);
|
||||
}
|
||||
|
||||
if (headers) {
|
||||
res.set(headers);
|
||||
}
|
||||
|
||||
res.status(status);
|
||||
|
||||
if (!Buffer.isBuffer(body) && typeof body === 'object') {
|
||||
if (req.query && req.query.callback) {
|
||||
res.jsonp(body);
|
||||
} else {
|
||||
res.json(body);
|
||||
}
|
||||
} else {
|
||||
res.send(body);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
LayergroupController.prototype.getAffectedTables = function(user, dbName, layergroupId, callback) {
|
||||
|
||||
if (this.layergroupAffectedTables.hasAffectedTables(dbName, layergroupId)) {
|
||||
return callback(null, this.layergroupAffectedTables.get(dbName, layergroupId));
|
||||
}
|
||||
|
||||
var self = this;
|
||||
step(
|
||||
function extractSQL() {
|
||||
step(
|
||||
function loadFromStore() {
|
||||
self.mapStore.load(layergroupId, this);
|
||||
},
|
||||
function getSQL(err, mapConfig) {
|
||||
assert.ifError(err);
|
||||
|
||||
var queries = [];
|
||||
mapConfig.getLayers().forEach(function(layer) {
|
||||
queries.push(layer.options.sql);
|
||||
if (layer.options.affected_tables) {
|
||||
layer.options.affected_tables.map(function(table) {
|
||||
queries.push('SELECT * FROM ' + table + ' LIMIT 0');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return queries.length ? queries.join(';') : null;
|
||||
},
|
||||
this
|
||||
);
|
||||
},
|
||||
function findAffectedTables(err, sql) {
|
||||
assert.ifError(err);
|
||||
|
||||
if ( ! sql ) {
|
||||
throw new Error("this request doesn't need an X-Cache-Channel generated");
|
||||
}
|
||||
|
||||
step(
|
||||
function getConnection() {
|
||||
self.pgConnection.getConnection(user, this);
|
||||
},
|
||||
function getAffectedTables(err, connection) {
|
||||
assert.ifError(err);
|
||||
|
||||
QueryTables.getAffectedTablesFromQuery(connection, sql, this);
|
||||
},
|
||||
this
|
||||
);
|
||||
},
|
||||
function buildCacheChannel(err, tables) {
|
||||
assert.ifError(err);
|
||||
self.layergroupAffectedTables.set(dbName, layergroupId, tables);
|
||||
|
||||
return tables;
|
||||
},
|
||||
callback
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
function validateLayerRouteMiddleware(req, res, next) {
|
||||
if (req.params.token === 'static') {
|
||||
return next('route');
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
@@ -1,729 +0,0 @@
|
||||
var _ = require('underscore');
|
||||
var windshaft = require('windshaft');
|
||||
var QueryTables = require('cartodb-query-tables');
|
||||
|
||||
var ResourceLocator = require('../models/resource-locator');
|
||||
|
||||
var cors = require('../middleware/cors');
|
||||
var userMiddleware = require('../middleware/user');
|
||||
const allowQueryParams = require('../middleware/allow-query-params');
|
||||
|
||||
var MapConfig = windshaft.model.MapConfig;
|
||||
var Datasource = windshaft.model.Datasource;
|
||||
|
||||
var NamedMapsCacheEntry = require('../cache/model/named_maps_entry');
|
||||
|
||||
var NamedMapMapConfigProvider = require('../models/mapconfig/provider/named-map-provider');
|
||||
var CreateLayergroupMapConfigProvider = require('../models/mapconfig/provider/create-layergroup-provider');
|
||||
|
||||
/**
|
||||
* @param {AuthApi} authApi
|
||||
* @param {PgConnection} pgConnection
|
||||
* @param {TemplateMaps} templateMaps
|
||||
* @param {MapBackend} mapBackend
|
||||
* @param metadataBackend
|
||||
* @param {SurrogateKeysCache} surrogateKeysCache
|
||||
* @param {UserLimitsApi} userLimitsApi
|
||||
* @param {LayergroupAffectedTables} layergroupAffectedTables
|
||||
* @param {MapConfigAdapter} mapConfigAdapter
|
||||
* @param {StatsBackend} statsBackend
|
||||
* @constructor
|
||||
*/
|
||||
function MapController(prepareContext, pgConnection, templateMaps, mapBackend, metadataBackend,
|
||||
surrogateKeysCache, userLimitsApi, layergroupAffectedTables, mapConfigAdapter,
|
||||
statsBackend) {
|
||||
this.pgConnection = pgConnection;
|
||||
this.templateMaps = templateMaps;
|
||||
this.mapBackend = mapBackend;
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.surrogateKeysCache = surrogateKeysCache;
|
||||
this.userLimitsApi = userLimitsApi;
|
||||
this.layergroupAffectedTables = layergroupAffectedTables;
|
||||
|
||||
this.mapConfigAdapter = mapConfigAdapter;
|
||||
this.resourceLocator = new ResourceLocator(global.environment);
|
||||
|
||||
this.statsBackend = statsBackend;
|
||||
this.prepareContext = prepareContext;
|
||||
}
|
||||
|
||||
module.exports = MapController;
|
||||
|
||||
MapController.prototype.register = function(app) {
|
||||
const { base_url_mapconfig, base_url_templated } = app;
|
||||
const useTemplate = true;
|
||||
|
||||
app.get(base_url_mapconfig, this.composeCreateMapMiddleware());
|
||||
app.post(base_url_mapconfig, this.composeCreateMapMiddleware());
|
||||
app.get(`${base_url_templated}/:template_id/jsonp`, this.composeCreateMapMiddleware(useTemplate));
|
||||
app.post(`${base_url_templated}/:template_id`, this.composeCreateMapMiddleware(useTemplate));
|
||||
app.options(app.base_url_mapconfig, cors('Content-Type'));
|
||||
};
|
||||
|
||||
MapController.prototype.composeCreateMapMiddleware = function (useTemplate = false) {
|
||||
const isTemplateInstantiation = useTemplate;
|
||||
const useTemplateHash = useTemplate;
|
||||
const includeQuery = !useTemplate;
|
||||
const label = useTemplate ? 'NAMED MAP LAYERGROUP' : 'ANONYMOUS LAYERGROUP';
|
||||
const addContext = !useTemplate;
|
||||
|
||||
return [
|
||||
cors(),
|
||||
userMiddleware,
|
||||
allowQueryParams(['aggregation']),
|
||||
this.prepareContext,
|
||||
this.initProfiler(isTemplateInstantiation),
|
||||
this.checkJsonContentType(),
|
||||
useTemplate ? this.checkInstantiteLayergroup() : this.checkCreateLayergroup(),
|
||||
useTemplate ? this.getTemplate() : this.prepareAdapterMapConfig(),
|
||||
useTemplate ? this.instantiateLayergroup() : this.createLayergroup(),
|
||||
this.incrementMapViewCount(),
|
||||
this.augmentLayergroupData(),
|
||||
this.getAffectedTables(),
|
||||
this.setCacheChannel(),
|
||||
this.setLastModified(),
|
||||
this.setLastUpdatedTimeToLayergroup(),
|
||||
this.setCacheControl(),
|
||||
this.setLayerStats(),
|
||||
this.setLayergroupIdHeader(useTemplateHash),
|
||||
this.setDataviewsAndWidgetsUrlsToLayergroupMetadata(),
|
||||
this.setAnalysesMetadataToLayergroup(includeQuery),
|
||||
this.setTurboCartoMetadataToLayergroup(),
|
||||
this.setAggregationMetadataToLayergroup(),
|
||||
this.setTilejsonMetadataToLayergroup(),
|
||||
this.setSurrogateKeyHeader(),
|
||||
this.sendResponse(),
|
||||
this.augmentError({ label, addContext })
|
||||
];
|
||||
};
|
||||
|
||||
MapController.prototype.initProfiler = function (isTemplateInstantiation) {
|
||||
const operation = isTemplateInstantiation ? 'instance_template' : 'createmap';
|
||||
|
||||
return function initProfilerMiddleware (req, res, next) {
|
||||
req.profiler.start(`windshaft-cartodb.${operation}_${req.method.toLowerCase()}`);
|
||||
req.profiler.done(`${operation}.initProfilerMiddleware`);
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
MapController.prototype.checkJsonContentType = function () {
|
||||
return function checkJsonContentTypeMiddleware(req, res, next) {
|
||||
if (req.method === 'POST' && !req.is('application/json')) {
|
||||
return next(new Error('POST data must be of type application/json'));
|
||||
}
|
||||
|
||||
req.profiler.done('checkJsonContentTypeMiddleware');
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
MapController.prototype.checkInstantiteLayergroup = function () {
|
||||
return function checkInstantiteLayergroupMiddleware(req, res, next) {
|
||||
if (req.method === 'GET') {
|
||||
const { callback, config } = req.query;
|
||||
|
||||
if (callback === undefined || callback.length === 0) {
|
||||
return next(new Error('callback parameter should be present and be a function name'));
|
||||
}
|
||||
|
||||
if (config) {
|
||||
try {
|
||||
req.body = JSON.parse(config);
|
||||
} catch(e) {
|
||||
return next(new Error('Invalid config parameter, should be a valid JSON'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
req.profiler.done('checkInstantiteLayergroup');
|
||||
|
||||
return next();
|
||||
};
|
||||
};
|
||||
|
||||
MapController.prototype.checkCreateLayergroup = function () {
|
||||
return function checkCreateLayergroupMiddleware (req, res, next) {
|
||||
if (req.method === 'GET') {
|
||||
const { config } = res.locals;
|
||||
|
||||
if (!config) {
|
||||
return next(new Error('layergroup GET needs a "config" parameter'));
|
||||
}
|
||||
|
||||
try {
|
||||
req.body = JSON.parse(config);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
}
|
||||
|
||||
req.profiler.done('checkCreateLayergroup');
|
||||
return next();
|
||||
};
|
||||
};
|
||||
|
||||
MapController.prototype.getTemplate = function () {
|
||||
return function getTemplateMiddleware (req, res, next) {
|
||||
const templateParams = req.body;
|
||||
const { user } = res.locals;
|
||||
|
||||
const mapconfigProvider = new NamedMapMapConfigProvider(
|
||||
this.templateMaps,
|
||||
this.pgConnection,
|
||||
this.metadataBackend,
|
||||
this.userLimitsApi,
|
||||
this.mapConfigAdapter,
|
||||
user,
|
||||
req.params.template_id,
|
||||
templateParams,
|
||||
res.locals.auth_token,
|
||||
res.locals
|
||||
);
|
||||
|
||||
mapconfigProvider.getMapConfig((err, mapconfig, rendererParams) => {
|
||||
req.profiler.done('named.getMapConfig');
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.locals.mapconfig = mapconfig;
|
||||
res.locals.rendererParams = rendererParams;
|
||||
res.locals.mapconfigProvider = mapconfigProvider;
|
||||
|
||||
next();
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
MapController.prototype.prepareAdapterMapConfig = function () {
|
||||
return function prepareAdapterMapConfigMiddleware(req, res, next) {
|
||||
const requestMapConfig = req.body;
|
||||
const { user, dbhost, dbport, dbname, dbuser, dbpassword, api_key } = res.locals;
|
||||
|
||||
const context = {
|
||||
analysisConfiguration: {
|
||||
user,
|
||||
db: {
|
||||
host: dbhost,
|
||||
port: dbport,
|
||||
dbname: dbname,
|
||||
user: dbuser,
|
||||
pass: dbpassword
|
||||
},
|
||||
batch: {
|
||||
username: user,
|
||||
apiKey: api_key
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.mapConfigAdapter.getMapConfig(user, requestMapConfig, res.locals, context, (err, requestMapConfig) => {
|
||||
req.profiler.done('anonymous.getMapConfig');
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
req.body = requestMapConfig;
|
||||
res.locals.context = context;
|
||||
|
||||
next();
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
MapController.prototype.createLayergroup = function () {
|
||||
return function createLayergroupMiddleware (req, res, next) {
|
||||
const requestMapConfig = req.body;
|
||||
const { context, user } = res.locals;
|
||||
const datasource = context.datasource || Datasource.EmptyDatasource();
|
||||
const mapconfig = new MapConfig(requestMapConfig, datasource);
|
||||
const mapconfigProvider =
|
||||
new CreateLayergroupMapConfigProvider(mapconfig, user, this.userLimitsApi, res.locals);
|
||||
|
||||
res.locals.mapconfig = mapconfig;
|
||||
res.locals.analysesResults = context.analysesResults;
|
||||
|
||||
this.mapBackend.createLayergroup(mapconfig, res.locals, mapconfigProvider, (err, layergroup) => {
|
||||
req.profiler.done('createLayergroup');
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.locals.layergroup = layergroup;
|
||||
|
||||
next();
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
MapController.prototype.instantiateLayergroup = function () {
|
||||
return function instantiateLayergroupMiddleware (req, res, next) {
|
||||
const { user, mapconfig, rendererParams } = res.locals;
|
||||
const mapconfigProvider =
|
||||
new CreateLayergroupMapConfigProvider(mapconfig, user, this.userLimitsApi, rendererParams);
|
||||
|
||||
this.mapBackend.createLayergroup(mapconfig, rendererParams, mapconfigProvider, (err, layergroup) => {
|
||||
req.profiler.done('createLayergroup');
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.locals.layergroup = layergroup;
|
||||
|
||||
const { mapconfigProvider } = res.locals;
|
||||
|
||||
res.locals.analysesResults = mapconfigProvider.analysesResults;
|
||||
res.locals.template = mapconfigProvider.template;
|
||||
res.locals.templateName = mapconfigProvider.getTemplateName();
|
||||
res.locals.context = mapconfigProvider.context;
|
||||
|
||||
next();
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
MapController.prototype.incrementMapViewCount = function () {
|
||||
return function incrementMapViewCountMiddleware(req, res, next) {
|
||||
const { mapconfig, user } = res.locals;
|
||||
|
||||
// Error won't blow up, just be logged.
|
||||
this.metadataBackend.incMapviewCount(user, mapconfig.obj().stat_tag, (err) => {
|
||||
req.profiler.done('incMapviewCount');
|
||||
|
||||
if (err) {
|
||||
global.logger.log(`ERROR: failed to increment mapview count for user '${user}': ${err.message}`);
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
MapController.prototype.augmentLayergroupData = function () {
|
||||
return function augmentLayergroupDataMiddleware (req, res, next) {
|
||||
const { layergroup } = res.locals;
|
||||
|
||||
// include in layergroup response the variables in serverMedata
|
||||
// those variables are useful to send to the client information
|
||||
// about how to reach this server or information about it
|
||||
_.extend(layergroup, global.environment.serverMetadata);
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
function getTemplateUrl(url) {
|
||||
return url.https || url.http;
|
||||
}
|
||||
|
||||
function getTilejson(tiles, grids) {
|
||||
const tilejson = {
|
||||
tilejson: '2.2.0',
|
||||
tiles: tiles.https || tiles.http
|
||||
};
|
||||
|
||||
if (grids) {
|
||||
tilejson.grids = grids.https || grids.http;
|
||||
}
|
||||
|
||||
return tilejson;
|
||||
}
|
||||
|
||||
MapController.prototype.setTilejsonMetadataToLayergroup = function () {
|
||||
return function augmentLayergroupTilejsonMiddleware (req, res, next) {
|
||||
const { layergroup, user, mapconfig } = res.locals;
|
||||
|
||||
const isVectorOnlyMapConfig = mapconfig.isVectorOnlyMapConfig();
|
||||
let hasMapnikLayers = false;
|
||||
layergroup.metadata.layers.forEach((layerMetadata, index) => {
|
||||
const layerId = mapconfig.getLayerId(index);
|
||||
const rasterResource = `${layergroup.layergroupid}/${layerId}/{z}/{x}/{y}.png`;
|
||||
if (mapconfig.layerType(index) === 'mapnik') {
|
||||
hasMapnikLayers = true;
|
||||
const vectorResource = `${layergroup.layergroupid}/${layerId}/{z}/{x}/{y}.mvt`;
|
||||
const layerTilejson = {
|
||||
vector: getTilejson(this.resourceLocator.getTileUrls(user, vectorResource))
|
||||
};
|
||||
if (!isVectorOnlyMapConfig) {
|
||||
let grids = null;
|
||||
const layer = mapconfig.getLayer(index);
|
||||
if (layer.options.interactivity) {
|
||||
const gridResource = `${layergroup.layergroupid}/${layerId}/{z}/{x}/{y}.grid.json`;
|
||||
grids = this.resourceLocator.getTileUrls(user, gridResource);
|
||||
}
|
||||
layerTilejson.raster = getTilejson(
|
||||
this.resourceLocator.getTileUrls(user, rasterResource),
|
||||
grids
|
||||
);
|
||||
}
|
||||
layerMetadata.tilejson = layerTilejson;
|
||||
} else {
|
||||
layerMetadata.tilejson = {
|
||||
raster: getTilejson(this.resourceLocator.getTileUrls(user, rasterResource))
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const tilejson = {};
|
||||
const url = {};
|
||||
|
||||
if (hasMapnikLayers) {
|
||||
const vectorResource = `${layergroup.layergroupid}/{z}/{x}/{y}.mvt`;
|
||||
tilejson.vector = getTilejson(
|
||||
this.resourceLocator.getTileUrls(user, vectorResource)
|
||||
);
|
||||
url.vector = getTemplateUrl(this.resourceLocator.getTemplateUrls(user, vectorResource));
|
||||
|
||||
if (!isVectorOnlyMapConfig) {
|
||||
const rasterResource = `${layergroup.layergroupid}/{z}/{x}/{y}.png`;
|
||||
tilejson.raster = getTilejson(
|
||||
this.resourceLocator.getTileUrls(user, rasterResource)
|
||||
);
|
||||
url.raster = getTemplateUrl(this.resourceLocator.getTemplateUrls(user, rasterResource));
|
||||
}
|
||||
}
|
||||
|
||||
layergroup.metadata.tilejson = tilejson;
|
||||
layergroup.metadata.url = url;
|
||||
|
||||
next();
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
MapController.prototype.getAffectedTables = function () {
|
||||
return function getAffectedTablesMiddleware (req, res, next) {
|
||||
const { dbname, layergroup, user, mapconfig } = res.locals;
|
||||
|
||||
this.pgConnection.getConnection(user, (err, connection) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
const sql = [];
|
||||
mapconfig.getLayers().forEach(function(layer) {
|
||||
sql.push(layer.options.sql);
|
||||
if (layer.options.affected_tables) {
|
||||
layer.options.affected_tables.map(function(table) {
|
||||
sql.push('SELECT * FROM ' + table + ' LIMIT 0');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
QueryTables.getAffectedTablesFromQuery(connection, sql.join(';'), (err, affectedTables) => {
|
||||
req.profiler.done('getAffectedTablesFromQuery');
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
// feed affected tables cache so it can be reused from, for instance, layergroup controller
|
||||
this.layergroupAffectedTables.set(dbname, layergroup.layergroupId, affectedTables);
|
||||
|
||||
res.locals.affectedTables = affectedTables;
|
||||
|
||||
next();
|
||||
});
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
MapController.prototype.setCacheChannel = function () {
|
||||
return function setCacheChannelMiddleware (req, res, next) {
|
||||
const { affectedTables } = res.locals;
|
||||
|
||||
if (req.method === 'GET') {
|
||||
res.set('X-Cache-Channel', affectedTables.getCacheChannel());
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
MapController.prototype.setLastModified = function () {
|
||||
return function setLastModifiedMiddleware (req, res, next) {
|
||||
if (req.method === 'GET') {
|
||||
res.set('Last-Modified', (new Date()).toUTCString());
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
MapController.prototype.setLastUpdatedTimeToLayergroup = function () {
|
||||
return function setLastUpdatedTimeToLayergroupMiddleware (req, res, next) {
|
||||
const { affectedTables, layergroup, analysesResults } = res.locals;
|
||||
|
||||
var lastUpdateTime = affectedTables.getLastUpdatedAt();
|
||||
|
||||
lastUpdateTime = getLastUpdatedTime(analysesResults, lastUpdateTime) || lastUpdateTime;
|
||||
|
||||
// last update for layergroup cache buster
|
||||
layergroup.layergroupid = layergroup.layergroupid + ':' + lastUpdateTime;
|
||||
layergroup.last_updated = new Date(lastUpdateTime).toISOString();
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
function getLastUpdatedTime(analysesResults, lastUpdateTime) {
|
||||
if (!Array.isArray(analysesResults)) {
|
||||
return lastUpdateTime;
|
||||
}
|
||||
return analysesResults.reduce(function(lastUpdateTime, analysis) {
|
||||
return analysis.getNodes().reduce(function(lastNodeUpdatedAtTime, node) {
|
||||
var nodeUpdatedAtDate = node.getUpdatedAt();
|
||||
var nodeUpdatedTimeAt = (nodeUpdatedAtDate && nodeUpdatedAtDate.getTime()) || 0;
|
||||
return nodeUpdatedTimeAt > lastNodeUpdatedAtTime ? nodeUpdatedTimeAt : lastNodeUpdatedAtTime;
|
||||
}, lastUpdateTime);
|
||||
}, lastUpdateTime);
|
||||
}
|
||||
|
||||
MapController.prototype.setCacheControl = function () {
|
||||
return function setCacheControlMiddleware (req, res, next) {
|
||||
if (req.method === 'GET') {
|
||||
var ttl = global.environment.varnish.layergroupTtl || 86400;
|
||||
res.set('Cache-Control', 'public,max-age='+ttl+',must-revalidate');
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
MapController.prototype.setLayerStats = function () {
|
||||
return function setLayerStatsMiddleware(req, res, next) {
|
||||
const { user, mapconfig, layergroup } = res.locals;
|
||||
|
||||
this.pgConnection.getConnection(user, (err, connection) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
this.statsBackend.getStats(mapconfig, connection, function(err, layersStats) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (layersStats.length > 0) {
|
||||
layergroup.metadata.layers.forEach(function (layer, index) {
|
||||
layer.meta.stats = layersStats[index];
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
MapController.prototype.setLayergroupIdHeader = function (useTemplateHash) {
|
||||
return function setLayergroupIdHeaderMiddleware (req, res, next) {
|
||||
const { layergroup, user, template } = res.locals;
|
||||
|
||||
if (useTemplateHash) {
|
||||
var templateHash = this.templateMaps.fingerPrint(template).substring(0, 8);
|
||||
layergroup.layergroupid = `${user}@${templateHash}@${layergroup.layergroupid}`;
|
||||
}
|
||||
|
||||
res.set('X-Layergroup-Id', layergroup.layergroupid);
|
||||
|
||||
next();
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
MapController.prototype.setDataviewsAndWidgetsUrlsToLayergroupMetadata = function () {
|
||||
return function setDataviewsAndWidgetsUrlsToLayergroupMetadataMiddleware (req, res, next) {
|
||||
const { layergroup, user, mapconfig } = res.locals;
|
||||
|
||||
this.addDataviewsAndWidgetsUrls(user, layergroup, mapconfig.obj());
|
||||
|
||||
next();
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
// TODO this should take into account several URL patterns
|
||||
MapController.prototype.addDataviewsAndWidgetsUrls = function(username, layergroup, mapConfig) {
|
||||
this.addDataviewsUrls(username, layergroup, mapConfig);
|
||||
this.addWidgetsUrl(username, layergroup, mapConfig);
|
||||
};
|
||||
|
||||
MapController.prototype.addDataviewsUrls = function(username, layergroup, mapConfig) {
|
||||
layergroup.metadata.dataviews = layergroup.metadata.dataviews || {};
|
||||
var dataviews = mapConfig.dataviews || {};
|
||||
|
||||
Object.keys(dataviews).forEach(function(dataviewName) {
|
||||
var resource = layergroup.layergroupid + '/dataview/' + dataviewName;
|
||||
layergroup.metadata.dataviews[dataviewName] = {
|
||||
url: this.resourceLocator.getUrls(username, resource)
|
||||
};
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
MapController.prototype.addWidgetsUrl = function(username, layergroup, mapConfig) {
|
||||
if (layergroup.metadata && Array.isArray(layergroup.metadata.layers) && Array.isArray(mapConfig.layers)) {
|
||||
layergroup.metadata.layers = layergroup.metadata.layers.map(function(layer, layerIndex) {
|
||||
var mapConfigLayer = mapConfig.layers[layerIndex];
|
||||
if (mapConfigLayer.options && mapConfigLayer.options.widgets) {
|
||||
layer.widgets = layer.widgets || {};
|
||||
Object.keys(mapConfigLayer.options.widgets).forEach(function(widgetName) {
|
||||
var resource = layergroup.layergroupid + '/' + layerIndex + '/widget/' + widgetName;
|
||||
layer.widgets[widgetName] = {
|
||||
type: mapConfigLayer.options.widgets[widgetName].type,
|
||||
url: this.resourceLocator.getUrls(username, resource)
|
||||
};
|
||||
}.bind(this));
|
||||
}
|
||||
return layer;
|
||||
}.bind(this));
|
||||
}
|
||||
};
|
||||
|
||||
MapController.prototype.setAnalysesMetadataToLayergroup = function (includeQuery) {
|
||||
return function setAnalysesMetadataToLayergroupMiddleware (req, res, next) {
|
||||
const { layergroup, user, analysesResults = [] } = res.locals;
|
||||
|
||||
this.addAnalysesMetadata(user, layergroup, analysesResults, includeQuery);
|
||||
|
||||
next();
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
MapController.prototype.addAnalysesMetadata = function(username, layergroup, analysesResults, includeQuery) {
|
||||
includeQuery = includeQuery || false;
|
||||
analysesResults = analysesResults || [];
|
||||
layergroup.metadata.analyses = [];
|
||||
|
||||
analysesResults.forEach(function(analysis) {
|
||||
var nodes = analysis.getNodes();
|
||||
layergroup.metadata.analyses.push({
|
||||
nodes: nodes.reduce(function(nodesIdMap, node) {
|
||||
if (node.params.id) {
|
||||
var nodeResource = layergroup.layergroupid + '/analysis/node/' + node.id();
|
||||
var nodeRepr = {
|
||||
status: node.getStatus(),
|
||||
url: this.resourceLocator.getUrls(username, nodeResource)
|
||||
};
|
||||
if (includeQuery) {
|
||||
nodeRepr.query = node.getQuery();
|
||||
}
|
||||
if (node.getStatus() === 'failed') {
|
||||
nodeRepr.error_message = node.getErrorMessage();
|
||||
}
|
||||
nodesIdMap[node.params.id] = nodeRepr;
|
||||
}
|
||||
|
||||
return nodesIdMap;
|
||||
}.bind(this), {})
|
||||
});
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
MapController.prototype.setTurboCartoMetadataToLayergroup = function () {
|
||||
return function setTurboCartoMetadataToLayergroupMiddleware (req, res, next) {
|
||||
const { layergroup, mapconfig, context } = res.locals;
|
||||
|
||||
addTurboCartoContextMetadata(layergroup, mapconfig.obj(), context);
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
function addTurboCartoContextMetadata(layergroup, mapConfig, context) {
|
||||
if (layergroup.metadata && Array.isArray(layergroup.metadata.layers) && Array.isArray(mapConfig.layers)) {
|
||||
layergroup.metadata.layers = layergroup.metadata.layers.map(function(layer, layerIndex) {
|
||||
if (context.turboCarto && Array.isArray(context.turboCarto.layers)) {
|
||||
layer.meta.cartocss_meta = context.turboCarto.layers[layerIndex];
|
||||
}
|
||||
return layer;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: see how evolve this function, it's a good candidate to be refactored
|
||||
MapController.prototype.setAggregationMetadataToLayergroup = function () {
|
||||
return function setAggregationMetadataToLayergroupMiddleware (req, res, next) {
|
||||
const { layergroup, mapconfig, context } = res.locals;
|
||||
|
||||
addAggregationContextMetadata(layergroup, mapconfig.obj(), context);
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
function addAggregationContextMetadata(layergroup, mapConfig, context) {
|
||||
if (layergroup.metadata && Array.isArray(layergroup.metadata.layers) && Array.isArray(mapConfig.layers)) {
|
||||
layergroup.metadata.layers = layergroup.metadata.layers.map(function(layer, layerIndex) {
|
||||
if (context.aggregation && Array.isArray(context.aggregation.layers)) {
|
||||
layer.meta.aggregation = context.aggregation.layers[layerIndex];
|
||||
}
|
||||
return layer;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
MapController.prototype.setSurrogateKeyHeader = function () {
|
||||
return function setSurrogateKeyHeaderMiddleware(req, res, next) {
|
||||
const { affectedTables, user, templateName } = res.locals;
|
||||
|
||||
if (req.method === 'GET' && affectedTables.tables && affectedTables.tables.length > 0) {
|
||||
this.surrogateKeysCache.tag(res, affectedTables);
|
||||
}
|
||||
|
||||
if (templateName) {
|
||||
this.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(user, templateName));
|
||||
}
|
||||
|
||||
next();
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
MapController.prototype.sendResponse = function () {
|
||||
return function sendResponseMiddleware (req, res) {
|
||||
req.profiler.done('res');
|
||||
const { layergroup } = res.locals;
|
||||
|
||||
res.status(200);
|
||||
|
||||
if (req.query && req.query.callback) {
|
||||
res.jsonp(layergroup);
|
||||
} else {
|
||||
res.json(layergroup);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
MapController.prototype.augmentError = function (options) {
|
||||
const { addContext = false, label = 'MAPS CONTROLLER' } = options;
|
||||
|
||||
return function augmentErrorMiddleware (err, req, res, next) {
|
||||
req.profiler.done('error');
|
||||
const { mapconfig } = res.locals;
|
||||
|
||||
if (addContext) {
|
||||
err = Number.isFinite(err.layerIndex) ? populateError(err, mapconfig) : err;
|
||||
}
|
||||
|
||||
err.label = label;
|
||||
|
||||
next(err);
|
||||
};
|
||||
};
|
||||
|
||||
function populateError(err, mapConfig) {
|
||||
var error = new Error(err.message);
|
||||
error.http_status = err.http_status;
|
||||
|
||||
if (!err.http_status && err.message.indexOf('column "the_geom_webmercator" does not exist') >= 0) {
|
||||
error.http_status = 400;
|
||||
}
|
||||
|
||||
error.type = 'layer';
|
||||
error.subtype = err.message.indexOf('Postgis Plugin') >= 0 ? 'query' : undefined;
|
||||
error.layer = {
|
||||
id: mapConfig.getLayerId(err.layerIndex),
|
||||
index: err.layerIndex,
|
||||
type: mapConfig.layerType(err.layerIndex)
|
||||
};
|
||||
|
||||
return error;
|
||||
}
|
||||
@@ -1,490 +0,0 @@
|
||||
const NamedMapsCacheEntry = require('../cache/model/named_maps_entry');
|
||||
const cors = require('../middleware/cors');
|
||||
const userMiddleware = require('../middleware/user');
|
||||
const allowQueryParams = require('../middleware/allow-query-params');
|
||||
const vectorError = require('../middleware/vector-error');
|
||||
|
||||
const DEFAULT_ZOOM_CENTER = {
|
||||
zoom: 1,
|
||||
center: {
|
||||
lng: 0,
|
||||
lat: 0
|
||||
}
|
||||
};
|
||||
|
||||
function numMapper(n) {
|
||||
return +n;
|
||||
}
|
||||
|
||||
function getRequestParams(locals) {
|
||||
const params = Object.assign({}, locals);
|
||||
|
||||
delete params.template;
|
||||
delete params.affectedTablesAndLastUpdate;
|
||||
delete params.namedMapProvider;
|
||||
delete params.allowedQueryParams;
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
function NamedMapsController(prepareContext, namedMapProviderCache, tileBackend, previewBackend,
|
||||
surrogateKeysCache, tablesExtentApi, metadataBackend) {
|
||||
this.namedMapProviderCache = namedMapProviderCache;
|
||||
this.tileBackend = tileBackend;
|
||||
this.previewBackend = previewBackend;
|
||||
this.surrogateKeysCache = surrogateKeysCache;
|
||||
this.tablesExtentApi = tablesExtentApi;
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.prepareContext = prepareContext;
|
||||
}
|
||||
|
||||
module.exports = NamedMapsController;
|
||||
|
||||
NamedMapsController.prototype.register = function(app) {
|
||||
app.get(
|
||||
app.base_url_templated + '/:template_id/:layer/:z/:x/:y.(:format)',
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.prepareContext,
|
||||
this.getNamedMapProvider('NAMED_MAP_TILE'),
|
||||
this.getAffectedTables(),
|
||||
this.getTile('NAMED_MAP_TILE'),
|
||||
this.setSurrogateKey(),
|
||||
this.setCacheChannelHeader(),
|
||||
this.setLastModifiedHeader(),
|
||||
this.setCacheControlHeader(),
|
||||
this.setContentTypeHeader(),
|
||||
this.respond(),
|
||||
vectorError()
|
||||
);
|
||||
|
||||
app.get(
|
||||
app.base_url_mapconfig + '/static/named/:template_id/:width/:height.:format',
|
||||
cors(),
|
||||
userMiddleware,
|
||||
allowQueryParams(['layer', 'zoom', 'lon', 'lat', 'bbox']),
|
||||
this.prepareContext,
|
||||
this.getNamedMapProvider('STATIC_VIZ_MAP'),
|
||||
this.getAffectedTables(),
|
||||
this.getTemplate('STATIC_VIZ_MAP'),
|
||||
this.prepareLayerFilterFromPreviewLayers('STATIC_VIZ_MAP'),
|
||||
this.getStaticImageOptions(),
|
||||
this.getImage('STATIC_VIZ_MAP'),
|
||||
this.incrementMapViews(),
|
||||
this.setSurrogateKey(),
|
||||
this.setCacheChannelHeader(),
|
||||
this.setLastModifiedHeader(),
|
||||
this.setCacheControlHeader(),
|
||||
this.setContentTypeHeader(),
|
||||
this.respond()
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.getNamedMapProvider = function (label) {
|
||||
return function getNamedMapProviderMiddleware (req, res, next) {
|
||||
const { user } = res.locals;
|
||||
const { config, auth_token } = req.query;
|
||||
const { template_id } = req.params;
|
||||
|
||||
// We force always the tile to be generated using PNG because
|
||||
// is the only format we support by now
|
||||
res.locals.format = 'png';
|
||||
res.locals.layer = res.locals.layer || 'all';
|
||||
|
||||
const params = getRequestParams(res.locals);
|
||||
|
||||
this.namedMapProviderCache.get(user, template_id, config, auth_token, params, (err, namedMapProvider) => {
|
||||
if (err) {
|
||||
err.label = label;
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.locals.namedMapProvider = namedMapProvider;
|
||||
|
||||
next();
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.getAffectedTables = function () {
|
||||
return function getAffectedTables (req, res, next) {
|
||||
const { namedMapProvider } = res.locals;
|
||||
|
||||
namedMapProvider.getAffectedTablesAndLastUpdatedTime((err, affectedTablesAndLastUpdate) => {
|
||||
req.profiler.done('affectedTables');
|
||||
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.locals.affectedTablesAndLastUpdate = affectedTablesAndLastUpdate;
|
||||
|
||||
next();
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.getTemplate = function (label) {
|
||||
return function getTemplateMiddleware (req, res, next) {
|
||||
const { namedMapProvider } = res.locals;
|
||||
|
||||
namedMapProvider.getTemplate((err, template) => {
|
||||
if (err) {
|
||||
err.label = label;
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.locals.template = template;
|
||||
|
||||
next();
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.prepareLayerFilterFromPreviewLayers = function (label) {
|
||||
return function prepareLayerFilterFromPreviewLayersMiddleware (req, res, next) {
|
||||
const { user, template } = res.locals;
|
||||
const { template_id } = req.params;
|
||||
const { config, auth_token } = req.query;
|
||||
|
||||
if (!template || !template.view || !template.view.preview_layers) {
|
||||
return next();
|
||||
}
|
||||
|
||||
var previewLayers = template.view.preview_layers;
|
||||
var layerVisibilityFilter = [];
|
||||
|
||||
template.layergroup.layers.forEach((layer, index) => {
|
||||
if (previewLayers[''+index] !== false && previewLayers[layer.id] !== false) {
|
||||
layerVisibilityFilter.push(''+index);
|
||||
}
|
||||
});
|
||||
|
||||
if (!layerVisibilityFilter.length) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const params = getRequestParams(res.locals);
|
||||
|
||||
// overwrites 'all' default filter
|
||||
params.layer = layerVisibilityFilter.join(',');
|
||||
|
||||
// recreates the provider
|
||||
this.namedMapProviderCache.get(user, template_id, config, auth_token, params, (err, provider) => {
|
||||
if (err) {
|
||||
err.label = label;
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.locals.namedMapProvider = provider;
|
||||
|
||||
next();
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.getTile = function (label) {
|
||||
return function getTileMiddleware (req, res, next) {
|
||||
const { namedMapProvider } = res.locals;
|
||||
|
||||
this.tileBackend.getTile(namedMapProvider, req.params, (err, tile, headers, stats) => {
|
||||
req.profiler.add(stats);
|
||||
|
||||
if (err) {
|
||||
err.label = label;
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.locals.body = tile;
|
||||
res.locals.headers = headers;
|
||||
res.locals.stats = stats;
|
||||
|
||||
next();
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.getStaticImageOptions = function () {
|
||||
return function getStaticImageOptionsMiddleware(req, res, next) {
|
||||
const { user, namedMapProvider, template } = res.locals;
|
||||
|
||||
const imageOpts = getImageOptions(res.locals, template);
|
||||
|
||||
if (imageOpts) {
|
||||
res.locals.imageOpts = imageOpts;
|
||||
return next();
|
||||
}
|
||||
|
||||
res.locals.imageOpts = DEFAULT_ZOOM_CENTER;
|
||||
|
||||
namedMapProvider.getAffectedTablesAndLastUpdatedTime((err, affectedTablesAndLastUpdate) => {
|
||||
if (err) {
|
||||
return next();
|
||||
}
|
||||
|
||||
var affectedTables = affectedTablesAndLastUpdate.tables || [];
|
||||
|
||||
if (affectedTables.length === 0) {
|
||||
return next();
|
||||
}
|
||||
|
||||
this.tablesExtentApi.getBounds(user, affectedTables, (err, bounds) => {
|
||||
if (err) {
|
||||
return next();
|
||||
}
|
||||
|
||||
res.locals.imageOpts = bounds;
|
||||
|
||||
return next();
|
||||
});
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
function getImageOptions (params, template) {
|
||||
const { zoom, lon, lat, bbox } = params;
|
||||
|
||||
let imageOpts = getImageOptionsFromCoordinates(zoom, lon, lat);
|
||||
if (imageOpts) {
|
||||
return imageOpts;
|
||||
}
|
||||
|
||||
imageOpts = getImageOptionsFromBoundingBox(bbox);
|
||||
if (imageOpts) {
|
||||
return imageOpts;
|
||||
}
|
||||
|
||||
imageOpts = getImageOptionsFromTemplate(template, zoom);
|
||||
if (imageOpts) {
|
||||
return imageOpts;
|
||||
}
|
||||
}
|
||||
|
||||
function getImageOptionsFromCoordinates (zoom, lon, lat) {
|
||||
if ([zoom, lon, lat].map(numMapper).every(Number.isFinite)) {
|
||||
return {
|
||||
zoom: zoom,
|
||||
center: {
|
||||
lng: lon,
|
||||
lat: lat
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function getImageOptionsFromTemplate (template, zoom) {
|
||||
if (template.view) {
|
||||
var zoomCenter = templateZoomCenter(template.view);
|
||||
if (zoomCenter) {
|
||||
if (Number.isFinite(+zoom)) {
|
||||
zoomCenter.zoom = +zoom;
|
||||
}
|
||||
|
||||
return zoomCenter;
|
||||
}
|
||||
|
||||
var bounds = templateBounds(template.view);
|
||||
if (bounds) {
|
||||
return bounds;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getImageOptionsFromBoundingBox (bbox = '') {
|
||||
var _bbox = bbox.split(',').map(numMapper);
|
||||
|
||||
if (_bbox.length === 4 && _bbox.every(Number.isFinite)) {
|
||||
return {
|
||||
bounds: {
|
||||
west: _bbox[0],
|
||||
south: _bbox[1],
|
||||
east: _bbox[2],
|
||||
north: _bbox[3]
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
NamedMapsController.prototype.getImage = function (label) {
|
||||
return function getImageMiddleware (req, res, next) {
|
||||
const { imageOpts, namedMapProvider } = res.locals;
|
||||
const { zoom, center, bounds } = imageOpts;
|
||||
|
||||
let { width, height } = req.params;
|
||||
|
||||
width = +width;
|
||||
height = +height;
|
||||
|
||||
const format = req.params.format === 'jpg' ? 'jpeg' : 'png';
|
||||
|
||||
if (zoom !== undefined && center) {
|
||||
return this.previewBackend.getImage(namedMapProvider, format, width, height, zoom, center,
|
||||
(err, image, headers, stats) => {
|
||||
if (err) {
|
||||
err.label = label;
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.locals.body = image;
|
||||
res.locals.headers = headers;
|
||||
res.locals.stats = stats;
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
this.previewBackend.getImage(namedMapProvider, format, width, height, bounds, (err, image, headers, stats) => {
|
||||
if (err) {
|
||||
err.label = label;
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.locals.body = image;
|
||||
res.locals.headers = headers;
|
||||
res.locals.stats = stats;
|
||||
|
||||
next();
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
function incrementMapViewsError (ctx) {
|
||||
return `ERROR: failed to increment mapview count for user '${ctx.user}': ${ctx.err}`;
|
||||
}
|
||||
|
||||
NamedMapsController.prototype.incrementMapViews = function () {
|
||||
return function incrementMapViewsMiddleware(req, res, next) {
|
||||
const { user, namedMapProvider } = res.locals;
|
||||
|
||||
namedMapProvider.getMapConfig((err, mapConfig) => {
|
||||
if (err) {
|
||||
global.logger.log(incrementMapViewsError({ user, err }));
|
||||
return next();
|
||||
}
|
||||
|
||||
const statTag = mapConfig.obj().stat_tag;
|
||||
|
||||
this.metadataBackend.incMapviewCount(user, statTag, (err) => {
|
||||
if (err) {
|
||||
global.logger.log(incrementMapViewsError({ user, err }));
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
function templateZoomCenter(view) {
|
||||
if (view.zoom !== undefined && view.center) {
|
||||
return {
|
||||
zoom: view.zoom,
|
||||
center: view.center
|
||||
};
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function templateBounds(view) {
|
||||
if (view.bounds) {
|
||||
var hasAllBounds = ['west', 'south', 'east', 'north'].every(prop => Number.isFinite(view.bounds[prop]));
|
||||
|
||||
if (hasAllBounds) {
|
||||
return {
|
||||
bounds: {
|
||||
west: view.bounds.west,
|
||||
south: view.bounds.south,
|
||||
east: view.bounds.east,
|
||||
north: view.bounds.north
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
NamedMapsController.prototype.setCacheChannelHeader = function () {
|
||||
return function setCacheChannelHeaderMiddleware (req, res, next) {
|
||||
const { affectedTablesAndLastUpdate } = res.locals;
|
||||
|
||||
if (!affectedTablesAndLastUpdate || !!affectedTablesAndLastUpdate.tables) {
|
||||
res.set('X-Cache-Channel', affectedTablesAndLastUpdate.getCacheChannel());
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.setSurrogateKey = function () {
|
||||
return function setSurrogateKeyMiddleware(req, res, next) {
|
||||
const { user, namedMapProvider, affectedTablesAndLastUpdate } = res.locals;
|
||||
|
||||
this.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(user, namedMapProvider.getTemplateName()));
|
||||
if (!affectedTablesAndLastUpdate || !!affectedTablesAndLastUpdate.tables) {
|
||||
if (affectedTablesAndLastUpdate.tables.length > 0) {
|
||||
this.surrogateKeysCache.tag(res, affectedTablesAndLastUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.setLastModifiedHeader = function () {
|
||||
return function setLastModifiedHeaderMiddleware(req, res, next) {
|
||||
const { affectedTablesAndLastUpdate } = res.locals;
|
||||
|
||||
if (!affectedTablesAndLastUpdate || !!affectedTablesAndLastUpdate.tables) {
|
||||
var lastModifiedDate;
|
||||
if (Number.isFinite(affectedTablesAndLastUpdate.lastUpdatedTime)) {
|
||||
lastModifiedDate = new Date(affectedTablesAndLastUpdate.getLastUpdatedAt());
|
||||
} else {
|
||||
lastModifiedDate = new Date();
|
||||
}
|
||||
|
||||
res.set('Last-Modified', lastModifiedDate.toUTCString());
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.setCacheControlHeader = function () {
|
||||
return function setCacheControlHeaderMiddleware(req, res, next) {
|
||||
const { affectedTablesAndLastUpdate } = res.locals;
|
||||
|
||||
res.set('Cache-Control', 'public,max-age=7200,must-revalidate');
|
||||
|
||||
if (!affectedTablesAndLastUpdate || !!affectedTablesAndLastUpdate.tables) {
|
||||
// we increase cache control as we can invalidate it
|
||||
res.set('Cache-Control', 'public,max-age=31536000');
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.setContentTypeHeader = function () {
|
||||
return function setContentTypeHeaderMiddleware(req, res, next) {
|
||||
const { headers = {} } = res.locals;
|
||||
|
||||
res.set('Content-Type', headers['content-type'] || headers['Content-Type'] || 'image/png');
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.respond = function () {
|
||||
return function respondMiddleware (req, res) {
|
||||
const { body, stats = {}, format } = res.locals;
|
||||
|
||||
req.profiler.done('render-' + format);
|
||||
req.profiler.add(stats);
|
||||
|
||||
res.status(200);
|
||||
res.send(body);
|
||||
};
|
||||
};
|
||||
@@ -1,204 +0,0 @@
|
||||
const { templateName } = require('../backends/template_maps');
|
||||
const cors = require('../middleware/cors');
|
||||
const userMiddleware = require('../middleware/user');
|
||||
|
||||
/**
|
||||
* @param {AuthApi} authApi
|
||||
* @param {PgConnection} pgConnection
|
||||
* @param {TemplateMaps} templateMaps
|
||||
* @constructor
|
||||
*/
|
||||
function NamedMapsAdminController(authApi, templateMaps) {
|
||||
this.authApi = authApi;
|
||||
this.templateMaps = templateMaps;
|
||||
}
|
||||
|
||||
module.exports = NamedMapsAdminController;
|
||||
|
||||
NamedMapsAdminController.prototype.register = function (app) {
|
||||
const { base_url_templated } = app;
|
||||
|
||||
app.post(
|
||||
`${base_url_templated}/`,
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.checkContentType('POST', 'POST TEMPLATE'),
|
||||
this.authorizedByAPIKey('create', 'POST TEMPLATE'),
|
||||
this.create()
|
||||
);
|
||||
|
||||
app.put(
|
||||
`${base_url_templated}/:template_id`,
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.checkContentType('PUT', 'PUT TEMPLATE'),
|
||||
this.authorizedByAPIKey('update', 'PUT TEMPLATE'),
|
||||
this.update()
|
||||
);
|
||||
|
||||
app.get(
|
||||
`${base_url_templated}/:template_id`,
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.authorizedByAPIKey('get', 'GET TEMPLATE'),
|
||||
this.retrieve()
|
||||
);
|
||||
|
||||
app.delete(
|
||||
`${base_url_templated}/:template_id`,
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.authorizedByAPIKey('delete', 'DELETE TEMPLATE'),
|
||||
this.destroy()
|
||||
);
|
||||
|
||||
app.get(
|
||||
`${base_url_templated}/`,
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.authorizedByAPIKey('list', 'GET TEMPLATE LIST'),
|
||||
this.list()
|
||||
);
|
||||
|
||||
app.options(
|
||||
`${base_url_templated}/:template_id`,
|
||||
cors('Content-Type')
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.authorizedByAPIKey = function (action, label) {
|
||||
return function authorizedByAPIKeyMiddleware (req, res, next) {
|
||||
const { user } = res.locals;
|
||||
|
||||
this.authApi.authorizedByAPIKey(user, req, (err, authenticated) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (!authenticated) {
|
||||
const error = new Error(`Only authenticated user can ${action} templated maps`);
|
||||
error.http_status = 403;
|
||||
error.label = label;
|
||||
return next(error);
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.checkContentType = function (action, label) {
|
||||
return function checkContentTypeMiddleware (req, res, next) {
|
||||
if (!req.is('application/json')) {
|
||||
const error = new Error(`template ${action} data must be of type application/json`);
|
||||
error.label = label;
|
||||
return next(error);
|
||||
}
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.create = function () {
|
||||
return function createTemplateMiddleware (req, res, next) {
|
||||
const { user } = res.locals;
|
||||
const template = req.body;
|
||||
|
||||
this.templateMaps.addTemplate(user, template, (err, templateId) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.status(200);
|
||||
|
||||
const method = req.query.callback ? 'jsonp' : 'json';
|
||||
res[method]({ template_id: templateId });
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.update = function () {
|
||||
return function updateTemplateMiddleware (req, res, next) {
|
||||
const { user } = res.locals;
|
||||
const template = req.body;
|
||||
const templateId = templateName(req.params.template_id);
|
||||
|
||||
this.templateMaps.updTemplate(user, templateId, template, (err) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.status(200);
|
||||
|
||||
const method = req.query.callback ? 'jsonp' : 'json';
|
||||
res[method]({ template_id: templateId });
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.retrieve = function () {
|
||||
return function retrieveTemplateMiddleware (req, res, next) {
|
||||
req.profiler.start('windshaft-cartodb.get_template');
|
||||
|
||||
const { user } = res.locals;
|
||||
const templateId = templateName(req.params.template_id);
|
||||
|
||||
this.templateMaps.getTemplate(user, templateId, (err, template) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (!template) {
|
||||
const error = new Error(`Cannot find template '${templateId}' of user '${user}'`);
|
||||
error.http_status = 404;
|
||||
return next(error);
|
||||
}
|
||||
// auth_id was added by ourselves,
|
||||
// so we remove it before returning to the user
|
||||
delete template.auth_id;
|
||||
|
||||
res.status(200);
|
||||
|
||||
const method = req.query.callback ? 'jsonp' : 'json';
|
||||
res[method]({ template });
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.destroy = function () {
|
||||
return function destroyTemplateMiddleware (req, res, next) {
|
||||
req.profiler.start('windshaft-cartodb.delete_template');
|
||||
|
||||
const { user } = res.locals;
|
||||
const templateId = templateName(req.params.template_id);
|
||||
|
||||
this.templateMaps.delTemplate(user, templateId, (err/* , tpl_val */) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.status(204);
|
||||
|
||||
const method = req.query.callback ? 'jsonp' : 'json';
|
||||
res[method]('');
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.list = function () {
|
||||
return function listTemplatesMiddleware (req, res, next) {
|
||||
req.profiler.start('windshaft-cartodb.get_template_list');
|
||||
|
||||
const { user } = res.locals;
|
||||
|
||||
this.templateMaps.listTemplates(user, (err, templateIds) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
res.status(200);
|
||||
|
||||
const method = req.query.callback ? 'jsonp' : 'json';
|
||||
res[method]({ template_ids: templateIds });
|
||||
});
|
||||
}.bind(this);
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
module.exports = function allowQueryParams(params) {
|
||||
if (!Array.isArray(params)) {
|
||||
throw new Error('allowQueryParams must receive an Array of params');
|
||||
}
|
||||
return function allowQueryParamsMiddleware(req, res, next) {
|
||||
res.locals.allowedQueryParams = params;
|
||||
next();
|
||||
};
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
const locals = require('./locals');
|
||||
const cleanUpQueryParams = require('./clean-up-query-params');
|
||||
const layergroupToken = require('./layergroup-token');
|
||||
const authorize = require('./authorize');
|
||||
const dbConnSetup = require('./db-conn-setup');
|
||||
|
||||
module.exports = function prepareContextMiddleware(authApi, pgConnection) {
|
||||
return [
|
||||
locals,
|
||||
cleanUpQueryParams(),
|
||||
layergroupToken,
|
||||
authorize(authApi),
|
||||
dbConnSetup(pgConnection)
|
||||
];
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
var LayergroupToken = require('../../models/layergroup-token');
|
||||
|
||||
module.exports = function layergroupTokenMiddleware(req, res, next) {
|
||||
if (!res.locals.token) {
|
||||
return next();
|
||||
}
|
||||
|
||||
var user = res.locals.user;
|
||||
|
||||
var layergroupToken = LayergroupToken.parse(res.locals.token);
|
||||
res.locals.token = layergroupToken.token;
|
||||
res.locals.cache_buster = layergroupToken.cacheBuster;
|
||||
|
||||
if (layergroupToken.signer) {
|
||||
res.locals.signer = layergroupToken.signer;
|
||||
if (!res.locals.signer) {
|
||||
res.locals.signer = user;
|
||||
} else if (res.locals.signer !== user) {
|
||||
var err = new Error(`Cannot use map signature of user "${res.locals.signer}" on db of user "${user}"`);
|
||||
err.type = 'auth';
|
||||
err.http_status = 403;
|
||||
if (req.query && req.query.callback) {
|
||||
err.http_status = 200;
|
||||
}
|
||||
|
||||
req.profiler.done('req2params');
|
||||
return next(err);
|
||||
}
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
module.exports = function localsMiddleware(req, res, next) {
|
||||
// save req.params in res.locals
|
||||
res.locals = Object.assign(req.params || {}, res.locals);
|
||||
|
||||
next();
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
module.exports = function cors(extraHeaders) {
|
||||
return function(req, res, next) {
|
||||
var baseHeaders = "X-Requested-With, X-Prototype-Version, X-CSRF-Token";
|
||||
if(extraHeaders) {
|
||||
baseHeaders += ", " + extraHeaders;
|
||||
}
|
||||
res.set("Access-Control-Allow-Origin", "*");
|
||||
res.set("Access-Control-Allow-Headers", baseHeaders);
|
||||
next();
|
||||
};
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const LZMA = require('lzma').LZMA;
|
||||
|
||||
const lzmaWorker = new LZMA();
|
||||
|
||||
module.exports = function lzmaMiddleware(req, res, next) {
|
||||
if (!req.query.hasOwnProperty('lzma')) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Decode (from base64)
|
||||
var lzma = new Buffer(req.query.lzma, 'base64')
|
||||
.toString('binary')
|
||||
.split('')
|
||||
.map(function(c) {
|
||||
return c.charCodeAt(0) - 128;
|
||||
});
|
||||
|
||||
// Decompress
|
||||
lzmaWorker.decompress(lzma, function(result) {
|
||||
try {
|
||||
delete req.query.lzma;
|
||||
Object.assign(req.query, JSON.parse(result));
|
||||
next();
|
||||
} catch (err) {
|
||||
next(new Error('Error parsing lzma as JSON: ' + err));
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
var CdbRequest = require('../models/cdb_request');
|
||||
var cdbRequest = new CdbRequest();
|
||||
|
||||
module.exports = function userMiddleware(req, res, next) {
|
||||
res.locals.user = cdbRequest.userByReq(req);
|
||||
|
||||
next();
|
||||
};
|
||||
@@ -4,22 +4,14 @@ const aggregationValidator = require('./aggregation-validator');
|
||||
const {
|
||||
createPositiveNumberValidator,
|
||||
createIncludesValueValidator,
|
||||
createAggregationColumnsValidator
|
||||
createAggregationColumnsValidator,
|
||||
createAggregationFiltersValidator
|
||||
} = aggregationValidator;
|
||||
|
||||
const SubstitutionTokens = require('../../utils/substitution-tokens');
|
||||
const queryUtils = require('../../utils/query-utils');
|
||||
|
||||
const removeDuplicates = arr => [...new Set(arr)];
|
||||
|
||||
function prepareSql(sql) {
|
||||
return sql && SubstitutionTokens.replace(sql, {
|
||||
bbox: 'ST_MakeEnvelope(0,0,0,0)',
|
||||
scale_denominator: '0',
|
||||
pixel_width: '1',
|
||||
pixel_height: '1'
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = class AggregationMapConfig extends MapConfig {
|
||||
static get AGGREGATIONS () {
|
||||
return aggregationQuery.SUPPORTED_AGGREGATE_FUNCTIONS;
|
||||
@@ -43,6 +35,18 @@ module.exports = class AggregationMapConfig extends MapConfig {
|
||||
];
|
||||
}
|
||||
|
||||
static get FILTER_PARAMETERS () {
|
||||
return [
|
||||
// TODO: valid combinations of parameters:
|
||||
// * Except for less/greater params, only one parameter allowed per filter.
|
||||
// * Any less parameter can be combined with one of the greater paramters. (to define a range)
|
||||
'less_than', 'less_than_or_equal_to',
|
||||
'greater_than', 'greater_than_or_equal_to',
|
||||
'equal', 'not_equal',
|
||||
'between', 'in', 'not_in'
|
||||
];
|
||||
}
|
||||
|
||||
static supportsGeometryType(geometryType) {
|
||||
return AggregationMapConfig.SUPPORTED_GEOMETRY_TYPES.includes(geometryType);
|
||||
}
|
||||
@@ -58,11 +62,15 @@ module.exports = class AggregationMapConfig extends MapConfig {
|
||||
const positiveNumberValidator = createPositiveNumberValidator(this);
|
||||
const includesValidPlacementsValidator = createIncludesValueValidator(this, AggregationMapConfig.PLACEMENTS);
|
||||
const aggregationColumnsValidator = createAggregationColumnsValidator(this, AggregationMapConfig.AGGREGATIONS);
|
||||
const aggregationFiltersValidator = createAggregationFiltersValidator(
|
||||
this, AggregationMapConfig.FILTER_PARAMETERS
|
||||
);
|
||||
|
||||
validate('resolution', positiveNumberValidator);
|
||||
validate('placement', includesValidPlacementsValidator);
|
||||
validate('threshold', positiveNumberValidator);
|
||||
validate('columns', aggregationColumnsValidator);
|
||||
validate('filters', aggregationFiltersValidator);
|
||||
|
||||
this.user = user;
|
||||
this.pgConnection = connection;
|
||||
@@ -77,7 +85,8 @@ module.exports = class AggregationMapConfig extends MapConfig {
|
||||
threshold = AggregationMapConfig.THRESHOLD,
|
||||
placement,
|
||||
columns = {},
|
||||
dimensions = {}
|
||||
dimensions = {},
|
||||
filters = {}
|
||||
} = this.getAggregation(index);
|
||||
|
||||
return aggregationQuery({
|
||||
@@ -87,6 +96,7 @@ module.exports = class AggregationMapConfig extends MapConfig {
|
||||
placement,
|
||||
columns,
|
||||
dimensions,
|
||||
filters,
|
||||
isDefaultAggregation: this._isDefaultLayerAggregation(index)
|
||||
});
|
||||
}
|
||||
@@ -152,21 +162,19 @@ module.exports = class AggregationMapConfig extends MapConfig {
|
||||
_getLayerAggregationRequiredColumns (index) {
|
||||
const { columns, dimensions } = this.getAggregation(index);
|
||||
|
||||
let finalColumns = ['cartodb_id', '_cdb_feature_count'];
|
||||
|
||||
let aggregatedColumns = [];
|
||||
if (columns) {
|
||||
aggregatedColumns = Object.keys(columns)
|
||||
.map(key => columns[key].aggregated_column)
|
||||
.filter(aggregatedColumn => typeof aggregatedColumn === 'string');
|
||||
aggregatedColumns = Object.keys(columns);
|
||||
}
|
||||
|
||||
let dimensionsColumns = [];
|
||||
if (dimensions) {
|
||||
dimensionsColumns = Object.keys(dimensions)
|
||||
.map(key => dimensions[key])
|
||||
.filter(dimension => typeof dimension === 'string');
|
||||
dimensionsColumns = Object.keys(dimensions);
|
||||
}
|
||||
|
||||
return removeDuplicates(aggregatedColumns.concat(dimensionsColumns));
|
||||
return removeDuplicates(finalColumns.concat(aggregatedColumns).concat(dimensionsColumns));
|
||||
}
|
||||
|
||||
doesLayerReachThreshold(index, featureCount) {
|
||||
@@ -188,7 +196,7 @@ module.exports = class AggregationMapConfig extends MapConfig {
|
||||
}
|
||||
|
||||
const sql = limitedQuery({
|
||||
query: prepareSql(layer.options.sql)
|
||||
query: queryUtils.substituteDummyTokens(layer.options.sql)
|
||||
});
|
||||
|
||||
connection.query(sql, (err, result) => {
|
||||
@@ -220,7 +228,8 @@ module.exports = class AggregationMapConfig extends MapConfig {
|
||||
_isDefaultAggregation (aggregation) {
|
||||
return aggregation.placement === undefined &&
|
||||
aggregation.columns === undefined &&
|
||||
this._isEmptyParameter(aggregation.dimensions);
|
||||
this._isEmptyParameter(aggregation.dimensions) &&
|
||||
this._isEmptyParameter(aggregation.filters);
|
||||
}
|
||||
|
||||
_isEmptyParameter(parameter) {
|
||||
|
||||
@@ -42,7 +42,8 @@ const queryForOptions = (options) => templateForOptions(options)({
|
||||
sourceQuery: options.query,
|
||||
res: 256/options.resolution,
|
||||
columns: options.columns,
|
||||
dimensions: options.dimensions
|
||||
dimensions: options.dimensions,
|
||||
filters: options.filters
|
||||
});
|
||||
|
||||
module.exports = queryForOptions;
|
||||
@@ -93,20 +94,23 @@ const aggregateColumnNames = (ctx, table) => {
|
||||
return sep(Object.keys(columns));
|
||||
};
|
||||
|
||||
const aggregateExpression = (column_name, column_parameters) => {
|
||||
const aggregate_function = column_parameters.aggregate_function || 'count';
|
||||
const aggregate_definition = SUPPORTED_AGGREGATE_FUNCTIONS[aggregate_function];
|
||||
if (!aggregate_definition) {
|
||||
throw new Error("Invalid Aggregate function: '" + aggregate_function + "'");
|
||||
}
|
||||
return aggregate_definition.sql(column_name, column_parameters);
|
||||
};
|
||||
|
||||
const aggregateColumnDefs = ctx => {
|
||||
let columns = aggregateColumns(ctx);
|
||||
return sep(Object.keys(columns).map(column_name => {
|
||||
const aggregate_function = columns[column_name].aggregate_function || 'count';
|
||||
const aggregate_definition = SUPPORTED_AGGREGATE_FUNCTIONS[aggregate_function];
|
||||
if (!aggregate_definition) {
|
||||
throw new Error("Invalid Aggregate function: '" + aggregate_function + "'");
|
||||
}
|
||||
const aggregate_expression = aggregate_definition.sql(column_name, columns[column_name]);
|
||||
const aggregate_expression = aggregateExpression(column_name, columns[column_name]);
|
||||
return `${aggregate_expression} AS ${column_name}`;
|
||||
}));
|
||||
};
|
||||
|
||||
|
||||
const aggregateDimensions = ctx => ctx.dimensions || {};
|
||||
|
||||
const dimensionNames = (ctx, table) => {
|
||||
@@ -127,34 +131,181 @@ const dimensionDefs = ctx => {
|
||||
}));
|
||||
};
|
||||
|
||||
const aggregateFilters = ctx => ctx.filters || {};
|
||||
|
||||
const filterConditionSQL = (expr, filter) => {
|
||||
// TODO: validate filter parameters (e.g. cannot have both greater_than and greater_than or equal to)
|
||||
|
||||
if (filter) {
|
||||
if (!Array.isArray(filter)) {
|
||||
filter = [filter];
|
||||
}
|
||||
if (filter.length > 0) {
|
||||
return filter.map(f => filterSingleConditionSQL(expr, f)).join(' OR ');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const filterSingleConditionSQL = (expr, filter) => {
|
||||
let cond;
|
||||
Object.keys(FILTERS).some(f => {
|
||||
cond = FILTERS[f](expr, filter);
|
||||
return cond;
|
||||
});
|
||||
return cond;
|
||||
};
|
||||
|
||||
const sqlQ = (value) => {
|
||||
if (isFinite(value)) {
|
||||
return String(value);
|
||||
}
|
||||
return `'${value}'`; // TODO: escape single quotes! (by doubling them)
|
||||
};
|
||||
|
||||
/* jshint eqeqeq: false */
|
||||
/* x != null is used to check for both null and undefined; triple !== wouldn't do the trick */
|
||||
|
||||
const FILTERS = {
|
||||
between: (expr, filter) => {
|
||||
const lo = filter.greater_than_or_equal_to, hi = filter.less_than_or_equal_to;
|
||||
if (lo != null && hi != null) {
|
||||
return `(${expr} BETWEEN ${sqlQ(lo)} AND ${sqlQ(hi)})`;
|
||||
}
|
||||
},
|
||||
in: (expr, filter) => {
|
||||
if (filter.in != null) {
|
||||
return `(${expr} IN (${filter.in.map(v => sqlQ(v)).join(',')}))`;
|
||||
}
|
||||
},
|
||||
notin: (expr, filter) => {
|
||||
if (filter.not_in != null) {
|
||||
return `(${expr} NOT IN (${filter.not_in.map(v => sqlQ(v)).join(',')}))`;
|
||||
}
|
||||
},
|
||||
equal: (expr, filter) => {
|
||||
if (filter.equal != null) {
|
||||
return `(${expr} = ${sqlQ(filter.equal)})`;
|
||||
}
|
||||
},
|
||||
not_equal: (expr, filter) => {
|
||||
if (filter.not_equal != null) {
|
||||
return `(${expr} <> ${sqlQ(filter.not_equal)})`;
|
||||
}
|
||||
},
|
||||
range: (expr, filter) => {
|
||||
let conds = [];
|
||||
if (filter.greater_than_or_equal_to != null) {
|
||||
conds.push(`(${expr} >= ${sqlQ(filter.greater_than_or_equal_to)})`);
|
||||
}
|
||||
if (filter.greater_than != null) {
|
||||
conds.push(`(${expr} > ${sqlQ(filter.greater_than)})`);
|
||||
}
|
||||
if (filter.less_than_or_equal_to != null) {
|
||||
conds.push(`(${expr} <= ${sqlQ(filter.less_than_or_equal_to)})`);
|
||||
}
|
||||
if (filter.less_than != null) {
|
||||
conds.push(`(${expr} < ${sqlQ(filter.less_than)})`);
|
||||
}
|
||||
if (conds.length > 0) {
|
||||
return conds.join(' AND ');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const filterConditions = ctx => {
|
||||
let columns = aggregateColumns(ctx);
|
||||
let dimensions = aggregateDimensions(ctx);
|
||||
let filters = aggregateFilters(ctx);
|
||||
return Object.keys(filters).map(filtered_column => {
|
||||
let filtered_expr;
|
||||
if (columns[filtered_column]) {
|
||||
filtered_expr = aggregateExpression(filtered_column, columns[filtered_column]);
|
||||
}
|
||||
else if (dimensions[filtered_column]) {
|
||||
filtered_expr = dimensions[filtered_column];
|
||||
}
|
||||
if (!filtered_expr) {
|
||||
throw new Error("Invalid filtered column: '" + filtered_column + "'");
|
||||
}
|
||||
return filterConditionSQL(filtered_expr, filters[filtered_column]);
|
||||
}).join(' AND ');
|
||||
};
|
||||
|
||||
const havingClause = ctx => {
|
||||
let cond = filterConditions(ctx);
|
||||
return cond ? `HAVING ${cond}` : '';
|
||||
};
|
||||
|
||||
// SQL expression to compute the aggregation resolution (grid cell size).
|
||||
// This is equivalent to `${256/ctx.res}*CDB_XYZ_Resolution(CDB_ZoomFromScale(!scale_denominator!))`
|
||||
// This is defined by the ctx.res parameter, which is the number of grid cells per tile linear dimension
|
||||
// (i.e. each tile is divided into ctx.res*ctx.res cells).
|
||||
const gridResolution = ctx => `(${256*0.00028/ctx.res}*!scale_denominator!)::double precision`;
|
||||
// We limit the the minimum resolution to avoid division by zero problems. The limit used is
|
||||
// the pixel size of zoom level 30 (i.e. 1/2*(30+8) of the full earth web-mercator extent), which is about 0.15 mm.
|
||||
// Computing this using !scale_denominator!, !pixel_width! or !pixel_height! produces
|
||||
// inaccurate results due to rounding present in those values.
|
||||
const gridResolution = ctx => {
|
||||
const minimumResolution = 2*Math.PI*6378137/Math.pow(2,38);
|
||||
const pixelSize = 'CDB_XYZ_Resolution(CDB_ZoomFromScale(!scale_denominator!))';
|
||||
return `GREATEST(${256/ctx.res}*${pixelSize}, ${minimumResolution})::double precision`;
|
||||
};
|
||||
|
||||
// Each aggregation cell is defined by the cell coordinates Floor(x/res), Floor(y/res),
|
||||
// i.e. they include the West and South borders but not the East and North ones.
|
||||
// So, to avoid picking points that don't belong to cells in the tile, given the tile
|
||||
// limits Xmin, Ymin, Xmax, Ymax (bbox), we should select points that satisfy
|
||||
// Xmin <= x < Xmax and Ymin <= y < Ymax (with x, y from the_geom_webmercator)
|
||||
// On the other hand we can efficiently filter spatially (relying on spatial indexing)
|
||||
// with `the_geom_webmercator && bbox` which is equivalent to
|
||||
// Xmin <= x <= Xmax and Ymin <= y <= Ymax
|
||||
// So, in order to be both efficient and accurate we will need to use both
|
||||
// conditions for spatial filtering.
|
||||
const spatialFilter = `
|
||||
(_cdb_query.the_geom_webmercator && _cdb_params.bbox) AND
|
||||
ST_X(_cdb_query.the_geom_webmercator) >= _cdb_params.xmin AND
|
||||
ST_X(_cdb_query.the_geom_webmercator) < _cdb_params.xmax AND
|
||||
ST_Y(_cdb_query.the_geom_webmercator) >= _cdb_params.ymin AND
|
||||
ST_Y(_cdb_query.the_geom_webmercator) < _cdb_params.ymax
|
||||
`;
|
||||
|
||||
// Notes:
|
||||
// * We need to filter spatially using !bbox! to make the queries efficient because
|
||||
// the filter added by Mapnik (wrapping the query)
|
||||
// is only applied after the aggregation.
|
||||
// * This queries are used for rendering and the_geom is omitted in the results for better performance
|
||||
// * If the MVT extent or tile buffer was 0 or a multiple of the resolution we could use directly
|
||||
// the bbox for them, but in general we need to find the nearest cell limits inside the bbox.
|
||||
// * bbox coordinates can have an error in the last digites; we apply a small correction before
|
||||
// applying CEIL or FLOOR to compensate for this.
|
||||
const sqlParams = (ctx) => `
|
||||
_cdb_res AS (
|
||||
SELECT
|
||||
${gridResolution(ctx)} AS res,
|
||||
!bbox! AS bbox,
|
||||
(2*2.220446049250313e-16::double precision) AS eps
|
||||
),
|
||||
_cdb_params AS (
|
||||
SELECT
|
||||
res,
|
||||
bbox,
|
||||
CEIL((ST_XMIN(bbox) - eps*ABS(ST_XMIN(bbox)))/res)*res AS xmin,
|
||||
FLOOR((ST_XMAX(bbox) + eps*ABS(ST_XMAX(bbox)))/res)*res AS xmax,
|
||||
CEIL((ST_YMIN(bbox) - eps*ABS(ST_YMIN(bbox)))/res)*res AS ymin,
|
||||
FLOOR((ST_YMAX(bbox) + eps*ABS(ST_YMAX(bbox)))/res)*res AS ymax
|
||||
FROM _cdb_res
|
||||
)
|
||||
`;
|
||||
|
||||
// The special default aggregation includes all the columns of a sample row per grid cell and
|
||||
// the count (_cdb_feature_count) of the aggregated rows.
|
||||
const defaultAggregationQueryTemplate = ctx => `
|
||||
WITH
|
||||
_cdb_params AS (
|
||||
SELECT
|
||||
${gridResolution(ctx)} AS res,
|
||||
!bbox! AS bbox
|
||||
),
|
||||
WITH ${sqlParams(ctx)},
|
||||
_cdb_clusters AS (
|
||||
SELECT
|
||||
MIN(cartodb_id) AS cartodb_id
|
||||
${dimensionDefs(ctx)}
|
||||
${aggregateColumnDefs(ctx)}
|
||||
FROM (${ctx.sourceQuery}) _cdb_query, _cdb_params
|
||||
WHERE _cdb_query.the_geom_webmercator && _cdb_params.bbox
|
||||
WHERE ${spatialFilter}
|
||||
GROUP BY
|
||||
Floor(ST_X(_cdb_query.the_geom_webmercator)/_cdb_params.res),
|
||||
Floor(ST_Y(_cdb_query.the_geom_webmercator)/_cdb_params.res)
|
||||
@@ -169,14 +320,9 @@ const defaultAggregationQueryTemplate = ctx => `
|
||||
|
||||
const aggregationQueryTemplates = {
|
||||
'centroid': ctx => `
|
||||
WITH
|
||||
_cdb_params AS (
|
||||
SELECT
|
||||
${gridResolution(ctx)} AS res,
|
||||
!bbox! AS bbox
|
||||
)
|
||||
WITH ${sqlParams(ctx)}
|
||||
SELECT
|
||||
row_number() over() AS cartodb_id,
|
||||
MIN(_cdb_query.cartodb_id) AS cartodb_id,
|
||||
ST_SetSRID(
|
||||
ST_MakePoint(
|
||||
AVG(ST_X(_cdb_query.the_geom_webmercator)),
|
||||
@@ -186,32 +332,30 @@ const aggregationQueryTemplates = {
|
||||
${dimensionDefs(ctx)}
|
||||
${aggregateColumnDefs(ctx)}
|
||||
FROM (${ctx.sourceQuery}) _cdb_query, _cdb_params
|
||||
WHERE _cdb_query.the_geom_webmercator && _cdb_params.bbox
|
||||
WHERE ${spatialFilter}
|
||||
GROUP BY
|
||||
Floor(ST_X(_cdb_query.the_geom_webmercator)/_cdb_params.res),
|
||||
Floor(ST_Y(_cdb_query.the_geom_webmercator)/_cdb_params.res)
|
||||
${dimensionNames(ctx)}
|
||||
${havingClause(ctx)}
|
||||
`,
|
||||
|
||||
'point-grid': ctx => `
|
||||
WITH
|
||||
_cdb_params AS (
|
||||
SELECT
|
||||
${gridResolution(ctx)} AS res,
|
||||
!bbox! AS bbox
|
||||
),
|
||||
WITH ${sqlParams(ctx)},
|
||||
_cdb_clusters AS (
|
||||
SELECT
|
||||
MIN(_cdb_query.cartodb_id) AS cartodb_id,
|
||||
Floor(ST_X(_cdb_query.the_geom_webmercator)/_cdb_params.res)::int AS _cdb_gx,
|
||||
Floor(ST_Y(_cdb_query.the_geom_webmercator)/_cdb_params.res)::int AS _cdb_gy
|
||||
${dimensionDefs(ctx)}
|
||||
${aggregateColumnDefs(ctx)}
|
||||
FROM (${ctx.sourceQuery}) _cdb_query, _cdb_params
|
||||
WHERE the_geom_webmercator && _cdb_params.bbox
|
||||
WHERE ${spatialFilter}
|
||||
GROUP BY _cdb_gx, _cdb_gy ${dimensionNames(ctx)}
|
||||
${havingClause(ctx)}
|
||||
)
|
||||
SELECT
|
||||
row_number() over() AS cartodb_id,
|
||||
_cdb_clusters.cartodb_id AS cartodb_id,
|
||||
ST_SetSRID(ST_MakePoint((_cdb_gx+0.5)*res, (_cdb_gy+0.5)*res), 3857) AS the_geom_webmercator
|
||||
${dimensionNames(ctx)}
|
||||
${aggregateColumnNames(ctx)}
|
||||
@@ -219,28 +363,24 @@ const aggregationQueryTemplates = {
|
||||
`,
|
||||
|
||||
'point-sample': ctx => `
|
||||
WITH
|
||||
_cdb_params AS (
|
||||
SELECT
|
||||
${gridResolution(ctx)} AS res,
|
||||
!bbox! AS bbox
|
||||
),
|
||||
WITH ${sqlParams(ctx)},
|
||||
_cdb_clusters AS (
|
||||
SELECT
|
||||
MIN(cartodb_id) AS cartodb_id
|
||||
${dimensionDefs(ctx)}
|
||||
${aggregateColumnDefs(ctx)}
|
||||
FROM (${ctx.sourceQuery}) _cdb_query, _cdb_params
|
||||
WHERE _cdb_query.the_geom_webmercator && _cdb_params.bbox
|
||||
WHERE ${spatialFilter}
|
||||
GROUP BY
|
||||
Floor(ST_X(_cdb_query.the_geom_webmercator)/_cdb_params.res),
|
||||
Floor(ST_Y(_cdb_query.the_geom_webmercator)/_cdb_params.res)
|
||||
${dimensionNames(ctx)}
|
||||
${havingClause(ctx)}
|
||||
)
|
||||
SELECT
|
||||
_cdb_clusters.cartodb_id,
|
||||
the_geom, the_geom_webmercator
|
||||
${dimensionNames(ctx, '_cdb_query')}
|
||||
${dimensionNames(ctx, '_cdb_clusters')}
|
||||
${aggregateColumnNames(ctx, '_cdb_clusters')}
|
||||
FROM
|
||||
_cdb_clusters INNER JOIN (${ctx.sourceQuery}) _cdb_query
|
||||
|
||||
@@ -42,6 +42,38 @@ module.exports.createAggregationColumnsValidator = function (mapconfig, validAgg
|
||||
};
|
||||
};
|
||||
|
||||
module.exports.createAggregationFiltersValidator = function (mapconfig, validParameters) {
|
||||
return function validateAggregationFilters (value, key, index) {
|
||||
const dims = mapconfig.getAggregation(index).dimensions || {};
|
||||
const cols = mapconfig.getAggregation(index).columns || {};
|
||||
const validKeys = Object.keys(dims).concat(Object.keys(cols));
|
||||
Object.keys(value).forEach((filteredName) => {
|
||||
// filteredName must be the name of either an aggregated column or a dimension in the same layer
|
||||
if (!validKeys.includes(filteredName)) {
|
||||
const message = `Invalid filtered column: ${filteredName}`;
|
||||
throw createLayerError(message, mapconfig, index);
|
||||
}
|
||||
// The filter parameters must be valid
|
||||
let filters = value[filteredName];
|
||||
// a single filter or an array of filters (to be OR-combined) are accepted
|
||||
if (!Array.isArray(filters)) {
|
||||
filters = [filters];
|
||||
}
|
||||
filters.forEach(params => {
|
||||
Object.keys(params).forEach(paramName => {
|
||||
if (!validParameters.includes(paramName)) {
|
||||
const message = `Invalid filter parameter name: ${paramName}`;
|
||||
throw createLayerError(message, mapconfig, index);
|
||||
}
|
||||
});
|
||||
// TODO: check parameter value (params[paramName]) to be of the correct type
|
||||
});
|
||||
// TODO: if multiple parameters within params check the combination is valid,
|
||||
// i.e. one of the *less* parameters and one of the *greater* parameters.
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
function createAggregationColumnNamesValidator(mapconfig) {
|
||||
return function validateAggregationColumnNames (value, key, index) {
|
||||
Object.keys(value).forEach((columnName) => {
|
||||
|
||||
@@ -2,19 +2,17 @@ const BaseDataview = require('./base');
|
||||
const debug = require('debug')('windshaft:dataview:aggregation');
|
||||
|
||||
const filteredQueryTpl = ctx => `
|
||||
filtered_source AS (
|
||||
SELECT *
|
||||
FROM (${ctx.query}) _cdb_filtered_source
|
||||
${ctx.aggregationColumn && ctx.isFloatColumn ? `
|
||||
WHERE
|
||||
${ctx.aggregationColumn} != 'infinity'::float
|
||||
AND
|
||||
${ctx.aggregationColumn} != '-infinity'::float
|
||||
AND
|
||||
${ctx.aggregationColumn} != 'NaN'::float` :
|
||||
''
|
||||
}
|
||||
)
|
||||
SELECT *
|
||||
FROM (${ctx.query}) _cdb_filtered_source
|
||||
${ctx.aggregationColumn && ctx.isFloatColumn ? `
|
||||
WHERE
|
||||
${ctx.aggregationColumn} != 'infinity'::float
|
||||
AND
|
||||
${ctx.aggregationColumn} != '-infinity'::float
|
||||
AND
|
||||
${ctx.aggregationColumn} != 'NaN'::float` :
|
||||
''
|
||||
}
|
||||
`;
|
||||
|
||||
const summaryQueryTpl = ctx => `
|
||||
@@ -43,8 +41,8 @@ const rankedCategoriesQueryTpl = ctx => `
|
||||
${ctx.column} AS category,
|
||||
${ctx.aggregationFn} AS value,
|
||||
row_number() OVER (ORDER BY ${ctx.aggregationFn} desc) as rank
|
||||
FROM filtered_source
|
||||
${ctx.aggregationColumn !== null ? `WHERE ${ctx.aggregationColumn} IS NOT NULL` : ''}
|
||||
FROM (${filteredQueryTpl(ctx)}) filtered_source
|
||||
WHERE ${ctx.aggregation === "count" ? `${ctx.column}` : `${ctx.aggregationColumn}`} IS NOT NULL
|
||||
GROUP BY ${ctx.column}
|
||||
ORDER BY 2 DESC
|
||||
)
|
||||
@@ -134,7 +132,6 @@ const aggregationFnQueryTpl = ctx => `${ctx.aggregation}(${ctx.aggregationColumn
|
||||
|
||||
const aggregationDataviewQueryTpl = ctx => `
|
||||
WITH
|
||||
${filteredQueryTpl(ctx)},
|
||||
${summaryQueryTpl(ctx)},
|
||||
${rankedCategoriesQueryTpl(ctx)},
|
||||
${categoriesSummaryMinMaxQueryTpl(ctx)},
|
||||
@@ -282,7 +279,7 @@ module.exports = class Aggregation extends BaseDataview {
|
||||
max_val = 0,
|
||||
categories_count = 0
|
||||
} = result.rows[0] || {};
|
||||
|
||||
|
||||
return {
|
||||
aggregation: this.aggregation,
|
||||
count: count,
|
||||
@@ -293,10 +290,10 @@ module.exports = class Aggregation extends BaseDataview {
|
||||
max: max_val,
|
||||
categoriesCount: categories_count,
|
||||
categories: result.rows.map(({ category, value, agg }) => {
|
||||
return {
|
||||
return {
|
||||
category: agg ? 'Other' : category,
|
||||
value,
|
||||
agg
|
||||
value,
|
||||
agg
|
||||
};
|
||||
})
|
||||
};
|
||||
|
||||
@@ -52,6 +52,11 @@ module.exports = class BaseDataview {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!result || !result.rows || !result.rows.length) {
|
||||
return callback(new Error('The column type could not be determined'));
|
||||
}
|
||||
|
||||
const pgType = result.rows[0].pg_typeof;
|
||||
callback(null, getPGTypeName(pgType));
|
||||
}, readOnlyTransaction);
|
||||
|
||||
@@ -2,6 +2,7 @@ const BaseHistogram = require('./base-histogram');
|
||||
const debug = require('debug')('windshaft:dataview:date-histogram');
|
||||
const utils = require('../../../utils/query-utils');
|
||||
|
||||
|
||||
/**
|
||||
* Gets the name of a timezone with the same offset as the required
|
||||
* using the pg_timezone_names table. We do this because it's simpler to pass
|
||||
|
||||
@@ -99,8 +99,14 @@ module.exports = class NumericHistogram extends BaseHistogram {
|
||||
var extra_tables = ``;
|
||||
var extra_queries = ``;
|
||||
var extra_groupby = ``;
|
||||
var extra_filter = ``;
|
||||
|
||||
if (ctx.start >= ctx.end) {
|
||||
if (ctx.start < ctx.end) {
|
||||
extra_filter = `
|
||||
WHERE __ctx_query.${ctx.column} >= ${ctx.start}
|
||||
AND __ctx_query.${ctx.column} <= ${ctx.end}
|
||||
`;
|
||||
} else {
|
||||
ctx.end = `__cdb_basics.__cdb_max_val`;
|
||||
ctx.start = `__cdb_basics.__cdb_min_val`;
|
||||
extra_groupby = `, __cdb_basics.__cdb_max_val, __cdb_basics.__cdb_min_val`;
|
||||
@@ -135,7 +141,7 @@ SELECT
|
||||
END AS bin
|
||||
FROM
|
||||
(
|
||||
${ctx.query}
|
||||
SELECT * FROM (${ctx.query}) __ctx_query${extra_tables} ${extra_filter}
|
||||
) __cdb_filtered_source_query${extra_tables}
|
||||
GROUP BY 10${extra_groupby}
|
||||
ORDER BY 10;`;
|
||||
|
||||
@@ -101,6 +101,12 @@ module.exports = class AggregationMapConfigAdapter {
|
||||
aggregationSql = sqlQueryWrap.replace(/<%=\s*sql\s*%>/g, aggregationSql);
|
||||
}
|
||||
|
||||
if (!layer.options.sql_raw) {
|
||||
// if sql_wrap is present, the original query should already be
|
||||
// in sql_raw (with sql being the wrapped query);
|
||||
// otherwise we keep the now the original query in sql_raw
|
||||
layer.options.sql_raw = layer.options.sql;
|
||||
}
|
||||
layer.options.sql = aggregationSql;
|
||||
|
||||
mapConfig.getLayerAggregationColumns(index, (err, columns) => {
|
||||
@@ -159,6 +165,7 @@ module.exports = class AggregationMapConfigAdapter {
|
||||
}
|
||||
|
||||
_getAggregationMetadata (mapConfig, layer, adapted) {
|
||||
// also: pre-aggr query, columns, ...
|
||||
if (!adapted) {
|
||||
return { png: false, mvt: false };
|
||||
}
|
||||
|
||||
@@ -108,8 +108,7 @@ MapConfigNamedLayersAdapter.prototype.getMapConfig = function (user, requestMapC
|
||||
var dbAuth = {};
|
||||
|
||||
if (_.some(layers, isNamedTypeLayer)) {
|
||||
// Lazy load dbAuth
|
||||
this.pgConnection.setDBAuth(user, dbAuth, function(err) {
|
||||
this.pgConnection.setDBAuth(user, dbAuth, 'master', function(err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ var step = require('step');
|
||||
var queue = require('queue-async');
|
||||
var _ = require('underscore');
|
||||
|
||||
function MapConfigOverviewsAdapter(overviewsMetadataApi, filterStatsApi) {
|
||||
this.overviewsMetadataApi = overviewsMetadataApi;
|
||||
this.filterStatsApi = filterStatsApi;
|
||||
function MapConfigOverviewsAdapter(overviewsMetadataBackend, filterStatsBackend) {
|
||||
this.overviewsMetadataBackend = overviewsMetadataBackend;
|
||||
this.filterStatsBackend = filterStatsBackend;
|
||||
}
|
||||
|
||||
module.exports = MapConfigOverviewsAdapter;
|
||||
@@ -25,7 +25,7 @@ MapConfigOverviewsAdapter.prototype.getMapConfig = function(user, requestMapConf
|
||||
if ( layer.type !== 'mapnik' && layer.type !== 'cartodb' ) {
|
||||
return done(null, layer);
|
||||
}
|
||||
self.overviewsMetadataApi.getOverviewsMetadata(user, layer.options.sql, function(err, metadata){
|
||||
self.overviewsMetadataBackend.getOverviewsMetadata(user, layer.options.sql, function(err, metadata){
|
||||
if (err) {
|
||||
done(err, layer);
|
||||
} else {
|
||||
@@ -53,7 +53,7 @@ MapConfigOverviewsAdapter.prototype.getMapConfig = function(user, requestMapConf
|
||||
function collectStatsData(err, filters, unfiltered_query) {
|
||||
var next_step = this;
|
||||
if ( filters ) {
|
||||
self.filterStatsApi.getFilterStats(
|
||||
self.filterStatsBackend.getFilterStats(
|
||||
user,
|
||||
unfiltered_query, filters,
|
||||
function(err, stats) {
|
||||
|
||||
@@ -11,6 +11,8 @@ var PostgresDatasource = require('../../../backends/turbo-carto-postgres-datasou
|
||||
|
||||
var MapConfig = require('windshaft').model.MapConfig;
|
||||
|
||||
const dbParamsFromReqParams = require('../../../utils/database-params');
|
||||
|
||||
function TurboCartoAdapter() {
|
||||
}
|
||||
|
||||
@@ -115,7 +117,9 @@ TurboCartoAdapter.prototype._parseCartoCss = function (username, params, layer,
|
||||
|
||||
var layerSql = layer.options.sql;
|
||||
var layerRawSql = layer.options.sql_raw;
|
||||
if (SubstitutionTokens.hasTokens(layerSql) && layerRawSql) {
|
||||
if (SubstitutionTokens.hasTokens(layerSql) && layerRawSql && layer.options.sql_wrap) {
|
||||
// For wrapped queries we'll derive the tokens from the data extent
|
||||
// instead of the whole Earth/root tile.
|
||||
var self = this;
|
||||
var tokensQuery = tokensQueryTpl({_sql: layerRawSql});
|
||||
return pg.query(tokensQuery, function(err, resultSet) {
|
||||
@@ -158,23 +162,3 @@ TurboCartoAdapter.prototype.process = function (psql, cartocss, sql, callback) {
|
||||
function shouldParseLayerCartocss(layer) {
|
||||
return layer && layer.options && layer.options.cartocss && layer.options.sql;
|
||||
}
|
||||
|
||||
function dbParamsFromReqParams(params) {
|
||||
var dbParams = {};
|
||||
if ( params.dbuser ) {
|
||||
dbParams.user = params.dbuser;
|
||||
}
|
||||
if ( params.dbpassword ) {
|
||||
dbParams.pass = params.dbpassword;
|
||||
}
|
||||
if ( params.dbhost ) {
|
||||
dbParams.host = params.dbhost;
|
||||
}
|
||||
if ( params.dbport ) {
|
||||
dbParams.port = params.dbport;
|
||||
}
|
||||
if ( params.dbname ) {
|
||||
dbParams.dbname = params.dbname;
|
||||
}
|
||||
return dbParams;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
const queryUtils = require('../../../utils/query-utils');
|
||||
const dateWrapper = require('../../../utils/date-wrapper');
|
||||
|
||||
/**
|
||||
* This middleware wraps the layer query transforming the date fields into numbers because mvt tiles
|
||||
* doesnt support dates as primitive type.
|
||||
*
|
||||
* - This middleware is ONLY activated when the `dates_as_numbers` option is enabled for some layer in the mapConfig.
|
||||
*/
|
||||
class VectorMapConfigAdapter {
|
||||
constructor(pgConnection) {
|
||||
this.pgConnection = pgConnection;
|
||||
}
|
||||
|
||||
getMapConfig(user, requestMapConfig, params, context, callback) {
|
||||
if (!this._isDatesAsNumbersFlagEnabled(requestMapConfig)) {
|
||||
return callback(null, requestMapConfig);
|
||||
}
|
||||
|
||||
this._wrapDates(requestMapConfig, user)
|
||||
.then(updatedRequestMapConfig => callback(null, updatedRequestMapConfig))
|
||||
.catch(callback);
|
||||
}
|
||||
|
||||
_wrapDates(requestMapConfig, user) {
|
||||
return Promise.all(requestMapConfig.layers.map(layer => this._wrapLayer(layer, user)))
|
||||
.then(() => requestMapConfig);
|
||||
}
|
||||
|
||||
_wrapLayer(layer, user) {
|
||||
if (!layer.options.dates_as_numbers || !layer.options.sql) {
|
||||
return Promise.resolve(layer);
|
||||
}
|
||||
const originalQuery = layer.options.sql;
|
||||
return this._getColumns(user, originalQuery)
|
||||
.then(result => {
|
||||
const newSqlQuery = dateWrapper.wrapDates(originalQuery, result.fields);
|
||||
layer.options.sql = newSqlQuery;
|
||||
return layer;
|
||||
});
|
||||
}
|
||||
|
||||
_getColumns(user, originalQuery) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.pgConnection.getConnection(user, (err, connection) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
const query = queryUtils.getQueryLimited(queryUtils.substituteDummyTokens(originalQuery), 0);
|
||||
queryUtils.queryPromise(connection, query)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_isDatesAsNumbersFlagEnabled(requestMapConfig) {
|
||||
return requestMapConfig.layers && requestMapConfig.layers.some(layer => layer.options.dates_as_numbers);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
module.exports = VectorMapConfigAdapter;
|
||||
@@ -0,0 +1,65 @@
|
||||
const QueryTables = require('cartodb-query-tables');
|
||||
|
||||
module.exports = class BaseMapConfigProvider {
|
||||
createAffectedTables (callback) {
|
||||
this.getMapConfig((err, mapConfig) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const { dbname } = this.params;
|
||||
const token = mapConfig.id();
|
||||
|
||||
const queries = [];
|
||||
|
||||
this.mapConfig.getLayers().forEach(layer => {
|
||||
queries.push(layer.options.sql);
|
||||
if (layer.options.affected_tables) {
|
||||
layer.options.affected_tables.map(table => {
|
||||
queries.push(`SELECT * FROM ${table} LIMIT 0`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const sql = queries.length ? queries.join(';') : null;
|
||||
|
||||
if (!sql) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
this.pgConnection.getConnection(this.user, (err, connection) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
QueryTables.getAffectedTablesFromQuery(connection, sql, (err, affectedTables) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
this.affectedTablesCache.set(dbname, token, affectedTables);
|
||||
|
||||
callback(null, affectedTables);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getAffectedTables (callback) {
|
||||
this.getMapConfig((err, mapConfig) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const { dbname } = this.params;
|
||||
const token = mapConfig.id();
|
||||
|
||||
if (this.affectedTablesCache.hasAffectedTables(dbname, token)) {
|
||||
const affectedTables = this.affectedTablesCache.get(dbname, token);
|
||||
return callback(null, affectedTables);
|
||||
}
|
||||
|
||||
return this.createAffectedTables(callback);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,48 +1,27 @@
|
||||
var assert = require('assert');
|
||||
var step = require('step');
|
||||
const MapStoreMapConfigProvider = require('./map-store-provider');
|
||||
|
||||
var MapStoreMapConfigProvider = require('./map-store-provider');
|
||||
module.exports = class CreateLayergroupMapConfigProvider extends MapStoreMapConfigProvider {
|
||||
constructor (mapConfig, user, userLimitsBackend, pgConnection, affectedTablesCache, params) {
|
||||
super(null, user, userLimitsBackend, pgConnection, affectedTablesCache, params);
|
||||
this.mapConfig = mapConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MapConfig} mapConfig
|
||||
* @param {String} user
|
||||
* @param {UserLimitsApi} userLimitsApi
|
||||
* @param {Object} params
|
||||
* @constructor
|
||||
* @type {CreateLayergroupMapConfigProvider}
|
||||
*/
|
||||
function CreateLayergroupMapConfigProvider(mapConfig, user, userLimitsApi, params) {
|
||||
this.mapConfig = mapConfig;
|
||||
this.user = user;
|
||||
this.userLimitsApi = userLimitsApi;
|
||||
this.params = params;
|
||||
this.cacheBuster = params.cache_buster || 0;
|
||||
}
|
||||
|
||||
module.exports = CreateLayergroupMapConfigProvider;
|
||||
|
||||
CreateLayergroupMapConfigProvider.prototype.getMapConfig = function(callback) {
|
||||
var self = this;
|
||||
var context = {};
|
||||
step(
|
||||
function prepareContextLimits() {
|
||||
self.userLimitsApi.getRenderLimits(self.user, self.params.api_key, this);
|
||||
},
|
||||
function handleRenderLimits(err, renderLimits) {
|
||||
assert.ifError(err);
|
||||
context.limits = renderLimits;
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
return callback(err, self.mapConfig, self.params, context);
|
||||
getMapConfig (callback) {
|
||||
if (this.mapConfig && this.params && this.context) {
|
||||
return callback(null, this.mapConfig, this.params, this.context);
|
||||
}
|
||||
);
|
||||
|
||||
const context = {};
|
||||
|
||||
this.userLimitsBackend.getRenderLimits(this.user, this.params.api_key, (err, renderLimits) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
context.limits = renderLimits;
|
||||
this.context = context;
|
||||
|
||||
return callback(null, this.mapConfig, this.params, context);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
CreateLayergroupMapConfigProvider.prototype.getKey = MapStoreMapConfigProvider.prototype.getKey;
|
||||
|
||||
CreateLayergroupMapConfigProvider.prototype.getCacheBuster = MapStoreMapConfigProvider.prototype.getCacheBuster;
|
||||
|
||||
CreateLayergroupMapConfigProvider.prototype.filter = MapStoreMapConfigProvider.prototype.filter;
|
||||
|
||||
CreateLayergroupMapConfigProvider.prototype.createKey = MapStoreMapConfigProvider.prototype.createKey;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user