Compare commits
764 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a20e789302 | ||
|
|
f41af41bd4 | ||
|
|
0a106cd038 | ||
|
|
1a78b8a75a | ||
|
|
e131df601c | ||
|
|
967d9b76e6 | ||
|
|
bee04e2553 | ||
|
|
37111f396d | ||
|
|
4df46fe5ea | ||
|
|
b1b2054f0a | ||
|
|
c1f2b96bfc | ||
|
|
804c6645fa | ||
|
|
5d6ccc07fd | ||
|
|
a585ba5480 | ||
|
|
448dcc7d82 | ||
|
|
0aaafa2068 | ||
|
|
1aa981d556 | ||
|
|
ccce598b04 | ||
|
|
667b2a9cb1 | ||
|
|
298882f410 | ||
|
|
6aaa5f99e2 | ||
|
|
22e3016cd3 | ||
|
|
d5c552a03a | ||
|
|
a5347c27e3 | ||
|
|
520e84e46b | ||
|
|
27521964c7 | ||
|
|
bdf4827300 | ||
|
|
172b3ece71 | ||
|
|
71146dbfaf | ||
|
|
38ca5db51b | ||
|
|
590233e3ee | ||
|
|
6f59c61c8b | ||
|
|
aff5fcda63 | ||
|
|
56d33b7f5b | ||
|
|
749b205944 | ||
|
|
ad0c035e2d | ||
|
|
d15ccd271e | ||
|
|
2aee357006 | ||
|
|
fc9dce0cca | ||
|
|
9149f72f42 | ||
|
|
743bb0723b | ||
|
|
0bf36fa058 | ||
|
|
970310bf7f | ||
|
|
4fc90db495 | ||
|
|
50ecdb5fee | ||
|
|
1ea4fc50c9 | ||
|
|
cda9a09b8e | ||
|
|
216c877f4b | ||
|
|
33fbff5011 | ||
|
|
c48e89826d | ||
|
|
52542e4a88 | ||
|
|
693a2e7bee | ||
|
|
f9ba3c41d3 | ||
|
|
ac153232d0 | ||
|
|
46289f27df | ||
|
|
05ccf20634 | ||
|
|
6acb873d95 | ||
|
|
65e8609fec | ||
|
|
677f6caab8 | ||
|
|
cb167313d2 | ||
|
|
2854d0252c | ||
|
|
717332d941 | ||
|
|
4607e4a12d | ||
|
|
3e7106002d | ||
|
|
08b91f935d | ||
|
|
1d08734721 | ||
|
|
b11b872b75 | ||
|
|
93bd2c9e50 | ||
|
|
658763da8c | ||
|
|
d2b5eaa8c3 | ||
|
|
eb5bf52bd9 | ||
|
|
c8000e5cf8 | ||
|
|
46c76d6a4c | ||
|
|
e6bec5ccb0 | ||
|
|
125587522f | ||
|
|
aeb9585708 | ||
|
|
8ed5df0072 | ||
|
|
6bbaeaa286 | ||
|
|
3d15551cb5 | ||
|
|
e0ffeb0adc | ||
|
|
e06f8fe25e | ||
|
|
da2228088e | ||
|
|
cdc39c8cae | ||
|
|
99fa66c026 | ||
|
|
d85a5d83b7 | ||
|
|
bb02494e02 | ||
|
|
39eb0f7bec | ||
|
|
5f7d5f6ec8 | ||
|
|
a4b2044e10 | ||
|
|
d1093686a3 | ||
|
|
12822c4341 | ||
|
|
fab87e2168 | ||
|
|
34e219353c | ||
|
|
3cf4a8f70b | ||
|
|
48172d4dc1 | ||
|
|
467bee4c91 | ||
|
|
3f2ef63976 | ||
|
|
235f5e4566 | ||
|
|
3f49743cd0 | ||
|
|
fb3afaa6ab | ||
|
|
b6c405bf68 | ||
|
|
da87a95dd9 | ||
|
|
cd7c604d10 | ||
|
|
b7227e0581 | ||
|
|
c564f5467a | ||
|
|
b04cc9c228 | ||
|
|
c0df0d12c6 | ||
|
|
5645cd16b3 | ||
|
|
eb6da1398e | ||
|
|
35c5cd34c2 | ||
|
|
6c51667ffb | ||
|
|
1396ca9fe3 | ||
|
|
56bb083239 | ||
|
|
ddcb812218 | ||
|
|
66c3d58b92 | ||
|
|
9326217c18 | ||
|
|
1207764c18 | ||
|
|
73a633ae7d | ||
|
|
3068ff1ea4 | ||
|
|
9ad6d0cbcc | ||
|
|
86389382fa | ||
|
|
c2bf7b075c | ||
|
|
294a222669 | ||
|
|
515146bf28 | ||
|
|
a1c08f9bf7 | ||
|
|
f8ff41be01 | ||
|
|
67ab12e8e7 | ||
|
|
d959ef5007 | ||
|
|
8cc4fe5b56 | ||
|
|
22a34d763c | ||
|
|
d30f710534 | ||
|
|
02304dc450 | ||
|
|
893fac31a7 | ||
|
|
abef8918c0 | ||
|
|
8380d291d0 | ||
|
|
251e636ad2 | ||
|
|
286059b8a3 | ||
|
|
b18bf967fd | ||
|
|
a81e98995a | ||
|
|
a797e13eb3 | ||
|
|
5e073f39bd | ||
|
|
8a88b29665 | ||
|
|
d77739dfa4 | ||
|
|
484e0fda2f | ||
|
|
3827901535 | ||
|
|
82648df21c | ||
|
|
1766cd0ad4 | ||
|
|
6af83d7630 | ||
|
|
28501f6b9d | ||
|
|
e3405ea2fc | ||
|
|
5c0f597cbb | ||
|
|
7289394f6a | ||
|
|
1ba1c488fa | ||
|
|
10f9f61e1e | ||
|
|
0e20958220 | ||
|
|
28cb05e45b | ||
|
|
c004e105ef | ||
|
|
f456237aa7 | ||
|
|
7f66189164 | ||
|
|
cac16f8b66 | ||
|
|
a706fd81ba | ||
|
|
43885f130b | ||
|
|
58be2b8fc5 | ||
|
|
78671aa499 | ||
|
|
d29da0bcc3 | ||
|
|
e9d0b3b77d | ||
|
|
4e6253b717 | ||
|
|
44eb323764 | ||
|
|
d4015085c7 | ||
|
|
b9c511ee60 | ||
|
|
64fe070ab2 | ||
|
|
5d750f3b98 | ||
|
|
664892bba9 | ||
|
|
38c50e0bec | ||
|
|
6c0e6210d6 | ||
|
|
f350206990 | ||
|
|
c8d2c9ea37 | ||
|
|
cab2d6d5d4 | ||
|
|
242e63716f | ||
|
|
c70b8cb5bf | ||
|
|
06138a82a8 | ||
|
|
678fbb1c8f | ||
|
|
2f310a15bd | ||
|
|
bf637ccd5b | ||
|
|
f387f2ee6f | ||
|
|
34d9e5a4eb | ||
|
|
54b7ee85c2 | ||
|
|
9083fc2e20 | ||
|
|
72a9a3e097 | ||
|
|
102228c55b | ||
|
|
148e6e6ae5 | ||
|
|
226653207a | ||
|
|
b93c09959c | ||
|
|
5abe25c316 | ||
|
|
1f03a6b181 | ||
|
|
16e8202782 | ||
|
|
4afa7f70d7 | ||
|
|
5045f81fe3 | ||
|
|
ec8fcc7302 | ||
|
|
2afb6b5ac2 | ||
|
|
19e2515a8e | ||
|
|
5d6156a257 | ||
|
|
9e217d9199 | ||
|
|
a224a0bf91 | ||
|
|
87bcb7ebf2 | ||
|
|
d06ba8b1f8 | ||
|
|
2b37a406bc | ||
|
|
0a507d02bc | ||
|
|
49fd75f0b6 | ||
|
|
514aa53152 | ||
|
|
8fe31c45f3 | ||
|
|
fe4c22d2ea | ||
|
|
d27cce915c | ||
|
|
1c3f2b93e3 | ||
|
|
21720267cf | ||
|
|
da832263a4 | ||
|
|
69bd14793f | ||
|
|
54dd15c0b0 | ||
|
|
3ce10690d6 | ||
|
|
6bfc5d8891 | ||
|
|
430e1513d8 | ||
|
|
28c8632532 | ||
|
|
89172f280f | ||
|
|
0a7506e4b2 | ||
|
|
4fd3c99531 | ||
|
|
1e4c63a6dc | ||
|
|
742420b159 | ||
|
|
b8783a6447 | ||
|
|
45dece65f2 | ||
|
|
c894414192 | ||
|
|
aa62529041 | ||
|
|
55f593eae6 | ||
|
|
f9d87bc40f | ||
|
|
818cdbd99b | ||
|
|
783eb0eec7 | ||
|
|
c22a35489d | ||
|
|
482feabce2 | ||
|
|
0a753400e0 | ||
|
|
a21648ab4a | ||
|
|
b4d03c074a | ||
|
|
75c8a73423 | ||
|
|
649383a3df | ||
|
|
52402c0333 | ||
|
|
cacb92b0c4 | ||
|
|
5463248578 | ||
|
|
aaf95b9223 | ||
|
|
6149df1810 | ||
|
|
ff47027a51 | ||
|
|
e5cae8b8e3 | ||
|
|
78b75c7a88 | ||
|
|
56d7c2c140 | ||
|
|
ad1abb28af | ||
|
|
f824fc5243 | ||
|
|
4a2cc6a5f8 | ||
|
|
ca612dd02a | ||
|
|
fedcb0d0f9 | ||
|
|
c9f0902703 | ||
|
|
1739cee11d | ||
|
|
178b9e8563 | ||
|
|
ac474cb253 | ||
|
|
79510185da | ||
|
|
c960535709 | ||
|
|
84cd93b1b0 | ||
|
|
134cc9ac0c | ||
|
|
615229fc31 | ||
|
|
4600005a86 | ||
|
|
383c8305cc | ||
|
|
b94dfe066d | ||
|
|
de267917f4 | ||
|
|
3f6afb4530 | ||
|
|
540fda1e6c | ||
|
|
e0e67df91c | ||
|
|
4899c7ffef | ||
|
|
ac42223439 | ||
|
|
1110abaa9a | ||
|
|
3023111896 | ||
|
|
66380197f4 | ||
|
|
8daa4bb08a | ||
|
|
b943b09532 | ||
|
|
eda18726fd | ||
|
|
f0920aedef | ||
|
|
b236112069 | ||
|
|
d3dafc8a40 | ||
|
|
c734f43643 | ||
|
|
0e8fb68794 | ||
|
|
f7b9287c93 | ||
|
|
85d4c81e58 | ||
|
|
ff19a8a2fe | ||
|
|
3bab081438 | ||
|
|
6dc9cc0b23 | ||
|
|
3134f40eac | ||
|
|
5cc31cabe2 | ||
|
|
8fd35849c7 | ||
|
|
c09899913f | ||
|
|
0bdeee64a7 | ||
|
|
ee8619c470 | ||
|
|
9d81321d78 | ||
|
|
ca63c2ef1a | ||
|
|
b0486f9bae | ||
|
|
2eb1c0f3e0 | ||
|
|
22b7828725 | ||
|
|
78404b1308 | ||
|
|
45698207d9 | ||
|
|
9bd862ffaf | ||
|
|
8139cdf8b2 | ||
|
|
a8898a8022 | ||
|
|
df5ec0f4d9 | ||
|
|
51ba3db4ac | ||
|
|
d31e52a625 | ||
|
|
3a8b99a14e | ||
|
|
fac1ab4a1c | ||
|
|
a9b0acc317 | ||
|
|
5cb2e5d3c5 | ||
|
|
e2ed0058d8 | ||
|
|
2f499a148a | ||
|
|
49204650c6 | ||
|
|
234576ab5f | ||
|
|
02cd6a43ad | ||
|
|
429f070372 | ||
|
|
3b9c561cee | ||
|
|
daeae5d95c | ||
|
|
33121871b0 | ||
|
|
f133d983e8 | ||
|
|
b6237c7bfa | ||
|
|
a5d9bfa0ec | ||
|
|
222cfb90fd | ||
|
|
f63fab40ed | ||
|
|
61ea05d1c2 | ||
|
|
64c3e68303 | ||
|
|
d4bb4edd1d | ||
|
|
419b29e609 | ||
|
|
c7ed3d34e8 | ||
|
|
1959a841fd | ||
|
|
ef5049f28f | ||
|
|
d5d9044686 | ||
|
|
5d632d936e | ||
|
|
90c4796d4e | ||
|
|
ada58f6ea2 | ||
|
|
b4ce13e429 | ||
|
|
11f7b38c69 | ||
|
|
9771979b8f | ||
|
|
c00a93f414 | ||
|
|
ecbc7a28e7 | ||
|
|
68dfed8b85 | ||
|
|
2437288d9d | ||
|
|
9c64d674b3 | ||
|
|
a4ecc18f2f | ||
|
|
1063d81c1b | ||
|
|
dcb9b8ec52 | ||
|
|
dbb23bf9f0 | ||
|
|
2a0b15f085 | ||
|
|
d0e2c9f898 | ||
|
|
d328b534a5 | ||
|
|
050e9776d1 | ||
|
|
c8ff61c531 | ||
|
|
cdc56e703c | ||
|
|
9a4794ee10 | ||
|
|
51907b9545 | ||
|
|
1f3b0beddf | ||
|
|
38e2c040d1 | ||
|
|
46860541fe | ||
|
|
c2e99219ef | ||
|
|
cc2cf78264 | ||
|
|
746292610a | ||
|
|
b05083bcfc | ||
|
|
cd13107a4d | ||
|
|
46254eaf74 | ||
|
|
086eff01a9 | ||
|
|
02949003a9 | ||
|
|
0a894da0df | ||
|
|
e2ab48bee2 | ||
|
|
132fce84c5 | ||
|
|
b1508af007 | ||
|
|
65dca454f4 | ||
|
|
3682740f08 | ||
|
|
a434015d5b | ||
|
|
2f4f719f55 | ||
|
|
75645e2d7a | ||
|
|
4d1a53c20f | ||
|
|
ee471184b9 | ||
|
|
4518b7cb6e | ||
|
|
306df5be5a | ||
|
|
33e8657e35 | ||
|
|
6fd3388fa2 | ||
|
|
4a89ad57d7 | ||
|
|
c0cfdad7d1 | ||
|
|
8f797c3c41 | ||
|
|
2576c3e7d5 | ||
|
|
3a936474cf | ||
|
|
a98f5bf08b | ||
|
|
03babcb43b | ||
|
|
9aa5a9e850 | ||
|
|
e3bffcd39d | ||
|
|
5fc2b46d56 | ||
|
|
7c69240748 | ||
|
|
ee43378c68 | ||
|
|
09981c2560 | ||
|
|
fd9534797c | ||
|
|
38e7e71328 | ||
|
|
271932a80d | ||
|
|
4f33e0d794 | ||
|
|
ec23bfc79b | ||
|
|
6c3fa045cd | ||
|
|
d75ee965ae | ||
|
|
5e9b2e45c7 | ||
|
|
e4a20fa954 | ||
|
|
a20900210d | ||
|
|
2650c3b3e6 | ||
|
|
25ef2610aa | ||
|
|
92f6f59e07 | ||
|
|
5e07cc2ad1 | ||
|
|
5593d92c4b | ||
|
|
29f32cb9cc | ||
|
|
1d4935cc9a | ||
|
|
f75b4312a1 | ||
|
|
23dd143fa5 | ||
|
|
7d42afcdb4 | ||
|
|
78b95d05d0 | ||
|
|
fb753e50a2 | ||
|
|
c863cdd9f6 | ||
|
|
a4ebce52db | ||
|
|
4a00a2d673 | ||
|
|
38f0e23efe | ||
|
|
7f14785091 | ||
|
|
db969a51ad | ||
|
|
3441ad6aa9 | ||
|
|
347dea8f66 | ||
|
|
a3112aa929 | ||
|
|
157946cc42 | ||
|
|
8ce25d958c | ||
|
|
7e099be134 | ||
|
|
6b2e2b2241 | ||
|
|
855e5c9e4c | ||
|
|
a24792f46d | ||
|
|
0eb57f6801 | ||
|
|
f1246cb060 | ||
|
|
7dd5c5b15d | ||
|
|
806c13beac | ||
|
|
69f110e037 | ||
|
|
d77075295e | ||
|
|
63a7ee08d0 | ||
|
|
b63a67a5b8 | ||
|
|
1ac8455dc2 | ||
|
|
9f52e58be8 | ||
|
|
4edf18f77a | ||
|
|
b5d2de8edc | ||
|
|
bd8d147a7d | ||
|
|
8ac041805c | ||
|
|
6e0dc8666d | ||
|
|
3e55bd2abb | ||
|
|
da1d0550f6 | ||
|
|
c37ef36a61 | ||
|
|
9e3e1cad9a | ||
|
|
e84f30488f | ||
|
|
49a60caffc | ||
|
|
392e004879 | ||
|
|
288656301b | ||
|
|
96740b82ed | ||
|
|
1be66e1552 | ||
|
|
5ba2dfbbd6 | ||
|
|
af4b3d81cd | ||
|
|
bbd42b73f2 | ||
|
|
8e2535745e | ||
|
|
4f75f6c07b | ||
|
|
0ede3013db | ||
|
|
0b79ac76db | ||
|
|
2739364193 | ||
|
|
adcff54589 | ||
|
|
734cfa6d83 | ||
|
|
7ea6b3e371 | ||
|
|
f1018f3272 | ||
|
|
151bdec1fd | ||
|
|
5d413ac1f9 | ||
|
|
37b1376767 | ||
|
|
00741bc0a4 | ||
|
|
c580600590 | ||
|
|
6373fe8652 | ||
|
|
5ce419d863 | ||
|
|
7be5361433 | ||
|
|
5332fd3baa | ||
|
|
77f1aa7e0c | ||
|
|
e1990fc2f9 | ||
|
|
ca6eb609b2 | ||
|
|
91ce3a5489 | ||
|
|
fc0dbaaab1 | ||
|
|
03dc260104 | ||
|
|
d644376f88 | ||
|
|
ed0bfa5f63 | ||
|
|
ca0b927f51 | ||
|
|
cd27d6aa02 | ||
|
|
7aefca3f82 | ||
|
|
3d409274e0 | ||
|
|
4491fa2faf | ||
|
|
0ed46930dd | ||
|
|
f3b7a857f2 | ||
|
|
3a22adf966 | ||
|
|
1c6a76af72 | ||
|
|
175d3ac317 | ||
|
|
175d070f09 | ||
|
|
339f1aafa9 | ||
|
|
fef0dc302a | ||
|
|
58fec46117 | ||
|
|
7be74d6ce1 | ||
|
|
d0f5ebd7ab | ||
|
|
92d33bf7fd | ||
|
|
490adbce4b | ||
|
|
fab7832dee | ||
|
|
e678957a8f | ||
|
|
e1e22de65f | ||
|
|
43312922fc | ||
|
|
01a22a45bb | ||
|
|
9524433437 | ||
|
|
14d5ee4178 | ||
|
|
e7c206762d | ||
|
|
69eaa72819 | ||
|
|
23edf78a67 | ||
|
|
44c5eb051d | ||
|
|
814b123b2b | ||
|
|
ff560ffde7 | ||
|
|
14f85abd39 | ||
|
|
ce97844f37 | ||
|
|
d27a281067 | ||
|
|
3611752677 | ||
|
|
b24858a17c | ||
|
|
26a967d0a7 | ||
|
|
c643160671 | ||
|
|
e7a0b246a3 | ||
|
|
3c061769c6 | ||
|
|
7e159c565b | ||
|
|
ff3d7ed7b2 | ||
|
|
cf71489a7f | ||
|
|
c7e5dbf158 | ||
|
|
34cf45bc9d | ||
|
|
7e058955ea | ||
|
|
a9e3bc3cda | ||
|
|
3ee064a59f | ||
|
|
f3ababffc1 | ||
|
|
0f8de9e74b | ||
|
|
91d5a0e4e4 | ||
|
|
e446160151 | ||
|
|
823925d091 | ||
|
|
994e58bef7 | ||
|
|
5c80ff8191 | ||
|
|
0f45675652 | ||
|
|
b2bbc329ea | ||
|
|
2024e89c6a | ||
|
|
1f8da14c2a | ||
|
|
660078f284 | ||
|
|
e9d925334c | ||
|
|
399561d076 | ||
|
|
7eae2a0618 | ||
|
|
e52cf0960c | ||
|
|
c39a6a6806 | ||
|
|
82cab3ccc7 | ||
|
|
e01730e8e4 | ||
|
|
eed33fc76d | ||
|
|
2a531024d7 | ||
|
|
48ad7059e1 | ||
|
|
6c063095a3 | ||
|
|
1d1a046439 | ||
|
|
fe7a2451ef | ||
|
|
a696bdc723 | ||
|
|
a98c884e1a | ||
|
|
431ca9c56f | ||
|
|
b56d2ec30b | ||
|
|
90ded34af7 | ||
|
|
7fed91900d | ||
|
|
b4799124e6 | ||
|
|
1bc5c04489 | ||
|
|
0a57e86cb8 | ||
|
|
3574700c2d | ||
|
|
ab879e2634 | ||
|
|
9034508244 | ||
|
|
b2b68ffd5c | ||
|
|
0594407b38 | ||
|
|
262f854e68 | ||
|
|
9258ad7ecc | ||
|
|
46fee774bd | ||
|
|
05ddf1d505 | ||
|
|
4c3e3005aa | ||
|
|
7d13603163 | ||
|
|
40af73d524 | ||
|
|
91b3e373b7 | ||
|
|
aa4bb62f38 | ||
|
|
9af372381c | ||
|
|
0c4e67d6a8 | ||
|
|
dd5209b9a7 | ||
|
|
44fc34b1ce | ||
|
|
1fdc0621e7 | ||
|
|
5974413d5c | ||
|
|
bb59902535 | ||
|
|
49d2f513c6 | ||
|
|
b1114fc606 | ||
|
|
227c2b336b | ||
|
|
ac7509b01a | ||
|
|
9b5482489e | ||
|
|
f079c24554 | ||
|
|
04da57fe0c | ||
|
|
aa6d01f151 | ||
|
|
435d902e45 | ||
|
|
664db4b5cf | ||
|
|
64f19b65ec | ||
|
|
398369a5c7 | ||
|
|
f2e043b063 | ||
|
|
6936107b68 | ||
|
|
c3e137bb00 | ||
|
|
cca570e832 | ||
|
|
815eac5a48 | ||
|
|
b023a155b7 | ||
|
|
33e77a42f2 | ||
|
|
664a4e673a | ||
|
|
eba97a41e5 | ||
|
|
9e491e7e9a | ||
|
|
522fc79d71 | ||
|
|
768d06c582 | ||
|
|
058f19ab36 | ||
|
|
788b2f0683 | ||
|
|
526e850f26 | ||
|
|
444595d49d | ||
|
|
eee4fc815e | ||
|
|
edacd85d5c | ||
|
|
52da3bfa55 | ||
|
|
35b9448e9a | ||
|
|
9959e009eb | ||
|
|
106b9a64b2 | ||
|
|
42d05f29ee | ||
|
|
fc3a959da1 | ||
|
|
20003c49ce | ||
|
|
d1d9401539 | ||
|
|
cc8a1df388 | ||
|
|
e9bc0732c0 | ||
|
|
8907082a85 | ||
|
|
87eb5407a8 | ||
|
|
a17916488b | ||
|
|
669707b26c | ||
|
|
40dc94e010 | ||
|
|
868930de46 | ||
|
|
f306c26da6 | ||
|
|
446e2d0802 | ||
|
|
0aab434f13 | ||
|
|
ff13996255 | ||
|
|
eccc3597aa | ||
|
|
6766b76545 | ||
|
|
e30b883906 | ||
|
|
70b4d5b7fd | ||
|
|
0fffafa1db | ||
|
|
21b8655f85 | ||
|
|
c8286233be | ||
|
|
b67f6053e8 | ||
|
|
967dca9578 | ||
|
|
a35b1e3e86 | ||
|
|
5b8ecd3df0 | ||
|
|
5ea5c1b2dc | ||
|
|
e36266a80f | ||
|
|
b1c9dd537e | ||
|
|
dd934a3913 | ||
|
|
7fa154c062 | ||
|
|
f7a763b637 | ||
|
|
ad1506ae97 | ||
|
|
32bcf9ca89 | ||
|
|
23aab7a09f | ||
|
|
37c970903e | ||
|
|
0684c1b9d3 | ||
|
|
468f641af8 | ||
|
|
6d2934b30b | ||
|
|
7018af18b6 | ||
|
|
f507f7a74b | ||
|
|
2f1cacdfc7 | ||
|
|
3a442bea44 | ||
|
|
49f5b0b480 | ||
|
|
01027b73da | ||
|
|
af42fba53b | ||
|
|
3e12bfe27a | ||
|
|
13764e18ce | ||
|
|
b2f3735e95 | ||
|
|
166e29e8ce | ||
|
|
32274e66fd | ||
|
|
15b88c6a67 | ||
|
|
2dae09c35b | ||
|
|
a6daca9628 | ||
|
|
14e71b929a | ||
|
|
6f7cb75256 | ||
|
|
5555b8ad8e | ||
|
|
e44d418db3 | ||
|
|
6bfedef7eb | ||
|
|
77cb3dbbdc | ||
|
|
17aebf53e2 | ||
|
|
02f117af1b | ||
|
|
b1ac5b8ca9 | ||
|
|
e2b976d9d0 | ||
|
|
8e95cf20c0 | ||
|
|
20d7f1a7c5 | ||
|
|
115d8fe685 | ||
|
|
2a366ec16f | ||
|
|
849caf9b58 | ||
|
|
ad570ab6f2 | ||
|
|
443c1100d7 | ||
|
|
7d0af4e259 | ||
|
|
81f60959e5 | ||
|
|
ef849aec34 | ||
|
|
dee00e6abd | ||
|
|
06d40e8b1e | ||
|
|
3f17c8b15a | ||
|
|
668b22628c | ||
|
|
c08db78a0b | ||
|
|
551b6d409a | ||
|
|
3ae66e4143 | ||
|
|
227937bf4c | ||
|
|
cb7ec5d556 | ||
|
|
8b2fa27ba7 | ||
|
|
962fa05574 | ||
|
|
75d07745e6 | ||
|
|
7b5111614c | ||
|
|
e60bb770db | ||
|
|
ba6dc62a38 | ||
|
|
e6aededf08 | ||
|
|
0aae29fb4b | ||
|
|
9ba65bd5a4 | ||
|
|
7a3498e8ec | ||
|
|
fe5c76d65b | ||
|
|
29a6658e3d | ||
|
|
2772fc62d2 | ||
|
|
0d4ac64f00 | ||
|
|
271887eb46 | ||
|
|
cd53eda0a5 | ||
|
|
6c301403e3 | ||
|
|
47af013157 | ||
|
|
35d4fb4d27 | ||
|
|
42e2f9e4b1 | ||
|
|
02bf1dd2d7 | ||
|
|
d365e092b9 | ||
|
|
45d1d07ea2 | ||
|
|
f14a61528a | ||
|
|
d3bcf6f80d | ||
|
|
eeea51e10d | ||
|
|
9337cd948c | ||
|
|
527e005952 | ||
|
|
1ff0954390 | ||
|
|
e82d688a18 | ||
|
|
95a6ad3b86 | ||
|
|
d01787842f | ||
|
|
c86f92f8eb | ||
|
|
003227fb29 | ||
|
|
869408b7b7 | ||
|
|
dc844f8131 | ||
|
|
71e9e62db0 | ||
|
|
6ff3b33cde | ||
|
|
32eeb57fce | ||
|
|
8bc38a375a | ||
|
|
c1fac13d6b | ||
|
|
6374d2e4b6 | ||
|
|
9c34428984 | ||
|
|
1d66e49910 | ||
|
|
4b562e6768 | ||
|
|
b4fbe0b8cf | ||
|
|
62514fc563 | ||
|
|
ef3cad6599 | ||
|
|
4e53803b3b | ||
|
|
40a73f2eaf | ||
|
|
31557b06be | ||
|
|
c8ea595f47 | ||
|
|
6a4b412cd3 | ||
|
|
2374711d63 | ||
|
|
213a3e297c |
@@ -1,4 +1,3 @@
|
||||
test/results/
|
||||
test/monkey/
|
||||
test/benchmark.js
|
||||
test/support/
|
||||
|
||||
31
.travis.yml
31
.travis.yml
@@ -1,27 +1,14 @@
|
||||
sudo: required
|
||||
dist: trusty
|
||||
addons:
|
||||
postgresql: "9.5"
|
||||
apt:
|
||||
sources:
|
||||
- ubuntu-toolchain-r-test
|
||||
packages:
|
||||
- postgresql-9.5-postgis-2.3
|
||||
- postgresql-plpython-9.5
|
||||
- pkg-config
|
||||
- libcairo2-dev
|
||||
- libjpeg8-dev
|
||||
- libgif-dev
|
||||
- libpango1.0-dev
|
||||
- g++-4.9
|
||||
|
||||
services:
|
||||
- docker
|
||||
|
||||
before_install:
|
||||
- createdb template_postgis
|
||||
- createuser publicuser
|
||||
- psql -c "CREATE EXTENSION postgis" template_postgis
|
||||
- docker pull cartoimages/windshaft-carto-testing
|
||||
|
||||
env:
|
||||
- NPROCS=1 JOBS=1 PGUSER=postgres CXX=g++-4.9
|
||||
script:
|
||||
- docker run -e POSTGIS_VERSION=2.4 -v `pwd`:/srv cartoimages/windshaft-carto-testing
|
||||
|
||||
language: generic
|
||||
|
||||
language: node_js
|
||||
node_js:
|
||||
- "6"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
1. Test (make clean all check), fix if broken before proceeding
|
||||
2. Ensure proper version in package.json
|
||||
3. Ensure NEWS section exists for the new version, review it, add release date
|
||||
4. Recreate yarn.lock with: `yarn upgrade`
|
||||
4. If there are modified dependencies in package.json, update them with `yarn upgrade {{package_name}}@{{version}}`
|
||||
5. Commit package.json, yarn.lock, NEWS
|
||||
6. git tag -a Major.Minor.Patch # use NEWS section as content
|
||||
7. Stub NEWS/package for next version
|
||||
|
||||
@@ -5,7 +5,7 @@ Make sure that you have the requirements needed. These are
|
||||
|
||||
- Core
|
||||
- Node.js >=6.9.x
|
||||
- yarn >=0.21.3
|
||||
- yarn >=0.27.5 <1.0.0
|
||||
- PostgreSQL >8.3.x, PostGIS >1.5.x
|
||||
- Redis >2.4.0 (http://www.redis.io)
|
||||
- Mapnik >3.x. See [Installing Mapnik](https://github.com/CartoDB/Windshaft#installing-mapnik).
|
||||
|
||||
2
Makefile
2
Makefile
@@ -16,7 +16,7 @@ config.status--test:
|
||||
./configure --environment=test
|
||||
|
||||
config/environments/test.js: config.status--test
|
||||
./config.status--test
|
||||
./config.status--test
|
||||
|
||||
TEST_SUITE := $(shell find test/{acceptance,integration,unit} -name "*.js")
|
||||
TEST_SUITE_UNIT := $(shell find test/unit -name "*.js")
|
||||
|
||||
203
NEWS.md
203
NEWS.md
@@ -1,5 +1,208 @@
|
||||
# Changelog
|
||||
|
||||
## 4.1.1
|
||||
Released 2017-11-29
|
||||
|
||||
Announcements:
|
||||
- Upgrades turbo-carto to [0.20.2](https://github.com/CartoDB/turbo-carto/releases/tag/0.20.2).
|
||||
|
||||
## 4.1.0
|
||||
Released 2017-mm-dd
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [4.0.1](https://github.com/CartoDB/windshaft/releases/tag/4.0.1).
|
||||
- Add `categories` query param to define the number of categories to be ranked for aggregation dataviews.
|
||||
|
||||
|
||||
## 4.0.1
|
||||
Released 2017-10-18
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.59.4](https://github.com/CartoDB/camshaft/releases/tag/0.59.4).
|
||||
- Upgrades windshaft to [4.0.0](https://github.com/CartoDB/windshaft/releases/tag/4.0.0).
|
||||
- Split and move `req2params` method to multiple middlewares.
|
||||
- Use express error handler middleware to respond in case of something went wrong.
|
||||
- Use `res.locals` object to share info between middlewares and leave `req.params` as an object containing properties mapped to the named route params.
|
||||
- Move `LZMA` decompression to its own middleware.
|
||||
- Implement stats middleware removing some duplicated code while sending response.
|
||||
|
||||
|
||||
## 4.0.0
|
||||
Released 2017-10-04
|
||||
|
||||
Backward incompatible changes:
|
||||
- Removes `list` dataview type.
|
||||
|
||||
Announcements:
|
||||
- Upgrades body-parser to 1.18.2.
|
||||
- Upgrades express to 4.16.0.
|
||||
- Upgrades debug to 3.1.0.
|
||||
- Upgrades request to 2.83.0.
|
||||
- Upgrades turbo-carto to [0.20.1](https://github.com/CartoDB/turbo-carto/releases/tag/0.20.1)
|
||||
- Upgrades cartodb-psql to [0.10.2](https://github.com/CartoDB/node-cartodb-psql/releases/tag/0.10.2).
|
||||
- Upgrades camshaft to [0.59.2](https://github.com/CartoDB/camshaft/releases/tag/0.59.2).
|
||||
- Upgrades windshaft to [3.3.3](https://github.com/CartoDB/windshaft/releases/tag/3.3.3).
|
||||
- Upgrades yarn minimum version requirement to v0.27.5
|
||||
|
||||
|
||||
## 3.13.0
|
||||
Released 2017-10-02
|
||||
- Upgrades camshaft, cartodb-query-tables, and turbo-carto: better support for query variables.
|
||||
|
||||
Bugfixes:
|
||||
- Bounding box parameter ignored in static named maps #735.
|
||||
- camhaft 0.59.1 fixes duplicate columns in aggregate-intersection analysis
|
||||
|
||||
## 3.12.10
|
||||
Released 2017-09-18
|
||||
- Upgrades windshaft to [3.3.2](https://github.com/CartoDB/windshaft/releases/tag/3.3.2).
|
||||
|
||||
## 3.12.9
|
||||
Released 2017-09-07
|
||||
|
||||
Bug fixes:
|
||||
- Do not use distinct when calculating quantiles. #743
|
||||
|
||||
## 3.12.8
|
||||
Released 2017-09-07
|
||||
|
||||
Bug fixes:
|
||||
- Integer out of range in date histograms. (https://github.com/CartoDB/support/issues/962)
|
||||
|
||||
## 3.12.7
|
||||
Released 2017-09-01
|
||||
|
||||
- Upgrades camshaft to [0.58.1](https://github.com/CartoDB/camshaft/releases/tag/0.58.1).
|
||||
|
||||
|
||||
## 3.12.6
|
||||
Released 2017-08-31
|
||||
|
||||
- Upgrades camshaft to [0.58.0](https://github.com/CartoDB/camshaft/releases/tag/0.58.0).
|
||||
|
||||
|
||||
## 3.12.5
|
||||
Released 2017-08-24
|
||||
|
||||
- Upgrades camshaft to [0.57.0](https://github.com/CartoDB/camshaft/releases/tag/0.57.0).
|
||||
|
||||
|
||||
## 3.12.4
|
||||
Released 2017-08-23
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.56.0](https://github.com/CartoDB/camshaft/releases/tag/0.56.0).
|
||||
|
||||
## 3.12.3
|
||||
Released 2017-08-22
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.55.8](https://github.com/CartoDB/camshaft/releases/tag/0.55.8).
|
||||
|
||||
## 3.12.2
|
||||
Released 2017-08-16
|
||||
|
||||
Bug fixes:
|
||||
- Polygon count problems #725.
|
||||
|
||||
|
||||
## 3.12.1
|
||||
Released 2017-08-13
|
||||
- Upgrades cartodb-psql to [0.10.1](https://github.com/CartoDB/node-cartodb-psql/releases/tag/0.10.1).
|
||||
- Upgrades windshaft to [3.3.1](https://github.com/CartoDB/windshaft/releases/tag/3.3.1).
|
||||
- Upgrades camshaft to [0.55.7](https://github.com/CartoDB/camshaft/releases/tag/0.55.7).
|
||||
|
||||
|
||||
## 3.12.0
|
||||
Released 2017-08-10
|
||||
|
||||
Announcements:
|
||||
- Apply max tile response time for requests to layergoup, tiles, static maps, attributes and dataviews services #717.
|
||||
- Upgrades windshaft to [3.3.0](https://github.com/CartoDB/windshaft/releases/tag/3.3.0).
|
||||
- Upgrades cartodb-redis to [0.14.0](https://github.com/CartoDB/node-cartodb-redis/releases/tag/0.14.0).
|
||||
|
||||
|
||||
## 3.11.0
|
||||
Released 2017-08-08
|
||||
|
||||
Announcements:
|
||||
- Allow to override with any aggregation for histograms instantiated w/o aggregation.
|
||||
|
||||
Bug fixes:
|
||||
- Apply timezone after truncating the minimun date for each bin to calculate timestamps in time-series.
|
||||
- Support timestamp with timezones to calculate the number of bins in time-series.
|
||||
- Fixed issue related to name collision while building time-series query.
|
||||
|
||||
|
||||
## 3.10.1
|
||||
Released 2017-08-04
|
||||
|
||||
Bug fixes:
|
||||
- Exclude Infinities & NaNs from ramps #719.
|
||||
- Fixed issue in time-series when aggregation starts at 1970-01-01 (epoch) #720.
|
||||
|
||||
|
||||
## 3.10.0
|
||||
Released 2017-08-03
|
||||
|
||||
Announcements:
|
||||
- Improve time-series dataview, now supports date aggregations (e.g: daily, weekly, monthly, etc.) and timezones (UTC by default) #698.
|
||||
- Support special numeric values (±Infinity, NaN) for json responses #706
|
||||
|
||||
|
||||
## 3.9.8
|
||||
Released 2017-07-21
|
||||
|
||||
- Upgrades windshaft to [3.2.2](https://github.com/CartoDB/windshaft/releases/tag/3.2.2).
|
||||
|
||||
|
||||
## 3.9.7
|
||||
Released 2017-07-20
|
||||
|
||||
Bug fixes:
|
||||
- Respond with 204 (No content) when vector tile has no data #712
|
||||
|
||||
Announcements:
|
||||
- Upgrades turbo-carto to [0.19.2](https://github.com/CartoDB/turbo-carto/releases/tag/0.19.2)
|
||||
|
||||
|
||||
## 3.9.6
|
||||
Released 2017-07-11
|
||||
|
||||
- Dataviews: support for aggregation in search results #708
|
||||
|
||||
|
||||
## 3.9.5
|
||||
Released 2017-06-27
|
||||
|
||||
- Dataviews: support special numeric values (±Infinity, NaN) #700
|
||||
|
||||
|
||||
## 3.9.4
|
||||
Released 2017-06-22
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.55.6](https://github.com/CartoDB/camshaft/releases/tag/0.55.6).
|
||||
|
||||
## 3.9.3
|
||||
Released 2017-06-16
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.55.5](https://github.com/CartoDB/camshaft/releases/tag/0.55.5).
|
||||
|
||||
## 3.9.2
|
||||
Released 2017-06-16
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.55.4](https://github.com/CartoDB/camshaft/releases/tag/0.55.4).
|
||||
|
||||
## 3.9.1
|
||||
Released 2017-06-06
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.55.3](https://github.com/CartoDB/camshaft/releases/tag/0.55.3).
|
||||
|
||||
|
||||
## 3.9.0
|
||||
Released 2017-05-31
|
||||
|
||||
|
||||
8
app.js
8
app.js
@@ -2,14 +2,20 @@ var http = require('http');
|
||||
var https = require('https');
|
||||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
|
||||
var _ = require('underscore');
|
||||
var semver = require('semver');
|
||||
|
||||
// jshint undef:false
|
||||
var log = console.log.bind(console);
|
||||
var logError = console.error.bind(console);
|
||||
// jshint undef:true
|
||||
|
||||
var nodejsVersion = process.versions.node;
|
||||
if (!semver.satisfies(nodejsVersion, '>=6.9.0')) {
|
||||
logError(`Node version ${nodejsVersion} is not supported, please use Node.js 6.9 or higher.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
var argv = require('yargs')
|
||||
.usage('Usage: $0 <environment> [options]')
|
||||
.help('h')
|
||||
|
||||
BIN
assets/render-timeout-fallback.mvt
Normal file
BIN
assets/render-timeout-fallback.mvt
Normal file
Binary file not shown.
@@ -107,6 +107,20 @@ var config = {
|
||||
// Milliseconds since last access before renderer cache item expires
|
||||
cache_ttl: 60000,
|
||||
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
|
||||
mvt: {
|
||||
//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
|
||||
}
|
||||
},
|
||||
mapnik: {
|
||||
// The size of the pool of internal mapnik backend
|
||||
// This pool size is per mapnik renderer created in Windshaft's RendererFactory
|
||||
@@ -324,8 +338,7 @@ 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
|
||||
layerMetadata: true
|
||||
|
||||
layerStats: true
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -101,6 +101,20 @@ var config = {
|
||||
// Milliseconds since last access before renderer cache item expires
|
||||
cache_ttl: 60000,
|
||||
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
|
||||
mvt: {
|
||||
//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
|
||||
}
|
||||
},
|
||||
mapnik: {
|
||||
// The size of the pool of internal mapnik backend
|
||||
// This pool size is per mapnik renderer created in Windshaft's RendererFactory
|
||||
@@ -324,7 +338,7 @@ 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
|
||||
layerMetadata: false
|
||||
layerStats: false
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -101,6 +101,20 @@ var config = {
|
||||
// Milliseconds since last access before renderer cache item expires
|
||||
cache_ttl: 60000,
|
||||
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
|
||||
mvt: {
|
||||
//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
|
||||
}
|
||||
},
|
||||
mapnik: {
|
||||
// The size of the pool of internal mapnik backend
|
||||
// This pool size is per mapnik renderer created in Windshaft's RendererFactory
|
||||
@@ -324,7 +338,7 @@ 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
|
||||
layerMetadata: true
|
||||
layerStats: true
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -100,6 +100,20 @@ var config = {
|
||||
// Milliseconds since last access before renderer cache item expires
|
||||
cache_ttl: 60000,
|
||||
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
|
||||
mvt: {
|
||||
//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
|
||||
}
|
||||
},
|
||||
mapnik: {
|
||||
// The size of the pool of internal mapnik backend
|
||||
// This pool size is per mapnik renderer created in Windshaft's RendererFactory
|
||||
@@ -318,7 +332,7 @@ 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
|
||||
layerMetadata: true
|
||||
layerStats: true
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
11
docker-test.sh
Normal file
11
docker-test.sh
Normal file
@@ -0,0 +1,11 @@
|
||||
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
|
||||
@@ -17,3 +17,4 @@ You can create two types of maps with the Maps API:
|
||||
* [Anonymous Maps](anonymous_maps.md)
|
||||
* [Named Maps](named_maps.md)
|
||||
* [Static Maps API](static_maps_api.md)
|
||||
* [MapConfig File Format]([local file in the docs repo](https://github.com/CartoDB/docs/blob/master/_app/_mapsapi/06-mapconfig.md))
|
||||
|
||||
@@ -8,50 +8,13 @@ This specification describes an extension for
|
||||
|
||||
This extension depends on Analyses extension. It extends MapConfig with a new attribute: `dataviews`.
|
||||
|
||||
It makes possible to get tabular data from analysis nodes: lists, aggregated lists, aggregations, and histograms.
|
||||
It makes possible to get tabular data from analysis nodes: aggregated lists, aggregations, and histograms.
|
||||
|
||||
## 2.1. Dataview types
|
||||
|
||||
### List
|
||||
|
||||
A list is a simple result set per row where is possible to retrieve several columns from the original layer query.
|
||||
|
||||
Definition
|
||||
```
|
||||
{
|
||||
// REQUIRED
|
||||
// string, `type` the list type
|
||||
“type”: “list”,
|
||||
// REQUIRED
|
||||
// object, `options` dataview params
|
||||
“options”: {
|
||||
// REQUIRED
|
||||
// array, `columns` to select for the list
|
||||
“columns”: [“name”, “description”]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Expected output
|
||||
```
|
||||
{
|
||||
"type": "list",
|
||||
"rows": [
|
||||
{
|
||||
"{columnName1}": "val1",
|
||||
"{columnName2}": 100
|
||||
},
|
||||
{
|
||||
"{columnName1}": "val2",
|
||||
"{columnName2}": 200
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Aggregation
|
||||
|
||||
An aggregation is very similar to a list but results are aggregated by a column and a given aggregation function.
|
||||
An aggregation is a list with aggregated results by a column and a given aggregation function.
|
||||
|
||||
Definition
|
||||
```
|
||||
|
||||
@@ -70,21 +70,203 @@ curl 'https://{username}.carto.com/api/v1/map' -H 'Content-Type: application/jso
|
||||
}
|
||||
```
|
||||
|
||||
### Retrieve resources from the layergroup
|
||||
## Map Tile Rendering
|
||||
|
||||
Map tiles create the graphical representation of your map in a web browser. The performance rendering of map tiles is dependent on the type of geospatial data model (raster or vector) that you are using.
|
||||
|
||||
- **Raster**: Generates map tiles based on a grid of pixels to represent your data. Each cell is a fixed size and contains values for particular map features. On the server-side, each request queries a dataset to retrieve data for each map tile. The grid size of map tiles can often lead to graphic quality issues.
|
||||
|
||||
- **Vector**: Generates map tiles based on pre-defined coordinates to represent your data, similar to how basemap image tiles are rendered. On the client-side, map tiles represent real-world geometries of a map. Depending on the coordinates, vertices are used to connect the data and display points, lines, or polygons for the map tiles.
|
||||
|
||||
## Retrieve resources from the layergroup
|
||||
|
||||
When you have a layergroup, there are several resources for retrieving layergoup details such as, accessing Mapnik tiles, getting individual layers, accessing defined Attributes, and blending and layer selection.
|
||||
|
||||
#### Mapnik tiles
|
||||
### Mapnik tiles
|
||||
|
||||
These tiles will get just the Mapnik layers. To get individual layers, see the following section.
|
||||
These raster tiles retrieve just the Mapnik layers. See [individual layers](#individual-layers) for details about how to retrieve other layers.
|
||||
|
||||
```bash
|
||||
https://{username}.carto.com/api/v1/map/{layergroupid}/{z}/{x}/{y}.png
|
||||
```
|
||||
|
||||
#### Individual layers
|
||||
### Mapbox Vector Tiles (MVT)
|
||||
|
||||
The MapConfig specification holds the layers definition in a 0-based index. Layers can be requested individually in different formats depending on the layer type.
|
||||
[Mapbox Vector Tiles (MVT)](https://www.mapbox.com/vector-tiles/specification/) are map tiles that store geographic vector data on the client-side. Browser performance is fast since you can pan and zoom without having to query the server.
|
||||
|
||||
CARTO uses a Web Graphics Library (WebGL) to process MVT files. This is useful since WebGL's are compatible with most web browsers, include support for multiple client-side mapping engines, and do not require additional information from the server; which makes it more efficient for rendering map tiles. However, you can use any implementation tool for processing MVT files.
|
||||
|
||||
The following examples describe how to fetch MVT tiles with a cURL request.
|
||||
|
||||
#### MVT and Windshaft
|
||||
|
||||
CARTO uses Windshaft as the map tiler library to render multilayer maps with the Maps API. You can use Windshaft to request MVT using the same layer type that is used for requesting raster tiles (Mapnik layer). Simply change the file format `.mvt` in the URL.
|
||||
|
||||
|
||||
```bash
|
||||
https://{username}.cartodb.com/api/v1/map/HASH/:layer/{z}/{x}/{y}.mvt
|
||||
```
|
||||
|
||||
The following example instantiates an anonymous map with layer options:
|
||||
|
||||
```bash
|
||||
{
|
||||
user_name: 'mycartodbuser',
|
||||
sublayers: [{
|
||||
sql: "SELECT * FROM table_name";
|
||||
cartocss: '#layer { marker-fill: #F0F0F0; }'
|
||||
}],
|
||||
maps_api_template: 'https://{user}.cartodb.com' // Optional
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: If no layer type is specified, Mapnik tiles are used by default. To access MVT tiles, specify `https://{username}.cartodb.com/api/v1/map/HASH/{z}/{x}/{y}.mvt` as the `maps_api_template` variable.
|
||||
|
||||
**Tip:** If you are using [Named Maps](https://carto.com/docs/carto-engine/maps-api/named-maps/) to instantiate a layer, indicate the MVT file format and layer in the response:
|
||||
|
||||
```bash
|
||||
https://{username}.cartodb.com/api/v1/map/named/:templateId/:layer/{z}/{x}/{y}.mvt
|
||||
```
|
||||
|
||||
For all layers in a Named Map, you must indicate Mapnik as the layer filter:
|
||||
|
||||
```bash
|
||||
https://{username}.cartodb.com/api/v1/map/named/:templateId/mapnik/{z}/{x}/{y}.mvt
|
||||
```
|
||||
|
||||
#### Layergroup Filter for MVT Tiles
|
||||
|
||||
To filter layers using Windshaft, use the following request where layers are numbered:
|
||||
|
||||
```bash
|
||||
https://{username}.cartodb.com/api/v1/map/HASH/0,1,2/{z}/{x}/{y}.mvt
|
||||
```
|
||||
|
||||
To request all layers, remove the layergroup filter parameter:
|
||||
|
||||
```bash
|
||||
https://{username}.cartodb.com/api/v1/map/HASH/{z}/{x}/{y}.mvt
|
||||
```
|
||||
|
||||
To filter a specific layer:
|
||||
|
||||
```bash
|
||||
https://{username}.cartodb.com/api/v1/map/HASH/2/{z}/{x}/{y}.mvt
|
||||
```
|
||||
|
||||
#### Example 1: MVT Tiles with Windshaft, CARTO.js, and MapboxGL
|
||||
|
||||
1) Import the required libraries:
|
||||
|
||||
```bash
|
||||
<script src='https://api.tiles.mapbox.com/mapbox-gl-js/v0.9.0/mapbox-gl.js'></script>
|
||||
<link href='https://api.tiles.mapbox.com/mapbox-gl-js/v0.9.0/mapbox-gl.css' rel='stylesheet' />
|
||||
<script src="http://libs.cartocdn.com/cartodb.js/v3/3.15/cartodb.core.js"></script>
|
||||
```
|
||||
|
||||
2) Configure Map Client:
|
||||
|
||||
```bash
|
||||
mapboxgl.accessToken = '{yourMapboxToken}';
|
||||
```
|
||||
|
||||
3) Create Map Object (Mapbox):
|
||||
|
||||
```bash
|
||||
var map = new mapboxgl.Map({
|
||||
container: 'map',
|
||||
zoom: 1,
|
||||
minZoom: 0,
|
||||
maxZoom: 18,
|
||||
center: [30, 0]
|
||||
});
|
||||
```
|
||||
|
||||
4) Define Layer Options (CARTO):
|
||||
|
||||
```bash
|
||||
var layerOptions = {
|
||||
user_name: "{username}",
|
||||
sublayers: [{
|
||||
sql: "SELECT * FROM {table_name}",
|
||||
cartocss: "...",
|
||||
}]
|
||||
};
|
||||
```
|
||||
|
||||
5) Request Tiles (from CARTO) and Set to Map Object (Mapbox):
|
||||
|
||||
**Note:** By default, [CARTO core functions](https://carto.com/docs/carto-engine/carto-js/core-api/) retrieve URLs for fully rendered tiles. You must replace the default format (.png) with the MVT format (.mvt).
|
||||
|
||||
|
||||
```bash
|
||||
cartodb.Tiles.getTiles(layerOptions, function(result, err) {
|
||||
var tiles = result.tiles.map(function(tileUrl) {
|
||||
return tileUrl
|
||||
.replace('{s}', 'a')
|
||||
.replace(/\.png/, '.mvt');
|
||||
});
|
||||
map.setStyle(simpleStyle(tiles));
|
||||
});
|
||||
```
|
||||
|
||||
#### Example 2: MVT Libraries with Windshaft and MapboxGL
|
||||
|
||||
When you are not including CARTO.js to implement MVT tiles, you must use the `map.setStyle` parameter to specify vector map rendering.
|
||||
|
||||
1) Import the required libraries:
|
||||
|
||||
```bash
|
||||
<script src='https://api.tiles.mapbox.com/mapbox-gl-js/v0.9.0/mapbox-gl.js'></script>
|
||||
<link href='https://api.tiles.mapbox.com/mapbox-gl-js/v0.9.0/mapbox-gl.css' rel='stylesheet'/>
|
||||
```
|
||||
|
||||
2) Configure Map Client:
|
||||
|
||||
```bash
|
||||
mapboxgl.accessToken = '{yourMapboxToken}';
|
||||
```
|
||||
|
||||
3) Create Map Object (Mapbox):
|
||||
|
||||
```bash
|
||||
var map = new mapboxgl.Map({
|
||||
container: 'map',
|
||||
zoom: 1,
|
||||
minZoom: 0,
|
||||
maxZoom: 18,
|
||||
center: [30, 0]
|
||||
});
|
||||
```
|
||||
|
||||
4) Set the Style
|
||||
|
||||
```bash
|
||||
map.setStyle({
|
||||
"version": 7,
|
||||
"glyphs": "...",
|
||||
"constants": {...},
|
||||
"sources": {
|
||||
"cartodb": {
|
||||
"type": "vector",
|
||||
"tiles": [ "http://{username}.cartodb.com/api/v1/map/named/templateId/mapnik/{z}/{x}/{y}.mvt"
|
||||
],
|
||||
"maxzoom": 18
|
||||
}
|
||||
},
|
||||
"layers": [{...}]
|
||||
});
|
||||
```
|
||||
|
||||
**Tip:** If you are using MapboxGL, see the following resource for additional information.
|
||||
|
||||
- [MapboxGL API Reference](https://www.mapbox.com/mapbox-gl-js/api/)
|
||||
- [MapboxGL Style Specifications](https://www.mapbox.com/mapbox-gl-js/style-spec/)
|
||||
- [Example of MapboxGL Implementation](https://www.mapbox.com/mapbox-gl-js/examples/)
|
||||
|
||||
### Individual layers
|
||||
|
||||
The MapConfig specification holds the layers definition in a 0-based index. Layers can be requested individually, in different formats, depending on the layer type.
|
||||
|
||||
Individual layers can be accessed using that 0-based index. For UTF grid tiles:
|
||||
|
||||
@@ -100,19 +282,19 @@ If the MapConfig had a Torque layer at index 1 it could be possible to request i
|
||||
https://{username}.carto.com/api/v1/map/{layergroupid}/1/{z}/{x}/{y}.torque.json
|
||||
```
|
||||
|
||||
#### Attributes defined in `attributes` section
|
||||
### Attributes defined in `attributes` section
|
||||
|
||||
```bash
|
||||
https://{username}.carto.com/api/v1/map/{layergroupid}/{layer}/attributes/{feature_id}
|
||||
```
|
||||
|
||||
Which returns JSON with the attributes defined, like:
|
||||
Which returns JSON with the attributes defined, such as:
|
||||
|
||||
```javascript
|
||||
{ "c": 1, "d": 2 }
|
||||
```
|
||||
|
||||
#### Blending and layer selection
|
||||
### Blending and layer selection
|
||||
|
||||
```bash
|
||||
https://{username}.carto.com/api/v1/map/{layergroupid}/{layer_filter}/{z}/{x}/{y}.png
|
||||
@@ -141,10 +323,7 @@ https://{username}.carto.com/api/v1/map/{layergroupid}/0,3,4/{z}/{x}/{y}.png
|
||||
Some notes about filtering:
|
||||
|
||||
- Invalid index values or out of bounds indexes will end in `Invalid layer filtering` errors.
|
||||
- Ordering is not considered. So right now filtering layers 0,3,4 is the very same thing as filtering 3,4,0. As this
|
||||
may change in the future **it is recommended** to always select the layers in ascending order so you will get a
|
||||
consistent behavior in the future.
|
||||
|
||||
- Ordering is not considered. So right now filtering layers 0,3,4 is the very same thing as filtering 3,4,0. As this may change in the future, **it is recommended** to always select the layers in ascending order so that you will always get consistent behavior.
|
||||
|
||||
## Create JSONP
|
||||
|
||||
@@ -185,7 +364,6 @@ callback({
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
## Remove
|
||||
|
||||
Anonymous Maps cannot be removed by an API call. They will expire after about five minutes, or sometimes longer. If an Anonymous Map expires and tiles are requested from it, an error will be raised. This could happen if a user leaves a map open and after time, returns to the map and attempts to interact with it in a way that requires new tiles (e.g. zoom). The client will need to go through the steps of creating the map again to fix the problem.
|
||||
|
||||
@@ -22,6 +22,6 @@ Errors are reported using standard HTTP codes and extended information encoded i
|
||||
|
||||
If you use JSONP, the 200 HTTP code is always returned so the JavaScript client can receive errors from the JSON object.
|
||||
|
||||
## CORS support
|
||||
## CORS Support
|
||||
|
||||
All the endpoints, which might be accessed using a web browser, add CORS headers and allow OPTIONS method.
|
||||
|
||||
@@ -152,7 +152,8 @@ It is important to note that generated images are cached from the live data refe
|
||||
* Timeout limits for generating static maps are the same across CARTO Builder and CARTO Engine. It is important to ensure timely processing of queries.
|
||||
* If you are publishing your map as a static image with the API, you must manually add [attributions](https://carto.com/attribution) for your static map image. For example, add the following attribution code:
|
||||
|
||||
{% highlight javascript %}attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, © <a href="https://carto.com/attributions">CARTO</a>
|
||||
{% highlight javascript %}
|
||||
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors, © <a href="https://carto.com/attributions">CARTO</a>
|
||||
{% endhighlight %}
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -19,22 +19,22 @@ function AuthApi(pgConnection, metadataBackend, mapStore, templateMaps) {
|
||||
|
||||
module.exports = AuthApi;
|
||||
|
||||
// Check if a request is authorized by a signer
|
||||
// Check if the user is authorized by a signer
|
||||
//
|
||||
// @param req express request object
|
||||
// @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(req, callback) {
|
||||
if ( ! req.params.token || ! req.params.signer ) {
|
||||
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 = req.params.token;
|
||||
var auth_token = req.params.auth_token;
|
||||
var layergroup_id = res.locals.token;
|
||||
var auth_token = res.locals.auth_token;
|
||||
|
||||
this.mapStore.load(layergroup_id, function(err, mapConfig) {
|
||||
if (err) {
|
||||
@@ -84,11 +84,12 @@ AuthApi.prototype.authorizedByAPIKey = function(user, req, callback) {
|
||||
* 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, callback) {
|
||||
AuthApi.prototype.authorize = function(req, res, callback) {
|
||||
var self = this;
|
||||
var user = req.context.user;
|
||||
var user = res.locals.user;
|
||||
|
||||
step(
|
||||
function () {
|
||||
@@ -101,11 +102,11 @@ AuthApi.prototype.authorize = function(req, callback) {
|
||||
// if not authorized by api_key, continue
|
||||
if (!authorized) {
|
||||
// not authorized by api_key, check if authorized by signer
|
||||
return self.authorizedBySigner(req, this);
|
||||
return self.authorizedBySigner(res, this);
|
||||
}
|
||||
|
||||
// authorized by api key, login as the given username and stop
|
||||
self.pgConnection.setDBAuth(user, req.params, function(err) {
|
||||
self.pgConnection.setDBAuth(user, res.locals, function(err) {
|
||||
callback(err, true); // authorized (or error)
|
||||
});
|
||||
},
|
||||
@@ -120,7 +121,7 @@ AuthApi.prototype.authorize = function(req, callback) {
|
||||
// if no signer name was given, let dbparams and
|
||||
// PostgreSQL do the rest.
|
||||
//
|
||||
if ( ! req.params.signer ) {
|
||||
if ( ! res.locals.signer ) {
|
||||
return callback(null, true); // authorized so far
|
||||
}
|
||||
|
||||
@@ -128,7 +129,7 @@ AuthApi.prototype.authorize = function(req, callback) {
|
||||
return callback(null, false);
|
||||
}
|
||||
|
||||
self.pgConnection.setDBAuth(user, req.params, function(err) {
|
||||
self.pgConnection.setDBAuth(user, res.locals, function(err) {
|
||||
req.profiler.done('setDBAuth');
|
||||
callback(err, true); // authorized (or error)
|
||||
});
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
var step = require('step');
|
||||
|
||||
/**
|
||||
*
|
||||
* @param metadataBackend
|
||||
@@ -13,16 +15,65 @@ function UserLimitsApi(metadataBackend, options) {
|
||||
|
||||
module.exports = UserLimitsApi;
|
||||
|
||||
UserLimitsApi.prototype.getRenderLimits = function (username, callback) {
|
||||
UserLimitsApi.prototype.getRenderLimits = function (username, apiKey, callback) {
|
||||
var self = this;
|
||||
this.metadataBackend.getTilerRenderLimit(username, function handleTilerLimits(err, renderLimit) {
|
||||
|
||||
var limits = {
|
||||
cacheOnTimeout: self.options.limits.cacheOnTimeout || false,
|
||||
render: self.options.limits.render || 0
|
||||
};
|
||||
|
||||
self.getTimeoutRenderLimit(username, apiKey, function (err, timeoutRenderLimit) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
return callback(null, {
|
||||
cacheOnTimeout: self.options.limits.cacheOnTimeout || false,
|
||||
render: renderLimit || self.options.limits.render || 0
|
||||
});
|
||||
if (timeoutRenderLimit && timeoutRenderLimit.render) {
|
||||
if (Number.isFinite(timeoutRenderLimit.render)) {
|
||||
limits.render = timeoutRenderLimit.render;
|
||||
}
|
||||
}
|
||||
|
||||
return callback(null, limits);
|
||||
});
|
||||
};
|
||||
|
||||
UserLimitsApi.prototype.getTimeoutRenderLimit = function (username, apiKey, callback) {
|
||||
var self = this;
|
||||
|
||||
step(
|
||||
function isAuthorized() {
|
||||
var next = this;
|
||||
|
||||
if (!apiKey) {
|
||||
return next(null, false);
|
||||
}
|
||||
|
||||
self.metadataBackend.getUserMapKey(username, function (err, userApiKey) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
return next(null, userApiKey === apiKey);
|
||||
});
|
||||
},
|
||||
function getUserTimeoutRenderLimits(err, authorized) {
|
||||
var next = this;
|
||||
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
self.metadataBackend.getUserTimeoutRenderLimits(username, function (err, timeoutRenderLimit) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
next(null, {
|
||||
render: authorized ? timeoutRenderLimit.render : timeoutRenderLimit.renderPublic
|
||||
});
|
||||
});
|
||||
},
|
||||
callback
|
||||
);
|
||||
};
|
||||
|
||||
@@ -43,53 +43,19 @@ DataviewBackend.prototype.getDataview = function (mapConfigProvider, user, param
|
||||
ownFilter = !!ownFilter;
|
||||
|
||||
var query = (ownFilter) ? dataviewDefinition.sql.own_filter_on : dataviewDefinition.sql.own_filter_off;
|
||||
var sourceId = dataviewDefinition.source.id; // node.id
|
||||
var layer = _.find(mapConfig.obj().layers, function(l) {
|
||||
return l.options.source && (l.options.source.id === sourceId);
|
||||
});
|
||||
var queryRewriteData = layer && layer.options.query_rewrite_data;
|
||||
if (queryRewriteData && dataviewDefinition.node.type === 'source') {
|
||||
queryRewriteData = _.extend({}, queryRewriteData, {
|
||||
filters: dataviewDefinition.node.filters,
|
||||
unfiltered_query: dataviewDefinition.sql.own_filter_on
|
||||
});
|
||||
}
|
||||
|
||||
if (params.bbox) {
|
||||
var bboxFilter = new BBoxFilter({column: 'the_geom_webmercator', srid: 3857}, {bbox: params.bbox});
|
||||
query = bboxFilter.sql(query);
|
||||
if ( queryRewriteData ) {
|
||||
var bbox_filter_definition = {
|
||||
type: 'bbox',
|
||||
options: {
|
||||
column: 'the_geom_webmercator',
|
||||
srid: 3857
|
||||
},
|
||||
params: {
|
||||
bbox: params.bbox
|
||||
}
|
||||
};
|
||||
queryRewriteData = _.extend(queryRewriteData, { bbox_filter: bbox_filter_definition });
|
||||
}
|
||||
}
|
||||
|
||||
var queryRewriteData = getQueryRewriteData(mapConfig, dataviewDefinition, params);
|
||||
|
||||
var dataviewFactory = DataviewFactoryWithOverviews.getFactory(
|
||||
overviewsQueryRewriter, queryRewriteData, { bbox: params.bbox }
|
||||
);
|
||||
|
||||
var overrideParams = _.reduce(_.pick(params, 'start', 'end', 'bins'),
|
||||
function castNumbers(overrides, val, k) {
|
||||
if (!Number.isFinite(+val)) {
|
||||
throw new Error('Invalid number format for parameter \'' + k + '\'');
|
||||
}
|
||||
overrides[k] = +val;
|
||||
return overrides;
|
||||
},
|
||||
{ownFilter: ownFilter}
|
||||
);
|
||||
|
||||
var dataview = dataviewFactory.getDataview(query, dataviewDefinition);
|
||||
dataview.getResult(pg, overrideParams, this);
|
||||
dataview.getResult(pg, getOverrideParams(params, ownFilter), this);
|
||||
},
|
||||
function returnCallback(err, result) {
|
||||
return callback(err, result);
|
||||
@@ -97,9 +63,57 @@ DataviewBackend.prototype.getDataview = function (mapConfigProvider, user, param
|
||||
);
|
||||
};
|
||||
|
||||
DataviewBackend.prototype.search = function (mapConfigProvider, user, params, callback) {
|
||||
var dataviewName = params.dataviewName;
|
||||
function getQueryRewriteData(mapConfig, dataviewDefinition, params) {
|
||||
var sourceId = dataviewDefinition.source.id; // node.id
|
||||
var layer = _.find(mapConfig.obj().layers, function(l) {
|
||||
return l.options.source && (l.options.source.id === sourceId);
|
||||
});
|
||||
var queryRewriteData = layer && layer.options.query_rewrite_data;
|
||||
if (queryRewriteData && dataviewDefinition.node.type === 'source') {
|
||||
queryRewriteData = _.extend({}, queryRewriteData, {
|
||||
filters: dataviewDefinition.node.filters,
|
||||
unfiltered_query: dataviewDefinition.sql.own_filter_on
|
||||
});
|
||||
}
|
||||
|
||||
if (params.bbox && queryRewriteData) {
|
||||
var bbox_filter_definition = {
|
||||
type: 'bbox',
|
||||
options: {
|
||||
column: 'the_geom_webmercator',
|
||||
srid: 3857
|
||||
},
|
||||
params: {
|
||||
bbox: params.bbox
|
||||
}
|
||||
};
|
||||
queryRewriteData = _.extend(queryRewriteData, { bbox_filter: bbox_filter_definition });
|
||||
}
|
||||
|
||||
return queryRewriteData;
|
||||
}
|
||||
|
||||
function getOverrideParams(params, ownFilter) {
|
||||
var overrideParams = _.reduce(_.pick(params, 'start', 'end', 'bins', 'offset', 'categories'),
|
||||
function castNumbers(overrides, val, k) {
|
||||
if (!Number.isFinite(+val)) {
|
||||
throw new Error('Invalid number format for parameter \'' + k + '\'');
|
||||
}
|
||||
overrides[k] = +val;
|
||||
return overrides;
|
||||
},
|
||||
{ownFilter: ownFilter}
|
||||
);
|
||||
|
||||
// validation will be delegated to the proper dataview
|
||||
if (params.aggregation !== undefined) {
|
||||
overrideParams.aggregation = params.aggregation;
|
||||
}
|
||||
|
||||
return overrideParams;
|
||||
}
|
||||
|
||||
DataviewBackend.prototype.search = function (mapConfigProvider, user, dataviewName, params, callback) {
|
||||
step(
|
||||
function getMapConfig() {
|
||||
mapConfigProvider.getMapConfig(this);
|
||||
|
||||
@@ -10,15 +10,21 @@ function createTemplate(method) {
|
||||
'max({{=it._column}}) max_val,',
|
||||
'avg({{=it._column}}) avg_val,',
|
||||
method,
|
||||
'FROM ({{=it._sql}}) _table_sql WHERE {{=it._column}} IS NOT NULL'
|
||||
'FROM ({{=it._sql}}) _table_sql WHERE {{=it._column}} IS NOT NULL',
|
||||
'AND',
|
||||
' {{=it._column}} != \'infinity\'::float',
|
||||
'AND',
|
||||
' {{=it._column}} != \'-infinity\'::float',
|
||||
'AND',
|
||||
' {{=it._column}} != \'NaN\'::float'
|
||||
].join('\n'));
|
||||
}
|
||||
|
||||
var methods = {
|
||||
quantiles: 'CDB_QuantileBins(array_agg(distinct({{=it._column}}::numeric)), {{=it._buckets}}) as quantiles',
|
||||
quantiles: 'CDB_QuantileBins(array_agg({{=it._column}}::numeric), {{=it._buckets}}) as quantiles',
|
||||
equal: 'CDB_EqualIntervalBins(array_agg({{=it._column}}::numeric), {{=it._buckets}}) as equal',
|
||||
jenks: 'CDB_JenksBins(array_agg(distinct({{=it._column}}::numeric)), {{=it._buckets}}) as jenks',
|
||||
headtails: 'CDB_HeadsTailsBins(array_agg(distinct({{=it._column}}::numeric)), {{=it._buckets}}) as headtails'
|
||||
jenks: 'CDB_JenksBins(array_agg({{=it._column}}::numeric), {{=it._buckets}}) as jenks',
|
||||
headtails: 'CDB_HeadsTailsBins(array_agg({{=it._column}}::numeric), {{=it._buckets}}) as headtails'
|
||||
};
|
||||
|
||||
var methodTemplates = Object.keys(methods).reduce(function(methodTemplates, methodName) {
|
||||
|
||||
@@ -1,152 +1,150 @@
|
||||
var step = require('step');
|
||||
var assert = require('assert');
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
var PSQL = require('cartodb-psql');
|
||||
|
||||
var util = require('util');
|
||||
var BaseController = require('./base');
|
||||
|
||||
var cors = require('../middleware/cors');
|
||||
var userMiddleware = require('../middleware/user');
|
||||
|
||||
function AnalysesController(authApi, pgConnection) {
|
||||
BaseController.call(this, authApi, pgConnection);
|
||||
function AnalysesController(prepareContext) {
|
||||
this.prepareContext = prepareContext;
|
||||
}
|
||||
|
||||
util.inherits(AnalysesController, BaseController);
|
||||
|
||||
module.exports = AnalysesController;
|
||||
|
||||
AnalysesController.prototype.register = function(app) {
|
||||
app.get(app.base_url_mapconfig + '/analyses/catalog', cors(), userMiddleware, this.catalog.bind(this));
|
||||
};
|
||||
|
||||
AnalysesController.prototype.sendResponse = function(req, res, resource) {
|
||||
res.set('Cache-Control', 'public,max-age=10,must-revalidate');
|
||||
this.send(req, res, resource, 200);
|
||||
};
|
||||
|
||||
AnalysesController.prototype.catalog = function(req, res) {
|
||||
var self = this;
|
||||
var username = req.context.user;
|
||||
|
||||
step(
|
||||
function reqParams() {
|
||||
self.req2params(req, this);
|
||||
},
|
||||
function catalogQuery(err) {
|
||||
assert.ifError(err);
|
||||
var pg = new PSQL(dbParamsFromReqParams(req.params));
|
||||
getMetadata(username, pg, this);
|
||||
},
|
||||
function prepareResponse(err, results) {
|
||||
assert.ifError(err);
|
||||
|
||||
var analysisIdToTable = results.tables.reduce(function(analysisIdToTable, table) {
|
||||
var analysisId = table.relname.split('_')[2];
|
||||
if (analysisId && analysisId.length === 40) {
|
||||
analysisIdToTable[analysisId] = table;
|
||||
}
|
||||
return analysisIdToTable;
|
||||
}, {});
|
||||
|
||||
var catalogWithTables = results.catalog.map(function(analysis) {
|
||||
if (analysisIdToTable.hasOwnProperty(analysis.node_id)) {
|
||||
analysis.table = analysisIdToTable[analysis.node_id];
|
||||
}
|
||||
return analysis;
|
||||
});
|
||||
|
||||
return catalogWithTables.sort(function(analysisA, analysisB) {
|
||||
if (!!analysisA.table && !!analysisB.table) {
|
||||
return analysisB.table.size - analysisA.table.size;
|
||||
}
|
||||
|
||||
if (!!analysisA.table) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!!analysisB.table) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return -1;
|
||||
});
|
||||
},
|
||||
function sendResponse(err, catalogWithTables) {
|
||||
if (err) {
|
||||
if (err.message.match(/permission\sdenied/)) {
|
||||
err = new Error('Unauthorized');
|
||||
err.http_status = 401;
|
||||
}
|
||||
self.sendError(req, res, err);
|
||||
} else {
|
||||
self.sendResponse(req, res, { catalog: catalogWithTables });
|
||||
}
|
||||
}
|
||||
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()
|
||||
);
|
||||
};
|
||||
|
||||
var catalogQueryTpl = dot.template(
|
||||
'SELECT analysis_def->>\'type\' as type, * FROM cartodb.cdb_analysis_catalog WHERE username = \'{{=it._username}}\''
|
||||
);
|
||||
|
||||
var tablesQueryTpl = dot.template([
|
||||
"WITH analysis_tables AS (",
|
||||
" SELECT",
|
||||
" n.nspname AS nspname,",
|
||||
" c.relname AS relname,",
|
||||
" pg_total_relation_size(",
|
||||
" format('%s.%s', pg_catalog.quote_ident(n.nspname), pg_catalog.quote_ident(c.relname))",
|
||||
" ) AS size,",
|
||||
" format('%s.%s', pg_catalog.quote_ident(nspname), pg_catalog.quote_ident(relname)) AS fully_qualified_name",
|
||||
" FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n",
|
||||
" WHERE c.relnamespace = n.oid",
|
||||
" AND pg_catalog.quote_ident(c.relname) ~ '^analysis_[a-z0-9]{10}_[a-z0-9]{40}$'",
|
||||
" AND n.nspname IN ('{{=it._username}}', 'public')",
|
||||
")",
|
||||
"SELECT *, pg_size_pretty(size) as size_pretty",
|
||||
"FROM analysis_tables",
|
||||
"ORDER BY size DESC"
|
||||
].join('\n'));
|
||||
|
||||
|
||||
function getMetadata(username, pg, callback) {
|
||||
var results = {
|
||||
catalog: [],
|
||||
tables: []
|
||||
AnalysesController.prototype.createPGClient = function () {
|
||||
return function createPGClientMiddleware (req, res, next) {
|
||||
res.locals.pg = new PSQL(dbParamsFromReqParams(res.locals));
|
||||
next();
|
||||
};
|
||||
step(
|
||||
function getCatalog() {
|
||||
pg.query(catalogQueryTpl({_username: username}), this, true); // use read-only transaction
|
||||
},
|
||||
function handleCatalog(err, resultSet) {
|
||||
assert.ifError(err);
|
||||
resultSet = resultSet || {};
|
||||
results.catalog = resultSet.rows || [];
|
||||
this();
|
||||
},
|
||||
function getTables(err) {
|
||||
assert.ifError(err);
|
||||
pg.query(tablesQueryTpl({_username: username}), this, true); // use read-only transaction
|
||||
},
|
||||
function handleTables(err, resultSet) {
|
||||
assert.ifError(err);
|
||||
resultSet = resultSet || {};
|
||||
results.tables = resultSet.rows || [];
|
||||
this();
|
||||
},
|
||||
function finish(err) {
|
||||
};
|
||||
|
||||
AnalysesController.prototype.getDataFromQuery = function ({ queryTemplate, key }) {
|
||||
const readOnlyTransactionOn = true;
|
||||
|
||||
return function getCatalogMiddleware(req, res, next) {
|
||||
const { pg, user } = res.locals;
|
||||
const sql = queryTemplate({ _username: user });
|
||||
|
||||
pg.query(sql, (err, resultSet = {}) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
return next(err);
|
||||
}
|
||||
|
||||
return callback(null, results);
|
||||
}
|
||||
);
|
||||
}
|
||||
res.locals[key] = resultSet.rows || [];
|
||||
|
||||
next();
|
||||
}, readOnlyTransactionOn);
|
||||
};
|
||||
};
|
||||
|
||||
AnalysesController.prototype.prepareResponse = function () {
|
||||
return function prepareResponseMiddleware (req, res, next) {
|
||||
const { catalog, tables } = res.locals;
|
||||
|
||||
const analysisIdToTable = tables.reduce((analysisIdToTable, table) => {
|
||||
const analysisId = table.relname.split('_')[2];
|
||||
|
||||
if (analysisId && analysisId.length === 40) {
|
||||
analysisIdToTable[analysisId] = table;
|
||||
}
|
||||
|
||||
return analysisIdToTable;
|
||||
}, {});
|
||||
|
||||
const analysisCatalog = catalog.map(analysis => {
|
||||
if (analysisIdToTable.hasOwnProperty(analysis.node_id)) {
|
||||
analysis.table = analysisIdToTable[analysis.node_id];
|
||||
}
|
||||
|
||||
return analysis;
|
||||
})
|
||||
.sort((analysisA, analysisB) => {
|
||||
if (!!analysisA.table && !!analysisB.table) {
|
||||
return analysisB.table.size - analysisA.table.size;
|
||||
}
|
||||
|
||||
if (!!analysisA.table) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!!analysisB.table) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return -1;
|
||||
});
|
||||
|
||||
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 () {
|
||||
return function unathorizedErrorMiddleware(err, req, res, next) {
|
||||
if (err.message.match(/permission\sdenied/)) {
|
||||
err = new Error('Unauthorized');
|
||||
err.http_status = 401;
|
||||
}
|
||||
|
||||
next(err);
|
||||
};
|
||||
};
|
||||
|
||||
const catalogQueryTpl = ctx => `
|
||||
SELECT analysis_def->>'type' as type, * FROM cdb_analysis_catalog WHERE username = '${ctx._username}'
|
||||
`;
|
||||
|
||||
var tablesQueryTpl = ctx => `
|
||||
WITH analysis_tables AS (
|
||||
SELECT
|
||||
n.nspname AS nspname,
|
||||
c.relname AS relname,
|
||||
pg_total_relation_size(
|
||||
format('%s.%s', pg_catalog.quote_ident(n.nspname), pg_catalog.quote_ident(c.relname))
|
||||
) AS size,
|
||||
format('%s.%s', pg_catalog.quote_ident(nspname), pg_catalog.quote_ident(relname)) AS fully_qualified_name
|
||||
FROM pg_catalog.pg_class c, pg_catalog.pg_namespace n
|
||||
WHERE c.relnamespace = n.oid
|
||||
AND pg_catalog.quote_ident(c.relname) ~ '^analysis_[a-z0-9]{10}_[a-z0-9]{40}$'
|
||||
AND n.nspname IN ('${ctx._username}', 'public')
|
||||
)
|
||||
SELECT *, pg_size_pretty(size) as size_pretty
|
||||
FROM analysis_tables
|
||||
ORDER BY size DESC
|
||||
`;
|
||||
|
||||
function dbParamsFromReqParams(params) {
|
||||
var dbParams = {};
|
||||
|
||||
@@ -1,289 +0,0 @@
|
||||
var assert = require('assert');
|
||||
|
||||
var _ = require('underscore');
|
||||
var step = require('step');
|
||||
var debug = require('debug')('windshaft:cartodb');
|
||||
|
||||
var LZMA = require('lzma').LZMA;
|
||||
var lzmaWorker = new LZMA();
|
||||
|
||||
// Whitelist query parameters and attach format
|
||||
var REQUEST_QUERY_PARAMS_WHITELIST = [
|
||||
'config',
|
||||
'map_key',
|
||||
'api_key',
|
||||
'auth_token',
|
||||
'callback',
|
||||
'zoom',
|
||||
'lon',
|
||||
'lat',
|
||||
// widgets & filters
|
||||
'filters', // json
|
||||
'own_filter', // 0, 1
|
||||
'bbox', // w,s,e,n
|
||||
'bins', // number
|
||||
'start', // number
|
||||
'end', // number
|
||||
'column_type', // string
|
||||
// widgets search
|
||||
'q'
|
||||
];
|
||||
|
||||
function BaseController(authApi, pgConnection) {
|
||||
this.authApi = authApi;
|
||||
this.pgConnection = pgConnection;
|
||||
}
|
||||
|
||||
module.exports = BaseController;
|
||||
|
||||
// jshint maxcomplexity:10
|
||||
/**
|
||||
* Whitelist input and get database name & default geometry type from
|
||||
* subdomain/user metadata held in CartoDB Redis
|
||||
* @param req - standard express request obj. Should have host & table
|
||||
* @param callback
|
||||
*/
|
||||
BaseController.prototype.req2params = function(req, callback){
|
||||
var self = this;
|
||||
|
||||
if ( req.query.lzma ) {
|
||||
|
||||
// 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) {
|
||||
req.profiler.done('lzma');
|
||||
try {
|
||||
delete req.query.lzma;
|
||||
_.extend(req.query, JSON.parse(result));
|
||||
self.req2params(req, callback);
|
||||
} catch (err) {
|
||||
req.profiler.done('req2params');
|
||||
callback(new Error('Error parsing lzma as JSON: ' + err));
|
||||
}
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
var allowedQueryParams = REQUEST_QUERY_PARAMS_WHITELIST;
|
||||
if (Array.isArray(req.context.allowedQueryParams)) {
|
||||
allowedQueryParams = allowedQueryParams.concat(req.context.allowedQueryParams);
|
||||
}
|
||||
req.query = _.pick(req.query, allowedQueryParams);
|
||||
req.params = _.extend({}, req.params); // shuffle things as request is a strange array/object
|
||||
|
||||
var user = req.context.user;
|
||||
|
||||
if ( req.params.token ) {
|
||||
// Token might match the following patterns:
|
||||
// - {user}@{tpl_id}@{token}:{cache_buster}
|
||||
var tksplit = req.params.token.split(':');
|
||||
req.params.token = tksplit[0];
|
||||
if ( tksplit.length > 1 ) {
|
||||
req.params.cache_buster= tksplit[1];
|
||||
}
|
||||
tksplit = req.params.token.split('@');
|
||||
if ( tksplit.length > 1 ) {
|
||||
req.params.signer = tksplit.shift();
|
||||
if ( ! req.params.signer ) {
|
||||
req.params.signer = user;
|
||||
}
|
||||
else if ( req.params.signer !== user ) {
|
||||
var err = new Error(
|
||||
'Cannot use map signature of user "' + req.params.signer + '" on db of user "' + user + '"'
|
||||
);
|
||||
err.http_status = 403;
|
||||
req.profiler.done('req2params');
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
if ( tksplit.length > 1 ) {
|
||||
/*var template_hash = */tksplit.shift(); // unused
|
||||
}
|
||||
req.params.token = tksplit.shift();
|
||||
}
|
||||
}
|
||||
|
||||
// bring all query values onto req.params object
|
||||
_.extend(req.params, req.query);
|
||||
|
||||
req.profiler.done('req2params.setup');
|
||||
|
||||
step(
|
||||
function getPrivacy(){
|
||||
self.authApi.authorize(req, this);
|
||||
},
|
||||
function validateAuthorization(err, authorized) {
|
||||
req.profiler.done('authorize');
|
||||
assert.ifError(err);
|
||||
if(!authorized) {
|
||||
err = new Error("Sorry, you are unauthorized (permission denied)");
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
function getDatabase(err){
|
||||
assert.ifError(err);
|
||||
self.pgConnection.setDBConn(user, req.params, this);
|
||||
},
|
||||
function finishSetup(err) {
|
||||
if ( err ) {
|
||||
req.profiler.done('req2params');
|
||||
return callback(err, req);
|
||||
}
|
||||
|
||||
// Add default database connection parameters
|
||||
// if none given
|
||||
_.defaults(req.params, {
|
||||
dbuser: global.environment.postgres.user,
|
||||
dbpassword: global.environment.postgres.password,
|
||||
dbhost: global.environment.postgres.host,
|
||||
dbport: global.environment.postgres.port
|
||||
});
|
||||
|
||||
req.profiler.done('req2params');
|
||||
callback(null, req);
|
||||
}
|
||||
);
|
||||
};
|
||||
// jshint maxcomplexity:6
|
||||
|
||||
// jshint maxcomplexity:9
|
||||
BaseController.prototype.send = function(req, res, body, status, headers) {
|
||||
if (req.params.dbhost) {
|
||||
res.set('X-Served-By-DB-Host', req.params.dbhost);
|
||||
}
|
||||
|
||||
res.set('X-Tiler-Profiler', req.profiler.toJSONString());
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
try {
|
||||
// May throw due to dns, see
|
||||
// See http://github.com/CartoDB/Windshaft/issues/166
|
||||
req.profiler.sendStats();
|
||||
} catch (err) {
|
||||
debug("error sending profiling stats: " + err);
|
||||
}
|
||||
};
|
||||
// jshint maxcomplexity:6
|
||||
|
||||
BaseController.prototype.sendError = function(req, res, err, label) {
|
||||
var allErrors = Array.isArray(err) ? err : [err];
|
||||
label = label || 'UNKNOWN';
|
||||
err = allErrors[0] || new Error(label);
|
||||
allErrors[0] = err;
|
||||
|
||||
var statusCode = findStatusCode(err);
|
||||
|
||||
debug('[%s ERROR] -- %d: %s, %s', label, statusCode, err, err.stack);
|
||||
|
||||
// If a callback was requested, force status to 200
|
||||
if (req.query && req.query.callback) {
|
||||
statusCode = 200;
|
||||
}
|
||||
|
||||
var errorResponseBody = {
|
||||
errors: allErrors.map(errorMessage),
|
||||
errors_with_context: allErrors.map(errorMessageWithContext)
|
||||
};
|
||||
|
||||
this.send(req, res, errorResponseBody, statusCode);
|
||||
};
|
||||
|
||||
function stripConnectionInfo(message) {
|
||||
// Strip connection info, if any
|
||||
return message
|
||||
// See https://github.com/CartoDB/Windshaft/issues/173
|
||||
.replace(/Connection string: '[^']*'\n\s/im, '')
|
||||
// See https://travis-ci.org/CartoDB/Windshaft/jobs/20703062#L1644
|
||||
.replace(/is the server.*encountered/im, 'encountered');
|
||||
}
|
||||
|
||||
function errorMessage(err) {
|
||||
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
|
||||
var message = (_.isString(err) ? err : err.message) || 'Unknown error';
|
||||
|
||||
return stripConnectionInfo(message);
|
||||
}
|
||||
|
||||
function errorMessageWithContext(err) {
|
||||
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
|
||||
var message = (_.isString(err) ? err : err.message) || 'Unknown error';
|
||||
|
||||
var error = {
|
||||
type: err.type || 'unknown',
|
||||
message: stripConnectionInfo(message),
|
||||
};
|
||||
|
||||
for (var prop in err) {
|
||||
// type & message are properties from Error's prototype and will be skipped
|
||||
if (err.hasOwnProperty(prop)) {
|
||||
error[prop] = err[prop];
|
||||
}
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
module.exports.errorMessage = errorMessage;
|
||||
|
||||
function findStatusCode(err) {
|
||||
var statusCode;
|
||||
if ( err.http_status ) {
|
||||
statusCode = err.http_status;
|
||||
} else {
|
||||
statusCode = statusFromErrorMessage('' + err);
|
||||
}
|
||||
return statusCode;
|
||||
}
|
||||
module.exports.findStatusCode = findStatusCode;
|
||||
|
||||
function statusFromErrorMessage(errMsg) {
|
||||
// Find an appropriate statusCode based on message
|
||||
// jshint maxcomplexity:7
|
||||
var statusCode = 400;
|
||||
if ( -1 !== errMsg.indexOf('permission denied') ) {
|
||||
statusCode = 403;
|
||||
}
|
||||
else if ( -1 !== errMsg.indexOf('authentication failed') ) {
|
||||
statusCode = 403;
|
||||
}
|
||||
else if (errMsg.match(/Postgis Plugin.*[\s|\n].*column.*does not exist/)) {
|
||||
statusCode = 400;
|
||||
}
|
||||
else if ( -1 !== errMsg.indexOf('does not exist') ) {
|
||||
if ( -1 !== errMsg.indexOf(' role ') ) {
|
||||
statusCode = 403; // role 'xxx' does not exist
|
||||
} else if ( errMsg.match(/function .* does not exist/) ) {
|
||||
statusCode = 400; // invalid SQL (SQL function does not exist)
|
||||
} else {
|
||||
statusCode = 404;
|
||||
}
|
||||
}
|
||||
return statusCode;
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
var assert = require('assert');
|
||||
var step = require('step');
|
||||
|
||||
var util = require('util');
|
||||
var BaseController = require('./base');
|
||||
|
||||
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');
|
||||
@@ -28,10 +26,8 @@ var QueryTables = require('cartodb-query-tables');
|
||||
* @param {AnalysisBackend} analysisBackend
|
||||
* @constructor
|
||||
*/
|
||||
function LayergroupController(authApi, pgConnection, mapStore, tileBackend, previewBackend, attributesBackend,
|
||||
function LayergroupController(prepareContext, pgConnection, mapStore, tileBackend, previewBackend, attributesBackend,
|
||||
surrogateKeysCache, userLimitsApi, layergroupAffectedTables, analysisBackend) {
|
||||
BaseController.call(this, authApi, pgConnection);
|
||||
|
||||
this.pgConnection = pgConnection;
|
||||
this.mapStore = mapStore;
|
||||
this.tileBackend = tileBackend;
|
||||
@@ -43,77 +39,142 @@ function LayergroupController(authApi, pgConnection, mapStore, tileBackend, prev
|
||||
|
||||
this.dataviewBackend = new DataviewBackend(analysisBackend);
|
||||
this.analysisStatusBackend = new AnalysisStatusBackend();
|
||||
}
|
||||
|
||||
util.inherits(LayergroupController, BaseController);
|
||||
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.tile.bind(this));
|
||||
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.tile.bind(this));
|
||||
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,
|
||||
this.layer.bind(this));
|
||||
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.attributes.bind(this));
|
||||
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.center.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.bbox.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.
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/:token/dataview/:dataviewName', cors(), userMiddleware,
|
||||
this.dataview.bind(this));
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/:token/:layer/widget/:dataviewName', cors(), userMiddleware,
|
||||
this.dataview.bind(this));
|
||||
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/:token/dataview/:dataviewName/search', cors(), userMiddleware,
|
||||
this.dataviewSearch.bind(this));
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/:token/:layer/widget/:dataviewName/search', cors(), userMiddleware,
|
||||
this.dataviewSearch.bind(this));
|
||||
var allowedDataviewQueryParams = [
|
||||
'filters', // json
|
||||
'own_filter', // 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/analysis/node/:nodeId', cors(), userMiddleware,
|
||||
this.analysisNodeStatus.bind(this));
|
||||
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) {
|
||||
LayergroupController.prototype.analysisNodeStatus = function(req, res, next) {
|
||||
var self = this;
|
||||
|
||||
step(
|
||||
function setupParams() {
|
||||
self.req2params(req, this);
|
||||
},
|
||||
function retrieveNodeStatus(err) {
|
||||
assert.ifError(err);
|
||||
self.analysisStatusBackend.getNodeStatus(req.params, this);
|
||||
function retrieveNodeStatus() {
|
||||
self.analysisStatusBackend.getNodeStatus(res.locals, this);
|
||||
},
|
||||
function finish(err, nodeStatus, stats) {
|
||||
req.profiler.add(stats || {});
|
||||
|
||||
if (err) {
|
||||
self.sendError(req, res, err, 'GET NODE STATUS');
|
||||
err.label = 'GET NODE STATUS';
|
||||
next(err);
|
||||
} else {
|
||||
self.sendResponse(req, res, nodeStatus, 200, {
|
||||
'Cache-Control': 'public,max-age=5',
|
||||
@@ -124,54 +185,50 @@ LayergroupController.prototype.analysisNodeStatus = function(req, res) {
|
||||
);
|
||||
};
|
||||
|
||||
LayergroupController.prototype.dataview = function(req, res) {
|
||||
LayergroupController.prototype.dataview = function(req, res, next) {
|
||||
var self = this;
|
||||
|
||||
step(
|
||||
function setupParams() {
|
||||
self.req2params(req, this);
|
||||
},
|
||||
function retrieveDataview(err) {
|
||||
assert.ifError(err);
|
||||
|
||||
function retrieveDataview() {
|
||||
var mapConfigProvider = new MapStoreMapConfigProvider(
|
||||
self.mapStore, req.context.user, self.userLimitsApi, req.params
|
||||
self.mapStore, res.locals.user, self.userLimitsApi, res.locals
|
||||
);
|
||||
self.dataviewBackend.getDataview(
|
||||
mapConfigProvider,
|
||||
res.locals.user,
|
||||
res.locals,
|
||||
this
|
||||
);
|
||||
self.dataviewBackend.getDataview(mapConfigProvider, req.context.user, req.params, this);
|
||||
},
|
||||
function finish(err, dataview, stats) {
|
||||
req.profiler.add(stats || {});
|
||||
|
||||
if (err) {
|
||||
self.sendError(req, res, err, 'GET DATAVIEW');
|
||||
err.label = 'GET DATAVIEW';
|
||||
next(err);
|
||||
} else {
|
||||
self.sendResponse(req, res, dataview, 200);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
LayergroupController.prototype.dataviewSearch = function(req, res) {
|
||||
LayergroupController.prototype.dataviewSearch = function(req, res, next) {
|
||||
var self = this;
|
||||
|
||||
step(
|
||||
function setupParams() {
|
||||
self.req2params(req, this);
|
||||
},
|
||||
function searchDataview(err) {
|
||||
assert.ifError(err);
|
||||
|
||||
function searchDataview() {
|
||||
var mapConfigProvider = new MapStoreMapConfigProvider(
|
||||
self.mapStore, req.context.user, self.userLimitsApi, req.params
|
||||
self.mapStore, res.locals.user, self.userLimitsApi, res.locals
|
||||
);
|
||||
self.dataviewBackend.search(mapConfigProvider, req.context.user, req.params, this);
|
||||
self.dataviewBackend.search(mapConfigProvider, res.locals.user, req.params.dataviewName, res.locals, this);
|
||||
},
|
||||
function finish(err, searchResult, stats) {
|
||||
req.profiler.add(stats || {});
|
||||
|
||||
if (err) {
|
||||
self.sendError(req, res, err, 'GET DATAVIEW SEARCH');
|
||||
err.label = 'GET DATAVIEW SEARCH';
|
||||
next(err);
|
||||
} else {
|
||||
self.sendResponse(req, res, searchResult, 200);
|
||||
}
|
||||
@@ -180,28 +237,24 @@ LayergroupController.prototype.dataviewSearch = function(req, res) {
|
||||
|
||||
};
|
||||
|
||||
LayergroupController.prototype.attributes = function(req, res) {
|
||||
LayergroupController.prototype.attributes = function(req, res, next) {
|
||||
var self = this;
|
||||
|
||||
req.profiler.start('windshaft.maplayer_attribute');
|
||||
|
||||
step(
|
||||
function setupParams() {
|
||||
self.req2params(req, this);
|
||||
},
|
||||
function retrieveFeatureAttributes(err) {
|
||||
assert.ifError(err);
|
||||
|
||||
function retrieveFeatureAttributes() {
|
||||
var mapConfigProvider = new MapStoreMapConfigProvider(
|
||||
self.mapStore, req.context.user, self.userLimitsApi, req.params
|
||||
self.mapStore, res.locals.user, self.userLimitsApi, res.locals
|
||||
);
|
||||
self.attributesBackend.getFeatureAttributes(mapConfigProvider, req.params, false, this);
|
||||
self.attributesBackend.getFeatureAttributes(mapConfigProvider, res.locals, false, this);
|
||||
},
|
||||
function finish(err, tile, stats) {
|
||||
req.profiler.add(stats || {});
|
||||
|
||||
if (err) {
|
||||
self.sendError(req, res, err, 'GET ATTRIBUTES');
|
||||
err.label = 'GET ATTRIBUTES';
|
||||
next(err);
|
||||
} else {
|
||||
self.sendResponse(req, res, tile, 200);
|
||||
}
|
||||
@@ -211,44 +264,41 @@ LayergroupController.prototype.attributes = function(req, res) {
|
||||
};
|
||||
|
||||
// Gets a tile for a given token and set of tile ZXY coords. (OSM style)
|
||||
LayergroupController.prototype.tile = function(req, res) {
|
||||
LayergroupController.prototype.tile = function(req, res, next) {
|
||||
req.profiler.start('windshaft.map_tile');
|
||||
this.tileOrLayer(req, res);
|
||||
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) {
|
||||
if (req.params.token === 'static') {
|
||||
return next();
|
||||
}
|
||||
req.profiler.start('windshaft.maplayer_tile');
|
||||
this.tileOrLayer(req, res);
|
||||
this.tileOrLayer(req, res, next);
|
||||
};
|
||||
|
||||
LayergroupController.prototype.tileOrLayer = function (req, res) {
|
||||
LayergroupController.prototype.tileOrLayer = function (req, res, next) {
|
||||
var self = this;
|
||||
|
||||
step(
|
||||
function mapController$prepareParams() {
|
||||
self.req2params(req, this);
|
||||
},
|
||||
function mapController$getTileOrGrid(err) {
|
||||
assert.ifError(err);
|
||||
function mapController$getTileOrGrid() {
|
||||
self.tileBackend.getTile(
|
||||
new MapStoreMapConfigProvider(self.mapStore, req.context.user, self.userLimitsApi, req.params),
|
||||
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);
|
||||
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) {
|
||||
LayergroupController.prototype.finalizeGetTileOrGrid = function(err, req, res, tile, headers, next) {
|
||||
var supportedFormats = {
|
||||
grid_json: true,
|
||||
json_torque: true,
|
||||
@@ -277,52 +327,50 @@ LayergroupController.prototype.finalizeGetTileOrGrid = function(err, req, res, t
|
||||
}
|
||||
err.message = errMsg;
|
||||
|
||||
this.sendError(req, res, err, 'TILE RENDER');
|
||||
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, 200, headers);
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
LayergroupController.prototype.staticMap = function(req, res, width, height, zoom /* bounds */, center, next) {
|
||||
var format = req.params.format === 'jpg' ? 'jpeg' : 'png';
|
||||
req.params.layer = 'all';
|
||||
req.params.format = 'png';
|
||||
req.params.format = req.params.format || 'png';
|
||||
res.locals.layer = res.locals.layer || 'all';
|
||||
|
||||
var self = this;
|
||||
|
||||
step(
|
||||
function reqParams() {
|
||||
self.req2params(req, this);
|
||||
},
|
||||
function getImage(err) {
|
||||
assert.ifError(err);
|
||||
function getImage() {
|
||||
if (center) {
|
||||
self.previewBackend.getImage(
|
||||
new MapStoreMapConfigProvider(self.mapStore, req.context.user, self.userLimitsApi, req.params),
|
||||
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, req.context.user, self.userLimitsApi, req.params),
|
||||
new MapStoreMapConfigProvider(self.mapStore, res.locals.user, self.userLimitsApi, res.locals),
|
||||
format, width, height, zoom /* bounds */, this);
|
||||
}
|
||||
},
|
||||
@@ -331,7 +379,8 @@ LayergroupController.prototype.staticMap = function(req, res, width, height, zoo
|
||||
req.profiler.add(stats || {});
|
||||
|
||||
if (err) {
|
||||
self.sendError(req, res, err, 'STATIC_MAP');
|
||||
err.label = 'STATIC_MAP';
|
||||
next(err);
|
||||
} else {
|
||||
res.set('Content-Type', headers['Content-Type'] || 'image/' + format);
|
||||
self.sendResponse(req, res, image, 200);
|
||||
@@ -349,18 +398,18 @@ LayergroupController.prototype.sendResponse = function(req, res, body, status, h
|
||||
|
||||
// Set Last-Modified header
|
||||
var lastUpdated;
|
||||
if (req.params.cache_buster) {
|
||||
if (res.locals.cache_buster) {
|
||||
// Assuming cache_buster is a timestamp
|
||||
lastUpdated = new Date(parseInt(req.params.cache_buster));
|
||||
lastUpdated = new Date(parseInt(res.locals.cache_buster));
|
||||
} else {
|
||||
lastUpdated = new Date();
|
||||
}
|
||||
res.set('Last-Modified', lastUpdated.toUTCString());
|
||||
|
||||
var dbName = req.params.dbname;
|
||||
var dbName = res.locals.dbname;
|
||||
step(
|
||||
function getAffectedTables() {
|
||||
self.getAffectedTables(req.context.user, dbName, req.params.token, this);
|
||||
self.getAffectedTables(res.locals.user, dbName, res.locals.token, this);
|
||||
},
|
||||
function sendResponse(err, affectedTables) {
|
||||
req.profiler.done('affectedTables');
|
||||
@@ -371,10 +420,24 @@ LayergroupController.prototype.sendResponse = function(req, res, body, status, h
|
||||
res.set('X-Cache-Channel', affectedTables.getCacheChannel());
|
||||
self.surrogateKeysCache.tag(res, affectedTables);
|
||||
}
|
||||
self.send(req, res, body, status, headers);
|
||||
|
||||
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) {
|
||||
@@ -436,3 +499,12 @@ LayergroupController.prototype.getAffectedTables = function(user, dbName, layerg
|
||||
callback
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
function validateLayerRouteMiddleware(req, res, next) {
|
||||
if (req.params.token === 'static') {
|
||||
return next('route');
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
@@ -1,14 +1,9 @@
|
||||
var _ = require('underscore');
|
||||
var assert = require('assert');
|
||||
var step = require('step');
|
||||
var windshaft = require('windshaft');
|
||||
var QueryTables = require('cartodb-query-tables');
|
||||
|
||||
var ResourceLocator = require('../models/resource-locator');
|
||||
|
||||
var util = require('util');
|
||||
var BaseController = require('./base');
|
||||
|
||||
var cors = require('../middleware/cors');
|
||||
var userMiddleware = require('../middleware/user');
|
||||
|
||||
@@ -20,7 +15,6 @@ 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
|
||||
@@ -34,12 +28,9 @@ var CreateLayergroupMapConfigProvider = require('../models/mapconfig/provider/cr
|
||||
* @param {StatsBackend} statsBackend
|
||||
* @constructor
|
||||
*/
|
||||
function MapController(authApi, pgConnection, templateMaps, mapBackend, metadataBackend,
|
||||
function MapController(prepareContext, pgConnection, templateMaps, mapBackend, metadataBackend,
|
||||
surrogateKeysCache, userLimitsApi, layergroupAffectedTables, mapConfigAdapter,
|
||||
statsBackend) {
|
||||
|
||||
BaseController.call(this, authApi, pgConnection);
|
||||
|
||||
this.pgConnection = pgConnection;
|
||||
this.templateMaps = templateMaps;
|
||||
this.mapBackend = mapBackend;
|
||||
@@ -52,334 +43,345 @@ function MapController(authApi, pgConnection, templateMaps, mapBackend, metadata
|
||||
this.resourceLocator = new ResourceLocator(global.environment);
|
||||
|
||||
this.statsBackend = statsBackend;
|
||||
this.prepareContext = prepareContext;
|
||||
}
|
||||
|
||||
util.inherits(MapController, BaseController);
|
||||
|
||||
module.exports = MapController;
|
||||
|
||||
|
||||
MapController.prototype.register = function(app) {
|
||||
app.get(app.base_url_mapconfig, cors(), userMiddleware, this.createGet.bind(this));
|
||||
app.post(app.base_url_mapconfig, cors(), userMiddleware, this.createPost.bind(this));
|
||||
app.get(app.base_url_templated + '/:template_id/jsonp', cors(), userMiddleware, this.jsonp.bind(this));
|
||||
app.post(app.base_url_templated + '/:template_id', cors(), userMiddleware, this.instantiate.bind(this));
|
||||
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.createGet = function(req, res){
|
||||
req.profiler.start('windshaft.createmap_get');
|
||||
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;
|
||||
|
||||
this.create(req, res, function createGet$prepareConfig(err, req) {
|
||||
assert.ifError(err);
|
||||
if ( ! req.params.config ) {
|
||||
throw new Error('layergroup GET needs a "config" parameter');
|
||||
}
|
||||
return JSON.parse(req.params.config);
|
||||
});
|
||||
return [
|
||||
cors(),
|
||||
userMiddleware,
|
||||
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.setSurrogateKeyHeader(),
|
||||
this.sendResponse(),
|
||||
this.augmentError({ label, addContext })
|
||||
];
|
||||
};
|
||||
|
||||
MapController.prototype.createPost = function(req, res) {
|
||||
req.profiler.start('windshaft.createmap_post');
|
||||
MapController.prototype.initProfiler = function (isTemplateInstantiation) {
|
||||
const operation = isTemplateInstantiation ? 'instance_template' : 'createmap';
|
||||
|
||||
this.create(req, res, function createPost$prepareConfig(err, req) {
|
||||
assert.ifError(err);
|
||||
if (!req.is('application/json')) {
|
||||
throw new Error('layergroup POST data must be of type application/json');
|
||||
}
|
||||
return req.body;
|
||||
});
|
||||
return function initProfilerMiddleware (req, res, next) {
|
||||
req.profiler.start(`windshaft-cartodb.${operation}_${req.method.toLowerCase()}`);
|
||||
req.profiler.done(`${operation}.initProfilerMiddleware`);
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
MapController.prototype.instantiate = function(req, res) {
|
||||
req.profiler.start('windshaft-cartodb.instance_template_post');
|
||||
|
||||
this.instantiateTemplate(req, res, function prepareTemplateParams(callback) {
|
||||
if (!req.is('application/json')) {
|
||||
return callback(new Error('Template POST data must be of type application/json'));
|
||||
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'));
|
||||
}
|
||||
return callback(null, req.body);
|
||||
});
|
||||
|
||||
req.profiler.done('checkJsonContentTypeMiddleware');
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
MapController.prototype.jsonp = function(req, res) {
|
||||
req.profiler.start('windshaft-cartodb.instance_template_get');
|
||||
MapController.prototype.checkInstantiteLayergroup = function () {
|
||||
return function checkInstantiteLayergroupMiddleware(req, res, next) {
|
||||
if (req.method === 'GET') {
|
||||
const { callback, config } = req.query;
|
||||
|
||||
this.instantiateTemplate(req, res, function prepareJsonTemplateParams(callback) {
|
||||
var err = null;
|
||||
if ( req.query.callback === undefined || req.query.callback.length === 0) {
|
||||
err = new Error('callback parameter should be present and be a function name');
|
||||
}
|
||||
if (callback === undefined || callback.length === 0) {
|
||||
return next(new Error('callback parameter should be present and be a function name'));
|
||||
}
|
||||
|
||||
var templateParams = {};
|
||||
if (req.query.config) {
|
||||
try {
|
||||
templateParams = JSON.parse(req.query.config);
|
||||
} catch(e) {
|
||||
err = new Error('Invalid config parameter, should be a valid JSON');
|
||||
if (config) {
|
||||
try {
|
||||
req.body = JSON.parse(config);
|
||||
} catch(e) {
|
||||
return next(new Error('Invalid config parameter, should be a valid JSON'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return callback(err, templateParams);
|
||||
});
|
||||
req.profiler.done('checkInstantiteLayergroup');
|
||||
|
||||
return next();
|
||||
};
|
||||
};
|
||||
|
||||
MapController.prototype.create = function(req, res, prepareConfigFn) {
|
||||
var self = this;
|
||||
MapController.prototype.checkCreateLayergroup = function () {
|
||||
return function checkCreateLayergroupMiddleware (req, res, next) {
|
||||
if (req.method === 'GET') {
|
||||
const { config } = res.locals;
|
||||
|
||||
var mapConfig;
|
||||
if (!config) {
|
||||
return next(new Error('layergroup GET needs a "config" parameter'));
|
||||
}
|
||||
|
||||
var context = {};
|
||||
try {
|
||||
req.body = JSON.parse(config);
|
||||
} catch (err) {
|
||||
return next(err);
|
||||
}
|
||||
}
|
||||
|
||||
step(
|
||||
function setupParams(){
|
||||
self.req2params(req, this);
|
||||
},
|
||||
prepareConfigFn,
|
||||
function prepareAdapterMapConfig(err, requestMapConfig) {
|
||||
assert.ifError(err);
|
||||
context.analysisConfiguration = {
|
||||
user: req.context.user,
|
||||
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: req.params.dbhost,
|
||||
port: req.params.dbport,
|
||||
dbname: req.params.dbname,
|
||||
user: req.params.dbuser,
|
||||
pass: req.params.dbpassword
|
||||
host: dbhost,
|
||||
port: dbport,
|
||||
dbname: dbname,
|
||||
user: dbuser,
|
||||
pass: dbpassword
|
||||
},
|
||||
batch: {
|
||||
username: req.context.user,
|
||||
apiKey: req.params.api_key
|
||||
username: user,
|
||||
apiKey: api_key
|
||||
}
|
||||
};
|
||||
self.mapConfigAdapter.getMapConfig(req.context.user, requestMapConfig, req.params, context, this);
|
||||
},
|
||||
function createLayergroup(err, requestMapConfig) {
|
||||
assert.ifError(err);
|
||||
var datasource = context.datasource || Datasource.EmptyDatasource();
|
||||
mapConfig = new MapConfig(requestMapConfig, datasource);
|
||||
self.mapBackend.createLayergroup(
|
||||
mapConfig, req.params,
|
||||
new CreateLayergroupMapConfigProvider(mapConfig, req.context.user, self.userLimitsApi, req.params),
|
||||
this
|
||||
);
|
||||
},
|
||||
function afterLayergroupCreate(err, layergroup) {
|
||||
assert.ifError(err);
|
||||
self.afterLayergroupCreate(req, res, mapConfig, layergroup, context.analysesResults, this);
|
||||
},
|
||||
function finish(err, layergroup) {
|
||||
}
|
||||
};
|
||||
|
||||
this.mapConfigAdapter.getMapConfig(user, requestMapConfig, res.locals, context, (err, requestMapConfig) => {
|
||||
req.profiler.done('anonymous.getMapConfig');
|
||||
if (err) {
|
||||
if (Number.isFinite(err.layerIndex)) {
|
||||
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)
|
||||
};
|
||||
|
||||
err = error;
|
||||
}
|
||||
self.sendError(req, res, err, 'ANONYMOUS LAYERGROUP');
|
||||
} else {
|
||||
var analysesResults = context.analysesResults || [];
|
||||
self.addDataviewsAndWidgetsUrls(req.context.user, layergroup, mapConfig.obj());
|
||||
self.addAnalysesMetadata(req.context.user, layergroup, analysesResults, true);
|
||||
addContextMetadata(layergroup, mapConfig.obj(), context);
|
||||
res.set('X-Layergroup-Id', layergroup.layergroupid);
|
||||
self.send(req, res, layergroup, 200);
|
||||
return next(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
function addContextMetadata(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;
|
||||
req.body = requestMapConfig;
|
||||
res.locals.context = context;
|
||||
|
||||
next();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
MapController.prototype.instantiateTemplate = function(req, res, prepareParamsFn) {
|
||||
var self = this;
|
||||
|
||||
var cdbuser = req.context.user;
|
||||
|
||||
var mapConfigProvider;
|
||||
var mapConfig;
|
||||
step(
|
||||
function setupParams(){
|
||||
self.req2params(req, this);
|
||||
},
|
||||
function getTemplateParams() {
|
||||
prepareParamsFn(this);
|
||||
},
|
||||
function getTemplate(err, templateParams) {
|
||||
assert.ifError(err);
|
||||
mapConfigProvider = new NamedMapMapConfigProvider(
|
||||
self.templateMaps,
|
||||
self.pgConnection,
|
||||
self.metadataBackend,
|
||||
self.userLimitsApi,
|
||||
self.mapConfigAdapter,
|
||||
cdbuser,
|
||||
req.params.template_id,
|
||||
templateParams,
|
||||
req.query.auth_token,
|
||||
req.params
|
||||
);
|
||||
mapConfigProvider.getMapConfig(this);
|
||||
},
|
||||
function createLayergroup(err, mapConfig_, rendererParams) {
|
||||
assert.ifError(err);
|
||||
mapConfig = mapConfig_;
|
||||
self.mapBackend.createLayergroup(
|
||||
mapConfig, rendererParams,
|
||||
new CreateLayergroupMapConfigProvider(mapConfig, cdbuser, self.userLimitsApi, rendererParams),
|
||||
this
|
||||
);
|
||||
},
|
||||
function afterLayergroupCreate(err, layergroup) {
|
||||
assert.ifError(err);
|
||||
self.afterLayergroupCreate(req, res, mapConfig, layergroup,
|
||||
mapConfigProvider.analysesResults,
|
||||
this);
|
||||
},
|
||||
function finishTemplateInstantiation(err, layergroup) {
|
||||
if (err) {
|
||||
self.sendError(req, res, err, 'NAMED MAP LAYERGROUP');
|
||||
} else {
|
||||
var templateHash = self.templateMaps.fingerPrint(mapConfigProvider.template).substring(0, 8);
|
||||
layergroup.layergroupid = cdbuser + '@' + templateHash + '@' + layergroup.layergroupid;
|
||||
|
||||
var _mapConfig = mapConfig.obj();
|
||||
self.addDataviewsAndWidgetsUrls(cdbuser, layergroup, _mapConfig);
|
||||
self.addAnalysesMetadata(cdbuser, layergroup, mapConfigProvider.analysesResults);
|
||||
addContextMetadata(layergroup, _mapConfig, mapConfigProvider.context);
|
||||
|
||||
res.set('X-Layergroup-Id', layergroup.layergroupid);
|
||||
self.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(cdbuser, mapConfigProvider.getTemplateName()));
|
||||
|
||||
self.send(req, res, layergroup, 200);
|
||||
}
|
||||
}
|
||||
);
|
||||
}.bind(this);
|
||||
};
|
||||
|
||||
MapController.prototype.afterLayergroupCreate =
|
||||
function(req, res, mapconfig, layergroup, analysesResults, callback) {
|
||||
var self = 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);
|
||||
|
||||
var username = req.context.user;
|
||||
res.locals.mapconfig = mapconfig;
|
||||
res.locals.analysesResults = context.analysesResults;
|
||||
|
||||
var tasksleft = 2; // redis key and affectedTables
|
||||
var errors = [];
|
||||
|
||||
var done = function(err) {
|
||||
if ( err ) {
|
||||
errors.push('' + err);
|
||||
}
|
||||
if ( ! --tasksleft ) {
|
||||
err = errors.length ? new Error(errors.join('\n')) : null;
|
||||
callback(err, layergroup);
|
||||
}
|
||||
};
|
||||
|
||||
// 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);
|
||||
|
||||
// Don't wait for the mapview count increment to
|
||||
// take place before proceeding. Error will be logged
|
||||
// asynchronously
|
||||
this.metadataBackend.incMapviewCount(username, mapconfig.obj().stat_tag, function(err) {
|
||||
req.profiler.done('incMapviewCount');
|
||||
if ( err ) {
|
||||
global.logger.log("ERROR: failed to increment mapview count for user '" + username + "': " + err);
|
||||
}
|
||||
done();
|
||||
});
|
||||
|
||||
var 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');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
var dbName = req.params.dbname;
|
||||
var layergroupId = layergroup.layergroupid;
|
||||
var dbConnection;
|
||||
|
||||
step(
|
||||
function getPgConnection() {
|
||||
self.pgConnection.getConnection(username, this);
|
||||
},
|
||||
function getAffectedTablesAndLastUpdatedTime(err, connection) {
|
||||
assert.ifError(err);
|
||||
dbConnection = connection;
|
||||
QueryTables.getAffectedTablesFromQuery(dbConnection, sql.join(';'), this);
|
||||
},
|
||||
function handleAffectedTablesAndLastUpdatedTime(err, result) {
|
||||
req.profiler.done('queryTablesAndLastUpdated');
|
||||
assert.ifError(err);
|
||||
// feed affected tables cache so it can be reused from, for instance, layergroup controller
|
||||
self.layergroupAffectedTables.set(dbName, layergroupId, result);
|
||||
|
||||
var lastUpdateTime = result.getLastUpdatedAt();
|
||||
lastUpdateTime = getLastUpdatedTime(analysesResults, lastUpdateTime) || lastUpdateTime;
|
||||
|
||||
// last update for layergroup cache buster
|
||||
layergroup.layergroupid = layergroup.layergroupid + ':' + lastUpdateTime;
|
||||
layergroup.last_updated = new Date(lastUpdateTime).toISOString();
|
||||
|
||||
if (req.method === 'GET') {
|
||||
var ttl = global.environment.varnish.layergroupTtl || 86400;
|
||||
res.set('Cache-Control', 'public,max-age='+ttl+',must-revalidate');
|
||||
res.set('Last-Modified', (new Date()).toUTCString());
|
||||
res.set('X-Cache-Channel', result.getCacheChannel());
|
||||
if (result.tables && result.tables.length > 0) {
|
||||
self.surrogateKeysCache.tag(res, result);
|
||||
}
|
||||
this.mapBackend.createLayergroup(mapconfig, res.locals, mapconfigProvider, (err, layergroup) => {
|
||||
req.profiler.done('createLayergroup');
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
function fetchLayersStats(err) {
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
self.statsBackend.getStats(mapconfig, dbConnection, function(err, layersStats) {
|
||||
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();
|
||||
};
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
||||
if (layersStats.length > 0) {
|
||||
layergroup.metadata.layers.forEach(function (layer, index) {
|
||||
layer.meta.stats = layersStats[index];
|
||||
});
|
||||
}
|
||||
return next();
|
||||
|
||||
// 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();
|
||||
});
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
});
|
||||
}.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) {
|
||||
@@ -395,34 +397,66 @@ function getLastUpdatedTime(analysesResults, lastUpdateTime) {
|
||||
}, lastUpdateTime);
|
||||
}
|
||||
|
||||
MapController.prototype.addAnalysesMetadata = function(username, layergroup, analysesResults, includeQuery) {
|
||||
includeQuery = includeQuery || false;
|
||||
analysesResults = analysesResults || [];
|
||||
layergroup.metadata.analyses = [];
|
||||
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');
|
||||
}
|
||||
|
||||
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;
|
||||
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);
|
||||
}
|
||||
|
||||
return nodesIdMap;
|
||||
}.bind(this), {})
|
||||
if (layersStats.length > 0) {
|
||||
layergroup.metadata.layers.forEach(function (layer, index) {
|
||||
layer.meta.stats = layersStats[index];
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
});
|
||||
}.bind(this));
|
||||
}.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
|
||||
@@ -461,3 +495,131 @@ MapController.prototype.addWidgetsUrl = function(username, layergroup, mapConfig
|
||||
}.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;
|
||||
|
||||
addContextMetadata(layergroup, mapconfig.obj(), context);
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
function addContextMetadata(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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -3,41 +3,46 @@ var assert = require('assert');
|
||||
var _ = require('underscore');
|
||||
var NamedMapsCacheEntry = require('../cache/model/named_maps_entry');
|
||||
|
||||
var util = require('util');
|
||||
var BaseController = require('./base');
|
||||
|
||||
var cors = require('../middleware/cors');
|
||||
var userMiddleware = require('../middleware/user');
|
||||
var allowQueryParams = require('../middleware/allow-query-params');
|
||||
var vectorError = require('../middleware/vector-error');
|
||||
|
||||
function NamedMapsController(authApi, pgConnection, namedMapProviderCache, tileBackend, previewBackend,
|
||||
function NamedMapsController(prepareContext, namedMapProviderCache, tileBackend, previewBackend,
|
||||
surrogateKeysCache, tablesExtentApi, metadataBackend) {
|
||||
BaseController.call(this, authApi, pgConnection);
|
||||
|
||||
this.namedMapProviderCache = namedMapProviderCache;
|
||||
this.tileBackend = tileBackend;
|
||||
this.previewBackend = previewBackend;
|
||||
this.surrogateKeysCache = surrogateKeysCache;
|
||||
this.tablesExtentApi = tablesExtentApi;
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.prepareContext = prepareContext;
|
||||
}
|
||||
|
||||
util.inherits(NamedMapsController, BaseController);
|
||||
|
||||
module.exports = NamedMapsController;
|
||||
|
||||
NamedMapsController.prototype.register = function(app) {
|
||||
app.get(app.base_url_templated +
|
||||
'/:template_id/:layer/:z/:x/:y.(:format)', cors(), userMiddleware,
|
||||
this.tile.bind(this));
|
||||
app.get(
|
||||
app.base_url_templated + '/:template_id/:layer/:z/:x/:y.(:format)',
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.prepareContext,
|
||||
this.tile.bind(this),
|
||||
vectorError()
|
||||
);
|
||||
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/static/named/:template_id/:width/:height.:format', cors(), userMiddleware, allowQueryParams(['layer']),
|
||||
this.staticMap.bind(this));
|
||||
app.get(
|
||||
app.base_url_mapconfig + '/static/named/:template_id/:width/:height.:format',
|
||||
cors(),
|
||||
userMiddleware,
|
||||
allowQueryParams(['layer', 'zoom', 'lon', 'lat', 'bbox']),
|
||||
this.prepareContext,
|
||||
this.staticMap.bind(this)
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.sendResponse = function(req, res, resource, headers, namedMapProvider) {
|
||||
this.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(req.context.user, namedMapProvider.getTemplateName()));
|
||||
NamedMapsController.prototype.sendResponse = function(req, res, body, headers, namedMapProvider) {
|
||||
this.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(res.locals.user, namedMapProvider.getTemplateName()));
|
||||
res.set('Content-Type', headers['content-type'] || headers['Content-Type'] || 'image/png');
|
||||
res.set('Cache-Control', 'public,max-age=7200,must-revalidate');
|
||||
|
||||
@@ -69,29 +74,26 @@ NamedMapsController.prototype.sendResponse = function(req, res, resource, header
|
||||
self.surrogateKeysCache.tag(res, result);
|
||||
}
|
||||
}
|
||||
self.send(req, res, resource, 200);
|
||||
res.status(200);
|
||||
res.send(body);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.tile = function(req, res) {
|
||||
NamedMapsController.prototype.tile = function(req, res, next) {
|
||||
var self = this;
|
||||
|
||||
var cdbUser = req.context.user;
|
||||
var cdbUser = res.locals.user;
|
||||
|
||||
var namedMapProvider;
|
||||
step(
|
||||
function reqParams() {
|
||||
self.req2params(req, this);
|
||||
},
|
||||
function getNamedMapProvider(err) {
|
||||
assert.ifError(err);
|
||||
function getNamedMapProvider() {
|
||||
self.namedMapProviderCache.get(
|
||||
cdbUser,
|
||||
req.params.template_id,
|
||||
req.query.config,
|
||||
req.query.auth_token,
|
||||
req.params,
|
||||
res.locals,
|
||||
this
|
||||
);
|
||||
},
|
||||
@@ -102,8 +104,10 @@ NamedMapsController.prototype.tile = function(req, res) {
|
||||
},
|
||||
function handleImage(err, tile, headers, stats) {
|
||||
req.profiler.add(stats);
|
||||
|
||||
if (err) {
|
||||
self.sendError(req, res, err, 'NAMED_MAP_TILE');
|
||||
err.label = 'NAMED_MAP_TILE';
|
||||
next(err);
|
||||
} else {
|
||||
self.sendResponse(req, res, tile, headers, namedMapProvider);
|
||||
}
|
||||
@@ -111,28 +115,24 @@ NamedMapsController.prototype.tile = function(req, res) {
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.staticMap = function(req, res) {
|
||||
NamedMapsController.prototype.staticMap = function(req, res, next) {
|
||||
var self = this;
|
||||
|
||||
var cdbUser = req.context.user;
|
||||
var cdbUser = res.locals.user;
|
||||
|
||||
var format = req.params.format === 'jpg' ? 'jpeg' : 'png';
|
||||
req.params.format = 'png';
|
||||
req.params.layer = 'all';
|
||||
res.locals.format = req.params.format || 'png';
|
||||
res.locals.layer = res.locals.layer || 'all';
|
||||
|
||||
var namedMapProvider;
|
||||
step(
|
||||
function reqParams() {
|
||||
self.req2params(req, this);
|
||||
},
|
||||
function getNamedMapProvider(err) {
|
||||
assert.ifError(err);
|
||||
function getNamedMapProvider() {
|
||||
self.namedMapProviderCache.get(
|
||||
cdbUser,
|
||||
req.params.template_id,
|
||||
req.query.config,
|
||||
req.query.auth_token,
|
||||
req.params,
|
||||
res.locals,
|
||||
this
|
||||
);
|
||||
},
|
||||
@@ -141,11 +141,11 @@ NamedMapsController.prototype.staticMap = function(req, res) {
|
||||
|
||||
namedMapProvider = _namedMapProvider;
|
||||
|
||||
self.prepareLayerFilterFromPreviewLayers(cdbUser, req, namedMapProvider, this);
|
||||
self.prepareLayerFilterFromPreviewLayers(cdbUser, req, res.locals, namedMapProvider, this);
|
||||
},
|
||||
function prepareImageOptions(err) {
|
||||
assert.ifError(err);
|
||||
self.getStaticImageOptions(cdbUser, req.params, namedMapProvider, this);
|
||||
self.getStaticImageOptions(cdbUser, res.locals, namedMapProvider, this);
|
||||
},
|
||||
function getImage(err, imageOpts) {
|
||||
assert.ifError(err);
|
||||
@@ -179,7 +179,8 @@ NamedMapsController.prototype.staticMap = function(req, res) {
|
||||
req.profiler.add(stats || {});
|
||||
|
||||
if (err) {
|
||||
self.sendError(req, res, err, 'STATIC_VIZ_MAP');
|
||||
err.label = 'STATIC_VIZ_MAP';
|
||||
next(err);
|
||||
} else {
|
||||
self.sendResponse(req, res, image, headers, namedMapProvider);
|
||||
}
|
||||
@@ -187,7 +188,13 @@ NamedMapsController.prototype.staticMap = function(req, res) {
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.prepareLayerFilterFromPreviewLayers = function (user, req, namedMapProvider, callback) {
|
||||
NamedMapsController.prototype.prepareLayerFilterFromPreviewLayers = function (
|
||||
user,
|
||||
req,
|
||||
params,
|
||||
namedMapProvider,
|
||||
callback
|
||||
) {
|
||||
var self = this;
|
||||
namedMapProvider.getTemplate(function (err, template) {
|
||||
if (err) {
|
||||
@@ -212,7 +219,7 @@ NamedMapsController.prototype.prepareLayerFilterFromPreviewLayers = function (us
|
||||
}
|
||||
|
||||
// overwrites 'all' default filter
|
||||
req.params.layer = layerVisibilityFilter.join(',');
|
||||
params.layer = layerVisibilityFilter.join(',');
|
||||
|
||||
// recreates the provider
|
||||
self.namedMapProviderCache.get(
|
||||
@@ -220,7 +227,7 @@ NamedMapsController.prototype.prepareLayerFilterFromPreviewLayers = function (us
|
||||
req.params.template_id,
|
||||
req.query.config,
|
||||
req.query.auth_token,
|
||||
req.params,
|
||||
params,
|
||||
callback
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2,9 +2,6 @@ var step = require('step');
|
||||
var assert = require('assert');
|
||||
var templateName = require('../backends/template_maps').templateName;
|
||||
|
||||
var util = require('util');
|
||||
var BaseController = require('./base');
|
||||
|
||||
var cors = require('../middleware/cors');
|
||||
var userMiddleware = require('../middleware/user');
|
||||
|
||||
@@ -15,30 +12,59 @@ var userMiddleware = require('../middleware/user');
|
||||
* @param {TemplateMaps} templateMaps
|
||||
* @constructor
|
||||
*/
|
||||
function NamedMapsAdminController(authApi, pgConnection, templateMaps) {
|
||||
BaseController.call(this, authApi, pgConnection);
|
||||
|
||||
function NamedMapsAdminController(authApi, templateMaps) {
|
||||
this.authApi = authApi;
|
||||
this.templateMaps = templateMaps;
|
||||
}
|
||||
|
||||
util.inherits(NamedMapsAdminController, BaseController);
|
||||
|
||||
module.exports = NamedMapsAdminController;
|
||||
|
||||
NamedMapsAdminController.prototype.register = function(app) {
|
||||
app.post(app.base_url_templated, cors(), userMiddleware, this.create.bind(this));
|
||||
app.put(app.base_url_templated + '/:template_id', cors(), userMiddleware, this.update.bind(this));
|
||||
app.get(app.base_url_templated + '/:template_id', cors(), userMiddleware, this.retrieve.bind(this));
|
||||
app.delete(app.base_url_templated + '/:template_id', cors(), userMiddleware, this.destroy.bind(this));
|
||||
app.get(app.base_url_templated, cors(), userMiddleware, this.list.bind(this));
|
||||
app.options(app.base_url_templated + '/:template_id', cors('Content-Type'));
|
||||
NamedMapsAdminController.prototype.register = function (app) {
|
||||
app.post(
|
||||
app.base_url_templated + '/',
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.create.bind(this)
|
||||
);
|
||||
|
||||
app.put(
|
||||
app.base_url_templated + '/:template_id',
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.update.bind(this)
|
||||
);
|
||||
|
||||
app.get(
|
||||
app.base_url_templated + '/:template_id',
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.retrieve.bind(this)
|
||||
);
|
||||
|
||||
app.delete(
|
||||
app.base_url_templated + '/:template_id',
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.destroy.bind(this)
|
||||
);
|
||||
|
||||
app.get(
|
||||
app.base_url_templated + '/',
|
||||
cors(),
|
||||
userMiddleware,
|
||||
this.list.bind(this)
|
||||
);
|
||||
|
||||
app.options(
|
||||
app.base_url_templated + '/:template_id',
|
||||
cors('Content-Type')
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.create = function(req, res) {
|
||||
NamedMapsAdminController.prototype.create = function(req, res, next) {
|
||||
var self = this;
|
||||
|
||||
var cdbuser = req.context.user;
|
||||
var cdbuser = res.locals.user;
|
||||
|
||||
step(
|
||||
function checkPerms(){
|
||||
@@ -55,14 +81,14 @@ NamedMapsAdminController.prototype.create = function(req, res) {
|
||||
assert.ifError(err);
|
||||
return { template_id: tpl_id };
|
||||
},
|
||||
finishFn(self, req, res, 'POST TEMPLATE')
|
||||
finishFn(self, req, res, 'POST TEMPLATE', null, next)
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.update = function(req, res) {
|
||||
NamedMapsAdminController.prototype.update = function(req, res, next) {
|
||||
var self = this;
|
||||
|
||||
var cdbuser = req.context.user;
|
||||
var cdbuser = res.locals.user;
|
||||
var template;
|
||||
var tpl_id;
|
||||
|
||||
@@ -84,16 +110,16 @@ NamedMapsAdminController.prototype.update = function(req, res) {
|
||||
|
||||
return { template_id: tpl_id };
|
||||
},
|
||||
finishFn(self, req, res, 'PUT TEMPLATE')
|
||||
finishFn(self, req, res, 'PUT TEMPLATE', null, next)
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.retrieve = function(req, res) {
|
||||
NamedMapsAdminController.prototype.retrieve = function(req, res, next) {
|
||||
var self = this;
|
||||
|
||||
req.profiler.start('windshaft-cartodb.get_template');
|
||||
|
||||
var cdbuser = req.context.user;
|
||||
var cdbuser = res.locals.user;
|
||||
var tpl_id;
|
||||
step(
|
||||
function checkPerms(){
|
||||
@@ -118,16 +144,16 @@ NamedMapsAdminController.prototype.retrieve = function(req, res) {
|
||||
delete tpl_val.auth_id;
|
||||
return { template: tpl_val };
|
||||
},
|
||||
finishFn(self, req, res, 'GET TEMPLATE')
|
||||
finishFn(self, req, res, 'GET TEMPLATE', null, next)
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.destroy = function(req, res) {
|
||||
NamedMapsAdminController.prototype.destroy = function(req, res, next) {
|
||||
var self = this;
|
||||
|
||||
req.profiler.start('windshaft-cartodb.delete_template');
|
||||
|
||||
var cdbuser = req.context.user;
|
||||
var cdbuser = res.locals.user;
|
||||
var tpl_id;
|
||||
step(
|
||||
function checkPerms(){
|
||||
@@ -144,15 +170,15 @@ NamedMapsAdminController.prototype.destroy = function(req, res) {
|
||||
assert.ifError(err);
|
||||
return '';
|
||||
},
|
||||
finishFn(self, req, res, 'DELETE TEMPLATE', 204)
|
||||
finishFn(self, req, res, 'DELETE TEMPLATE', 204, next)
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsAdminController.prototype.list = function(req, res) {
|
||||
NamedMapsAdminController.prototype.list = function(req, res, next) {
|
||||
var self = this;
|
||||
req.profiler.start('windshaft-cartodb.get_template_list');
|
||||
|
||||
var cdbuser = req.context.user;
|
||||
var cdbuser = res.locals.user;
|
||||
|
||||
step(
|
||||
function checkPerms(){
|
||||
@@ -168,16 +194,23 @@ NamedMapsAdminController.prototype.list = function(req, res) {
|
||||
assert.ifError(err);
|
||||
return { template_ids: tpl_ids };
|
||||
},
|
||||
finishFn(self, req, res, 'GET TEMPLATE LIST')
|
||||
finishFn(self, req, res, 'GET TEMPLATE LIST', null, next)
|
||||
);
|
||||
};
|
||||
|
||||
function finishFn(controller, req, res, description, status) {
|
||||
return function finish(err, response){
|
||||
function finishFn(controller, req, res, description, status, next) {
|
||||
return function finish(err, body){
|
||||
if (err) {
|
||||
controller.sendError(req, res, err, description);
|
||||
err.label = description;
|
||||
next(err);
|
||||
} else {
|
||||
controller.send(req, res, response, status || 200);
|
||||
res.status(status || 200);
|
||||
|
||||
if (req.query && req.query.callback) {
|
||||
res.jsonp(body);
|
||||
} else {
|
||||
res.json(body);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ module.exports = function allowQueryParams(params) {
|
||||
throw new Error('allowQueryParams must receive an Array of params');
|
||||
}
|
||||
return function allowQueryParamsMiddleware(req, res, next) {
|
||||
req.context.allowedQueryParams = params;
|
||||
res.locals.allowedQueryParams = params;
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
20
lib/cartodb/middleware/context/authorize.js
Normal file
20
lib/cartodb/middleware/context/authorize.js
Normal file
@@ -0,0 +1,20 @@
|
||||
module.exports = function authorizeMiddleware (authApi) {
|
||||
return function (req, res, next) {
|
||||
req.profiler.done('req2params.setup');
|
||||
|
||||
authApi.authorize(req, res, (err, authorized) => {
|
||||
req.profiler.done('authorize');
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if(!authorized) {
|
||||
err = new Error("Sorry, you are unauthorized (permission denied)");
|
||||
err.http_status = 403;
|
||||
return next(err);
|
||||
}
|
||||
|
||||
return next();
|
||||
});
|
||||
};
|
||||
};
|
||||
32
lib/cartodb/middleware/context/clean-up-query-params.js
Normal file
32
lib/cartodb/middleware/context/clean-up-query-params.js
Normal file
@@ -0,0 +1,32 @@
|
||||
const _ = require('underscore');
|
||||
|
||||
// Whitelist query parameters and attach format
|
||||
const REQUEST_QUERY_PARAMS_WHITELIST = [
|
||||
'config',
|
||||
'map_key',
|
||||
'api_key',
|
||||
'auth_token',
|
||||
'callback',
|
||||
'zoom',
|
||||
'lon',
|
||||
'lat',
|
||||
// analysis
|
||||
'filters' // json
|
||||
];
|
||||
|
||||
module.exports = function cleanUpQueryParamsMiddleware () {
|
||||
return function cleanUpQueryParams (req, res, next) {
|
||||
var allowedQueryParams = REQUEST_QUERY_PARAMS_WHITELIST;
|
||||
|
||||
if (Array.isArray(res.locals.allowedQueryParams)) {
|
||||
allowedQueryParams = allowedQueryParams.concat(res.locals.allowedQueryParams);
|
||||
}
|
||||
|
||||
req.query = _.pick(req.query, allowedQueryParams);
|
||||
|
||||
// bring all query values onto res.locals object
|
||||
_.extend(res.locals, req.query);
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
31
lib/cartodb/middleware/context/db-conn-setup.js
Normal file
31
lib/cartodb/middleware/context/db-conn-setup.js
Normal file
@@ -0,0 +1,31 @@
|
||||
const _ = require('underscore');
|
||||
|
||||
module.exports = function dbConnSetupMiddleware(pgConnection) {
|
||||
return function dbConnSetup(req, res, next) {
|
||||
const user = res.locals.user;
|
||||
pgConnection.setDBConn(user, res.locals, (err) => {
|
||||
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);
|
||||
});
|
||||
};
|
||||
};
|
||||
15
lib/cartodb/middleware/context/index.js
Normal file
15
lib/cartodb/middleware/context/index.js
Normal file
@@ -0,0 +1,15 @@
|
||||
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)
|
||||
];
|
||||
};
|
||||
32
lib/cartodb/middleware/context/layergroup-token.js
Normal file
32
lib/cartodb/middleware/context/layergroup-token.js
Normal file
@@ -0,0 +1,32 @@
|
||||
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();
|
||||
};
|
||||
6
lib/cartodb/middleware/context/locals.js
Normal file
6
lib/cartodb/middleware/context/locals.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = function localsMiddleware(req, res, next) {
|
||||
// save req.params in res.locals
|
||||
res.locals = Object.assign(req.params || {}, res.locals);
|
||||
|
||||
next();
|
||||
};
|
||||
162
lib/cartodb/middleware/error-middleware.js
Normal file
162
lib/cartodb/middleware/error-middleware.js
Normal file
@@ -0,0 +1,162 @@
|
||||
const _ = require('underscore');
|
||||
const debug = require('debug')('windshaft:cartodb:error-middleware');
|
||||
|
||||
module.exports = function errorMiddleware (/* options */) {
|
||||
return function error (err, req, res, next) {
|
||||
// jshint unused:false
|
||||
// jshint maxcomplexity:9
|
||||
var allErrors = Array.isArray(err) ? err : [err];
|
||||
|
||||
allErrors = populateTimeoutErrors(allErrors);
|
||||
|
||||
const label = err.label || 'UNKNOWN';
|
||||
err = allErrors[0] || new Error(label);
|
||||
allErrors[0] = err;
|
||||
|
||||
var statusCode = findStatusCode(err);
|
||||
|
||||
if (err.message === 'Tile does not exist' && res.locals.format === 'mvt') {
|
||||
statusCode = 204;
|
||||
}
|
||||
|
||||
debug('[%s ERROR] -- %d: %s, %s', label, statusCode, err, err.stack);
|
||||
|
||||
// If a callback was requested, force status to 200
|
||||
if (req.query && req.query.callback) {
|
||||
statusCode = 200;
|
||||
}
|
||||
|
||||
var errorResponseBody = {
|
||||
errors: allErrors.map(errorMessage),
|
||||
errors_with_context: allErrors.map(errorMessageWithContext)
|
||||
};
|
||||
|
||||
res.status(statusCode);
|
||||
|
||||
if (req.query && req.query.callback) {
|
||||
res.jsonp(errorResponseBody);
|
||||
} else {
|
||||
res.json(errorResponseBody);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
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 populateTimeoutErrors (errors) {
|
||||
return errors.map(function (error) {
|
||||
if (isRenderTimeoutError(error)) {
|
||||
error.subtype = 'render';
|
||||
}
|
||||
|
||||
if (isDatasourceTimeoutError(error)) {
|
||||
error.subtype = 'datasource';
|
||||
}
|
||||
|
||||
if (isTimeoutError(error)) {
|
||||
error.message = 'You are over platform\'s limits. Please contact us to know more details';
|
||||
error.type = 'limit';
|
||||
error.http_status = 429;
|
||||
}
|
||||
|
||||
return error;
|
||||
});
|
||||
}
|
||||
|
||||
function findStatusCode(err) {
|
||||
var statusCode;
|
||||
if ( err.http_status ) {
|
||||
statusCode = err.http_status;
|
||||
} else {
|
||||
statusCode = statusFromErrorMessage('' + err);
|
||||
}
|
||||
return statusCode;
|
||||
}
|
||||
|
||||
module.exports.findStatusCode = findStatusCode;
|
||||
|
||||
function statusFromErrorMessage(errMsg) {
|
||||
// Find an appropriate statusCode based on message
|
||||
// jshint maxcomplexity:7
|
||||
var statusCode = 400;
|
||||
if ( -1 !== errMsg.indexOf('permission denied') ) {
|
||||
statusCode = 403;
|
||||
}
|
||||
else if ( -1 !== errMsg.indexOf('authentication failed') ) {
|
||||
statusCode = 403;
|
||||
}
|
||||
else if (errMsg.match(/Postgis Plugin.*[\s|\n].*column.*does not exist/)) {
|
||||
statusCode = 400;
|
||||
}
|
||||
else if ( -1 !== errMsg.indexOf('does not exist') ) {
|
||||
if ( -1 !== errMsg.indexOf(' role ') ) {
|
||||
statusCode = 403; // role 'xxx' does not exist
|
||||
} else if ( errMsg.match(/function .* does not exist/) ) {
|
||||
statusCode = 400; // invalid SQL (SQL function does not exist)
|
||||
} else {
|
||||
statusCode = 404;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return statusCode;
|
||||
}
|
||||
|
||||
function errorMessage(err) {
|
||||
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
|
||||
var message = (_.isString(err) ? err : err.message) || 'Unknown error';
|
||||
|
||||
return stripConnectionInfo(message);
|
||||
}
|
||||
|
||||
module.exports.errorMessage = errorMessage;
|
||||
|
||||
function stripConnectionInfo(message) {
|
||||
// Strip connection info, if any
|
||||
return message
|
||||
// See https://github.com/CartoDB/Windshaft/issues/173
|
||||
.replace(/Connection string: '[^']*'\n\s/im, '')
|
||||
// See https://travis-ci.org/CartoDB/Windshaft/jobs/20703062#L1644
|
||||
.replace(/is the server.*encountered/im, 'encountered');
|
||||
}
|
||||
|
||||
var ERROR_INFO_TO_EXPOSE = {
|
||||
message: true,
|
||||
layer: true,
|
||||
type: true,
|
||||
analysis: true,
|
||||
subtype: true
|
||||
};
|
||||
|
||||
function shouldBeExposed (prop) {
|
||||
return !!ERROR_INFO_TO_EXPOSE[prop];
|
||||
}
|
||||
|
||||
function errorMessageWithContext(err) {
|
||||
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
|
||||
var message = (_.isString(err) ? err : err.message) || 'Unknown error';
|
||||
|
||||
var error = {
|
||||
type: err.type || 'unknown',
|
||||
message: stripConnectionInfo(message),
|
||||
};
|
||||
|
||||
for (var prop in err) {
|
||||
// type & message are properties from Error's prototype and will be skipped
|
||||
if (err.hasOwnProperty(prop) && shouldBeExposed(prop)) {
|
||||
error[prop] = err[prop];
|
||||
}
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
30
lib/cartodb/middleware/lzma.js
Normal file
30
lib/cartodb/middleware/lzma.js
Normal file
@@ -0,0 +1,30 @@
|
||||
'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));
|
||||
}
|
||||
});
|
||||
};
|
||||
27
lib/cartodb/middleware/stats.js
Normal file
27
lib/cartodb/middleware/stats.js
Normal file
@@ -0,0 +1,27 @@
|
||||
const Profiler = require('../stats/profiler_proxy');
|
||||
const debug = require('debug')('windshaft:cartodb:stats');
|
||||
const onHeaders = require('on-headers');
|
||||
|
||||
module.exports = function statsMiddleware(options) {
|
||||
const { enabled = true, statsClient } = options;
|
||||
|
||||
return function stats(req, res, next) {
|
||||
req.profiler = new Profiler({
|
||||
statsd_client: statsClient,
|
||||
profile: enabled
|
||||
});
|
||||
|
||||
onHeaders(res, () => res.set('X-Tiler-Profiler', req.profiler.toJSONString()));
|
||||
|
||||
res.on('finish', () => {
|
||||
try {
|
||||
// May throw due to dns, see: http://github.com/CartoDB/Windshaft/issues/166
|
||||
req.profiler.sendStats();
|
||||
} catch (err) {
|
||||
debug("error sending profiling stats: " + err);
|
||||
}
|
||||
});
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
@@ -2,6 +2,7 @@ var CdbRequest = require('../models/cdb_request');
|
||||
var cdbRequest = new CdbRequest();
|
||||
|
||||
module.exports = function userMiddleware(req, res, next) {
|
||||
req.context.user = cdbRequest.userByReq(req);
|
||||
res.locals.user = cdbRequest.userByReq(req);
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
30
lib/cartodb/middleware/vector-error.js
Normal file
30
lib/cartodb/middleware/vector-error.js
Normal file
@@ -0,0 +1,30 @@
|
||||
const fs = require('fs');
|
||||
|
||||
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)) {
|
||||
res.set('Content-Type', 'application/x-protobuf');
|
||||
return res.status(429).send(timeoutErrorVectorTile);
|
||||
}
|
||||
}
|
||||
|
||||
next(err);
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -1,71 +1,178 @@
|
||||
var _ = require('underscore');
|
||||
var BaseWidget = require('./base');
|
||||
var debug = require('debug')('windshaft:widget:aggregation');
|
||||
const BaseDataview = require('./base');
|
||||
const debug = require('debug')('windshaft:dataview:aggregation');
|
||||
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
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` :
|
||||
''
|
||||
}
|
||||
)
|
||||
`;
|
||||
|
||||
var summaryQueryTpl = dot.template([
|
||||
'summary AS (',
|
||||
' SELECT',
|
||||
' count(1) AS count,',
|
||||
' sum(CASE WHEN {{=it._column}} IS NULL THEN 1 ELSE 0 END) AS nulls_count',
|
||||
' FROM ({{=it._query}}) _cdb_aggregation_nulls',
|
||||
')'
|
||||
].join('\n'));
|
||||
const summaryQueryTpl = ctx => `
|
||||
summary AS (
|
||||
SELECT
|
||||
count(1) AS count,
|
||||
sum(CASE WHEN ${ctx.column} IS NULL THEN 1 ELSE 0 END) AS nulls_count
|
||||
${ctx.isFloatColumn ? `,
|
||||
sum(
|
||||
CASE
|
||||
WHEN ${ctx.aggregationColumn} = 'infinity'::float OR ${ctx.aggregationColumn} = '-infinity'::float
|
||||
THEN 1
|
||||
ELSE 0
|
||||
END
|
||||
) AS infinities_count,
|
||||
sum(CASE WHEN ${ctx.aggregationColumn} = 'NaN'::float THEN 1 ELSE 0 END) AS nans_count` :
|
||||
''
|
||||
}
|
||||
FROM (${ctx.query}) _cdb_aggregation_nulls
|
||||
)
|
||||
`;
|
||||
|
||||
var rankedCategoriesQueryTpl = dot.template([
|
||||
'categories AS(',
|
||||
' SELECT {{=it._column}} AS category, {{=it._aggregation}} AS value,',
|
||||
' row_number() OVER (ORDER BY {{=it._aggregation}} desc) as rank',
|
||||
' FROM ({{=it._query}}) _cdb_aggregation_all',
|
||||
' {{?it._aggregationColumn!==null}}WHERE {{=it._aggregationColumn}} IS NOT NULL{{?}}',
|
||||
' GROUP BY {{=it._column}}',
|
||||
' ORDER BY 2 DESC',
|
||||
')'
|
||||
].join('\n'));
|
||||
const rankedCategoriesQueryTpl = ctx => `
|
||||
categories AS(
|
||||
SELECT
|
||||
${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` : ''}
|
||||
GROUP BY ${ctx.column}
|
||||
ORDER BY 2 DESC
|
||||
)
|
||||
`;
|
||||
|
||||
var categoriesSummaryMinMaxQueryTpl = dot.template([
|
||||
'categories_summary_min_max AS(',
|
||||
' SELECT max(value) max_val, min(value) min_val',
|
||||
' FROM categories',
|
||||
')'
|
||||
].join('\n'));
|
||||
const categoriesSummaryMinMaxQueryTpl = () => `
|
||||
categories_summary_min_max AS(
|
||||
SELECT
|
||||
max(value) max_val,
|
||||
min(value) min_val
|
||||
FROM categories
|
||||
)
|
||||
`;
|
||||
|
||||
var categoriesSummaryCountQueryTpl = dot.template([
|
||||
'categories_summary_count AS(',
|
||||
' SELECT count(1) AS categories_count',
|
||||
' FROM (',
|
||||
' SELECT {{=it._column}} AS category',
|
||||
' FROM ({{=it._query}}) _cdb_categories',
|
||||
' GROUP BY {{=it._column}}',
|
||||
' ) _cdb_categories_count',
|
||||
')'
|
||||
].join('\n'));
|
||||
const categoriesSummaryCountQueryTpl = ctx => `
|
||||
categories_summary_count AS(
|
||||
SELECT count(1) AS categories_count
|
||||
FROM (
|
||||
SELECT ${ctx.column} AS category
|
||||
FROM (${ctx.query}) _cdb_categories
|
||||
GROUP BY ${ctx.column}
|
||||
) _cdb_categories_count
|
||||
)
|
||||
`;
|
||||
|
||||
var rankedAggregationQueryTpl = dot.template([
|
||||
'SELECT CAST(category AS text), value, false as agg, nulls_count, min_val, max_val, count, categories_count',
|
||||
' FROM categories, summary, categories_summary_min_max, categories_summary_count',
|
||||
' WHERE rank < {{=it._limit}}',
|
||||
'UNION ALL',
|
||||
'SELECT \'Other\' category, {{=it._aggregationFn}}(value) as value, true as agg, nulls_count, min_val, max_val,',
|
||||
' count, categories_count',
|
||||
' FROM categories, summary, categories_summary_min_max, categories_summary_count',
|
||||
' WHERE rank >= {{=it._limit}}',
|
||||
'GROUP BY nulls_count, min_val, max_val, count, categories_count'
|
||||
].join('\n'));
|
||||
const specialNumericValuesColumns = () => `, nans_count, infinities_count`;
|
||||
|
||||
var aggregationQueryTpl = dot.template([
|
||||
'SELECT CAST({{=it._column}} AS text) AS category, {{=it._aggregation}} AS value, false as agg,',
|
||||
' nulls_count, min_val, max_val, count, categories_count',
|
||||
'FROM ({{=it._query}}) _cdb_aggregation_all, summary, categories_summary_min_max, categories_summary_count',
|
||||
'GROUP BY category, nulls_count, min_val, max_val, count, categories_count',
|
||||
'ORDER BY value DESC'
|
||||
].join('\n'));
|
||||
const rankedAggregationQueryTpl = ctx => `
|
||||
SELECT
|
||||
CAST(category AS text),
|
||||
value,
|
||||
false as agg,
|
||||
nulls_count,
|
||||
min_val,
|
||||
max_val,
|
||||
count,
|
||||
categories_count
|
||||
${ctx.isFloatColumn ? `${specialNumericValuesColumns(ctx)}` : '' }
|
||||
FROM categories, summary, categories_summary_min_max, categories_summary_count
|
||||
WHERE rank < ${ctx.limit}
|
||||
UNION ALL
|
||||
SELECT
|
||||
'Other' category,
|
||||
${ctx.aggregation !== 'count' ? ctx.aggregation : 'sum'}(value) as value,
|
||||
true as agg,
|
||||
nulls_count,
|
||||
min_val,
|
||||
max_val,
|
||||
count,
|
||||
categories_count
|
||||
${ctx.isFloatColumn ? `${specialNumericValuesColumns(ctx)}` : '' }
|
||||
FROM categories, summary, categories_summary_min_max, categories_summary_count
|
||||
WHERE rank >= ${ctx.limit}
|
||||
GROUP BY
|
||||
nulls_count,
|
||||
min_val,
|
||||
max_val,
|
||||
count,
|
||||
categories_count
|
||||
${ctx.isFloatColumn ? `${specialNumericValuesColumns(ctx)}` : '' }
|
||||
`;
|
||||
|
||||
var CATEGORIES_LIMIT = 6;
|
||||
const aggregationQueryTpl = ctx => `
|
||||
SELECT
|
||||
CAST(${ctx.column} AS text) AS category,
|
||||
${ctx.aggregationFn} AS value,
|
||||
false as agg,
|
||||
nulls_count,
|
||||
min_val,
|
||||
max_val,
|
||||
count,
|
||||
categories_count
|
||||
${ctx.isFloatColumn ? `${specialNumericValuesColumns(ctx)}` : '' }
|
||||
FROM (${ctx.query}) _cdb_aggregation_all, summary, categories_summary_min_max, categories_summary_count
|
||||
GROUP BY
|
||||
category,
|
||||
nulls_count,
|
||||
min_val,
|
||||
max_val,
|
||||
count,
|
||||
categories_count
|
||||
${ctx.isFloatColumn ? `${specialNumericValuesColumns(ctx)}` : '' }
|
||||
ORDER BY value DESC
|
||||
`;
|
||||
|
||||
var VALID_OPERATIONS = {
|
||||
const aggregationFnQueryTpl = ctx => `${ctx.aggregation}(${ctx.aggregationColumn})`;
|
||||
|
||||
const aggregationDataviewQueryTpl = ctx => `
|
||||
WITH
|
||||
${filteredQueryTpl(ctx)},
|
||||
${summaryQueryTpl(ctx)},
|
||||
${rankedCategoriesQueryTpl(ctx)},
|
||||
${categoriesSummaryMinMaxQueryTpl(ctx)},
|
||||
${categoriesSummaryCountQueryTpl(ctx)}
|
||||
${!!ctx.override.ownFilter ? `${aggregationQueryTpl(ctx)}` : `${rankedAggregationQueryTpl(ctx)}`}
|
||||
`;
|
||||
|
||||
const filterCategoriesQueryTpl = ctx => `
|
||||
SELECT
|
||||
${ctx.column} AS category,
|
||||
${ctx.value} AS value
|
||||
FROM (${ctx.query}) _cdb_aggregation_search
|
||||
WHERE CAST(${ctx.column} as text) ILIKE ${ctx.userQuery}
|
||||
GROUP BY ${ctx.column}
|
||||
`;
|
||||
|
||||
const searchQueryTpl = ctx => `
|
||||
WITH
|
||||
search_unfiltered AS (
|
||||
${ctx.searchUnfiltered}
|
||||
),
|
||||
search_filtered AS (
|
||||
${ctx.searchFiltered}
|
||||
),
|
||||
search_union AS (
|
||||
SELECT * FROM search_unfiltered
|
||||
UNION ALL
|
||||
SELECT * FROM search_filtered
|
||||
)
|
||||
SELECT category, sum(value) AS value
|
||||
FROM search_union
|
||||
GROUP BY category
|
||||
ORDER BY value desc
|
||||
`;
|
||||
|
||||
const CATEGORIES_LIMIT = 6;
|
||||
|
||||
const VALID_OPERATIONS = {
|
||||
count: [],
|
||||
sum: ['aggregationColumn'],
|
||||
avg: ['aggregationColumn'],
|
||||
@@ -73,7 +180,7 @@ var VALID_OPERATIONS = {
|
||||
max: ['aggregationColumn']
|
||||
};
|
||||
|
||||
var TYPE = 'aggregation';
|
||||
const TYPE = 'aggregation';
|
||||
|
||||
/**
|
||||
{
|
||||
@@ -84,210 +191,154 @@ var TYPE = 'aggregation';
|
||||
}
|
||||
}
|
||||
*/
|
||||
function Aggregation(query, options) {
|
||||
if (!_.isString(options.column)) {
|
||||
throw new Error('Aggregation expects `column` in widget options');
|
||||
module.exports = class Aggregation extends BaseDataview {
|
||||
constructor (query, options = {}, queries = {}) {
|
||||
super();
|
||||
|
||||
this._checkOptions(options);
|
||||
|
||||
this.query = query;
|
||||
this.queries = queries;
|
||||
this.column = options.column;
|
||||
this.aggregation = options.aggregation;
|
||||
this.aggregationColumn = options.aggregationColumn;
|
||||
this._isFloatColumn = null;
|
||||
}
|
||||
|
||||
if (!_.isString(options.aggregation)) {
|
||||
throw new Error('Aggregation expects `aggregation` operation in widget options');
|
||||
}
|
||||
|
||||
if (!VALID_OPERATIONS[options.aggregation]) {
|
||||
throw new Error("Aggregation does not support '" + options.aggregation + "' operation");
|
||||
}
|
||||
|
||||
var requiredOptions = VALID_OPERATIONS[options.aggregation];
|
||||
var missingOptions = _.difference(requiredOptions, Object.keys(options));
|
||||
if (missingOptions.length > 0) {
|
||||
throw new Error(
|
||||
"Aggregation '" + options.aggregation + "' is missing some options: " + missingOptions.join(',')
|
||||
);
|
||||
}
|
||||
|
||||
BaseWidget.apply(this);
|
||||
|
||||
this.query = query;
|
||||
this.column = options.column;
|
||||
this.aggregation = options.aggregation;
|
||||
this.aggregationColumn = options.aggregationColumn;
|
||||
}
|
||||
|
||||
Aggregation.prototype = new BaseWidget();
|
||||
Aggregation.prototype.constructor = Aggregation;
|
||||
|
||||
module.exports = Aggregation;
|
||||
|
||||
Aggregation.prototype.sql = function(psql, override, callback) {
|
||||
if (!callback) {
|
||||
callback = override;
|
||||
override = {};
|
||||
}
|
||||
|
||||
var _query = this.query;
|
||||
|
||||
var aggregationSql;
|
||||
|
||||
if (!!override.ownFilter) {
|
||||
aggregationSql = [
|
||||
this.getCategoriesCTESql(_query, this.column, this.aggregation, this.aggregationColumn),
|
||||
aggregationQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column,
|
||||
_aggregation: this.getAggregationSql(),
|
||||
_limit: CATEGORIES_LIMIT
|
||||
})
|
||||
].join('\n');
|
||||
} else {
|
||||
aggregationSql = [
|
||||
this.getCategoriesCTESql(_query, this.column, this.aggregation, this.aggregationColumn),
|
||||
rankedAggregationQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column,
|
||||
_aggregationFn: this.aggregation !== 'count' ? this.aggregation : 'sum',
|
||||
_limit: CATEGORIES_LIMIT
|
||||
})
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
debug(aggregationSql);
|
||||
|
||||
return callback(null, aggregationSql);
|
||||
};
|
||||
|
||||
Aggregation.prototype.getCategoriesCTESql = function(query, column, aggregation, aggregationColumn) {
|
||||
return [
|
||||
"WITH",
|
||||
[
|
||||
summaryQueryTpl({
|
||||
_query: query,
|
||||
_column: column
|
||||
}),
|
||||
rankedCategoriesQueryTpl({
|
||||
_query: query,
|
||||
_column: column,
|
||||
_aggregation: this.getAggregationSql(),
|
||||
_aggregationColumn: aggregation !== 'count' ? aggregationColumn : null
|
||||
}),
|
||||
categoriesSummaryMinMaxQueryTpl({
|
||||
_query: query,
|
||||
_column: column
|
||||
}),
|
||||
categoriesSummaryCountQueryTpl({
|
||||
_query: query,
|
||||
_column: column
|
||||
})
|
||||
].join(',\n')
|
||||
].join('\n');
|
||||
};
|
||||
|
||||
var aggregationFnQueryTpl = dot.template('{{=it._aggregationFn}}({{=it._aggregationColumn}})');
|
||||
Aggregation.prototype.getAggregationSql = function() {
|
||||
return aggregationFnQueryTpl({
|
||||
_aggregationFn: this.aggregation,
|
||||
_aggregationColumn: this.aggregationColumn || 1
|
||||
});
|
||||
};
|
||||
|
||||
Aggregation.prototype.format = function(result) {
|
||||
var categories = [];
|
||||
var count = 0;
|
||||
var nulls = 0;
|
||||
var minValue = 0;
|
||||
var maxValue = 0;
|
||||
var categoriesCount = 0;
|
||||
|
||||
|
||||
if (result.rows.length) {
|
||||
var firstRow = result.rows[0];
|
||||
count = firstRow.count;
|
||||
nulls = firstRow.nulls_count;
|
||||
minValue = firstRow.min_val;
|
||||
maxValue = firstRow.max_val;
|
||||
categoriesCount = firstRow.categories_count;
|
||||
|
||||
result.rows.forEach(function(row) {
|
||||
categories.push(_.omit(row, 'count', 'nulls_count', 'min_val', 'max_val', 'categories_count'));
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
aggregation: this.aggregation,
|
||||
count: count,
|
||||
nulls: nulls,
|
||||
min: minValue,
|
||||
max: maxValue,
|
||||
categoriesCount: categoriesCount,
|
||||
categories: categories
|
||||
};
|
||||
};
|
||||
|
||||
var filterCategoriesQueryTpl = dot.template([
|
||||
'SELECT {{=it._column}} AS category, {{=it._value}} AS value',
|
||||
'FROM ({{=it._query}}) _cdb_aggregation_search',
|
||||
'WHERE CAST({{=it._column}} as text) ILIKE {{=it._userQuery}}',
|
||||
'GROUP BY {{=it._column}}'
|
||||
].join('\n'));
|
||||
|
||||
var searchQueryTpl = dot.template([
|
||||
'WITH',
|
||||
'search_unfiltered AS (',
|
||||
' {{=it._searchUnfiltered}}',
|
||||
'),',
|
||||
'search_filtered AS (',
|
||||
' {{=it._searchFiltered}}',
|
||||
'),',
|
||||
'search_union AS (',
|
||||
' SELECT * FROM search_unfiltered',
|
||||
' UNION ALL',
|
||||
' SELECT * FROM search_filtered',
|
||||
')',
|
||||
'SELECT category, sum(value) AS value',
|
||||
'FROM search_union',
|
||||
'GROUP BY category',
|
||||
'ORDER BY value desc'
|
||||
].join('\n'));
|
||||
|
||||
|
||||
Aggregation.prototype.search = function(psql, userQuery, callback) {
|
||||
var self = this;
|
||||
|
||||
var _userQuery = psql.escapeLiteral('%' + userQuery + '%');
|
||||
|
||||
// TODO unfiltered will be wrong as filters are already applied at this point
|
||||
var query = searchQueryTpl({
|
||||
_searchUnfiltered: filterCategoriesQueryTpl({
|
||||
_query: this.query,
|
||||
_column: this.column,
|
||||
_value: '0',
|
||||
_userQuery: _userQuery
|
||||
}),
|
||||
_searchFiltered: filterCategoriesQueryTpl({
|
||||
_query: this.query,
|
||||
_column: this.column,
|
||||
_value: 'count(1)',
|
||||
_userQuery: _userQuery
|
||||
})
|
||||
});
|
||||
|
||||
psql.query(query, function(err, result) {
|
||||
if (err) {
|
||||
return callback(err, result);
|
||||
_checkOptions (options) {
|
||||
if (typeof options.column !== 'string') {
|
||||
throw new Error(`Aggregation expects 'column' in dataview options`);
|
||||
}
|
||||
|
||||
return callback(null, {type: self.getType(), categories: result.rows });
|
||||
}, true); // use read-only transaction
|
||||
};
|
||||
if (typeof options.aggregation !== 'string') {
|
||||
throw new Error(`Aggregation expects 'aggregation' operation in dataview options`);
|
||||
}
|
||||
|
||||
Aggregation.prototype.getType = function() {
|
||||
return TYPE;
|
||||
};
|
||||
if (!VALID_OPERATIONS[options.aggregation]) {
|
||||
throw new Error(`Aggregation does not support '${options.aggregation}' operation`);
|
||||
}
|
||||
|
||||
Aggregation.prototype.toString = function() {
|
||||
return JSON.stringify({
|
||||
_type: TYPE,
|
||||
_query: this.query,
|
||||
_column: this.column,
|
||||
_aggregation: this.aggregation
|
||||
});
|
||||
const requiredOptions = VALID_OPERATIONS[options.aggregation];
|
||||
const missingOptions = requiredOptions.filter(requiredOption => !options.hasOwnProperty(requiredOption));
|
||||
|
||||
if (missingOptions.length > 0) {
|
||||
throw new Error(
|
||||
`Aggregation '${options.aggregation}' is missing some options: ${missingOptions.join(',')}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
sql (psql, override, callback) {
|
||||
if (!callback) {
|
||||
callback = override;
|
||||
override = {};
|
||||
}
|
||||
|
||||
if (this._shouldCheckColumnType()) {
|
||||
this._isFloatColumn = false;
|
||||
this.getColumnType(psql, this.aggregationColumn, this.queries.no_filters, (err, type) => {
|
||||
if (!err && !!type) {
|
||||
this._isFloatColumn = type.float;
|
||||
}
|
||||
this.sql(psql, override, callback);
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
const limit = Number.isFinite(override.categories) && override.categories > 0 ?
|
||||
override.categories :
|
||||
CATEGORIES_LIMIT;
|
||||
|
||||
const aggregationSql = aggregationDataviewQueryTpl({
|
||||
override: override,
|
||||
query: this.query,
|
||||
column: this.column,
|
||||
aggregation: this.aggregation,
|
||||
aggregationColumn: this.aggregation !== 'count' ? this.aggregationColumn : null,
|
||||
aggregationFn: aggregationFnQueryTpl({
|
||||
aggregation: this.aggregation,
|
||||
aggregationColumn: this.aggregationColumn || 1
|
||||
}),
|
||||
isFloatColumn: this._isFloatColumn,
|
||||
limit
|
||||
});
|
||||
|
||||
debug(aggregationSql);
|
||||
|
||||
return callback(null, aggregationSql);
|
||||
}
|
||||
|
||||
_shouldCheckColumnType () {
|
||||
return this.aggregationColumn && this._isFloatColumn === null;
|
||||
}
|
||||
|
||||
format (result) {
|
||||
const {
|
||||
count = 0,
|
||||
nulls_count = 0,
|
||||
nans_count = 0,
|
||||
infinities_count = 0,
|
||||
min_val = 0,
|
||||
max_val = 0,
|
||||
categories_count = 0
|
||||
} = result.rows[0] || {};
|
||||
|
||||
return {
|
||||
aggregation: this.aggregation,
|
||||
count: count,
|
||||
nulls: nulls_count,
|
||||
nans: nans_count,
|
||||
infinities: infinities_count,
|
||||
min: min_val,
|
||||
max: max_val,
|
||||
categoriesCount: categories_count,
|
||||
categories: result.rows.map(({ category, value, agg }) => ({ category, value, agg }))
|
||||
};
|
||||
}
|
||||
|
||||
search (psql, userQuery, callback) {
|
||||
const escapedUserQuery = psql.escapeLiteral(`%${userQuery}%`);
|
||||
const value = this.aggregation !== 'count' && this.aggregationColumn ?
|
||||
`${this.aggregation}(${this.aggregationColumn})` :
|
||||
'count(1)';
|
||||
|
||||
// TODO unfiltered will be wrong as filters are already applied at this point
|
||||
const query = searchQueryTpl({
|
||||
searchUnfiltered: filterCategoriesQueryTpl({
|
||||
query: this.query,
|
||||
column: this.column,
|
||||
value: '0',
|
||||
userQuery: escapedUserQuery
|
||||
}),
|
||||
searchFiltered: filterCategoriesQueryTpl({
|
||||
query: this.query,
|
||||
column: this.column,
|
||||
value: value,
|
||||
userQuery: escapedUserQuery
|
||||
})
|
||||
});
|
||||
|
||||
debug(query);
|
||||
|
||||
psql.query(query, (err, result) => {
|
||||
if (err) {
|
||||
return callback(err, result);
|
||||
}
|
||||
|
||||
return callback(null, {type: this.getType(), categories: result.rows });
|
||||
}, true); // use read-only transaction
|
||||
}
|
||||
|
||||
getType () {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
toString () {
|
||||
return JSON.stringify({
|
||||
_type: TYPE,
|
||||
_query: this.query,
|
||||
_column: this.column,
|
||||
_aggregation: this.aggregation
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,26 +1,59 @@
|
||||
function BaseDataview() {}
|
||||
const FLOAT_OIDS = {
|
||||
700: true,
|
||||
701: true,
|
||||
1700: true
|
||||
};
|
||||
|
||||
module.exports = BaseDataview;
|
||||
const DATE_OIDS = {
|
||||
1082: true,
|
||||
1114: true,
|
||||
1184: true
|
||||
};
|
||||
|
||||
BaseDataview.prototype.getResult = function(psql, override, callback) {
|
||||
var self = this;
|
||||
this.sql(psql, override, function(err, query) {
|
||||
psql.query(query, function(err, result) {
|
||||
const columnTypeQueryTpl = ctx => `SELECT pg_typeof(${ctx.column})::oid FROM (${ctx.query}) _cdb_column_type limit 1`;
|
||||
|
||||
function getPGTypeName (pgType) {
|
||||
return {
|
||||
float: FLOAT_OIDS.hasOwnProperty(pgType),
|
||||
date: DATE_OIDS.hasOwnProperty(pgType)
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = class BaseDataview {
|
||||
getResult (psql, override, callback) {
|
||||
this.sql(psql, override, (err, query) => {
|
||||
if (err) {
|
||||
return callback(err, result);
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
result = self.format(result, override);
|
||||
result.type = self.getType();
|
||||
psql.query(query, (err, result) => {
|
||||
if (err) {
|
||||
return callback(err, result);
|
||||
}
|
||||
|
||||
return callback(null, result);
|
||||
result = this.format(result, override);
|
||||
result.type = this.getType();
|
||||
|
||||
}, true); // use read-only transaction
|
||||
});
|
||||
return callback(null, result);
|
||||
|
||||
};
|
||||
|
||||
BaseDataview.prototype.search = function(psql, userQuery, callback) {
|
||||
return callback(null, this.format({ rows: [] }));
|
||||
}, true); // use read-only transaction
|
||||
});
|
||||
}
|
||||
|
||||
search (psql, userQuery, callback) {
|
||||
return callback(null, this.format({ rows: [] }));
|
||||
}
|
||||
|
||||
getColumnType (psql, column, query, callback) {
|
||||
const readOnlyTransaction = true;
|
||||
const columnTypeQuery = columnTypeQueryTpl({ column, query });
|
||||
|
||||
psql.query(columnTypeQuery, (err, result) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
const pgType = result.rows[0].pg_typeof;
|
||||
callback(null, getPGTypeName(pgType));
|
||||
}, readOnlyTransaction);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
var dataviews = require('./');
|
||||
const dataviews = require('./');
|
||||
|
||||
var DataviewFactory = {
|
||||
dataviews: Object.keys(dataviews).reduce(function(allDataviews, dataviewClassName) {
|
||||
allDataviews[dataviewClassName.toLowerCase()] = dataviews[dataviewClassName];
|
||||
return allDataviews;
|
||||
}, {}),
|
||||
module.exports = class DataviewFactory {
|
||||
static get dataviews() {
|
||||
return Object.keys(dataviews).reduce((allDataviews, dataviewClassName) => {
|
||||
allDataviews[dataviewClassName.toLowerCase()] = dataviews[dataviewClassName];
|
||||
return allDataviews;
|
||||
}, {});
|
||||
}
|
||||
|
||||
static getDataview (query, dataviewDefinition) {
|
||||
const { type, options, sql } = dataviewDefinition;
|
||||
|
||||
getDataview: function(query, dataviewDefinition) {
|
||||
var type = dataviewDefinition.type;
|
||||
if (!this.dataviews[type]) {
|
||||
throw new Error('Invalid dataview type: "' + type + '"');
|
||||
}
|
||||
return new this.dataviews[type](query, dataviewDefinition.options, dataviewDefinition.sql);
|
||||
|
||||
return new this.dataviews[type](query, options, sql);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = DataviewFactory;
|
||||
|
||||
@@ -1,18 +1,36 @@
|
||||
var _ = require('underscore');
|
||||
var BaseWidget = require('./base');
|
||||
var debug = require('debug')('windshaft:widget:formula');
|
||||
const BaseDataview = require('./base');
|
||||
const debug = require('debug')('windshaft:dataview:formula');
|
||||
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
const countInfinitiesQueryTpl = ctx => `
|
||||
SELECT count(1) FROM (${ctx.query}) __cdb_formula_infinities
|
||||
WHERE ${ctx.column} = 'infinity'::float OR ${ctx.column} = '-infinity'::float
|
||||
`;
|
||||
|
||||
var formulaQueryTpl = dot.template([
|
||||
'SELECT',
|
||||
'{{=it._operation}}({{=it._column}}) AS result,',
|
||||
'(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls WHERE {{=it._column}} IS NULL) AS nulls_count',
|
||||
'FROM ({{=it._query}}) _cdb_formula'
|
||||
].join('\n'));
|
||||
const countNansQueryTpl = ctx => `
|
||||
SELECT count(1) FROM (${ctx.query}) __cdb_formula_nans
|
||||
WHERE ${ctx.column} = 'NaN'::float
|
||||
`;
|
||||
|
||||
var VALID_OPERATIONS = {
|
||||
const filterOutSpecialNumericValuesTpl = ctx => `
|
||||
WHERE
|
||||
${ctx.column} != 'infinity'::float
|
||||
AND
|
||||
${ctx.column} != '-infinity'::float
|
||||
AND
|
||||
${ctx.column} != 'NaN'::float
|
||||
`;
|
||||
|
||||
const formulaQueryTpl = ctx => `
|
||||
SELECT
|
||||
${ctx.operation}(${ctx.column}) AS result,
|
||||
(SELECT count(1) FROM (${ctx.query}) _cdb_formula_nulls WHERE ${ctx.column} IS NULL) AS nulls_count
|
||||
${ctx.isFloatColumn ? `,(${countInfinitiesQueryTpl(ctx)}) AS infinities_count` : ''}
|
||||
${ctx.isFloatColumn ? `,(${countNansQueryTpl(ctx)}) AS nans_count` : ''}
|
||||
FROM (${ctx.query}) __cdb_formula
|
||||
${ctx.isFloatColumn && ctx.operation !== 'count' ? `${filterOutSpecialNumericValuesTpl(ctx)}` : ''}
|
||||
`;
|
||||
|
||||
const VALID_OPERATIONS = {
|
||||
count: true,
|
||||
avg: true,
|
||||
sum: true,
|
||||
@@ -20,7 +38,7 @@ var VALID_OPERATIONS = {
|
||||
max: true
|
||||
};
|
||||
|
||||
var TYPE = 'formula';
|
||||
const TYPE = 'formula';
|
||||
|
||||
/**
|
||||
{
|
||||
@@ -31,74 +49,90 @@ var TYPE = 'formula';
|
||||
}
|
||||
}
|
||||
*/
|
||||
function Formula(query, options) {
|
||||
if (!_.isString(options.operation)) {
|
||||
throw new Error('Formula expects `operation` in widget options');
|
||||
module.exports = class Formula extends BaseDataview {
|
||||
constructor (query, options = {}, queries = {}) {
|
||||
super();
|
||||
|
||||
this._checkOptions(options);
|
||||
|
||||
this.query = query;
|
||||
this.queries = queries;
|
||||
this.column = options.column || '1';
|
||||
this.operation = options.operation;
|
||||
this._isFloatColumn = null;
|
||||
}
|
||||
|
||||
if (!VALID_OPERATIONS[options.operation]) {
|
||||
throw new Error("Formula does not support '" + options.operation + "' operation");
|
||||
_checkOptions (options) {
|
||||
if (typeof options.operation !== 'string') {
|
||||
throw new Error(`Formula expects 'operation' in dataview options`);
|
||||
}
|
||||
|
||||
if (!VALID_OPERATIONS[options.operation]) {
|
||||
throw new Error(`Formula does not support '${options.operation}' operation`);
|
||||
}
|
||||
|
||||
if (options.operation !== 'count' && typeof options.column !== 'string') {
|
||||
throw new Error(`Formula expects 'column' in dataview options`);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.operation !== 'count' && !_.isString(options.column)) {
|
||||
throw new Error('Formula expects `column` in widget options');
|
||||
|
||||
sql (psql, override, callback) {
|
||||
if (!callback) {
|
||||
callback = override;
|
||||
override = {};
|
||||
}
|
||||
|
||||
if (this._isFloatColumn === null) {
|
||||
this._isFloatColumn = false;
|
||||
this.getColumnType(psql, this.column, this.queries.no_filters, (err, type) => {
|
||||
if (!err && !!type) {
|
||||
this._isFloatColumn = type.float;
|
||||
}
|
||||
this.sql(psql, override, callback);
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
const formulaSql = formulaQueryTpl({
|
||||
isFloatColumn: this._isFloatColumn,
|
||||
query: this.query,
|
||||
operation: this.operation,
|
||||
column: this.column
|
||||
});
|
||||
|
||||
debug(formulaSql);
|
||||
|
||||
return callback(null, formulaSql);
|
||||
}
|
||||
|
||||
BaseWidget.apply(this);
|
||||
format (res) {
|
||||
const {
|
||||
result = 0,
|
||||
nulls_count = 0,
|
||||
nans_count,
|
||||
infinities_count
|
||||
} = res.rows[0] || {};
|
||||
|
||||
this.query = query;
|
||||
this.column = options.column || '1';
|
||||
this.operation = options.operation;
|
||||
}
|
||||
|
||||
Formula.prototype = new BaseWidget();
|
||||
Formula.prototype.constructor = Formula;
|
||||
|
||||
module.exports = Formula;
|
||||
|
||||
Formula.prototype.sql = function(psql, override, callback) {
|
||||
if (!callback) {
|
||||
callback = override;
|
||||
override = {};
|
||||
return {
|
||||
operation: this.operation,
|
||||
result,
|
||||
nulls: nulls_count,
|
||||
nans: nans_count,
|
||||
infinities: infinities_count
|
||||
};
|
||||
}
|
||||
|
||||
var _query = this.query;
|
||||
var formulaSql = formulaQueryTpl({
|
||||
_query: _query,
|
||||
_operation: this.operation,
|
||||
_column: this.column
|
||||
});
|
||||
getType () {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
debug(formulaSql);
|
||||
|
||||
return callback(null, formulaSql);
|
||||
};
|
||||
|
||||
Formula.prototype.format = function(result) {
|
||||
var formattedResult = {
|
||||
operation: this.operation,
|
||||
result: 0,
|
||||
nulls: 0
|
||||
};
|
||||
|
||||
if (result.rows.length) {
|
||||
formattedResult.operation = this.operation;
|
||||
formattedResult.result = result.rows[0].result;
|
||||
formattedResult.nulls = result.rows[0].nulls_count;
|
||||
}
|
||||
|
||||
return formattedResult;
|
||||
};
|
||||
|
||||
Formula.prototype.getType = function() {
|
||||
return TYPE;
|
||||
};
|
||||
|
||||
Formula.prototype.toString = function() {
|
||||
return JSON.stringify({
|
||||
_type: TYPE,
|
||||
_query: this.query,
|
||||
_column: this.column,
|
||||
_operation: this.operation
|
||||
});
|
||||
toString () {
|
||||
return JSON.stringify({
|
||||
_type: TYPE,
|
||||
_query: this.query,
|
||||
_column: this.column,
|
||||
_operation: this.operation
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,308 +1,72 @@
|
||||
var _ = require('underscore');
|
||||
var BaseWidget = require('./base');
|
||||
var debug = require('debug')('windshaft:dataview:histogram');
|
||||
const debug = require('debug')('windshaft:dataview:histogram');
|
||||
const NumericHistogram = require('./histograms/numeric-histogram');
|
||||
const DateHistogram = require('./histograms/date-histogram');
|
||||
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
const DATE_HISTOGRAM = 'DateHistogram';
|
||||
const NUMERIC_HISTOGRAM = 'NumericHistogram';
|
||||
|
||||
var columnTypeQueryTpl = dot.template(
|
||||
'SELECT pg_typeof({{=it.column}})::oid FROM ({{=it.query}}) _cdb_histogram_column_type limit 1'
|
||||
);
|
||||
var columnCastTpl = dot.template("date_part('epoch', {{=it.column}})");
|
||||
module.exports = class Histogram {
|
||||
constructor (query, options, queries) {
|
||||
this.query = query;
|
||||
this.options = options || {};
|
||||
this.queries = queries;
|
||||
|
||||
var BIN_MIN_NUMBER = 6;
|
||||
var BIN_MAX_NUMBER = 48;
|
||||
|
||||
var basicsQueryTpl = dot.template([
|
||||
'basics AS (',
|
||||
' SELECT',
|
||||
' max({{=it._column}}) AS max_val, min({{=it._column}}) AS min_val,',
|
||||
' avg({{=it._column}}) AS avg_val, count(1) AS total_rows',
|
||||
' FROM ({{=it._query}}) _cdb_basics',
|
||||
')'
|
||||
].join(' \n'));
|
||||
|
||||
var overrideBasicsQueryTpl = dot.template([
|
||||
'basics AS (',
|
||||
' SELECT',
|
||||
' max({{=it._end}}) AS max_val, min({{=it._start}}) AS min_val,',
|
||||
' avg({{=it._column}}) AS avg_val, count(1) AS total_rows',
|
||||
' FROM ({{=it._query}}) _cdb_basics',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var iqrQueryTpl = dot.template([
|
||||
'iqrange AS (',
|
||||
' SELECT max(quartile_max) - min(quartile_max) AS iqr',
|
||||
' FROM (',
|
||||
' SELECT quartile, max(_cdb_iqr_column) AS quartile_max from (',
|
||||
' SELECT {{=it._column}} AS _cdb_iqr_column, ntile(4) over (order by {{=it._column}}',
|
||||
' ) AS quartile',
|
||||
' FROM ({{=it._query}}) _cdb_rank) _cdb_quartiles',
|
||||
' WHERE quartile = 1 or quartile = 3',
|
||||
' GROUP BY quartile',
|
||||
' ) _cdb_iqr',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var binsQueryTpl = dot.template([
|
||||
'bins AS (',
|
||||
' SELECT CASE WHEN total_rows = 0 OR iqr = 0',
|
||||
' THEN 1',
|
||||
' ELSE GREATEST(',
|
||||
' LEAST({{=it._minBins}}, CAST(total_rows AS INT)),',
|
||||
' LEAST(',
|
||||
' CAST(((max_val - min_val) / (2 * iqr * power(total_rows, 1/3))) AS INT),',
|
||||
' {{=it._maxBins}}',
|
||||
' )',
|
||||
' )',
|
||||
' END AS bins_number',
|
||||
' FROM basics, iqrange, ({{=it._query}}) _cdb_bins',
|
||||
' LIMIT 1',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var overrideBinsQueryTpl = dot.template([
|
||||
'bins AS (',
|
||||
' SELECT {{=it._bins}} AS bins_number',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var nullsQueryTpl = dot.template([
|
||||
'nulls AS (',
|
||||
' SELECT',
|
||||
' count(*) AS nulls_count',
|
||||
' FROM ({{=it._query}}) _cdb_histogram_nulls',
|
||||
' WHERE {{=it._column}} IS NULL',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var histogramQueryTpl = dot.template([
|
||||
'SELECT',
|
||||
' (max_val - min_val) / cast(bins_number as float) AS bin_width,',
|
||||
' bins_number,',
|
||||
' nulls_count,',
|
||||
' avg_val,',
|
||||
' CASE WHEN min_val = max_val',
|
||||
' THEN 0',
|
||||
' ELSE GREATEST(1, LEAST(WIDTH_BUCKET({{=it._column}}, min_val, max_val, bins_number), bins_number)) - 1',
|
||||
' END AS bin,',
|
||||
' min({{=it._column}})::numeric AS min,',
|
||||
' max({{=it._column}})::numeric AS max,',
|
||||
' avg({{=it._column}})::numeric AS avg,',
|
||||
' count(*) AS freq',
|
||||
'FROM ({{=it._query}}) _cdb_histogram, basics, nulls, bins',
|
||||
'WHERE {{=it._column}} IS NOT NULL',
|
||||
'GROUP BY bin, bins_number, bin_width, nulls_count, avg_val',
|
||||
'ORDER BY bin'
|
||||
].join('\n'));
|
||||
|
||||
|
||||
var TYPE = 'histogram';
|
||||
|
||||
/**
|
||||
{
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'name',
|
||||
bins: 10 // OPTIONAL
|
||||
}
|
||||
}
|
||||
*/
|
||||
function Histogram(query, options, queries) {
|
||||
if (!_.isString(options.column)) {
|
||||
throw new Error('Histogram expects `column` in widget options');
|
||||
this.histogramImplementation = this._getHistogramImplementation();
|
||||
}
|
||||
|
||||
this.query = query;
|
||||
this.queries = queries;
|
||||
this.column = options.column;
|
||||
this.bins = options.bins;
|
||||
_getHistogramImplementation (override) {
|
||||
let implementation = null;
|
||||
|
||||
this._columnType = null;
|
||||
}
|
||||
|
||||
Histogram.prototype = new BaseWidget();
|
||||
Histogram.prototype.constructor = Histogram;
|
||||
|
||||
module.exports = Histogram;
|
||||
|
||||
var DATE_OIDS = {
|
||||
1082: true,
|
||||
1114: true,
|
||||
1184: true
|
||||
};
|
||||
|
||||
Histogram.prototype.sql = function(psql, override, callback) {
|
||||
if (!callback) {
|
||||
callback = override;
|
||||
override = {};
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
var _column = this.column;
|
||||
|
||||
var columnTypeQuery = columnTypeQueryTpl({
|
||||
column: _column, query: this.queries.no_filters
|
||||
});
|
||||
|
||||
if (this._columnType === null) {
|
||||
psql.query(columnTypeQuery, function(err, result) {
|
||||
// assume numeric, will fail later
|
||||
self._columnType = 'numeric';
|
||||
if (!err && !!result.rows[0]) {
|
||||
var pgType = result.rows[0].pg_typeof;
|
||||
if (DATE_OIDS.hasOwnProperty(pgType)) {
|
||||
self._columnType = 'date';
|
||||
}
|
||||
}
|
||||
self.sql(psql, override, callback);
|
||||
}, true); // use read-only transaction
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this._columnType === 'date') {
|
||||
_column = columnCastTpl({column: _column});
|
||||
}
|
||||
|
||||
var _query = this.query;
|
||||
|
||||
var basicsQuery, binsQuery;
|
||||
|
||||
if (override && _.has(override, 'start') && _.has(override, 'end') && _.has(override, 'bins')) {
|
||||
debug('overriding with %j', override);
|
||||
basicsQuery = overrideBasicsQueryTpl({
|
||||
_query: _query,
|
||||
_column: _column,
|
||||
_start: getBinStart(override),
|
||||
_end: getBinEnd(override)
|
||||
});
|
||||
|
||||
binsQuery = [
|
||||
overrideBinsQueryTpl({
|
||||
_bins: override.bins
|
||||
})
|
||||
].join(',\n');
|
||||
} else {
|
||||
basicsQuery = basicsQueryTpl({
|
||||
_query: _query,
|
||||
_column: _column
|
||||
});
|
||||
|
||||
if (override && _.has(override, 'bins')) {
|
||||
binsQuery = [
|
||||
overrideBinsQueryTpl({
|
||||
_bins: override.bins
|
||||
})
|
||||
].join(',\n');
|
||||
} else {
|
||||
binsQuery = [
|
||||
iqrQueryTpl({
|
||||
_query: _query,
|
||||
_column: _column
|
||||
}),
|
||||
binsQueryTpl({
|
||||
_query: _query,
|
||||
_minBins: BIN_MIN_NUMBER,
|
||||
_maxBins: BIN_MAX_NUMBER
|
||||
})
|
||||
].join(',\n');
|
||||
switch (this._getHistogramSubtype(override)) {
|
||||
case DATE_HISTOGRAM:
|
||||
debug('Delegating to DateHistogram with options: %j and overriding: %j', this.options, override);
|
||||
implementation = new DateHistogram(this.query, this.options, this.queries);
|
||||
break;
|
||||
case NUMERIC_HISTOGRAM:
|
||||
debug('Delegating to NumericHistogram with options: %j and overriding: %j', this.options, override);
|
||||
implementation = new NumericHistogram(this.query, this.options, this.queries);
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unsupported Histogram type');
|
||||
}
|
||||
|
||||
return implementation;
|
||||
}
|
||||
|
||||
_getHistogramSubtype (override) {
|
||||
if(this._isDateHistogram(override)) {
|
||||
return DATE_HISTOGRAM;
|
||||
}
|
||||
|
||||
var histogramSql = [
|
||||
"WITH",
|
||||
[
|
||||
basicsQuery,
|
||||
binsQuery,
|
||||
nullsQueryTpl({
|
||||
_query: _query,
|
||||
_column: _column
|
||||
})
|
||||
].join(',\n'),
|
||||
histogramQueryTpl({
|
||||
_query: _query,
|
||||
_column: _column
|
||||
})
|
||||
].join('\n');
|
||||
return NUMERIC_HISTOGRAM;
|
||||
}
|
||||
|
||||
debug(histogramSql);
|
||||
_isDateHistogram (override = {}) {
|
||||
return (this.options.hasOwnProperty('aggregation') || override.hasOwnProperty('aggregation'));
|
||||
}
|
||||
|
||||
return callback(null, histogramSql);
|
||||
};
|
||||
|
||||
Histogram.prototype.format = function(result, override) {
|
||||
override = override || {};
|
||||
var buckets = [];
|
||||
|
||||
var binsCount = getBinsCount(override);
|
||||
var width = getWidth(override);
|
||||
var binsStart = getBinStart(override);
|
||||
var nulls = 0;
|
||||
var avg;
|
||||
|
||||
if (result.rows.length) {
|
||||
var firstRow = result.rows[0];
|
||||
binsCount = firstRow.bins_number;
|
||||
width = firstRow.bin_width || width;
|
||||
avg = firstRow.avg_val;
|
||||
nulls = firstRow.nulls_count;
|
||||
binsStart = override.hasOwnProperty('start') ? getBinStart(override) : firstRow.min;
|
||||
|
||||
buckets = result.rows.map(function(row) {
|
||||
return _.omit(row, 'bins_number', 'bin_width', 'nulls_count', 'avg_val');
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
bin_width: width,
|
||||
bins_count: binsCount,
|
||||
bins_start: binsStart,
|
||||
nulls: nulls,
|
||||
avg: avg,
|
||||
bins: buckets
|
||||
};
|
||||
};
|
||||
|
||||
function getBinStart(override) {
|
||||
if (override.hasOwnProperty('start') && override.hasOwnProperty('end')) {
|
||||
return Math.min(override.start, override.end);
|
||||
}
|
||||
return override.start || 0;
|
||||
}
|
||||
|
||||
function getBinEnd(override) {
|
||||
if (override.hasOwnProperty('start') && override.hasOwnProperty('end')) {
|
||||
return Math.max(override.start, override.end);
|
||||
}
|
||||
return override.end || 0;
|
||||
}
|
||||
|
||||
function getBinsCount(override) {
|
||||
return override.bins || 0;
|
||||
}
|
||||
|
||||
function getWidth(override) {
|
||||
var width = 0;
|
||||
var binsCount = override.bins;
|
||||
|
||||
if (binsCount && Number.isFinite(override.start) && Number.isFinite(override.end)) {
|
||||
width = (override.end - override.start) / binsCount;
|
||||
}
|
||||
|
||||
return width;
|
||||
}
|
||||
|
||||
Histogram.prototype.getType = function() {
|
||||
return TYPE;
|
||||
};
|
||||
|
||||
Histogram.prototype.toString = function() {
|
||||
return JSON.stringify({
|
||||
_type: TYPE,
|
||||
_column: this.column,
|
||||
_query: this.query
|
||||
});
|
||||
getResult (psql, override, callback) {
|
||||
this.histogramImplementation = this._getHistogramImplementation(override);
|
||||
this.histogramImplementation.getResult(psql, override, callback);
|
||||
}
|
||||
|
||||
// In order to keep previous behaviour with overviews,
|
||||
// we have to expose the following methods to bypass
|
||||
// the concrete overview implementation
|
||||
|
||||
sql (psql, override, callback) {
|
||||
this.histogramImplementation.sql(psql, override, callback);
|
||||
}
|
||||
|
||||
format (result, override) {
|
||||
return this.histogramImplementation.format(result, override);
|
||||
}
|
||||
|
||||
getType () {
|
||||
return this.histogramImplementation.getType();
|
||||
}
|
||||
|
||||
toString () {
|
||||
return this.histogramImplementation.toString();
|
||||
}
|
||||
};
|
||||
|
||||
85
lib/cartodb/models/dataview/histograms/base-histogram.js
Normal file
85
lib/cartodb/models/dataview/histograms/base-histogram.js
Normal file
@@ -0,0 +1,85 @@
|
||||
const BaseDataview = require('../base');
|
||||
|
||||
const TYPE = 'histogram';
|
||||
|
||||
module.exports = class BaseHistogram extends BaseDataview {
|
||||
constructor (query, options, queries) {
|
||||
super();
|
||||
|
||||
if (typeof options.column !== 'string') {
|
||||
throw new Error('Histogram expects `column` in widget options');
|
||||
}
|
||||
|
||||
this.query = query;
|
||||
this.queries = queries;
|
||||
this.column = options.column;
|
||||
this.bins = options.bins;
|
||||
|
||||
this._columnType = null;
|
||||
}
|
||||
|
||||
sql (psql, override, callback) {
|
||||
if (!callback) {
|
||||
callback = override;
|
||||
override = {};
|
||||
}
|
||||
|
||||
if (this._columnType === null) {
|
||||
this.getColumnType(psql, this.column, this.queries.no_filters, (err, type) => {
|
||||
// assume numeric, will fail later
|
||||
this._columnType = 'numeric';
|
||||
if (!err && !!type) {
|
||||
this._columnType = Object.keys(type).find(function (key) {
|
||||
return type[key];
|
||||
});
|
||||
}
|
||||
this.sql(psql, override, callback);
|
||||
}, true); // use read-only transaction
|
||||
return null;
|
||||
}
|
||||
|
||||
return this._buildQuery(psql, override, callback);
|
||||
}
|
||||
|
||||
format (result, override) {
|
||||
const histogram = this._getSummary(result, override);
|
||||
histogram.bins = this._getBuckets(result);
|
||||
return histogram;
|
||||
}
|
||||
|
||||
getType () {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
toString () {
|
||||
return JSON.stringify({
|
||||
_type: TYPE,
|
||||
_column: this.column,
|
||||
_query: this.query
|
||||
});
|
||||
}
|
||||
|
||||
_hasOverridenRange (override) {
|
||||
return override && override.hasOwnProperty('start') && override.hasOwnProperty('end');
|
||||
}
|
||||
|
||||
_getBinStart (override = {}) {
|
||||
if (this._hasOverridenRange(override)) {
|
||||
return Math.min(override.start, override.end);
|
||||
}
|
||||
|
||||
return override.start || 0;
|
||||
}
|
||||
|
||||
_getBinEnd (override = {}) {
|
||||
if (this._hasOverridenRange(override)) {
|
||||
return Math.max(override.start, override.end);
|
||||
}
|
||||
|
||||
return override.end || 0;
|
||||
}
|
||||
|
||||
_getBinsCount (override = {}) {
|
||||
return override.bins || 0;
|
||||
}
|
||||
};
|
||||
302
lib/cartodb/models/dataview/histograms/date-histogram.js
Normal file
302
lib/cartodb/models/dataview/histograms/date-histogram.js
Normal file
@@ -0,0 +1,302 @@
|
||||
const BaseHistogram = require('./base-histogram');
|
||||
const debug = require('debug')('windshaft:dataview:date-histogram');
|
||||
|
||||
const dateIntervalQueryTpl = ctx => `
|
||||
WITH
|
||||
__cdb_dates AS (
|
||||
SELECT
|
||||
MAX(${ctx.column}::timestamp) AS __cdb_end,
|
||||
MIN(${ctx.column}::timestamp) AS __cdb_start
|
||||
FROM (${ctx.query}) __cdb_source
|
||||
),
|
||||
__cdb_interval_in_days AS (
|
||||
SELECT
|
||||
DATE_PART('day', __cdb_end - __cdb_start) AS __cdb_days
|
||||
FROM __cdb_dates
|
||||
),
|
||||
__cdb_interval_in_hours AS (
|
||||
SELECT
|
||||
__cdb_days * 24 + DATE_PART('hour', __cdb_end - __cdb_start) AS __cdb_hours
|
||||
FROM __cdb_interval_in_days, __cdb_dates
|
||||
),
|
||||
__cdb_interval_in_minutes AS (
|
||||
SELECT
|
||||
__cdb_hours * 60 + DATE_PART('minute', __cdb_end - __cdb_start) AS __cdb_minutes
|
||||
FROM __cdb_interval_in_hours, __cdb_dates
|
||||
),
|
||||
__cdb_interval_in_seconds AS (
|
||||
SELECT
|
||||
__cdb_minutes * 60 + DATE_PART('second', __cdb_end - __cdb_start) AS __cdb_seconds
|
||||
FROM __cdb_interval_in_minutes, __cdb_dates
|
||||
)
|
||||
SELECT
|
||||
ROUND(__cdb_days / 365) AS year,
|
||||
ROUND(__cdb_days / 90) AS quarter,
|
||||
ROUND(__cdb_days / 30) AS month,
|
||||
ROUND(__cdb_days / 7) AS week,
|
||||
__cdb_days AS day,
|
||||
__cdb_hours AS hour,
|
||||
__cdb_minutes AS minute,
|
||||
__cdb_seconds AS second
|
||||
FROM __cdb_interval_in_days, __cdb_interval_in_hours, __cdb_interval_in_minutes, __cdb_interval_in_seconds
|
||||
`;
|
||||
|
||||
const nullsQueryTpl = ctx => `
|
||||
__cdb_nulls AS (
|
||||
SELECT
|
||||
count(*) AS __cdb_nulls_count
|
||||
FROM (${ctx.query}) __cdb_histogram_nulls
|
||||
WHERE ${ctx.column} IS NULL
|
||||
)
|
||||
`;
|
||||
|
||||
const dateBasicsQueryTpl = ctx => `
|
||||
__cdb_basics AS (
|
||||
SELECT
|
||||
max(date_part('epoch', ${ctx.column})) AS __cdb_max_val,
|
||||
min(date_part('epoch', ${ctx.column})) AS __cdb_min_val,
|
||||
avg(date_part('epoch', ${ctx.column})) AS __cdb_avg_val,
|
||||
min(
|
||||
date_trunc(
|
||||
'${ctx.aggregation}', ${ctx.column}::timestamp AT TIME ZONE '${ctx.offset}'
|
||||
)
|
||||
) AS __cdb_start_date,
|
||||
max(${ctx.column}::timestamp AT TIME ZONE '${ctx.offset}') AS __cdb_end_date,
|
||||
count(1) AS __cdb_total_rows
|
||||
FROM (${ctx.query}) __cdb_basics_query
|
||||
)
|
||||
`;
|
||||
|
||||
const dateOverrideBasicsQueryTpl = ctx => `
|
||||
__cdb_basics AS (
|
||||
SELECT
|
||||
max(${ctx.end})::float AS __cdb_max_val,
|
||||
min(${ctx.start})::float AS __cdb_min_val,
|
||||
avg(date_part('epoch', ${ctx.column})) AS __cdb_avg_val,
|
||||
min(
|
||||
date_trunc(
|
||||
'${ctx.aggregation}',
|
||||
TO_TIMESTAMP(${ctx.start})::timestamp AT TIME ZONE '${ctx.offset}'
|
||||
)
|
||||
) AS __cdb_start_date,
|
||||
max(
|
||||
TO_TIMESTAMP(${ctx.end})::timestamp AT TIME ZONE '${ctx.offset}'
|
||||
) AS __cdb_end_date,
|
||||
count(1) AS __cdb_total_rows
|
||||
FROM (${ctx.query}) __cdb_basics_query
|
||||
)
|
||||
`;
|
||||
|
||||
const dateBinsQueryTpl = ctx => `
|
||||
__cdb_bins AS (
|
||||
SELECT
|
||||
__cdb_bins_array,
|
||||
ARRAY_LENGTH(__cdb_bins_array, 1) AS __cdb_bins_number
|
||||
FROM (
|
||||
SELECT
|
||||
ARRAY(
|
||||
SELECT GENERATE_SERIES(
|
||||
__cdb_start_date::timestamptz,
|
||||
__cdb_end_date::timestamptz,
|
||||
${ctx.aggregation === 'quarter' ? `'3 month'::interval` : `'1 ${ctx.aggregation}'::interval`}
|
||||
)
|
||||
) AS __cdb_bins_array
|
||||
FROM __cdb_basics
|
||||
) __cdb_bins_array_query
|
||||
)
|
||||
`;
|
||||
|
||||
const dateHistogramQueryTpl = ctx => `
|
||||
SELECT
|
||||
(__cdb_max_val - __cdb_min_val) / cast(__cdb_bins_number as float) AS bin_width,
|
||||
__cdb_bins_number AS bins_number,
|
||||
__cdb_nulls_count AS nulls_count,
|
||||
CASE WHEN __cdb_min_val = __cdb_max_val
|
||||
THEN 0
|
||||
ELSE GREATEST(
|
||||
1,
|
||||
LEAST(
|
||||
WIDTH_BUCKET(
|
||||
${ctx.column}::timestamp AT TIME ZONE '${ctx.offset}',
|
||||
__cdb_bins_array
|
||||
),
|
||||
__cdb_bins_number
|
||||
)
|
||||
) - 1
|
||||
END AS bin,
|
||||
min(
|
||||
date_part(
|
||||
'epoch',
|
||||
date_trunc(
|
||||
'${ctx.aggregation}', ${ctx.column}::timestamp AT TIME ZONE '${ctx.offset}'
|
||||
) AT TIME ZONE '${ctx.offset}'
|
||||
)
|
||||
)::numeric AS timestamp,
|
||||
date_part('epoch', __cdb_start_date)::numeric AS timestamp_start,
|
||||
min(date_part('epoch', ${ctx.column}))::numeric AS min,
|
||||
max(date_part('epoch', ${ctx.column}))::numeric AS max,
|
||||
avg(date_part('epoch', ${ctx.column}))::numeric AS avg,
|
||||
count(*) AS freq
|
||||
FROM (${ctx.query}) __cdb_histogram, __cdb_basics, __cdb_bins, __cdb_nulls
|
||||
WHERE date_part('epoch', ${ctx.column}) IS NOT NULL
|
||||
GROUP BY bin, bins_number, bin_width, nulls_count, timestamp_start
|
||||
ORDER BY bin
|
||||
`;
|
||||
|
||||
const MAX_INTERVAL_VALUE = 366;
|
||||
|
||||
const DATE_AGGREGATIONS = {
|
||||
'auto': true,
|
||||
'minute': true,
|
||||
'hour': true,
|
||||
'day': true,
|
||||
'week': true,
|
||||
'month': true,
|
||||
'quarter': true,
|
||||
'year': true
|
||||
};
|
||||
|
||||
/**
|
||||
date_histogram: {
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'date', // column data type: date
|
||||
aggregation: 'day' // MANDATORY
|
||||
offset: -7200 // OPTIONAL (UTC offset in seconds)
|
||||
}
|
||||
}
|
||||
*/
|
||||
module.exports = class DateHistogram extends BaseHistogram {
|
||||
constructor (query, options, queries) {
|
||||
super(query, options, queries);
|
||||
|
||||
this.aggregation = options.aggregation;
|
||||
this.offset = options.offset;
|
||||
}
|
||||
|
||||
_buildQueryTpl (ctx) {
|
||||
return `
|
||||
WITH
|
||||
${this._hasOverridenRange(ctx.override) ? dateOverrideBasicsQueryTpl(ctx) : dateBasicsQueryTpl(ctx)},
|
||||
${dateBinsQueryTpl(ctx)},
|
||||
${nullsQueryTpl(ctx)}
|
||||
${dateHistogramQueryTpl(ctx)}
|
||||
`;
|
||||
}
|
||||
|
||||
_buildQuery (psql, override, callback) {
|
||||
if (!this._isValidAggregation(override)) {
|
||||
return callback(new Error('Invalid aggregation value. Valid ones: ' +
|
||||
Object.keys(DATE_AGGREGATIONS).join(', ')
|
||||
));
|
||||
}
|
||||
|
||||
if (this._getAggregation(override) === 'auto') {
|
||||
this._getAutomaticAggregation(psql, function (err, aggregation) {
|
||||
if (err || aggregation === 'none') {
|
||||
this.aggregation = 'day';
|
||||
} else {
|
||||
this.aggregation = aggregation;
|
||||
}
|
||||
override.aggregation = this.aggregation;
|
||||
this._buildQuery(psql, override, callback);
|
||||
}.bind(this));
|
||||
return null;
|
||||
}
|
||||
|
||||
const histogramSql = this._buildQueryTpl({
|
||||
override: override,
|
||||
query: this.query,
|
||||
column: this.column,
|
||||
aggregation: this._getAggregation(override),
|
||||
start: this._getBinStart(override),
|
||||
end: this._getBinEnd(override),
|
||||
offset: this._parseOffset(override)
|
||||
});
|
||||
|
||||
debug(histogramSql);
|
||||
|
||||
return callback(null, histogramSql);
|
||||
}
|
||||
|
||||
_isValidAggregation (override) {
|
||||
return DATE_AGGREGATIONS.hasOwnProperty(this._getAggregation(override));
|
||||
}
|
||||
|
||||
_getAutomaticAggregation (psql, callback) {
|
||||
const dateIntervalQuery = dateIntervalQueryTpl({
|
||||
query: this.query,
|
||||
column: this.column
|
||||
});
|
||||
|
||||
psql.query(dateIntervalQuery, function (err, result) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
const aggegations = result.rows[0];
|
||||
const aggregation = Object.keys(aggegations)
|
||||
.map(key => ({ name: key, value: aggegations[key] }))
|
||||
.reduce((closer, current) => {
|
||||
if (current.value > MAX_INTERVAL_VALUE) {
|
||||
return closer;
|
||||
}
|
||||
|
||||
const closerDiff = MAX_INTERVAL_VALUE - closer.value;
|
||||
const currentDiff = MAX_INTERVAL_VALUE - current.value;
|
||||
|
||||
if (Number.isFinite(current.value) && closerDiff > currentDiff) {
|
||||
return current;
|
||||
}
|
||||
|
||||
return closer;
|
||||
}, { name: 'none', value: -1 });
|
||||
|
||||
callback(null, aggregation.name);
|
||||
});
|
||||
}
|
||||
|
||||
_getSummary (result, override) {
|
||||
const firstRow = result.rows[0] || {};
|
||||
|
||||
return {
|
||||
aggregation: this._getAggregation(override),
|
||||
offset: this._getOffset(override),
|
||||
timestamp_start: firstRow.timestamp_start,
|
||||
|
||||
bin_width: firstRow.bin_width,
|
||||
bins_count: firstRow.bins_number,
|
||||
bins_start: firstRow.timestamp,
|
||||
nulls: firstRow.nulls_count,
|
||||
infinities: firstRow.infinities_count,
|
||||
nans: firstRow.nans_count,
|
||||
avg: firstRow.avg_val
|
||||
};
|
||||
}
|
||||
|
||||
_getBuckets (result) {
|
||||
return result.rows.map(({ bin, min, max, avg, freq, timestamp }) => ({ bin, min, max, avg, freq, timestamp }));
|
||||
}
|
||||
|
||||
_getAggregation (override = {}) {
|
||||
return override.aggregation ? override.aggregation : this.aggregation;
|
||||
}
|
||||
|
||||
_getOffset (override = {}) {
|
||||
return Number.isFinite(override.offset) ? override.offset : (this.offset || 0);
|
||||
}
|
||||
|
||||
_parseOffset (override) {
|
||||
if (this._shouldIgnoreOffset(override)) {
|
||||
return '0';
|
||||
}
|
||||
|
||||
const offsetInHours = Math.ceil(this._getOffset(override) / 3600);
|
||||
|
||||
return '' + offsetInHours;
|
||||
}
|
||||
|
||||
_shouldIgnoreOffset (override) {
|
||||
return (this._getAggregation(override) === 'hour' || this._getAggregation(override) === 'minute');
|
||||
}
|
||||
};
|
||||
234
lib/cartodb/models/dataview/histograms/numeric-histogram.js
Normal file
234
lib/cartodb/models/dataview/histograms/numeric-histogram.js
Normal file
@@ -0,0 +1,234 @@
|
||||
const BaseHistogram = require('./base-histogram');
|
||||
const debug = require('debug')('windshaft:dataview:numeric-histogram');
|
||||
|
||||
const columnCastTpl = ctx => `date_part('epoch', ${ctx.column})`;
|
||||
|
||||
const filterOutSpecialNumericValues = ctx => `
|
||||
${ctx.column} != 'infinity'::float
|
||||
AND
|
||||
${ctx.column} != '-infinity'::float
|
||||
AND
|
||||
${ctx.column} != 'NaN'::float
|
||||
`;
|
||||
|
||||
const filteredQueryTpl = ctx => `
|
||||
__cdb_filtered_source AS (
|
||||
SELECT *
|
||||
FROM (${ctx.query}) __cdb_filtered_source_query
|
||||
WHERE ${ctx.column} IS NOT NULL
|
||||
${ctx.isFloatColumn ? `AND ${filterOutSpecialNumericValues(ctx)}` : ''}
|
||||
)
|
||||
`;
|
||||
|
||||
const basicsQueryTpl = ctx => `
|
||||
__cdb_basics AS (
|
||||
SELECT
|
||||
max(${ctx.column}) AS __cdb_max_val, min(${ctx.column}) AS __cdb_min_val,
|
||||
avg(${ctx.column}) AS __cdb_avg_val, count(1) AS __cdb_total_rows
|
||||
FROM __cdb_filtered_source
|
||||
)
|
||||
`;
|
||||
|
||||
const overrideBasicsQueryTpl = ctx => `
|
||||
__cdb_basics AS (
|
||||
SELECT
|
||||
max(${ctx.end}) AS __cdb_max_val, min(${ctx.start}) AS __cdb_min_val,
|
||||
avg(${ctx.column}) AS __cdb_avg_val, count(1) AS __cdb_total_rows
|
||||
FROM __cdb_filtered_source
|
||||
)
|
||||
`;
|
||||
|
||||
const iqrQueryTpl = ctx => `
|
||||
__cdb_iqrange AS (
|
||||
SELECT max(quartile_max) - min(quartile_max) AS __cdb_iqr
|
||||
FROM (
|
||||
SELECT quartile, max(_cdb_iqr_column) AS quartile_max from (
|
||||
SELECT ${ctx.column} AS _cdb_iqr_column, ntile(4) over (order by ${ctx.column}
|
||||
) AS quartile
|
||||
FROM __cdb_filtered_source) _cdb_quartiles
|
||||
WHERE quartile = 1 or quartile = 3
|
||||
GROUP BY quartile
|
||||
) __cdb_iqr
|
||||
)
|
||||
`;
|
||||
|
||||
const binsQueryTpl = ctx => `
|
||||
__cdb_bins AS (
|
||||
SELECT
|
||||
CASE WHEN __cdb_total_rows = 0 OR __cdb_iqr = 0
|
||||
THEN 1
|
||||
ELSE GREATEST(
|
||||
LEAST(${ctx.minBins}, CAST(__cdb_total_rows AS INT)),
|
||||
LEAST(
|
||||
CAST(((__cdb_max_val - __cdb_min_val) / (2 * __cdb_iqr * power(__cdb_total_rows, 1/3))) AS INT),
|
||||
${ctx.maxBins}
|
||||
)
|
||||
)
|
||||
END AS __cdb_bins_number
|
||||
FROM __cdb_basics, __cdb_iqrange, __cdb_filtered_source
|
||||
LIMIT 1
|
||||
)
|
||||
`;
|
||||
|
||||
const overrideBinsQueryTpl = ctx => `
|
||||
__cdb_bins AS (
|
||||
SELECT ${ctx.override.bins} AS __cdb_bins_number
|
||||
)
|
||||
`;
|
||||
|
||||
const nullsQueryTpl = ctx => `
|
||||
__cdb_nulls AS (
|
||||
SELECT
|
||||
count(*) AS __cdb_nulls_count
|
||||
FROM (${ctx.query}) __cdb_histogram_nulls
|
||||
WHERE ${ctx.column} IS NULL
|
||||
)
|
||||
`;
|
||||
|
||||
const infinitiesQueryTpl = ctx => `
|
||||
__cdb_infinities AS (
|
||||
SELECT
|
||||
count(*) AS __cdb_infinities_count
|
||||
FROM (${ctx.query}) __cdb_infinities_query
|
||||
WHERE
|
||||
${ctx.column} = 'infinity'::float
|
||||
OR
|
||||
${ctx.column} = '-infinity'::float
|
||||
)
|
||||
`;
|
||||
|
||||
const nansQueryTpl = ctx => `
|
||||
__cdb_nans AS (
|
||||
SELECT
|
||||
count(*) AS __cdb_nans_count
|
||||
FROM (${ctx.query}) __cdb_nans_query
|
||||
WHERE ${ctx.column} = 'NaN'::float
|
||||
)
|
||||
`;
|
||||
|
||||
const specialNumericValuesColumnDefinitionTpl = () => `
|
||||
__cdb_infinities_count AS infinities_count,
|
||||
__cdb_nans_count AS nans_count
|
||||
`;
|
||||
|
||||
const specialNumericValuesCTETpl = () => `
|
||||
__cdb_infinities, __cdb_nans
|
||||
`;
|
||||
|
||||
const specialNumericValuesColumnTpl = () => `
|
||||
infinities_count, nans_count
|
||||
`;
|
||||
|
||||
const histogramQueryTpl = ctx => `
|
||||
SELECT
|
||||
(__cdb_max_val - __cdb_min_val) / cast(__cdb_bins_number as float) AS bin_width,
|
||||
__cdb_bins_number AS bins_number,
|
||||
__cdb_nulls_count AS nulls_count,
|
||||
${ctx.isFloatColumn ? `${specialNumericValuesColumnDefinitionTpl()},` : ''}
|
||||
__cdb_avg_val AS avg_val,
|
||||
CASE WHEN __cdb_min_val = __cdb_max_val
|
||||
THEN 0
|
||||
ELSE GREATEST(
|
||||
1,
|
||||
LEAST(
|
||||
WIDTH_BUCKET(${ctx.column}, __cdb_min_val, __cdb_max_val, __cdb_bins_number),
|
||||
__cdb_bins_number
|
||||
)
|
||||
) - 1
|
||||
END AS bin,
|
||||
min(${ctx.column})::numeric AS min,
|
||||
max(${ctx.column})::numeric AS max,
|
||||
avg(${ctx.column})::numeric AS avg,
|
||||
count(*) AS freq
|
||||
FROM __cdb_filtered_source, __cdb_basics, __cdb_nulls, __cdb_bins
|
||||
${ctx.isFloatColumn ? `, ${specialNumericValuesCTETpl()}` : ''}
|
||||
GROUP BY bin, bins_number, bin_width, nulls_count, avg_val
|
||||
${ctx.isFloatColumn ? `, ${specialNumericValuesColumnTpl()}` : ''}
|
||||
ORDER BY bin
|
||||
`;
|
||||
|
||||
const BIN_MIN_NUMBER = 6;
|
||||
const BIN_MAX_NUMBER = 48;
|
||||
|
||||
/**
|
||||
Numeric histogram:
|
||||
{
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'name', // column data type: numeric
|
||||
bins: 10 // OPTIONAL
|
||||
}
|
||||
}
|
||||
*/
|
||||
module.exports = class NumericHistogram extends BaseHistogram {
|
||||
constructor (query, options, queries) {
|
||||
super(query, options, queries);
|
||||
}
|
||||
|
||||
_buildQuery (psql, override, callback) {
|
||||
const histogramSql = this._buildQueryTpl({
|
||||
override: override,
|
||||
column: this._columnType === 'date' ? columnCastTpl({ column: this.column }) : this.column,
|
||||
isFloatColumn: this._columnType === 'float',
|
||||
query: this.query,
|
||||
start: this._getBinStart(override),
|
||||
end: this._getBinEnd(override),
|
||||
minBins: BIN_MIN_NUMBER,
|
||||
maxBins: BIN_MAX_NUMBER,
|
||||
});
|
||||
|
||||
debug(histogramSql);
|
||||
|
||||
return callback(null, histogramSql);
|
||||
}
|
||||
|
||||
_buildQueryTpl (ctx) {
|
||||
return `
|
||||
WITH
|
||||
${filteredQueryTpl(ctx)},
|
||||
${this._hasOverridenRange(ctx.override) ? overrideBasicsQueryTpl(ctx) : basicsQueryTpl(ctx)},
|
||||
${this._hasOverridenBins(ctx.override) ?
|
||||
overrideBinsQueryTpl(ctx) :
|
||||
`${iqrQueryTpl(ctx)}, ${binsQueryTpl(ctx)}`
|
||||
},
|
||||
${nullsQueryTpl(ctx)}
|
||||
${ctx.isFloatColumn ? `,${infinitiesQueryTpl(ctx)}, ${nansQueryTpl(ctx)}` : ''}
|
||||
${histogramQueryTpl(ctx)}
|
||||
`;
|
||||
}
|
||||
|
||||
_hasOverridenBins (override) {
|
||||
return override && override.hasOwnProperty('bins');
|
||||
}
|
||||
|
||||
_getSummary (result, override) {
|
||||
const firstRow = result.rows[0] || {};
|
||||
|
||||
return {
|
||||
bin_width: firstRow.bin_width,
|
||||
bins_count: firstRow.bins_number,
|
||||
bins_start: this._populateBinStart(firstRow, override),
|
||||
nulls: firstRow.nulls_count,
|
||||
infinities: firstRow.infinities_count,
|
||||
nans: firstRow.nans_count,
|
||||
avg: firstRow.avg_val,
|
||||
};
|
||||
}
|
||||
|
||||
_getBuckets (result) {
|
||||
return result.rows.map(({ bin, min, max, avg, freq }) => ({ bin, min, max, avg, freq }));
|
||||
}
|
||||
|
||||
_populateBinStart (firstRow, override = {}) {
|
||||
let binStart;
|
||||
|
||||
if (override.hasOwnProperty('start')) {
|
||||
binStart = this._getBinStart(override);
|
||||
} else {
|
||||
binStart = firstRow.min;
|
||||
}
|
||||
|
||||
return binStart;
|
||||
}
|
||||
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
module.exports = {
|
||||
Aggregation: require('./aggregation'),
|
||||
Formula: require('./formula'),
|
||||
Histogram: require('./histogram'),
|
||||
List: require('./list')
|
||||
Histogram: require('./histogram')
|
||||
};
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
|
||||
var BaseWidget = require('./base');
|
||||
|
||||
var TYPE = 'list';
|
||||
|
||||
var listSqlTpl = dot.template('select {{=it._columns}} from ({{=it._query}}) as _cdb_list');
|
||||
|
||||
/**
|
||||
{
|
||||
type: 'list',
|
||||
options: {
|
||||
columns: ['name', 'description']
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
function List(query, options) {
|
||||
options = options || {};
|
||||
|
||||
if (!Array.isArray(options.columns)) {
|
||||
throw new Error('List expects `columns` array in widget options');
|
||||
}
|
||||
|
||||
BaseWidget.apply(this);
|
||||
|
||||
this.query = query;
|
||||
this.columns = options.columns;
|
||||
}
|
||||
|
||||
List.prototype = new BaseWidget();
|
||||
List.prototype.constructor = List;
|
||||
|
||||
module.exports = List;
|
||||
|
||||
List.prototype.sql = function(psql, override, callback) {
|
||||
if (!callback) {
|
||||
callback = override;
|
||||
}
|
||||
|
||||
var listSql = listSqlTpl({
|
||||
_query: this.query,
|
||||
_columns: this.columns.join(', ')
|
||||
});
|
||||
|
||||
return callback(null, listSql);
|
||||
};
|
||||
|
||||
List.prototype.format = function(result) {
|
||||
return {
|
||||
rows: result.rows
|
||||
};
|
||||
};
|
||||
|
||||
List.prototype.getType = function() {
|
||||
return TYPE;
|
||||
};
|
||||
|
||||
List.prototype.toString = function() {
|
||||
return JSON.stringify({
|
||||
_type: TYPE,
|
||||
_query: this.query,
|
||||
_columns: this.columns.join(', ')
|
||||
});
|
||||
};
|
||||
@@ -1,14 +1,36 @@
|
||||
var BaseOverviewsDataview = require('./base');
|
||||
var BaseDataview = require('../aggregation');
|
||||
var debug = require('debug')('windshaft:widget:aggregation:overview');
|
||||
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
|
||||
var filteredQueryTpl = dot.template([
|
||||
'filtered_source AS (',
|
||||
' SELECT *',
|
||||
' FROM ({{=it._query}}) _cdb_filtered_source',
|
||||
' {{?it._aggregationColumn && it._isFloatColumn}}WHERE',
|
||||
' {{=it._aggregationColumn}} != \'infinity\'::float',
|
||||
' AND',
|
||||
' {{=it._aggregationColumn}} != \'-infinity\'::float',
|
||||
' AND',
|
||||
' {{=it._aggregationColumn}} != \'NaN\'::float{{?}}',
|
||||
')'
|
||||
].join(' \n'));
|
||||
|
||||
var summaryQueryTpl = dot.template([
|
||||
'summary AS (',
|
||||
' SELECT',
|
||||
' sum(_feature_count) AS count,',
|
||||
' sum(CASE WHEN {{=it._column}} IS NULL THEN 1 ELSE 0 END) AS nulls_count',
|
||||
' {{?it._isFloatColumn}},sum(',
|
||||
' CASE',
|
||||
' WHEN {{=it._aggregationColumn}} = \'infinity\'::float OR {{=it._aggregationColumn}} = \'-infinity\'::float',
|
||||
' THEN 1',
|
||||
' ELSE 0',
|
||||
' END',
|
||||
' ) AS infinities_count,',
|
||||
' sum(CASE WHEN {{=it._aggregationColumn}} = \'NaN\'::float THEN 1 ELSE 0 END) AS nans_count{{?}}',
|
||||
' FROM ({{=it._query}}) _cdb_aggregation_nulls',
|
||||
')'
|
||||
].join('\n'));
|
||||
@@ -17,7 +39,7 @@ var rankedCategoriesQueryTpl = dot.template([
|
||||
'categories AS(',
|
||||
' SELECT {{=it._column}} AS category, {{=it._aggregation}} AS value,',
|
||||
' row_number() OVER (ORDER BY {{=it._aggregation}} desc) as rank',
|
||||
' FROM ({{=it._query}}) _cdb_aggregation_all',
|
||||
' FROM filtered_source',
|
||||
' {{?it._aggregationColumn!==null}}WHERE {{=it._aggregationColumn}} IS NOT NULL{{?}}',
|
||||
' GROUP BY {{=it._column}}',
|
||||
' ORDER BY 2 DESC',
|
||||
@@ -36,40 +58,46 @@ var categoriesSummaryCountQueryTpl = dot.template([
|
||||
' SELECT count(1) AS categories_count',
|
||||
' FROM (',
|
||||
' SELECT {{=it._column}} AS category',
|
||||
' FROM ({{=it._query}}) _cdb_categories',
|
||||
' FROM filtered_source',
|
||||
' GROUP BY {{=it._column}}',
|
||||
' ) _cdb_categories_count',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var rankedAggregationQueryTpl = dot.template([
|
||||
'SELECT CAST(category AS text), value, false as agg, nulls_count, min_val, max_val, count, categories_count',
|
||||
'SELECT CAST(category AS text), value, false as agg, nulls_count, min_val, max_val,',
|
||||
' count, categories_count{{?it._isFloatColumn}}, nans_count, infinities_count{{?}}',
|
||||
' FROM categories, summary, categories_summary_min_max, categories_summary_count',
|
||||
' WHERE rank < {{=it._limit}}',
|
||||
'UNION ALL',
|
||||
'SELECT \'Other\' category, sum(value), true as agg, nulls_count, min_val, max_val, count, categories_count',
|
||||
'SELECT \'Other\' category, sum(value), true as agg, nulls_count, min_val, max_val,',
|
||||
' count, categories_count{{?it._isFloatColumn}}, nans_count, infinities_count{{?}}',
|
||||
' FROM categories, summary, categories_summary_min_max, categories_summary_count',
|
||||
' WHERE rank >= {{=it._limit}}',
|
||||
'GROUP BY nulls_count, min_val, max_val, count, categories_count'
|
||||
'GROUP BY nulls_count, min_val, max_val, count,',
|
||||
' categories_count{{?it._isFloatColumn}}, nans_count, infinities_count{{?}}'
|
||||
].join('\n'));
|
||||
|
||||
var aggregationQueryTpl = dot.template([
|
||||
'SELECT CAST({{=it._column}} AS text) AS category, {{=it._aggregation}} AS value, false as agg,',
|
||||
' nulls_count, min_val, max_val, count, categories_count',
|
||||
'FROM ({{=it._query}}) _cdb_aggregation_all, summary, categories_summary_min_max, categories_summary_count',
|
||||
'GROUP BY category, nulls_count, min_val, max_val, count, categories_count',
|
||||
' nulls_count, min_val, max_val, count, categories_count{{?it._isFloatColumn}}, nans_count, infinities_count{{?}}',
|
||||
'FROM filtered_source, summary, categories_summary_min_max, categories_summary_count',
|
||||
'GROUP BY category, nulls_count, min_val, max_val, count,',
|
||||
' categories_count{{?it._isFloatColumn}}, nans_count, infinities_count{{?}}',
|
||||
'ORDER BY value DESC'
|
||||
].join('\n'));
|
||||
|
||||
var CATEGORIES_LIMIT = 6;
|
||||
|
||||
function Aggregation(query, options, queryRewriter, queryRewriteData, params) {
|
||||
BaseOverviewsDataview.call(this, query, options, BaseDataview, queryRewriter, queryRewriteData, params);
|
||||
function Aggregation(query, options, queryRewriter, queryRewriteData, params, queries) {
|
||||
BaseOverviewsDataview.call(this, query, options, BaseDataview, queryRewriter, queryRewriteData, params, queries);
|
||||
|
||||
this.query = query;
|
||||
this.queries = queries;
|
||||
this.column = options.column;
|
||||
this.aggregation = options.aggregation;
|
||||
this.aggregationColumn = options.aggregationColumn;
|
||||
this._isFloatColumn = null;
|
||||
}
|
||||
|
||||
Aggregation.prototype = Object.create(BaseOverviewsDataview.prototype);
|
||||
@@ -78,27 +106,49 @@ Aggregation.prototype.constructor = Aggregation;
|
||||
module.exports = Aggregation;
|
||||
|
||||
Aggregation.prototype.sql = function(psql, override, callback) {
|
||||
var self = this;
|
||||
|
||||
if (!callback) {
|
||||
callback = override;
|
||||
override = {};
|
||||
}
|
||||
|
||||
var _query = this.rewrittenQuery(this.query);
|
||||
var _aggregationColumn = this.aggregation !== 'count' ? this.aggregationColumn : null;
|
||||
|
||||
if (this.aggregationColumn && this._isFloatColumn === null) {
|
||||
this._isFloatColumn = false;
|
||||
this.getColumnType(psql, this.aggregationColumn, this.queries.no_filters, function (err, type) {
|
||||
if (!err && !!type) {
|
||||
self._isFloatColumn = type.float;
|
||||
}
|
||||
self.sql(psql, override, callback);
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
var aggregationSql;
|
||||
if (!!override.ownFilter) {
|
||||
aggregationSql = [
|
||||
"WITH",
|
||||
[
|
||||
summaryQueryTpl({
|
||||
filteredQueryTpl({
|
||||
_isFloatColumn: this._isFloatColumn,
|
||||
_query: _query,
|
||||
_column: this.column
|
||||
_column: this.column,
|
||||
_aggregationColumn: _aggregationColumn
|
||||
}),
|
||||
summaryQueryTpl({
|
||||
_isFloatColumn: this._isFloatColumn,
|
||||
_query: _query,
|
||||
_column: this.column,
|
||||
_aggregationColumn: _aggregationColumn
|
||||
}),
|
||||
rankedCategoriesQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column,
|
||||
_aggregation: this.getAggregationSql(),
|
||||
_aggregationColumn: this.aggregation !== 'count' ? this.aggregationColumn : null
|
||||
_aggregationColumn: _aggregationColumn
|
||||
}),
|
||||
categoriesSummaryMinMaxQueryTpl({
|
||||
_query: _query,
|
||||
@@ -110,6 +160,7 @@ Aggregation.prototype.sql = function(psql, override, callback) {
|
||||
})
|
||||
].join(',\n'),
|
||||
aggregationQueryTpl({
|
||||
_isFloatColumn: this._isFloatColumn,
|
||||
_query: _query,
|
||||
_column: this.column,
|
||||
_aggregation: this.getAggregationSql(),
|
||||
@@ -120,15 +171,23 @@ Aggregation.prototype.sql = function(psql, override, callback) {
|
||||
aggregationSql = [
|
||||
"WITH",
|
||||
[
|
||||
summaryQueryTpl({
|
||||
filteredQueryTpl({
|
||||
_isFloatColumn: this._isFloatColumn,
|
||||
_query: _query,
|
||||
_column: this.column
|
||||
_column: this.column,
|
||||
_aggregationColumn: _aggregationColumn
|
||||
}),
|
||||
summaryQueryTpl({
|
||||
_isFloatColumn: this._isFloatColumn,
|
||||
_query: _query,
|
||||
_column: this.column,
|
||||
_aggregationColumn: _aggregationColumn
|
||||
}),
|
||||
rankedCategoriesQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column,
|
||||
_aggregation: this.getAggregationSql(),
|
||||
_aggregationColumn: this.aggregation !== 'count' ? this.aggregationColumn : null
|
||||
_aggregationColumn: _aggregationColumn
|
||||
}),
|
||||
categoriesSummaryMinMaxQueryTpl({
|
||||
_query: _query,
|
||||
@@ -140,6 +199,7 @@ Aggregation.prototype.sql = function(psql, override, callback) {
|
||||
})
|
||||
].join(',\n'),
|
||||
rankedAggregationQueryTpl({
|
||||
_isFloatColumn: this._isFloatColumn,
|
||||
_query: _query,
|
||||
_column: this.column,
|
||||
_limit: CATEGORIES_LIMIT
|
||||
@@ -147,6 +207,8 @@ Aggregation.prototype.sql = function(psql, override, callback) {
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
debug(aggregationSql);
|
||||
|
||||
return callback(null, aggregationSql);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
var _ = require('underscore');
|
||||
var BaseDataview = require('../base');
|
||||
|
||||
function BaseOverviewsDataview(query, queryOptions, BaseDataview, queryRewriter, queryRewriteData, options) {
|
||||
function BaseOverviewsDataview(query, queryOptions, BaseDataview, queryRewriter, queryRewriteData, options, queries) {
|
||||
this.BaseDataview = BaseDataview;
|
||||
this.query = query;
|
||||
this.queryOptions = queryOptions;
|
||||
this.queryRewriter = queryRewriter;
|
||||
this.queryRewriteData = queryRewriteData;
|
||||
this.options = options;
|
||||
this.baseDataview = new this.BaseDataview(this.query, this.queryOptions);
|
||||
this.queries = queries;
|
||||
this.baseDataview = new this.BaseDataview(this.query, this.queryOptions, this.queries);
|
||||
}
|
||||
|
||||
module.exports = BaseOverviewsDataview;
|
||||
|
||||
@@ -1,34 +1,61 @@
|
||||
var BaseOverviewsDataview = require('./base');
|
||||
var BaseDataview = require('../formula');
|
||||
var debug = require('debug')('windshaft:widget:formula:overview');
|
||||
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
|
||||
var formulaQueryTpls = {
|
||||
'count': dot.template([
|
||||
'SELECT',
|
||||
'sum(_feature_count) AS result,',
|
||||
'(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls WHERE {{=it._column}} IS NULL) AS nulls_count',
|
||||
'FROM ({{=it._query}}) _cdb_formula'
|
||||
].join('\n')),
|
||||
'sum': dot.template([
|
||||
'SELECT',
|
||||
'sum({{=it._column}}*_feature_count) AS result,',
|
||||
'(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls WHERE {{=it._column}} IS NULL) AS nulls_count',
|
||||
'FROM ({{=it._query}}) _cdb_formula'
|
||||
].join('\n')),
|
||||
'avg': dot.template([
|
||||
'SELECT',
|
||||
'sum({{=it._column}}*_feature_count)/sum(_feature_count) AS result,',
|
||||
'(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls WHERE {{=it._column}} IS NULL) AS nulls_count',
|
||||
'FROM ({{=it._query}}) _cdb_formula'
|
||||
].join('\n')),
|
||||
'count': dot.template([
|
||||
'SELECT',
|
||||
'sum(_feature_count) AS result,',
|
||||
'(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls WHERE {{=it._column}} IS NULL) AS nulls_count',
|
||||
'{{?it._isFloatColumn}},(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_infinities',
|
||||
' WHERE {{=it._column}} = \'infinity\'::float OR {{=it._column}} = \'-infinity\'::float) AS infinities_count,',
|
||||
'(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nans',
|
||||
' WHERE {{=it._column}} = \'NaN\'::float) AS nans_count{{?}}',
|
||||
'FROM ({{=it._query}}) _cdb_formula'
|
||||
].join('\n')),
|
||||
'sum': dot.template([
|
||||
'SELECT',
|
||||
'sum({{=it._column}}*_feature_count) AS result,',
|
||||
'(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls WHERE {{=it._column}} IS NULL) AS nulls_count',
|
||||
'{{?it._isFloatColumn}},(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_infinities',
|
||||
' WHERE {{=it._column}} = \'infinity\'::float OR {{=it._column}} = \'-infinity\'::float) AS infinities_count',
|
||||
',(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nans',
|
||||
' WHERE {{=it._column}} = \'NaN\'::float) AS nans_count{{?}}',
|
||||
'FROM ({{=it._query}}) _cdb_formula',
|
||||
'{{?it._isFloatColumn}}WHERE',
|
||||
' {{=it._column}} != \'infinity\'::float',
|
||||
'AND',
|
||||
' {{=it._column}} != \'-infinity\'::float',
|
||||
'AND',
|
||||
' {{=it._column}} != \'NaN\'::float{{?}}'
|
||||
].join('\n')),
|
||||
'avg': dot.template([
|
||||
'SELECT',
|
||||
'sum({{=it._column}}*_feature_count)/sum(_feature_count) AS result,',
|
||||
'(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls WHERE {{=it._column}} IS NULL) AS nulls_count',
|
||||
'{{?it._isFloatColumn}},(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_infinities',
|
||||
' WHERE {{=it._column}} = \'infinity\'::float OR {{=it._column}} = \'-infinity\'::float) AS infinities_count',
|
||||
',(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nans',
|
||||
' WHERE {{=it._column}} = \'NaN\'::float) AS nans_count{{?}}',
|
||||
'FROM ({{=it._query}}) _cdb_formula',
|
||||
'{{?it._isFloatColumn}}WHERE',
|
||||
' {{=it._column}} != \'infinity\'::float',
|
||||
'AND',
|
||||
' {{=it._column}} != \'-infinity\'::float',
|
||||
'AND',
|
||||
' {{=it._column}} != \'NaN\'::float{{?}}'
|
||||
].join('\n')),
|
||||
};
|
||||
|
||||
function Formula(query, options, queryRewriter, queryRewriteData, params) {
|
||||
BaseOverviewsDataview.call(this, query, options, BaseDataview, queryRewriter, queryRewriteData, params);
|
||||
function Formula(query, options, queryRewriter, queryRewriteData, params, queries) {
|
||||
BaseOverviewsDataview.call(this, query, options, BaseDataview, queryRewriter, queryRewriteData, params, queries);
|
||||
this.column = options.column || '1';
|
||||
this.operation = options.operation;
|
||||
this._isFloatColumn = null;
|
||||
this.queries = queries;
|
||||
}
|
||||
|
||||
Formula.prototype = Object.create(BaseOverviewsDataview.prototype);
|
||||
@@ -36,21 +63,38 @@ Formula.prototype.constructor = Formula;
|
||||
|
||||
module.exports = Formula;
|
||||
|
||||
Formula.prototype.sql = function(psql, override, callback) {
|
||||
Formula.prototype.sql = function (psql, override, callback) {
|
||||
var self = this;
|
||||
var formulaQueryTpl = formulaQueryTpls[this.operation];
|
||||
|
||||
if ( formulaQueryTpl ) {
|
||||
if (formulaQueryTpl) {
|
||||
// supported formula for use with overviews
|
||||
if (this._isFloatColumn === null) {
|
||||
this._isFloatColumn = false;
|
||||
this.getColumnType(psql, this.column, this.queries.no_filters, function (err, type) {
|
||||
if (!err && !!type) {
|
||||
self._isFloatColumn = type.float;
|
||||
}
|
||||
self.sql(psql, override, callback);
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
var formulaSql = formulaQueryTpl({
|
||||
_query: this.rewrittenQuery(this.query),
|
||||
_isFloatColumn: this._isFloatColumn,
|
||||
_query: this.rewrittenQuery(this.query),
|
||||
_operation: this.operation,
|
||||
_column: this.column
|
||||
_column: this.column
|
||||
});
|
||||
|
||||
callback = callback || override;
|
||||
|
||||
debug(formulaSql);
|
||||
|
||||
return callback(null, formulaSql);
|
||||
}
|
||||
|
||||
|
||||
// default behaviour
|
||||
return this.defaultSql(psql, override, callback);
|
||||
};
|
||||
|
||||
@@ -1,23 +1,35 @@
|
||||
var _ = require('underscore');
|
||||
var BaseOverviewsDataview = require('./base');
|
||||
var BaseDataview = require('../histogram');
|
||||
var debug = require('debug')('windshaft:dataview:histogram:overview');
|
||||
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
|
||||
var columnTypeQueryTpl = dot.template(
|
||||
'SELECT pg_typeof({{=it.column}})::oid FROM ({{=it.query}}) _cdb_histogram_column_type limit 1'
|
||||
);
|
||||
|
||||
var BIN_MIN_NUMBER = 6;
|
||||
var BIN_MAX_NUMBER = 48;
|
||||
|
||||
var filteredQueryTpl = dot.template([
|
||||
'filtered_source AS (',
|
||||
' SELECT *',
|
||||
' FROM ({{=it._query}}) _cdb_filtered_source',
|
||||
' WHERE',
|
||||
' {{=it._column}} IS NOT NULL',
|
||||
' {{?it._isFloatColumn}}AND',
|
||||
' {{=it._column}} != \'infinity\'::float',
|
||||
' AND',
|
||||
' {{=it._column}} != \'-infinity\'::float',
|
||||
' AND',
|
||||
' {{=it._column}} != \'NaN\'::float{{?}}',
|
||||
')'
|
||||
].join(' \n'));
|
||||
|
||||
var basicsQueryTpl = dot.template([
|
||||
'basics AS (',
|
||||
' SELECT',
|
||||
' max({{=it._column}}) AS max_val, min({{=it._column}}) AS min_val,',
|
||||
' sum({{=it._column}}*_feature_count)/sum(_feature_count) AS avg_val, sum(_feature_count) AS total_rows',
|
||||
' FROM ({{=it._query}}) _cdb_basics',
|
||||
' FROM filtered_source',
|
||||
')'
|
||||
].join(' \n'));
|
||||
|
||||
@@ -26,7 +38,7 @@ var overrideBasicsQueryTpl = dot.template([
|
||||
' SELECT',
|
||||
' max({{=it._end}}) AS max_val, min({{=it._start}}) AS min_val,',
|
||||
' sum({{=it._column}}*_feature_count)/sum(_feature_count) AS avg_val, sum(_feature_count) AS total_rows',
|
||||
' FROM ({{=it._query}}) _cdb_basics',
|
||||
' FROM filtered_source',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
@@ -37,7 +49,7 @@ var iqrQueryTpl = dot.template([
|
||||
' SELECT quartile, max(_cdb_iqr_column) AS quartile_max from (',
|
||||
' SELECT {{=it._column}} AS _cdb_iqr_column, ntile(4) over (order by {{=it._column}}',
|
||||
' ) AS quartile',
|
||||
' FROM ({{=it._query}}) _cdb_rank) _cdb_quartiles',
|
||||
' FROM filtered_source) _cdb_quartiles',
|
||||
' WHERE quartile = 1 or quartile = 3',
|
||||
' GROUP BY quartile',
|
||||
' ) _cdb_iqr',
|
||||
@@ -56,7 +68,7 @@ var binsQueryTpl = dot.template([
|
||||
' )',
|
||||
' )',
|
||||
' END AS bins_number',
|
||||
' FROM basics, iqrange, ({{=it._query}}) _cdb_bins',
|
||||
' FROM basics, iqrange, filtered_source',
|
||||
' LIMIT 1',
|
||||
')'
|
||||
].join('\n'));
|
||||
@@ -76,11 +88,34 @@ var nullsQueryTpl = dot.template([
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var infinitiesQueryTpl = dot.template([
|
||||
'infinities AS (',
|
||||
' SELECT',
|
||||
' count(*) AS infinities_count',
|
||||
' FROM ({{=it._query}}) _cdb_histogram_infinities',
|
||||
' WHERE',
|
||||
' {{=it._column}} = \'infinity\'::float',
|
||||
' OR',
|
||||
' {{=it._column}} = \'-infinity\'::float',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var nansQueryTpl = dot.template([
|
||||
'nans AS (',
|
||||
' SELECT',
|
||||
' count(*) AS nans_count',
|
||||
' FROM ({{=it._query}}) _cdb_histogram_infinities',
|
||||
' WHERE {{=it._column}} = \'NaN\'::float',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var histogramQueryTpl = dot.template([
|
||||
'SELECT',
|
||||
' (max_val - min_val) / cast(bins_number as float) AS bin_width,',
|
||||
' bins_number,',
|
||||
' nulls_count,',
|
||||
' {{?it._isFloatColumn}}infinities_count,',
|
||||
' nans_count,{{?}}',
|
||||
' avg_val,',
|
||||
' CASE WHEN min_val = max_val',
|
||||
' THEN 0',
|
||||
@@ -90,14 +125,14 @@ var histogramQueryTpl = dot.template([
|
||||
' max({{=it._column}})::numeric AS max,',
|
||||
' sum({{=it._column}}*_feature_count)/sum(_feature_count)::numeric AS avg,',
|
||||
' sum(_feature_count) AS freq',
|
||||
'FROM ({{=it._query}}) _cdb_histogram, basics, nulls, bins',
|
||||
'WHERE {{=it._column}} IS NOT NULL',
|
||||
'FROM filtered_source, basics, nulls, bins{{?it._isFloatColumn}},infinities, nans{{?}}',
|
||||
'GROUP BY bin, bins_number, bin_width, nulls_count, avg_val',
|
||||
' {{?it._isFloatColumn}}, infinities_count, nans_count{{?}}',
|
||||
'ORDER BY bin'
|
||||
].join('\n'));
|
||||
|
||||
function Histogram(query, options, queryRewriter, queryRewriteData, params, queries) {
|
||||
BaseOverviewsDataview.call(this, query, options, BaseDataview, queryRewriter, queryRewriteData, params);
|
||||
BaseOverviewsDataview.call(this, query, options, BaseDataview, queryRewriter, queryRewriteData, params, queries);
|
||||
|
||||
this.query = query;
|
||||
this.queries = queries;
|
||||
@@ -112,36 +147,23 @@ Histogram.prototype.constructor = Histogram;
|
||||
|
||||
module.exports = Histogram;
|
||||
|
||||
|
||||
var DATE_OIDS = {
|
||||
1082: true,
|
||||
1114: true,
|
||||
1184: true
|
||||
};
|
||||
|
||||
Histogram.prototype.sql = function(psql, override, callback) {
|
||||
var self = this;
|
||||
|
||||
if (!callback) {
|
||||
callback = override;
|
||||
override = {};
|
||||
}
|
||||
|
||||
var self = this;
|
||||
|
||||
var _column = this.column;
|
||||
|
||||
var columnTypeQuery = columnTypeQueryTpl({
|
||||
column: _column, query: this.rewrittenQuery(this.queries.no_filters)
|
||||
});
|
||||
|
||||
if (this._columnType === null) {
|
||||
psql.query(columnTypeQuery, function(err, result) {
|
||||
this.getColumnType(psql, this.column, this.queries.no_filters, function (err, type) {
|
||||
// assume numeric, will fail later
|
||||
self._columnType = 'numeric';
|
||||
if (!err && !!result.rows[0]) {
|
||||
var pgType = result.rows[0].pg_typeof;
|
||||
if (DATE_OIDS.hasOwnProperty(pgType)) {
|
||||
self._columnType = 'date';
|
||||
}
|
||||
if (!err && !!type) {
|
||||
self._columnType = Object.keys(type).find(function (key) {
|
||||
return type[key];
|
||||
});
|
||||
}
|
||||
self.sql(psql, override, callback);
|
||||
}, true); // use read-only transaction
|
||||
@@ -154,11 +176,24 @@ Histogram.prototype.sql = function(psql, override, callback) {
|
||||
return this.defaultSql(psql, override, callback);
|
||||
}
|
||||
|
||||
var histogramSql = this._buildQuery(override);
|
||||
|
||||
return callback(null, histogramSql);
|
||||
};
|
||||
|
||||
Histogram.prototype._buildQuery = function (override) {
|
||||
var filteredQuery, basicsQuery, binsQuery;
|
||||
var _column = this.column;
|
||||
var _query = this.rewrittenQuery(this.query);
|
||||
|
||||
var basicsQuery, binsQuery;
|
||||
filteredQuery = filteredQueryTpl({
|
||||
_isFloatColumn: this._columnType === 'float',
|
||||
_query: _query,
|
||||
_column: _column
|
||||
});
|
||||
|
||||
if (override && _.has(override, 'start') && _.has(override, 'end') && _.has(override, 'bins')) {
|
||||
if (this._shouldOverride(override)) {
|
||||
debug('overriding with %j', override);
|
||||
basicsQuery = overrideBasicsQueryTpl({
|
||||
_query: _query,
|
||||
_column: _column,
|
||||
@@ -177,7 +212,7 @@ Histogram.prototype.sql = function(psql, override, callback) {
|
||||
_column: _column
|
||||
});
|
||||
|
||||
if (override && _.has(override, 'bins')) {
|
||||
if (this._shouldOverrideBins(override)) {
|
||||
binsQuery = [
|
||||
overrideBinsQueryTpl({
|
||||
_bins: override.bins
|
||||
@@ -198,22 +233,50 @@ Histogram.prototype.sql = function(psql, override, callback) {
|
||||
}
|
||||
}
|
||||
|
||||
var cteSql = [
|
||||
filteredQuery,
|
||||
basicsQuery,
|
||||
binsQuery,
|
||||
nullsQueryTpl({
|
||||
_query: _query,
|
||||
_column: _column
|
||||
})
|
||||
];
|
||||
|
||||
var histogramSql = [
|
||||
"WITH",
|
||||
[
|
||||
basicsQuery,
|
||||
binsQuery,
|
||||
nullsQueryTpl({
|
||||
if (this._columnType === 'float') {
|
||||
cteSql.push(
|
||||
infinitiesQueryTpl({
|
||||
_query: _query,
|
||||
_column: _column
|
||||
}),
|
||||
nansQueryTpl({
|
||||
_query: _query,
|
||||
_column: _column
|
||||
})
|
||||
].join(',\n'),
|
||||
);
|
||||
}
|
||||
|
||||
var histogramSql = [
|
||||
"WITH",
|
||||
cteSql.join(',\n'),
|
||||
histogramQueryTpl({
|
||||
_isFloatColumn: this._columnType === 'float',
|
||||
_query: _query,
|
||||
_column: _column
|
||||
})
|
||||
].join('\n');
|
||||
|
||||
return callback(null, histogramSql);
|
||||
debug(histogramSql);
|
||||
|
||||
return histogramSql;
|
||||
};
|
||||
|
||||
Histogram.prototype._shouldOverride = function (override) {
|
||||
return override && _.has(override, 'start') && _.has(override, 'end') && _.has(override, 'bins');
|
||||
};
|
||||
|
||||
Histogram.prototype._shouldOverrideBins = function (override) {
|
||||
return override && _.has(override, 'bins');
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
module.exports = {
|
||||
Aggregation: require('./aggregation'),
|
||||
Formula: require('./formula'),
|
||||
Histogram: require('./histogram'),
|
||||
List: require('./list')
|
||||
Histogram: require('./histogram')
|
||||
};
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
var BaseOverviewsDataview = require('./base');
|
||||
var BaseDataview = require('../list');
|
||||
|
||||
function List(query, options, queryRewriter, queryRewriteData, params) {
|
||||
BaseOverviewsDataview.call(this, query, options, BaseDataview, queryRewriter, queryRewriteData, params);
|
||||
}
|
||||
|
||||
List.prototype = Object.create(BaseOverviewsDataview.prototype);
|
||||
List.prototype.constructor = List;
|
||||
|
||||
module.exports = List;
|
||||
@@ -8,7 +8,7 @@ var filterQueryTpl = dot.template([
|
||||
].join('\n'));
|
||||
|
||||
var bboxFilterTpl = dot.template(
|
||||
'{{=it._column}} && ST_Transform(ST_MakeEnvelope({{=it._bbox}}, 4326), {{=it._srid}})'
|
||||
'ST_Intersects({{=it._column}}, ST_Transform(ST_MakeEnvelope({{=it._bbox}}, 4326), {{=it._srid}}))'
|
||||
);
|
||||
|
||||
var LATITUDE_MAX_VALUE = 85.0511287798066;
|
||||
@@ -66,7 +66,8 @@ function getBoundingBoxes(west, south, east, north) {
|
||||
bboxes.push([west, south, east, north]);
|
||||
} else {
|
||||
bboxes.push([west, south, 180, north]);
|
||||
bboxes.push([-180, south, east % 180, north]);
|
||||
// here we assume west,east have been adjusted => west >= -180 => east > 180
|
||||
bboxes.push([-180, south, east - 360, north]);
|
||||
}
|
||||
|
||||
return bboxes;
|
||||
|
||||
@@ -26,7 +26,7 @@ CreateLayergroupMapConfigProvider.prototype.getMapConfig = function(callback) {
|
||||
var context = {};
|
||||
step(
|
||||
function prepareContextLimits() {
|
||||
self.userLimitsApi.getRenderLimits(self.user, this);
|
||||
self.userLimitsApi.getRenderLimits(self.user, self.params.api_key, this);
|
||||
},
|
||||
function handleRenderLimits(err, renderLimits) {
|
||||
assert.ifError(err);
|
||||
|
||||
@@ -27,7 +27,7 @@ MapStoreMapConfigProvider.prototype.getMapConfig = function(callback) {
|
||||
var context = {};
|
||||
step(
|
||||
function prepareContextLimits() {
|
||||
self.userLimitsApi.getRenderLimits(self.user, this);
|
||||
self.userLimitsApi.getRenderLimits(self.user, self.params.api_key, this);
|
||||
},
|
||||
function handleRenderLimits(err, renderLimits) {
|
||||
assert.ifError(err);
|
||||
|
||||
@@ -114,7 +114,7 @@ NamedMapMapConfigProvider.prototype.getMapConfig = function(callback) {
|
||||
function prepareContextLimits(err, _mapConfig) {
|
||||
assert.ifError(err);
|
||||
mapConfig = _mapConfig;
|
||||
self.userLimitsApi.getRenderLimits(self.owner, this);
|
||||
self.userLimitsApi.getRenderLimits(self.owner, self.params.api_key, this);
|
||||
},
|
||||
function cacheAndReturnMapConfig(err, renderLimits) {
|
||||
self.err = err;
|
||||
|
||||
@@ -12,7 +12,8 @@ var VarnishHttpCacheBackend = require('./cache/backend/varnish_http');
|
||||
var FastlyCacheBackend = require('./cache/backend/fastly');
|
||||
|
||||
var StatsClient = require('./stats/client');
|
||||
var Profiler = require('./stats/profiler_proxy');
|
||||
const stats = require('./middleware/stats');
|
||||
|
||||
var RendererStatsReporter = require('./stats/reporter/renderer');
|
||||
|
||||
var windshaft = require('windshaft');
|
||||
@@ -44,6 +45,11 @@ var MapConfigAdapter = require('./models/mapconfig/adapter');
|
||||
|
||||
var StatsBackend = require('./backends/stats');
|
||||
|
||||
const lzmaMiddleware = require('./middleware/lzma');
|
||||
const errorMiddleware = require('./middleware/error-middleware');
|
||||
|
||||
const prepareContextMiddleware = require('./middleware/context');
|
||||
|
||||
module.exports = function(serverOptions) {
|
||||
// Make stats client globally accessible
|
||||
global.statsClient = StatsClient.getInstance(serverOptions.statsd);
|
||||
@@ -118,8 +124,27 @@ module.exports = function(serverOptions) {
|
||||
var onTileErrorStrategy;
|
||||
if (global.environment.enabledFeatures.onTileErrorStrategy !== false) {
|
||||
onTileErrorStrategy = function onTileErrorStrategy$TimeoutTile(err, tile, headers, stats, format, callback) {
|
||||
if (err && err.message === 'Render timed out' && format === 'png') {
|
||||
return callback(null, timeoutErrorTile, { 'Content-Type': 'image/png' }, {});
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -133,7 +158,8 @@ module.exports = function(serverOptions) {
|
||||
grainstore: serverOptions.grainstore,
|
||||
mapnik: serverOptions.renderer.mapnik
|
||||
},
|
||||
http: serverOptions.renderer.http
|
||||
http: serverOptions.renderer.http,
|
||||
mvt: serverOptions.renderer.mvt
|
||||
});
|
||||
|
||||
// initialize render cache
|
||||
@@ -187,12 +213,16 @@ module.exports = function(serverOptions) {
|
||||
|
||||
var versions = getAndValidateVersions(serverOptions);
|
||||
|
||||
const prepareContext = typeof serverOptions.req2params === 'function' ?
|
||||
serverOptions.req2params :
|
||||
prepareContextMiddleware(authApi, pgConnection);
|
||||
|
||||
/*******************************************************************************************************************
|
||||
* Routing
|
||||
******************************************************************************************************************/
|
||||
|
||||
new controller.Layergroup(
|
||||
authApi,
|
||||
prepareContext,
|
||||
pgConnection,
|
||||
mapStore,
|
||||
tileBackend,
|
||||
@@ -205,7 +235,7 @@ module.exports = function(serverOptions) {
|
||||
).register(app);
|
||||
|
||||
new controller.Map(
|
||||
authApi,
|
||||
prepareContext,
|
||||
pgConnection,
|
||||
templateMaps,
|
||||
mapBackend,
|
||||
@@ -218,8 +248,7 @@ module.exports = function(serverOptions) {
|
||||
).register(app);
|
||||
|
||||
new controller.NamedMaps(
|
||||
authApi,
|
||||
pgConnection,
|
||||
prepareContext,
|
||||
namedMapProviderCache,
|
||||
tileBackend,
|
||||
previewBackend,
|
||||
@@ -228,9 +257,9 @@ module.exports = function(serverOptions) {
|
||||
metadataBackend
|
||||
).register(app);
|
||||
|
||||
new controller.NamedMapsAdmin(authApi, pgConnection, templateMaps).register(app);
|
||||
new controller.NamedMapsAdmin(authApi, templateMaps).register(app);
|
||||
|
||||
new controller.Analyses(authApi, pgConnection).register(app);
|
||||
new controller.Analyses(prepareContext).register(app);
|
||||
|
||||
new controller.ServerInfo(versions).register(app);
|
||||
|
||||
@@ -238,6 +267,8 @@ module.exports = function(serverOptions) {
|
||||
* END Routing
|
||||
******************************************************************************************************************/
|
||||
|
||||
app.use(errorMiddleware());
|
||||
|
||||
return app;
|
||||
};
|
||||
|
||||
@@ -310,15 +341,28 @@ function bootstrap(opts) {
|
||||
app.enable('jsonp callback');
|
||||
app.disable('x-powered-by');
|
||||
app.disable('etag');
|
||||
|
||||
// Fix: https://github.com/CartoDB/Windshaft-cartodb/issues/705
|
||||
// See: http://expressjs.com/en/4x/api.html#app.set
|
||||
app.set('json replacer', function (key, value) {
|
||||
if (value !== value) {
|
||||
return 'NaN';
|
||||
}
|
||||
|
||||
if (value === Infinity) {
|
||||
return 'Infinity';
|
||||
}
|
||||
|
||||
if (value === -Infinity) {
|
||||
return '-Infinity';
|
||||
}
|
||||
|
||||
return value;
|
||||
});
|
||||
|
||||
app.use(bodyParser.json());
|
||||
|
||||
app.use(function bootstrap$prepareRequestResponse(req, res, next) {
|
||||
req.context = req.context || {};
|
||||
req.profiler = new Profiler({
|
||||
statsd_client: global.statsClient,
|
||||
profile: opts.useProfiler
|
||||
});
|
||||
|
||||
if (global.environment && global.environment.api_hostname) {
|
||||
res.set('X-Served-By-Host', global.environment.api_hostname);
|
||||
}
|
||||
@@ -326,6 +370,13 @@ function bootstrap(opts) {
|
||||
next();
|
||||
});
|
||||
|
||||
app.use(stats({
|
||||
enabled: opts.useProfiler,
|
||||
statsClient: global.statsClient
|
||||
}));
|
||||
|
||||
app.use(lzmaMiddleware);
|
||||
|
||||
// temporary measure until we upgrade to newer version expressjs so we can check err.status
|
||||
app.use(function(err, req, res, next) {
|
||||
if (err) {
|
||||
|
||||
@@ -81,6 +81,7 @@ module.exports = {
|
||||
statsInterval: rendererConfig.statsInterval
|
||||
},
|
||||
renderer: {
|
||||
mvt: rendererConfig.mvt,
|
||||
mapnik: _.defaults(rendererConfig.mapnik, {
|
||||
geojson: {
|
||||
dbPoolParams: {
|
||||
|
||||
41
package.json
41
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "windshaft-cartodb",
|
||||
"version": "3.9.0",
|
||||
"version": "4.1.1",
|
||||
"description": "A map tile server for CartoDB",
|
||||
"keywords": [
|
||||
"cartodb"
|
||||
@@ -16,48 +16,59 @@
|
||||
"contributors": [
|
||||
"Simon Tokumine <simon@vizzuality.com>",
|
||||
"Javi Santana <jsantana@vizzuality.com>",
|
||||
"Sandro Santilli <strk@vizzuality.com>"
|
||||
"Sandro Santilli <strk@vizzuality.com>",
|
||||
"Carlos Matallín <matallo@carto.com>",
|
||||
"Daniel Garcia Aubert <dgaubert@carto.com>",
|
||||
"Mario de Frutos <mario.defrutos@carto.com>"
|
||||
],
|
||||
"dependencies": {
|
||||
"body-parser": "~1.14.0",
|
||||
"camshaft": "0.55.2",
|
||||
"cartodb-psql": "0.8.0",
|
||||
"cartodb-query-tables": "0.2.0",
|
||||
"cartodb-redis": "0.13.2",
|
||||
"debug": "~2.2.0",
|
||||
"body-parser": "^1.18.2",
|
||||
"camshaft": "0.59.4",
|
||||
"cartodb-psql": "0.10.2",
|
||||
"cartodb-query-tables": "0.3.0",
|
||||
"cartodb-redis": "0.14.0",
|
||||
"debug": "^3.1.0",
|
||||
"dot": "~1.0.2",
|
||||
"express": "~4.13.3",
|
||||
"express": "~4.16.0",
|
||||
"fastly-purge": "~1.0.1",
|
||||
"log4js": "cartodb/log4js-node#cdb",
|
||||
"lru-cache": "2.6.5",
|
||||
"lzma": "~2.3.2",
|
||||
"node-statsd": "~0.0.7",
|
||||
"on-headers": "^1.0.1",
|
||||
"queue-async": "~1.0.7",
|
||||
"redis-mpool": "0.4.1",
|
||||
"request": "~2.79.0",
|
||||
"request": "^2.83.0",
|
||||
"semver": "~5.3.0",
|
||||
"step": "~0.0.6",
|
||||
"step-profiler": "~0.3.0",
|
||||
"turbo-carto": "0.19.1",
|
||||
"turbo-carto": "0.20.2",
|
||||
"underscore": "~1.6.0",
|
||||
"windshaft": "3.2.1",
|
||||
"windshaft": "4.0.1",
|
||||
"yargs": "~5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"istanbul": "~0.4.3",
|
||||
"jshint": "~2.9.4",
|
||||
"mocha": "~3.4.1",
|
||||
"moment": "~2.18.1",
|
||||
"nock": "~2.11.0",
|
||||
"redis": "~0.12.1",
|
||||
"semver": "~1.1.4",
|
||||
"strftime": "~0.8.2"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "jshint lib test",
|
||||
"preinstall": "make pre-install",
|
||||
"test": "make test-all"
|
||||
"test": "make test-all",
|
||||
"update-internal-deps": "rm -rf node_modules && rm -f yarn.lock && yarn",
|
||||
"docker-install": "sudo apt install docker.io && sudo usermod -aG docker $(whoami)",
|
||||
"docker-pull": "docker pull cartoimages/windshaft-testing",
|
||||
"docker-test": "docker run -v `pwd`:/srv cartoimages/windshaft-testing bash docker-test.sh",
|
||||
"docker-bash": "docker run -it -v `pwd`:/srv cartoimages/windshaft-testing bash",
|
||||
"docker-publish": "docker push cartoimages/windshaft-carto-testing"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9",
|
||||
"yarn": "^0.21.3"
|
||||
"yarn": ">=0.27.5 <1.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ if test x"$OPT_COVERAGE" = xyes; then
|
||||
./node_modules/.bin/istanbul cover node_modules/.bin/_mocha -- -u tdd -t 5000 ${TESTS}
|
||||
else
|
||||
echo "Running tests"
|
||||
mocha -u tdd -t 5000 ${TESTS}
|
||||
./node_modules/.bin/_mocha -c -u tdd -t 5000 ${TESTS}
|
||||
fi
|
||||
ret=$?
|
||||
|
||||
|
||||
37
scripts/mvt-timeout-error.py
Executable file
37
scripts/mvt-timeout-error.py
Executable file
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import mapbox_vector_tile
|
||||
|
||||
lines_list = []
|
||||
|
||||
# main diagonal line
|
||||
lines_list.append({ "geometry":"LINESTRING (0 0, 4096 4096)"})
|
||||
|
||||
# diagonal lines
|
||||
for i in range(4096/32, 4096, 4096/32):
|
||||
start = i
|
||||
end = 4096 - i
|
||||
|
||||
lines_list.append({ "geometry":"LINESTRING (0 " + str(start) + ", " + str(end) + " 4096)" })
|
||||
lines_list.append({ "geometry":"LINESTRING (" + str(start) + " 0, 4096 " + str(end) + ")" })
|
||||
|
||||
# box lines
|
||||
lines_list.append({ "geometry":"LINESTRING (0 0, 0 4096)"})
|
||||
lines_list.append({ "geometry":"LINESTRING (0 4096, 4096 4096)"})
|
||||
lines_list.append({ "geometry":"LINESTRING (4096 4096, 4096 0)"})
|
||||
lines_list.append({ "geometry":"LINESTRING (4096 0, 0 0)"})
|
||||
|
||||
|
||||
tile = mapbox_vector_tile.encode([
|
||||
{
|
||||
"name": "errorTileSquareLayer",
|
||||
"features": [{ "geometry":"POLYGON ((0 0, 0 4096, 4096 4096, 4096 0, 0 0))" }]
|
||||
},
|
||||
{
|
||||
"name": "errorTileStripesLayer",
|
||||
"features": lines_list
|
||||
}
|
||||
])
|
||||
|
||||
with open('./assets/render-timeout-fallback.mvt', 'w+') as f:
|
||||
f.write(tile)
|
||||
114
test/acceptance/analysis/analyses-controller.js
Normal file
114
test/acceptance/analysis/analyses-controller.js
Normal file
@@ -0,0 +1,114 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var TestClient = require('../../support/test-client');
|
||||
|
||||
|
||||
describe('analyses controller', function () {
|
||||
const mapConfig = {
|
||||
version: '1.5.0',
|
||||
layers:
|
||||
[{
|
||||
type: 'cartodb',
|
||||
options:
|
||||
{
|
||||
source: { id: 'a1' },
|
||||
cartocss: TestClient.CARTOCSS.POLYGONS,
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}],
|
||||
dataviews: {},
|
||||
analyses:
|
||||
[{
|
||||
id: 'a1',
|
||||
type: 'buffer',
|
||||
params: {
|
||||
source: {
|
||||
type: 'source',
|
||||
params: {
|
||||
query: 'select * from analysis_banks limit 1'
|
||||
}
|
||||
},
|
||||
radius: 250
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
});
|
||||
|
||||
it('should get an array of analyses from catalog', function (done) {
|
||||
this.testClient.getAnalysesCatalog({}, (err, result) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
assert.ok(Array.isArray(result.catalog));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should support jsonp responses', function (done) {
|
||||
this.testClient.getAnalysesCatalog({ jsonp: 'jsonp_test' }, (err, result) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
assert.ok(result);
|
||||
|
||||
let didRunJsonCallback = false;
|
||||
// jshint ignore:start
|
||||
function jsonp_test(body) {
|
||||
assert.ok(Array.isArray(body.catalog));
|
||||
didRunJsonCallback = true;
|
||||
}
|
||||
|
||||
eval(result);
|
||||
// jshint ignore:end
|
||||
|
||||
assert.ok(didRunJsonCallback);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should respond "unauthorized" when missing api_key', function (done) {
|
||||
const apiKey = this.testClient.apiKey;
|
||||
this.testClient.apiKey = null;
|
||||
|
||||
this.testClient.getAnalysesCatalog({ status: 401 }, (err, result) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
assert.deepEqual(result.errors[0], 'Unauthorized');
|
||||
this.testClient.apiKey = apiKey;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should get an array of analyses from catalog', function (done) {
|
||||
this.testClient.getTile(0, 0, 0, (err) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
this.testClient.getAnalysesCatalog({}, (err, result) => {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
assert.ok(Array.isArray(result.catalog));
|
||||
assert.ok(result.catalog.length >= 2); // buffer & source at least
|
||||
|
||||
result.catalog
|
||||
.filter(analysis => analysis.node_id === '0a215e1f3405381cf0ea6b3b0deb6fdcfdc2fcaa')
|
||||
.forEach(analysis => assert.equal(analysis.type, 'buffer'));
|
||||
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,81 +0,0 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var TestClient = require('../../support/test-client');
|
||||
|
||||
describe('analysis-layers-dataviews-geojson', function() {
|
||||
|
||||
function createMapConfig(layers, dataviews, analysis) {
|
||||
return {
|
||||
version: '1.5.0',
|
||||
layers: layers,
|
||||
dataviews: dataviews || {},
|
||||
analyses: analysis || []
|
||||
};
|
||||
}
|
||||
|
||||
var CARTOCSS = [
|
||||
"#points {",
|
||||
" marker-fill-opacity: 1.0;",
|
||||
" marker-line-color: #FFF;",
|
||||
" marker-line-width: 0.5;",
|
||||
" marker-line-opacity: 1.0;",
|
||||
" marker-placement: point;",
|
||||
" marker-type: ellipse;",
|
||||
" marker-width: 8;",
|
||||
" marker-fill: red;",
|
||||
" marker-allow-overlap: true;",
|
||||
"}"
|
||||
].join('\n');
|
||||
|
||||
var mapConfig = createMapConfig(
|
||||
[
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"source": {
|
||||
"id": "2570e105-7b37-40d2-bdf4-1af889598745"
|
||||
},
|
||||
"cartocss": CARTOCSS,
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
{
|
||||
pop_max_histogram: {
|
||||
source: {
|
||||
id: '2570e105-7b37-40d2-bdf4-1af889598745'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'pop_max'
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
{
|
||||
"id": "2570e105-7b37-40d2-bdf4-1af889598745",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from populated_places_simple_reduced"
|
||||
}
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
it('should get pop_max column from dataview', function(done) {
|
||||
var testClient = new TestClient(mapConfig, 1234);
|
||||
|
||||
testClient.getTile(0, 0, 0, {format: 'geojson', layers: 0}, function(err, res, geojson) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.ok(Array.isArray(geojson.features));
|
||||
assert.ok(geojson.features.length > 0);
|
||||
var feature = geojson.features[0];
|
||||
assert.ok(feature.properties.hasOwnProperty('pop_max'), 'Missing pop_max property');
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -5,34 +5,34 @@ var TestClient = require('../../support/test-client');
|
||||
var dot = require('dot');
|
||||
var debug = require('debug')('windshaft:cartodb:test');
|
||||
|
||||
describe('analysis-layers use cases', function() {
|
||||
describe('analysis-layers use cases', function () {
|
||||
|
||||
|
||||
var multitypeStyleTemplate = dot.template([
|
||||
"#points['mapnik::geometry_type'=1] {",
|
||||
" marker-fill-opacity: {{=it._opacity}};",
|
||||
" marker-line-color: #FFF;",
|
||||
" marker-line-width: 0.5;",
|
||||
" marker-line-opacity: {{=it._opacity}};",
|
||||
" marker-placement: point;",
|
||||
" marker-type: ellipse;",
|
||||
" marker-width: 8;",
|
||||
" marker-fill: {{=it._color}};",
|
||||
" marker-allow-overlap: true;",
|
||||
"}",
|
||||
"#lines['mapnik::geometry_type'=2] {",
|
||||
" line-color: {{=it._color}};",
|
||||
" line-width: 2;",
|
||||
" line-opacity: {{=it._opacity}};",
|
||||
"}",
|
||||
"#polygons['mapnik::geometry_type'=3] {",
|
||||
" polygon-fill: {{=it._color}};",
|
||||
" polygon-opacity: {{=it._opacity}};",
|
||||
" line-color: #FFF;",
|
||||
" line-width: 0.5;",
|
||||
" line-opacity: {{=it._opacity}};",
|
||||
"}"
|
||||
].join('\n'));
|
||||
var multitypeStyleTemplate = dot.template(
|
||||
`#points['mapnik::geometry_type'=1] {
|
||||
marker-fill-opacity: {{=it._opacity}};
|
||||
marker-line-color: #FFF;
|
||||
marker-line-width: 0.5;
|
||||
marker-line-opacity: {{=it._opacity}};
|
||||
marker-placement: point;
|
||||
marker-type: ellipse;
|
||||
marker-width: 8;
|
||||
marker-fill: {{=it._color}};
|
||||
marker-allow-overlap: true;
|
||||
}
|
||||
#lines['mapnik::geometry_type'=2] {
|
||||
line-color: {{=it._color}};
|
||||
line-width: 2;
|
||||
line-opacity: {{=it._opacity}};
|
||||
}
|
||||
#polygons['mapnik::geometry_type'=3] {
|
||||
polygon-fill: {{=it._color}};
|
||||
polygon-opacity: {{=it._opacity}};
|
||||
line-color: #FFF;
|
||||
line-width: 0.5;
|
||||
line-opacity: {{=it._opacity}};
|
||||
}`
|
||||
);
|
||||
|
||||
|
||||
function cartocss(color, opacity) {
|
||||
@@ -47,18 +47,53 @@ describe('analysis-layers use cases', function() {
|
||||
version: '1.5.0',
|
||||
layers: layers,
|
||||
dataviews: dataviews || {},
|
||||
analysis: analysis || []
|
||||
analyses: analysis || []
|
||||
};
|
||||
}
|
||||
|
||||
function analysisDef(analysis) {
|
||||
return JSON.stringify(analysis);
|
||||
}
|
||||
|
||||
var DEFAULT_MULTITYPE_STYLE = cartocss();
|
||||
|
||||
var TILE_ANALYSIS_TABLES = { z: 14, x: 8023, y: 6177 };
|
||||
|
||||
var pointInPolygonDef = {
|
||||
id: 'a1',
|
||||
type: 'point-in-polygon',
|
||||
params: {
|
||||
points_source: {
|
||||
type: 'source',
|
||||
params: {
|
||||
query: 'select * from analysis_rent_listings'
|
||||
}
|
||||
},
|
||||
polygons_source: {
|
||||
type: 'buffer',
|
||||
params: {
|
||||
source: {
|
||||
type: 'source',
|
||||
params: {
|
||||
query: 'select * from analysis_banks'
|
||||
}
|
||||
},
|
||||
radius: 250
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var bufferDef = {
|
||||
id: 'b1',
|
||||
type: 'buffer',
|
||||
params: {
|
||||
source: {
|
||||
type: 'source',
|
||||
params: {
|
||||
query: 'select * from analysis_banks'
|
||||
}
|
||||
},
|
||||
radius: 250
|
||||
}
|
||||
};
|
||||
|
||||
var useCases = [
|
||||
{
|
||||
desc: '1 mapnik layer',
|
||||
@@ -68,7 +103,7 @@ describe('analysis-layers use cases', function() {
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: "select * from analysis_rent_listings",
|
||||
sql: 'select * from analysis_rent_listings',
|
||||
cartocss: DEFAULT_MULTITYPE_STYLE,
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
@@ -83,7 +118,7 @@ describe('analysis-layers use cases', function() {
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: "select * from analysis_banks",
|
||||
sql: 'select * from analysis_banks',
|
||||
cartocss: cartocss('#2167AB'),
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
@@ -91,7 +126,7 @@ describe('analysis-layers use cases', function() {
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: "select * from analysis_rent_listings",
|
||||
sql: 'select * from analysis_rent_listings',
|
||||
cartocss: DEFAULT_MULTITYPE_STYLE,
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
@@ -105,30 +140,27 @@ describe('analysis-layers use cases', function() {
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: "select * from analysis_rent_listings",
|
||||
sql: 'select * from analysis_rent_listings',
|
||||
cartocss: DEFAULT_MULTITYPE_STYLE,
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'analysis',
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
def: analysisDef({
|
||||
"type": "buffer",
|
||||
"params": {
|
||||
"source": {
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from analysis_banks"
|
||||
}
|
||||
},
|
||||
"radius": 250
|
||||
}
|
||||
}),
|
||||
cartocss: cartocss('black', 0.5)
|
||||
source: {
|
||||
id: 'b1'
|
||||
},
|
||||
cartocss: DEFAULT_MULTITYPE_STYLE,
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
])
|
||||
],
|
||||
{},
|
||||
[
|
||||
bufferDef
|
||||
]
|
||||
)
|
||||
},
|
||||
|
||||
{
|
||||
@@ -137,531 +169,115 @@ describe('analysis-layers use cases', function() {
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: "select * from analysis_rent_listings",
|
||||
sql: 'select * from analysis_rent_listings',
|
||||
cartocss: DEFAULT_MULTITYPE_STYLE,
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'analysis',
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
def: analysisDef({
|
||||
"type": "point-in-polygon",
|
||||
"params": {
|
||||
"pointsSource": {
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from analysis_rent_listings"
|
||||
}
|
||||
},
|
||||
"polygonsSource": {
|
||||
"type": "buffer",
|
||||
"params": {
|
||||
"source": {
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from analysis_banks"
|
||||
}
|
||||
},
|
||||
"radius": 250
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
cartocss: cartocss('green', 1.0)
|
||||
source: {
|
||||
id: 'a1'
|
||||
},
|
||||
cartocss: DEFAULT_MULTITYPE_STYLE,
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
])
|
||||
],
|
||||
{},
|
||||
[
|
||||
pointInPolygonDef
|
||||
]
|
||||
)
|
||||
},
|
||||
|
||||
{
|
||||
desc: 'point-in-polygon from buffer atm-machines and rent listings + rent listings',
|
||||
mapConfig: mapConfig([
|
||||
{
|
||||
type: 'analysis',
|
||||
options: {
|
||||
def: analysisDef({
|
||||
"type": "point-in-polygon",
|
||||
"params": {
|
||||
"pointsSource": {
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from analysis_rent_listings"
|
||||
}
|
||||
},
|
||||
"polygonsSource": {
|
||||
"type": "buffer",
|
||||
"params": {
|
||||
"source": {
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from analysis_banks"
|
||||
}
|
||||
},
|
||||
"radius": 250
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
cartocss: cartocss('green', 1.0)
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: "select * from analysis_rent_listings",
|
||||
cartocss: DEFAULT_MULTITYPE_STYLE,
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
|
||||
{
|
||||
desc: 'buffer + point-in-polygon from buffer atm-machines and rent listings + rent listings',
|
||||
mapConfig: mapConfig([
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: "select * from analysis_rent_listings",
|
||||
cartocss: DEFAULT_MULTITYPE_STYLE,
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'analysis',
|
||||
options: {
|
||||
def: analysisDef({
|
||||
"type": "buffer",
|
||||
"params": {
|
||||
"source": {
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from analysis_banks"
|
||||
}
|
||||
},
|
||||
"radius": 300
|
||||
}
|
||||
}),
|
||||
cartocss: cartocss('magenta', 0.5)
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'analysis',
|
||||
options: {
|
||||
def: analysisDef({
|
||||
"type": "point-in-polygon",
|
||||
"params": {
|
||||
"pointsSource": {
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from analysis_rent_listings"
|
||||
}
|
||||
},
|
||||
"polygonsSource": {
|
||||
"type": "buffer",
|
||||
"params": {
|
||||
"source": {
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from analysis_banks"
|
||||
}
|
||||
},
|
||||
"radius": 300
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
cartocss: cartocss('green', 1.0)
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
|
||||
{
|
||||
skip: true,
|
||||
desc: 'buffer + point-in-polygon from buffer atm-machines and rent listings + rent listings',
|
||||
mapConfig: mapConfig([
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
"source": { id: "a" },
|
||||
"cartocss": DEFAULT_MULTITYPE_STYLE,
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
"source": { id: "b1" },
|
||||
"cartocss": cartocss('green', 1.0),
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
"source": { id: "b2" },
|
||||
"cartocss": cartocss('magenta', 0.5),
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
id: "b2",
|
||||
options: {
|
||||
def: analysisDef({
|
||||
"type": "count-in-polygon",
|
||||
"id": "a0",
|
||||
"params": {
|
||||
"columnName": 'count_airbnb',
|
||||
"pointsSource": {
|
||||
"type": "source",
|
||||
"params": {
|
||||
query: "select * from analysis_rent_listings"
|
||||
},
|
||||
dataviews: {
|
||||
price_histogram: {
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'price'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"polygonsSource": {
|
||||
"id": "b1",
|
||||
"type": "buffer",
|
||||
"params": {
|
||||
"source": {
|
||||
"id": "b0",
|
||||
"type": "source",
|
||||
"params": {
|
||||
query: "select * from analysis_banks"
|
||||
}
|
||||
},
|
||||
"radius": 250
|
||||
},
|
||||
dataviews: {
|
||||
bank_category: {
|
||||
type: 'aggregation',
|
||||
options: {
|
||||
column: 'bank'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
dataviews: {
|
||||
count_histogram: {
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'count_airbnb'
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
cartocss: cartocss('green', 1.0)
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
|
||||
{
|
||||
skip: true,
|
||||
desc: 'I. Distribution centers',
|
||||
mapConfig: mapConfig(
|
||||
// layers
|
||||
[
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
"source": { id: "b0" },
|
||||
"cartocss": [
|
||||
"#distribution_centers {",
|
||||
" marker-fill-opacity: 1.0;",
|
||||
" marker-line-color: #FFF;",
|
||||
" marker-line-width: 0.5;",
|
||||
" marker-line-opacity: 0.7;",
|
||||
" marker-placement: point;",
|
||||
" marker-type: ellipse;",
|
||||
" marker-width: 8;",
|
||||
" marker-fill: blue;",
|
||||
" marker-allow-overlap: true;",
|
||||
"}"
|
||||
].join('\n'),
|
||||
"cartocss_version": "2.3.0"
|
||||
source: {
|
||||
id: 'a1'
|
||||
},
|
||||
cartocss: DEFAULT_MULTITYPE_STYLE,
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
"source": { id: "a0" },
|
||||
"cartocss": [
|
||||
"#shops {",
|
||||
" marker-fill-opacity: 1.0;",
|
||||
" marker-line-color: #FFF;",
|
||||
" marker-line-width: 0.5;",
|
||||
" marker-line-opacity: 0.7;",
|
||||
" marker-placement: point;",
|
||||
" marker-type: ellipse;",
|
||||
" marker-width: 8;",
|
||||
" marker-fill: red;",
|
||||
" marker-allow-overlap: true;",
|
||||
"}"
|
||||
].join('\n'),
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
"source": { id: "a1" },
|
||||
"cartocss": [
|
||||
"#routing {",
|
||||
" line-color: ramp([routing_time], colorbrewer(Reds));",
|
||||
" line-width: ramp([routing_time], 2, 8);",
|
||||
" line-opacity: 1.0;",
|
||||
"}"
|
||||
].join('\n'),
|
||||
"cartocss_version": "2.3.0"
|
||||
sql: 'select * from analysis_rent_listings',
|
||||
cartocss: DEFAULT_MULTITYPE_STYLE,
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
],
|
||||
// dataviews
|
||||
{
|
||||
distribution_center_name_category: {
|
||||
source: { id: 'b0' },
|
||||
type: 'aggregation',
|
||||
options: {
|
||||
column: 'name'
|
||||
}
|
||||
},
|
||||
time_histogram: {
|
||||
source: { id: 'a1' },
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'routing_time'
|
||||
}
|
||||
},
|
||||
distance_histogram: {
|
||||
source: { id: 'a1' },
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'routing_distance'
|
||||
}
|
||||
}
|
||||
},
|
||||
// analysis
|
||||
{},
|
||||
[
|
||||
{
|
||||
id: 'a1',
|
||||
type: 'routing-n-to-n',
|
||||
params: {
|
||||
// distanceColumn: 'routing_distance',
|
||||
// timeColumn: 'routing_time',
|
||||
originSource: {
|
||||
id: 'b0',
|
||||
type: 'source',
|
||||
params: {
|
||||
query: 'select * from distribution_centers'
|
||||
}
|
||||
},
|
||||
destinationSource: {
|
||||
id: 'a0',
|
||||
type: 'source',
|
||||
params: {
|
||||
query: 'select * from shops'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pointInPolygonDef
|
||||
]
|
||||
)
|
||||
},
|
||||
|
||||
{
|
||||
skip: true,
|
||||
desc: 'II. Population analysis',
|
||||
desc: 'buffer + point-in-polygon from buffer atm-machines and rent listings + rent listings',
|
||||
mapConfig: mapConfig(
|
||||
// layers
|
||||
[
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
"source": { id: "a2" },
|
||||
"cartocss": [
|
||||
"#count_in_polygon {",
|
||||
" polygon-opacity: 1.0",
|
||||
" line-color: #FFF;",
|
||||
" line-width: 0.5;",
|
||||
" line-opacity: 0.7",
|
||||
" polygon-fill: ramp([estimated_people], colorbrewer(Reds));",
|
||||
"}"
|
||||
].join('\n'),
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
"source": { id: "a0" },
|
||||
"cartocss": DEFAULT_MULTITYPE_STYLE,
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
// dataviews
|
||||
{
|
||||
total_population_formula: {
|
||||
"source": { id: "a3" },
|
||||
type: 'formula',
|
||||
options: {
|
||||
column: 'total_population',
|
||||
operation: 'sum'
|
||||
}
|
||||
},
|
||||
people_histogram: { // this injects a range filter at `a2` node output
|
||||
"source": { id: "a2" },
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'estimated_people'
|
||||
}
|
||||
},
|
||||
subway_line_category: { // this injects a category filter at `a0` node output
|
||||
"source": { id: "a0" },
|
||||
type: 'aggregation',
|
||||
options: {
|
||||
column: 'subway_line'
|
||||
}
|
||||
}
|
||||
},
|
||||
// analysis
|
||||
[
|
||||
{
|
||||
id: 'a3',
|
||||
// this will union the polygons, produce just one polygon, and calculate the total population for it
|
||||
type: 'total-population',
|
||||
params: {
|
||||
columnName: 'total_population',
|
||||
source: {
|
||||
id: 'a2',
|
||||
type: 'estimated-population',
|
||||
params: {
|
||||
columnName: 'estimated_people',
|
||||
source: {
|
||||
id: 'a1',
|
||||
type: 'trade-area',
|
||||
params: {
|
||||
source: {
|
||||
"id": "a0",
|
||||
"type": "source",
|
||||
"params": {
|
||||
query: "select * from subway_stops"
|
||||
}
|
||||
},
|
||||
kind: 'walk',
|
||||
time: 300
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
])
|
||||
},
|
||||
|
||||
{
|
||||
skip: true,
|
||||
desc: 'III. Point in polygon',
|
||||
mapConfig: mapConfig(
|
||||
// layers
|
||||
[
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
"source": { id: "a1" },
|
||||
"cartocss": [
|
||||
"#count_in_polygon {",
|
||||
" polygon-opacity: 1.0",
|
||||
" line-color: #FFF;",
|
||||
" line-width: 0.5;",
|
||||
" line-opacity: 0.7",
|
||||
" polygon-fill: ramp([count_people], colorbrewer(Reds));",
|
||||
"}"
|
||||
].join('\n'),
|
||||
"cartocss_version": "2.3.0"
|
||||
sql: 'select * from analysis_rent_listings',
|
||||
cartocss: DEFAULT_MULTITYPE_STYLE,
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
source: {
|
||||
id: 'a1'
|
||||
},
|
||||
cartocss: DEFAULT_MULTITYPE_STYLE,
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
source: {
|
||||
id: 'b1'
|
||||
},
|
||||
cartocss: DEFAULT_MULTITYPE_STYLE,
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
],
|
||||
// dataviews
|
||||
{
|
||||
age_histogram: {
|
||||
"source": { id: "a0" },
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'age'
|
||||
}
|
||||
},
|
||||
income_histogram: {
|
||||
"source": { id: "a0" },
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'income'
|
||||
}
|
||||
},
|
||||
gender_category: {
|
||||
"source": { id: "a0" },
|
||||
type: 'aggregation',
|
||||
options: {
|
||||
column: 'gender'
|
||||
}
|
||||
}
|
||||
},
|
||||
// analysis
|
||||
{},
|
||||
[
|
||||
{
|
||||
"id": "a1",
|
||||
"type": "count-in-polygon",
|
||||
"params": {
|
||||
"columnName": 'count_people',
|
||||
"pointsSource": {
|
||||
"id": 'a0',
|
||||
"type": "source",
|
||||
"params": {
|
||||
query: "select the_geom, age, gender, income from people"
|
||||
}
|
||||
},
|
||||
"polygonsSource": {
|
||||
"id": "b0",
|
||||
"type": "source",
|
||||
"params": {
|
||||
query: "select * from postal_codes"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
bufferDef,
|
||||
pointInPolygonDef
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
];
|
||||
|
||||
useCases.forEach(function(useCase, imageIdx) {
|
||||
useCases.forEach(function (useCase) {
|
||||
if (!!useCase.skip) {
|
||||
debug(JSON.stringify(useCase.mapConfig, null, 4));
|
||||
return debug(JSON.stringify(useCase.mapConfig, null, 4));
|
||||
}
|
||||
it.skip('should implement use case: "' + useCase.desc + '"', function(done) {
|
||||
it(`should implement use case: '${useCase.desc}'`, function (done) {
|
||||
|
||||
var testClient = new TestClient(useCase.mapConfig, 1234);
|
||||
|
||||
var tile = useCase.tile || TILE_ANALYSIS_TABLES;
|
||||
|
||||
testClient.getTile(tile.z, tile.x, tile.y, function(err, res, image) {
|
||||
testClient.getTile(tile.z, tile.x, tile.y, function (err, res, image) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
image.save('/tmp/tests/' + imageIdx + '---' + useCase.desc.replace(/\s/g, '-') + '.png');
|
||||
//image.save('/tmp/tests/' + imageIdx + '---' + useCase.desc.replace(/\s/g, '-') + '.png');
|
||||
|
||||
assert.equal(image.width(), 256);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ var serverOptions = require('../../../lib/cartodb/server_options');
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
var TestClient = require('../../support/test-client');
|
||||
|
||||
var LayergroupToken = require('../../support/layergroup-token');
|
||||
var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token');
|
||||
|
||||
describe('named-maps analysis', function() {
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ require('../support/test_helper');
|
||||
var fs = require('fs');
|
||||
var assert = require('../support/assert');
|
||||
var TestClient = require('../support/test-client');
|
||||
var serverOptions = require('../../lib/cartodb/server_options');
|
||||
var mapnik = require('windshaft').mapnik;
|
||||
var IMAGE_TOLERANCE_PER_MIL = 5;
|
||||
|
||||
@@ -124,24 +125,37 @@ describe('buffer size per format', function () {
|
||||
}
|
||||
];
|
||||
|
||||
afterEach(function(done) {
|
||||
if (this.testClient) {
|
||||
return this.testClient.drain(done);
|
||||
}
|
||||
return done();
|
||||
});
|
||||
|
||||
const originalUsePostGIS = serverOptions.renderer.mvt.usePostGIS;
|
||||
testCases.forEach(function (test) {
|
||||
it(test.desc, function (done) {
|
||||
var testClient = new TestClient(test.mapConfig, 1234);
|
||||
var coords = test.coords;
|
||||
var options = {
|
||||
format: test.format,
|
||||
layers: test.layers
|
||||
};
|
||||
testClient.getTile(coords.z, coords.x, coords.y, options, function (err, res, tile) {
|
||||
assert.ifError(err);
|
||||
// To generate images use:
|
||||
// tile.save(test.fixturePath);
|
||||
test.assert(tile, function (err) {
|
||||
var testFn = (usePostGIS) => {
|
||||
it(test.desc, function (done) {
|
||||
serverOptions.renderer.mvt.usePostGIS = usePostGIS;
|
||||
this.testClient = new TestClient(test.mapConfig, 1234);
|
||||
serverOptions.renderer.mvt.usePostGIS = originalUsePostGIS;
|
||||
var coords = test.coords;
|
||||
var options = {
|
||||
format: test.format,
|
||||
layers: test.layers
|
||||
};
|
||||
this.testClient.getTile(coords.z, coords.x, coords.y, options, function (err, res, tile) {
|
||||
assert.ifError(err);
|
||||
testClient.drain(done);
|
||||
// To generate images use:
|
||||
// tile.save(test.fixturePath);
|
||||
test.assert(tile, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
if (process.env.POSTGIS_VERSION === '2.4' && test.format === 'mvt'){
|
||||
testFn(true);
|
||||
}
|
||||
testFn(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -260,23 +274,27 @@ describe('buffer size per format for named maps', function () {
|
||||
}
|
||||
];
|
||||
|
||||
afterEach(function(done) {
|
||||
if (this.testClient) {
|
||||
return this.testClient.drain(done);
|
||||
}
|
||||
return done();
|
||||
});
|
||||
|
||||
testCases.forEach(function (test) {
|
||||
it(test.desc, function (done) {
|
||||
var testClient = new TestClient(test.template, 1234);
|
||||
this.testClient = new TestClient(test.template, 1234);
|
||||
var coords = test.coords;
|
||||
var options = {
|
||||
format: test.format,
|
||||
placeholders: test.placeholders,
|
||||
layers: test.layers
|
||||
};
|
||||
testClient.getTile(coords.z, coords.x, coords.y, options, function (err, res, tile) {
|
||||
this.testClient.getTile(coords.z, coords.x, coords.y, options, function (err, res, tile) {
|
||||
assert.ifError(err);
|
||||
// To generate images use:
|
||||
//tile.save('./test/fixtures/buffer-size/tile-7.64.48-buffer-size-0-test.png');
|
||||
test.assert(tile, function (err) {
|
||||
assert.ifError(err);
|
||||
testClient.drain(done);
|
||||
});
|
||||
test.assert(tile, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -416,26 +434,40 @@ describe('buffer size per format for named maps w/o placeholders', function () {
|
||||
|
||||
];
|
||||
|
||||
afterEach(function(done) {
|
||||
if (this.testClient) {
|
||||
return this.testClient.drain(done);
|
||||
}
|
||||
return done();
|
||||
});
|
||||
|
||||
const originalUsePostGIS = serverOptions.renderer.mvt.usePostGIS;
|
||||
testCases.forEach(function (test) {
|
||||
it(test.desc, function (done) {
|
||||
var testClient = new TestClient(test.template, 1234);
|
||||
var coords = test.coords;
|
||||
var options = {
|
||||
format: test.format,
|
||||
placeholders: test.placeholders,
|
||||
layers: test.layers
|
||||
};
|
||||
testClient.getTile(coords.z, coords.x, coords.y, options, function (err, res, tile) {
|
||||
assert.ifError(err);
|
||||
// To generate images use:
|
||||
//tile.save(test.fixturePath);
|
||||
// require('fs').writeFileSync(test.fixturePath, JSON.stringify(tile));
|
||||
// require('fs').writeFileSync(test.fixturePath, tile.getDataSync());
|
||||
test.assert(tile, function (err) {
|
||||
assert.ifError(err);
|
||||
testClient.drain(done);
|
||||
var testFn = (usePostGIS) => {
|
||||
it(test.desc + `(${usePostGIS? 'PostGIS':'mapnik'})`, function (done) {
|
||||
serverOptions.renderer.mvt.usePostGIS = usePostGIS;
|
||||
test.template.name += '_1';
|
||||
this.testClient = new TestClient(test.template, 1234);
|
||||
serverOptions.renderer.mvt.usePostGIS = originalUsePostGIS;
|
||||
var coords = test.coords;
|
||||
var options = {
|
||||
format: test.format,
|
||||
placeholders: test.placeholders,
|
||||
layers: test.layers
|
||||
};
|
||||
this.testClient.getTile(coords.z, coords.x, coords.y, options, function (err, res, tile) {
|
||||
assert.ifError(err);
|
||||
// To generate images use:
|
||||
//tile.save(test.fixturePath);
|
||||
// require('fs').writeFileSync(test.fixturePath, JSON.stringify(tile));
|
||||
// require('fs').writeFileSync(test.fixturePath, tile.getDataSync());
|
||||
test.assert(tile, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
if (process.env.POSTGIS_VERSION === '2.4' && test.format === 'mvt'){
|
||||
testFn(true);
|
||||
}
|
||||
testFn(false);
|
||||
});
|
||||
});
|
||||
|
||||
2
test/acceptance/cache/cache_headers.js
vendored
2
test/acceptance/cache/cache_headers.js
vendored
@@ -8,7 +8,7 @@ var serverOptions = require('../../../lib/cartodb/server_options');
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
server.setMaxListeners(0);
|
||||
|
||||
var LayergroupToken = require('../../support/layergroup-token');
|
||||
var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token');
|
||||
|
||||
describe('get requests with cache headers', function() {
|
||||
|
||||
|
||||
@@ -145,4 +145,283 @@ describe('aggregations happy cases', function() {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
var widgetSearchExpects = {
|
||||
'count': [ { category: 'other_a', value: 3 } ],
|
||||
'sum': [ { category: 'other_a', value: 6 } ],
|
||||
'avg': [ { category: 'other_a', value: 2 } ],
|
||||
'max': [ { category: 'other_a', value: 3 } ],
|
||||
'min': [ { category: 'other_a', value: 1 } ]
|
||||
};
|
||||
|
||||
Object.keys(operations_and_values).forEach(function (operation) {
|
||||
var description = 'should search OTHER category using "' + operation + '"';
|
||||
|
||||
it(description, function (done) {
|
||||
this.testClient = new TestClient(aggregationOperationMapConfig(operation, query_other, 'cat', 'val'));
|
||||
this.testClient.widgetSearch('cat', 'other_a', function (err, res, searchResult) {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.ok(searchResult);
|
||||
assert.equal(searchResult.type, 'aggregation');
|
||||
|
||||
assert.equal(searchResult.categories.length, 1);
|
||||
assert.deepEqual(
|
||||
searchResult.categories,
|
||||
widgetSearchExpects[operation]
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('aggregation-dataview: special float values', function() {
|
||||
|
||||
afterEach(function(done) {
|
||||
if (this.testClient) {
|
||||
this.testClient.drain(done);
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
function createMapConfig(layers, dataviews, analysis) {
|
||||
return {
|
||||
version: '1.5.0',
|
||||
layers: layers,
|
||||
dataviews: dataviews || {},
|
||||
analyses: analysis || []
|
||||
};
|
||||
}
|
||||
|
||||
var mapConfig = createMapConfig(
|
||||
[
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"source": {
|
||||
"id": "a0"
|
||||
},
|
||||
"cartocss": "#points { marker-width: 10; marker-fill: red; }",
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
{
|
||||
val_aggregation: {
|
||||
source: {
|
||||
id: 'a0'
|
||||
},
|
||||
type: 'aggregation',
|
||||
options: {
|
||||
column: 'cat',
|
||||
aggregation: 'avg',
|
||||
aggregationColumn: 'val'
|
||||
}
|
||||
},
|
||||
sum_aggregation_numeric: {
|
||||
source: {
|
||||
id: 'a1'
|
||||
},
|
||||
type: 'aggregation',
|
||||
options: {
|
||||
column: 'cat',
|
||||
aggregation: 'sum',
|
||||
aggregationColumn: 'val'
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
{
|
||||
"id": "a0",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": [
|
||||
'SELECT',
|
||||
' null::geometry the_geom_webmercator,',
|
||||
' CASE',
|
||||
' WHEN x % 4 = 0 THEN \'infinity\'::float',
|
||||
' WHEN x % 4 = 1 THEN \'-infinity\'::float',
|
||||
' WHEN x % 4 = 2 THEN \'NaN\'::float',
|
||||
' ELSE x',
|
||||
' END AS val,',
|
||||
' CASE',
|
||||
' WHEN x % 2 = 0 THEN \'category_1\'',
|
||||
' ELSE \'category_2\'',
|
||||
' END AS cat',
|
||||
'FROM generate_series(1, 1000) x'
|
||||
].join('\n')
|
||||
}
|
||||
}, {
|
||||
"id": "a1",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": [
|
||||
'SELECT',
|
||||
' null::geometry the_geom_webmercator,',
|
||||
' CASE',
|
||||
' WHEN x % 3 = 0 THEN \'NaN\'::numeric',
|
||||
' WHEN x % 3 = 1 THEN x',
|
||||
' ELSE x',
|
||||
' END AS val,',
|
||||
' CASE',
|
||||
' WHEN x % 2 = 0 THEN \'category_1\'',
|
||||
' ELSE \'category_2\'',
|
||||
' END AS cat',
|
||||
'FROM generate_series(1, 1000) x'
|
||||
].join('\n')
|
||||
}
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
// Source a0
|
||||
// -----------------------------------------------
|
||||
// the_geom_webmercator | val | cat
|
||||
// ----------------------+-----------+------------
|
||||
// | -Infinity | category_2
|
||||
// | NaN | category_1
|
||||
// | 3 | category_2
|
||||
// | Infinity | category_1
|
||||
// | -Infinity | category_2
|
||||
// | NaN | category_1
|
||||
// | 7 | category_2
|
||||
// | Infinity | category_1
|
||||
// | -Infinity | category_2
|
||||
// | NaN | category_1
|
||||
// | 11 | category_2
|
||||
// | " | "
|
||||
|
||||
var filters = [{ own_filter: 0 }, {}];
|
||||
filters.forEach(function (filter) {
|
||||
it('should handle special float values using filter: ' + JSON.stringify(filter), function(done) {
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('val_aggregation', { own_filter: 0 }, function(err, dataview) {
|
||||
assert.ifError(err);
|
||||
assert.ok(dataview.infinities === (250 + 250));
|
||||
assert.ok(dataview.nans === 250);
|
||||
assert.ok(dataview.categories.length === 1);
|
||||
dataview.categories.forEach(function (category) {
|
||||
assert.ok(category.category === 'category_2');
|
||||
assert.ok(category.value === 501);
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle special numeric values using filter: ' + JSON.stringify(filter), function(done) {
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('sum_aggregation_numeric', { own_filter: 0 }, function(err, dataview) {
|
||||
assert.ifError(err);
|
||||
assert.ok(dataview.nans === 333);
|
||||
assert.ok(dataview.categories.length === 2);
|
||||
dataview.categories.forEach(function (category) {
|
||||
assert.ok(category.value !== null);
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('aggregation dataview tuned by categories query param', function () {
|
||||
const mapConfig = {
|
||||
version: '1.5.0',
|
||||
layers: [
|
||||
{
|
||||
type: "cartodb",
|
||||
options: {
|
||||
source: {
|
||||
"id": "a0"
|
||||
},
|
||||
cartocss: "#points { marker-width: 10; marker-fill: red; }",
|
||||
cartocss_version: "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
dataviews: {
|
||||
categories: {
|
||||
source: {
|
||||
id: 'a0'
|
||||
},
|
||||
type: 'aggregation',
|
||||
options: {
|
||||
column: 'cat',
|
||||
aggregation: 'sum',
|
||||
aggregationColumn: 'val'
|
||||
}
|
||||
}
|
||||
},
|
||||
analyses: [
|
||||
{
|
||||
id: "a0",
|
||||
type: "source",
|
||||
params: {
|
||||
query: `
|
||||
SELECT
|
||||
null::geometry the_geom_webmercator,
|
||||
CASE
|
||||
WHEN x % 4 = 0 THEN 1
|
||||
WHEN x % 4 = 1 THEN 2
|
||||
WHEN x % 4 = 2 THEN 3
|
||||
ELSE 4
|
||||
END AS val,
|
||||
CASE
|
||||
WHEN x % 4 = 0 THEN 'category_1'
|
||||
WHEN x % 4 = 1 THEN 'category_2'
|
||||
WHEN x % 4 = 2 THEN 'category_3'
|
||||
ELSE 'category_4'
|
||||
END AS cat
|
||||
FROM generate_series(1, 1000) x
|
||||
`
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
beforeEach(function () {
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
});
|
||||
|
||||
afterEach(function (done) {
|
||||
this.testClient.drain(done);
|
||||
});
|
||||
|
||||
var scenarios = [
|
||||
{
|
||||
params: { own_filter: 0, categories: -1 },
|
||||
categoriesExpected: 4
|
||||
},
|
||||
{
|
||||
params: { own_filter: 0, categories: 0 },
|
||||
categoriesExpected: 4
|
||||
},
|
||||
{
|
||||
params: { own_filter: 0, categories: 1 },
|
||||
categoriesExpected: 1
|
||||
},
|
||||
{
|
||||
params: { own_filter: 0, categories: 2 },
|
||||
categoriesExpected: 2
|
||||
},
|
||||
{
|
||||
params: { own_filter: 0, categories: 4 },
|
||||
categoriesExpected: 4
|
||||
},
|
||||
{
|
||||
params: { own_filter: 0, categories: 5 },
|
||||
categoriesExpected: 4
|
||||
}
|
||||
];
|
||||
|
||||
scenarios.forEach(function (scenario) {
|
||||
it(`should handle cartegories to customize aggregations: ${JSON.stringify(scenario.params)}`, function (done) {
|
||||
this.testClient.getDataview('categories', scenario.params, (err, dataview) => {
|
||||
assert.ifError(err);
|
||||
assert.equal(dataview.categories.length, scenario.categoriesExpected);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
80
test/acceptance/dataviews/formula.js
Normal file
80
test/acceptance/dataviews/formula.js
Normal file
@@ -0,0 +1,80 @@
|
||||
require('../../support/test_helper');
|
||||
var assert = require('../../support/assert');
|
||||
var TestClient = require('../../support/test-client');
|
||||
|
||||
function createMapConfig(layers, dataviews, analysis) {
|
||||
return {
|
||||
version: '1.5.0',
|
||||
layers: layers,
|
||||
dataviews: dataviews || {},
|
||||
analyses: analysis || []
|
||||
};
|
||||
}
|
||||
|
||||
describe('formula-dataview: special float values', function() {
|
||||
|
||||
afterEach(function(done) {
|
||||
if (this.testClient) {
|
||||
this.testClient.drain(done);
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
var mapConfig = createMapConfig(
|
||||
[
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"source": {
|
||||
"id": "a0"
|
||||
},
|
||||
"cartocss": "#points { marker-width: 10; marker-fill: red; }",
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
{
|
||||
val_formula: {
|
||||
source: {
|
||||
id: 'a0'
|
||||
},
|
||||
type: 'formula',
|
||||
options: {
|
||||
column: 'val',
|
||||
operation: 'avg'
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
{
|
||||
"id": "a0",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": [
|
||||
'SELECT',
|
||||
' null::geometry the_geom_webmercator,',
|
||||
' CASE',
|
||||
' WHEN x % 4 = 0 THEN \'infinity\'::float',
|
||||
' WHEN x % 4 = 1 THEN \'-infinity\'::float',
|
||||
' WHEN x % 4 = 2 THEN \'NaN\'::float',
|
||||
' ELSE x',
|
||||
' END AS val',
|
||||
'FROM generate_series(1, 1000) x'
|
||||
].join('\n')
|
||||
}
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
it('should filter infinities out and count them in the summary', function(done) {
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getDataview('val_formula', {}, function(err, dataview) {
|
||||
assert.ok(!err, err);
|
||||
assert.equal(dataview.result, 501);
|
||||
assert.ok(dataview.infinities === (250 + 250));
|
||||
assert.ok(dataview.nans === 250);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -124,6 +124,13 @@ describe('dataviews using tables with overviews', function() {
|
||||
params: {
|
||||
query: 'select * from test_table_overviews'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'data-source-special-float-values',
|
||||
type: 'source',
|
||||
params: {
|
||||
query: 'select * from test_special_float_values_table_overviews'
|
||||
}
|
||||
}
|
||||
],
|
||||
dataviews: {
|
||||
@@ -144,6 +151,17 @@ describe('dataviews using tables with overviews', function() {
|
||||
aggregationColumn: 'name',
|
||||
}
|
||||
},
|
||||
test_categories_special_values: {
|
||||
type: 'aggregation',
|
||||
source: {
|
||||
id: 'data-source-special-float-values'
|
||||
},
|
||||
options: {
|
||||
column: 'name',
|
||||
aggregation: 'sum',
|
||||
aggregationColumn: 'value',
|
||||
}
|
||||
},
|
||||
test_histogram: {
|
||||
type: 'histogram',
|
||||
source: {id: 'data-source'},
|
||||
@@ -160,6 +178,16 @@ describe('dataviews using tables with overviews', function() {
|
||||
bins: 2
|
||||
}
|
||||
},
|
||||
test_histogram_special_values: {
|
||||
type: 'histogram',
|
||||
source: {
|
||||
id: 'data-source-special-float-values'
|
||||
},
|
||||
options: {
|
||||
column: 'value',
|
||||
bins: 2
|
||||
}
|
||||
},
|
||||
test_avg: {
|
||||
type: 'formula',
|
||||
source: {id: 'data-source'},
|
||||
@@ -168,6 +196,16 @@ describe('dataviews using tables with overviews', function() {
|
||||
operation: 'avg'
|
||||
}
|
||||
},
|
||||
test_formula_sum_special_values: {
|
||||
type: 'formula',
|
||||
source: {
|
||||
id: 'data-source-special-float-values'
|
||||
},
|
||||
options: {
|
||||
column: 'value',
|
||||
operation: 'sum'
|
||||
}
|
||||
},
|
||||
test_count: {
|
||||
type: 'formula',
|
||||
source: {id: 'data-source'},
|
||||
@@ -202,6 +240,17 @@ describe('dataviews using tables with overviews', function() {
|
||||
cartocss_version: '2.3.0',
|
||||
source: { id: 'data-source' }
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from test_special_float_values_table_overviews',
|
||||
cartocss: '#layer { marker-fill: red; marker-width: 32; marker-allow-overlap: true; }',
|
||||
cartocss_version: '2.3.0',
|
||||
source: {
|
||||
id: 'data-source-special-float-values'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -212,7 +261,14 @@ describe('dataviews using tables with overviews', function() {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(formula_result, {"operation":"sum","result":15,"nulls":0,"type":"formula"});
|
||||
assert.deepEqual(formula_result, {
|
||||
"operation":"sum",
|
||||
"result":15,
|
||||
"infinities": 0,
|
||||
"nans": 0,
|
||||
"nulls":0,
|
||||
"type":"formula"
|
||||
});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
@@ -224,7 +280,14 @@ describe('dataviews using tables with overviews', function() {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(formula_result, {"operation":"avg","result":3,"nulls":0,"type":"formula"});
|
||||
assert.deepEqual(formula_result, {
|
||||
"operation":"avg",
|
||||
"result":3,
|
||||
"nulls":0,
|
||||
"type":"formula",
|
||||
"infinities": 0,
|
||||
"nans": 0
|
||||
});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
@@ -236,7 +299,14 @@ describe('dataviews using tables with overviews', function() {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(formula_result, {"operation":"count","result":5,"nulls":0,"type":"formula"});
|
||||
assert.deepEqual(formula_result, {
|
||||
"operation":"count",
|
||||
"result":5,
|
||||
"nulls":0,
|
||||
"type":"formula",
|
||||
"infinities": 0,
|
||||
"nans": 0
|
||||
});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
@@ -248,7 +318,14 @@ describe('dataviews using tables with overviews', function() {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(formula_result, {"operation":"max","result":5,"nulls":0,"type":"formula"});
|
||||
assert.deepEqual(formula_result, {
|
||||
"operation": "max",
|
||||
"result": 5,
|
||||
"nulls": 0,
|
||||
"infinities": 0,
|
||||
"nans": 0,
|
||||
"type": "formula"
|
||||
});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
@@ -260,7 +337,14 @@ describe('dataviews using tables with overviews', function() {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(formula_result, {"operation":"min","result":1,"nulls":0,"type":"formula"});
|
||||
assert.deepEqual(formula_result, {
|
||||
"operation": "min",
|
||||
"result": 1,
|
||||
"nulls": 0,
|
||||
"infinities": 0,
|
||||
"nans": 0,
|
||||
"type": "formula"
|
||||
});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
@@ -275,7 +359,14 @@ describe('dataviews using tables with overviews', function() {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(formula_result, {"operation":"sum","result":15,"nulls":0,"type":"formula"});
|
||||
assert.deepEqual(formula_result, {
|
||||
"operation":"sum",
|
||||
"result":15,
|
||||
"nulls":0,
|
||||
"infinities": 0,
|
||||
"nans": 0,
|
||||
"type":"formula"
|
||||
});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
@@ -372,7 +463,14 @@ describe('dataviews using tables with overviews', function() {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(formula_result, {"operation":"sum","result":1,"nulls":0,"type":"formula"});
|
||||
assert.deepEqual(formula_result, {
|
||||
"operation":"sum",
|
||||
"result":1,
|
||||
"nulls":0,
|
||||
"infinities": 0,
|
||||
"nans": 0,
|
||||
"type":"formula"
|
||||
});
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
@@ -383,7 +481,14 @@ describe('dataviews using tables with overviews', function() {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(formula_result, {"operation":"avg","result":1,"nulls":0,"type":"formula"});
|
||||
assert.deepEqual(formula_result, {
|
||||
"operation":"avg",
|
||||
"result":1,
|
||||
"nulls":0,
|
||||
"infinities": 0,
|
||||
"nans": 0,
|
||||
"type":"formula"
|
||||
});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
@@ -395,7 +500,14 @@ describe('dataviews using tables with overviews', function() {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(formula_result, {"operation":"count","result":1,"nulls":0,"type":"formula"});
|
||||
assert.deepEqual(formula_result, {
|
||||
"operation":"count",
|
||||
"result":1,
|
||||
"infinities": 0,
|
||||
"nans": 0,
|
||||
"nulls":0,
|
||||
"type":"formula"
|
||||
});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
@@ -407,7 +519,14 @@ describe('dataviews using tables with overviews', function() {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(formula_result, {"operation":"max","result":1,"nulls":0,"type":"formula"});
|
||||
assert.deepEqual(formula_result, {
|
||||
"operation": "max",
|
||||
"result": 1,
|
||||
"nulls": 0,
|
||||
"infinities": 0,
|
||||
"nans": 0,
|
||||
"type": "formula"
|
||||
});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
@@ -419,7 +538,14 @@ describe('dataviews using tables with overviews', function() {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(formula_result, {"operation":"min","result":1,"nulls":0,"type":"formula"});
|
||||
assert.deepEqual(formula_result, {
|
||||
"operation": "min",
|
||||
"result": 1,
|
||||
"nulls": 0,
|
||||
"infinities": 0,
|
||||
"nans": 0,
|
||||
"type": "formula"
|
||||
});
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
@@ -437,7 +563,14 @@ describe('dataviews using tables with overviews', function() {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(formula_result, {"operation":"sum","result":1,"nulls":0,"type":"formula"});
|
||||
assert.deepEqual(formula_result, {
|
||||
"operation":"sum",
|
||||
"result":1,
|
||||
"nulls":0,
|
||||
"infinities": 0,
|
||||
"nans": 0,
|
||||
"type":"formula"
|
||||
});
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
@@ -445,5 +578,69 @@ describe('dataviews using tables with overviews', function() {
|
||||
|
||||
});
|
||||
|
||||
describe('aggregation special float values', function () {
|
||||
var params = {};
|
||||
|
||||
it("should expose an aggregation dataview filtering special float values out", function (done) {
|
||||
var testClient = new TestClient(overviewsMapConfig);
|
||||
testClient.getDataview('test_categories_special_values', params, function (err, dataview) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(dataview, {
|
||||
aggregation: 'sum',
|
||||
count: 5,
|
||||
nulls: 0,
|
||||
nans: 1,
|
||||
infinities: 1,
|
||||
min: 6,
|
||||
max: 6,
|
||||
categoriesCount: 1,
|
||||
categories: [ { category: 'Hawai', value: 6, agg: false } ],
|
||||
type: 'aggregation'
|
||||
});
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should expose a histogram dataview filtering special float values out', function (done) {
|
||||
var testClient = new TestClient(overviewsMapConfig);
|
||||
testClient.getDataview('test_histogram_special_values', params, function (err, dataview) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(dataview, {
|
||||
bin_width: 0,
|
||||
bins_count: 1,
|
||||
bins_start: 3,
|
||||
nulls: 0,
|
||||
infinities: 1,
|
||||
nans: 1,
|
||||
avg: 3,
|
||||
bins: [ { bin: 0, min: 3, max: 3, avg: 3, freq: 2 } ],
|
||||
type: 'histogram'
|
||||
});
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should expose a formula (sum) dataview filtering special float values out', function (done) {
|
||||
var testClient = new TestClient(overviewsMapConfig);
|
||||
testClient.getDataview('test_formula_sum_special_values', params, function (err, dataview) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.deepEqual(dataview, {
|
||||
operation: 'sum',
|
||||
result: 6,
|
||||
nulls: 0,
|
||||
nans: 1,
|
||||
infinities: 1,
|
||||
type: 'formula'
|
||||
});
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
var assert = require('../support/assert');
|
||||
var step = require('step');
|
||||
var LayergroupToken = require('../support/layergroup-token');
|
||||
var LayergroupToken = require('../../lib/cartodb/models/layergroup-token');
|
||||
var testHelper = require(__dirname + '/../support/test_helper');
|
||||
var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/server');
|
||||
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options');
|
||||
|
||||
@@ -1,343 +0,0 @@
|
||||
require('../support/test_helper');
|
||||
|
||||
var assert = require('../support/assert');
|
||||
var TestClient = require('../support/test-client');
|
||||
|
||||
|
||||
describe('use only needed columns', function() {
|
||||
|
||||
function getFeatureByCartodbId(features, cartodbId) {
|
||||
for (var i = 0, len = features.length; i < len; i++) {
|
||||
if (features[i].properties.cartodb_id === cartodbId) {
|
||||
return features[i];
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
var options = { format: 'geojson', layer: 0 };
|
||||
|
||||
afterEach(function(done) {
|
||||
if (this.testClient) {
|
||||
this.testClient.drain(done);
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('with aggregation widget, interactivity and cartocss columns', function(done) {
|
||||
var widgetMapConfig = {
|
||||
version: '1.5.0',
|
||||
layers: [{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from populated_places_simple_reduced',
|
||||
cartocss: '#layer0 { marker-fill: red; marker-width: 10; [name="Madrid"] { marker-fill: green; } }',
|
||||
cartocss_version: '2.0.1',
|
||||
widgets: {
|
||||
adm0name: {
|
||||
type: 'aggregation',
|
||||
options: {
|
||||
column: 'adm0name',
|
||||
aggregation: 'sum',
|
||||
aggregationColumn: 'pop_max'
|
||||
}
|
||||
}
|
||||
},
|
||||
interactivity: "cartodb_id,pop_min"
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(widgetMapConfig);
|
||||
this.testClient.getTile(0, 0, 0, options, function (err, res, geojsonTile) {
|
||||
assert.ok(!err, err);
|
||||
assert.deepEqual(getFeatureByCartodbId(geojsonTile.features, 1109).properties, {
|
||||
cartodb_id: 1109,
|
||||
name: 'Mardin',
|
||||
adm0name: 'Turkey',
|
||||
pop_max: 71373,
|
||||
pop_min: 57586
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not duplicate columns', function(done) {
|
||||
var widgetMapConfig = {
|
||||
version: '1.5.0',
|
||||
layers: [{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from populated_places_simple_reduced',
|
||||
cartocss: ['#layer0 {',
|
||||
'marker-fill: red;',
|
||||
'marker-width: 10;',
|
||||
'[name="Madrid"] { marker-fill: green; } ',
|
||||
'[pop_max>100000] { marker-fill: black; } ',
|
||||
'}'].join('\n'),
|
||||
cartocss_version: '2.3.0',
|
||||
widgets: {
|
||||
adm0name: {
|
||||
type: 'aggregation',
|
||||
options: {
|
||||
column: 'adm0name',
|
||||
aggregation: 'sum',
|
||||
aggregationColumn: 'pop_max'
|
||||
}
|
||||
}
|
||||
},
|
||||
interactivity: "cartodb_id,pop_max"
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(widgetMapConfig);
|
||||
this.testClient.getTile(0, 0, 0, options, function (err, res, geojsonTile) {
|
||||
assert.ok(!err, err);
|
||||
assert.deepEqual(getFeatureByCartodbId(geojsonTile.features, 1109).properties, {
|
||||
cartodb_id: 1109,
|
||||
name: 'Mardin',
|
||||
adm0name: 'Turkey',
|
||||
pop_max: 71373
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('with formula widget, no interactivity and no cartocss columns', function(done) {
|
||||
var formulaWidgetMapConfig = {
|
||||
version: '1.5.0',
|
||||
layers: [{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from populated_places_simple_reduced where pop_max > 0 and pop_max < 600000',
|
||||
cartocss: '#layer0 { marker-fill: red; marker-width: 10; }',
|
||||
cartocss_version: '2.0.1',
|
||||
interactivity: 'cartodb_id',
|
||||
widgets: {
|
||||
pop_max_f: {
|
||||
type: 'formula',
|
||||
options: {
|
||||
column: 'pop_max',
|
||||
operation: 'count'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(formulaWidgetMapConfig);
|
||||
this.testClient.getTile(0, 0, 0, options, function (err, res, geojsonTile) {
|
||||
assert.ok(!err, err);
|
||||
assert.deepEqual(getFeatureByCartodbId(geojsonTile.features, 1109).properties, {
|
||||
cartodb_id: 1109,
|
||||
pop_max: 71373
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('with cartocss with multiple expressions', function(done) {
|
||||
var formulaWidgetMapConfig = {
|
||||
version: '1.5.0',
|
||||
layers: [{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from populated_places_simple_reduced where pop_max > 0 and pop_max < 600000',
|
||||
cartocss: '#layer0 { marker-fill: red; marker-width: 10; }' +
|
||||
'#layer0 { marker-width: 14; [name="Madrid"] { marker-width: 20; } }' +
|
||||
'#layer0[pop_max>1000] { marker-width: 14; [name="Madrid"] { marker-width: 20; } }' +
|
||||
'#layer0[adm0name=~".*Turkey*"] { marker-width: 14; [name="Madrid"] { marker-width: 20; } }',
|
||||
cartocss_version: '2.0.1',
|
||||
interactivity: 'cartodb_id'
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(formulaWidgetMapConfig);
|
||||
this.testClient.getTile(0, 0, 0, options, function (err, res, geojsonTile) {
|
||||
assert.ok(!err, err);
|
||||
assert.deepEqual(getFeatureByCartodbId(geojsonTile.features, 1109).properties, {
|
||||
cartodb_id: 1109,
|
||||
pop_max:71373,
|
||||
name:"Mardin",
|
||||
adm0name:"Turkey"
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with mapnik substitution tokens', function(done) {
|
||||
var cartocss = [
|
||||
"#layer {",
|
||||
" line-width: 2;",
|
||||
" line-color: #3B3B58;",
|
||||
" line-opacity: 1;",
|
||||
" polygon-opacity: 0.7;",
|
||||
" polygon-fill: ramp([points_count], (#E5F5F9,#99D8C9,#2CA25F))",
|
||||
"}"
|
||||
].join('\n');
|
||||
|
||||
var sql = [
|
||||
'WITH hgrid AS (',
|
||||
' SELECT CDB_HexagonGrid(',
|
||||
' ST_Expand(!bbox!, greatest(!pixel_width!,!pixel_height!) * 100),',
|
||||
' greatest(!pixel_width!,!pixel_height!) * 100',
|
||||
' ) as cell',
|
||||
')',
|
||||
'SELECT',
|
||||
' hgrid.cell as the_geom_webmercator,',
|
||||
' count(1) as points_count,',
|
||||
' count(1)/power(100 * CDB_XYZ_Resolution(CDB_ZoomFromScale(!scale_denominator!)), 2) as points_density,',
|
||||
' 1 as cartodb_id',
|
||||
'FROM hgrid, (SELECT * FROM populated_places_simple_reduced) i',
|
||||
'where ST_Intersects(i.the_geom_webmercator, hgrid.cell)',
|
||||
'GROUP BY hgrid.cell'
|
||||
].join('\n');
|
||||
|
||||
var mapConfig = {
|
||||
"version": "1.4.0",
|
||||
"layers": [
|
||||
{
|
||||
"type": 'mapnik',
|
||||
"options": {
|
||||
"cartocss_version": '2.3.0',
|
||||
"sql": sql,
|
||||
"cartocss": cartocss
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(mapConfig);
|
||||
this.testClient.getTile(0, 0, 0, { format: 'geojson', layer: 0 }, function(err, res, geojson) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.ok(geojson);
|
||||
assert.equal(geojson.features.length, 5);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip empty and null columns for geojson tiles', function(done) {
|
||||
|
||||
var mapConfig = {
|
||||
"analyses": [
|
||||
{
|
||||
"id": "a0",
|
||||
"params": {
|
||||
"query": "SELECT * FROM test_table"
|
||||
},
|
||||
"type": "source"
|
||||
}
|
||||
],
|
||||
"dataviews": {
|
||||
"4e7b0e07-6d21-4b83-9adb-6d7e17eea6ca": {
|
||||
"options": {
|
||||
"aggregationColumn": null,
|
||||
"column": "cartodb_id",
|
||||
"operation": "avg"
|
||||
},
|
||||
"source": {
|
||||
"id": "a0"
|
||||
},
|
||||
"type": "formula"
|
||||
},
|
||||
"74f590f8-625c-4e95-922f-34ad3e9919c0": {
|
||||
"options": {
|
||||
"aggregation": "sum",
|
||||
"aggregationColumn": "cartodb_id",
|
||||
"column": "name"
|
||||
},
|
||||
"source": {
|
||||
"id": "a0"
|
||||
},
|
||||
"type": "aggregation"
|
||||
},
|
||||
"98a75757-3006-400a-b028-fb613a6c0b69": {
|
||||
"options": {
|
||||
"aggregationColumn": null,
|
||||
"column": "cartodb_id",
|
||||
"operation": "sum"
|
||||
},
|
||||
"source": {
|
||||
"id": "a0"
|
||||
},
|
||||
"type": "formula"
|
||||
},
|
||||
"ebbc97b2-87d2-4895-9e1f-2f012df3679d": {
|
||||
"options": {
|
||||
"aggregationColumn": null,
|
||||
"bins": "12",
|
||||
"column": "cartodb_id"
|
||||
},
|
||||
"source": {
|
||||
"id": "a0"
|
||||
},
|
||||
"type": "histogram"
|
||||
},
|
||||
"ebc0653f-3581-469c-8b31-c969e440a865": {
|
||||
"options": {
|
||||
"aggregationColumn": null,
|
||||
"column": "cartodb_id",
|
||||
"operation": "avg"
|
||||
},
|
||||
"source": {
|
||||
"id": "a0"
|
||||
},
|
||||
"type": "formula"
|
||||
}
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"options": {
|
||||
"subdomains": "abcd",
|
||||
"urlTemplate": "http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png"
|
||||
},
|
||||
"type": "http"
|
||||
},
|
||||
{
|
||||
"options": {
|
||||
"attributes": {
|
||||
"columns": [
|
||||
"name",
|
||||
"address"
|
||||
],
|
||||
"id": "cartodb_id"
|
||||
},
|
||||
"cartocss": "#layer { marker-width: 10; marker-fill: red; }",
|
||||
"cartocss_version": "2.3.0",
|
||||
"interactivity": "cartodb_id",
|
||||
"layer_name": "wadus",
|
||||
"source": {
|
||||
"id": "a0"
|
||||
}
|
||||
},
|
||||
"type": "cartodb"
|
||||
},
|
||||
{
|
||||
"options": {
|
||||
"subdomains": "abcd",
|
||||
"urlTemplate": "http://{s}.basemaps.cartocdn.com/light_only_labels/{z}/{x}/{y}.png"
|
||||
},
|
||||
"type": "http"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(mapConfig);
|
||||
this.testClient.getTile(0, 0, 0, { format: 'geojson', layer: 0 }, function(err, res, geojson) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.ok(geojson);
|
||||
assert.equal(geojson.features.length, 5);
|
||||
|
||||
assert.deepEqual(Object.keys(geojson.features[0].properties), ['cartodb_id', 'name']);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,311 +0,0 @@
|
||||
var testHelper = require('../support/test_helper');
|
||||
|
||||
var assert = require('../support/assert');
|
||||
var _ = require('underscore');
|
||||
var redis = require('redis');
|
||||
|
||||
var CartodbWindshaft = require('../../lib/cartodb/server');
|
||||
var serverOptions = require('../../lib/cartodb/server_options');
|
||||
|
||||
var LayergroupToken = require('../support/layergroup-token');
|
||||
|
||||
describe('render limits', function() {
|
||||
|
||||
var layergroupUrl = '/api/v1/map';
|
||||
|
||||
var redisClient = redis.createClient(global.environment.redis.port);
|
||||
|
||||
var server;
|
||||
var keysToDelete;
|
||||
beforeEach(function() {
|
||||
keysToDelete = {};
|
||||
server = new CartodbWindshaft(serverOptions);
|
||||
server.setMaxListeners(0);
|
||||
});
|
||||
|
||||
afterEach(function(done) {
|
||||
testHelper.deleteRedisKeys(keysToDelete, done);
|
||||
});
|
||||
|
||||
var user = 'localhost';
|
||||
|
||||
var pointSleepSql = "SELECT pg_sleep(0.5)," +
|
||||
" 'SRID=3857;POINT(0 0)'::geometry the_geom_webmercator, 1 cartodb_id";
|
||||
var pointCartoCss = '#layer { marker-fill:red; }';
|
||||
var polygonSleepSql = "SELECT pg_sleep(0.5)," +
|
||||
" ST_Buffer('SRID=3857;POINT(0 0)'::geometry, 100000000) the_geom_webmercator, 1 cartodb_id";
|
||||
var polygonCartoCss = '#layer { polygon-fill:red; }';
|
||||
|
||||
function singleLayergroupConfig(sql, cartocss) {
|
||||
return {
|
||||
version: '1.0.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: sql,
|
||||
cartocss: cartocss,
|
||||
cartocss_version: '2.0.1'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
function createRequest(layergroup, userHost) {
|
||||
return {
|
||||
url: layergroupUrl,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: userHost,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(layergroup)
|
||||
};
|
||||
}
|
||||
|
||||
function withRenderLimit(user, renderLimit, callback) {
|
||||
redisClient.SELECT(5, function(err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
var userLimitsKey = 'limits:tiler:' + user;
|
||||
redisClient.HSET(userLimitsKey, 'render', renderLimit, function(err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
keysToDelete[userLimitsKey] = 5;
|
||||
return callback();
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
describe('with onTileErrorStrategy DISABLED', function() {
|
||||
var onTileErrorStrategyEnabled;
|
||||
before(function() {
|
||||
onTileErrorStrategyEnabled = global.environment.enabledFeatures.onTileErrorStrategy;
|
||||
global.environment.enabledFeatures.onTileErrorStrategy = false;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
global.environment.enabledFeatures.onTileErrorStrategy = onTileErrorStrategyEnabled;
|
||||
});
|
||||
|
||||
it("layergroup creation fails if test tile is slow", function(done) {
|
||||
withRenderLimit(user, 50, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var layergroup = singleLayergroupConfig(polygonSleepSql, polygonCartoCss);
|
||||
assert.response(server,
|
||||
createRequest(layergroup, user),
|
||||
{
|
||||
status: 400
|
||||
},
|
||||
function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.deepEqual(parsed.errors, [ 'Render timed out' ]);
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("layergroup creation does not fail if user limit is high enough even if test tile is slow", function(done) {
|
||||
withRenderLimit(user, 5000, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var layergroup = singleLayergroupConfig(polygonSleepSql, polygonCartoCss);
|
||||
assert.response(server,
|
||||
createRequest(layergroup, user),
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.layergroupid);
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("layergroup creation works if test tile is fast but tile request fails if they are slow", function(done) {
|
||||
withRenderLimit(user, 50, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var layergroup = singleLayergroupConfig(pointSleepSql, pointCartoCss);
|
||||
assert.response(server,
|
||||
createRequest(layergroup, user),
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(JSON.parse(res.body).layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
assert.response(server,
|
||||
{
|
||||
url: layergroupUrl + _.template('/<%= layergroupId %>/<%= z %>/<%= x %>/<%= y %>.png', {
|
||||
layergroupId: JSON.parse(res.body).layergroupid,
|
||||
z: 0,
|
||||
x: 0,
|
||||
y: 0
|
||||
}),
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
},
|
||||
encoding: 'binary'
|
||||
},
|
||||
{
|
||||
status: 400
|
||||
},
|
||||
function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.deepEqual(parsed.errors, ['Render timed out']);
|
||||
done();
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("tile request does not fail if user limit is high enough", function(done) {
|
||||
withRenderLimit(user, 5000, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var layergroup = singleLayergroupConfig(pointSleepSql, pointCartoCss);
|
||||
assert.response(server,
|
||||
createRequest(layergroup, user),
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(JSON.parse(res.body).layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
assert.response(server,
|
||||
{
|
||||
url: layergroupUrl + _.template('/<%= layergroupId %>/<%= z %>/<%= x %>/<%= y %>.png', {
|
||||
layergroupId: JSON.parse(res.body).layergroupid,
|
||||
z: 0,
|
||||
x: 0,
|
||||
y: 0
|
||||
}),
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
},
|
||||
encoding: 'binary'
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'image/png'
|
||||
}
|
||||
},
|
||||
function(res, err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('with onTileErrorStrategy', function() {
|
||||
|
||||
it("layergroup creation works even if test tile is slow", function(done) {
|
||||
withRenderLimit(user, 50, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var layergroup = singleLayergroupConfig(polygonSleepSql, polygonCartoCss);
|
||||
assert.response(server,
|
||||
createRequest(layergroup, user),
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.layergroupid);
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(parsed.layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("layergroup creation and tile requests works even if they are slow but returns fallback", function(done) {
|
||||
withRenderLimit(user, 50, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var layergroup = singleLayergroupConfig(pointSleepSql, pointCartoCss);
|
||||
assert.response(server,
|
||||
createRequest(layergroup, user),
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(JSON.parse(res.body).layergroupid).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
assert.response(server,
|
||||
{
|
||||
url: layergroupUrl + _.template('/<%= layergroupId %>/<%= z %>/<%= x %>/<%= y %>.png', {
|
||||
layergroupId: JSON.parse(res.body).layergroupid,
|
||||
z: 0,
|
||||
x: 0,
|
||||
y: 0
|
||||
}),
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
},
|
||||
encoding: 'binary'
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'image/png'
|
||||
}
|
||||
},
|
||||
function(res, err) {
|
||||
if (err) {
|
||||
done(err);
|
||||
}
|
||||
var referenceImagePath = './test/fixtures/render-timeout-fallback.png';
|
||||
assert.imageBufferIsSimilarToFile(res.body, referenceImagePath, 25,
|
||||
function(imgErr/*, similarity*/) {
|
||||
done(imgErr);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -9,7 +9,7 @@ var mapnik = require('windshaft').mapnik;
|
||||
var semver = require('semver');
|
||||
|
||||
var helper = require(__dirname + '/../support/test_helper');
|
||||
var LayergroupToken = require('../support/layergroup-token');
|
||||
var LayergroupToken = require('../../lib/cartodb/models/layergroup-token');
|
||||
|
||||
var windshaft_fixtures = __dirname + '/../../node_modules/windshaft/test/fixtures';
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ var assert = require('../support/assert');
|
||||
|
||||
var _ = require('underscore');
|
||||
|
||||
var LayergroupToken = require('../support/layergroup-token');
|
||||
var LayergroupToken = require('../../lib/cartodb/models/layergroup-token');
|
||||
|
||||
var PgQueryRunner = require('../../lib/cartodb/backends/pg_query_runner');
|
||||
var QueryTables = require('cartodb-query-tables');
|
||||
|
||||
507
test/acceptance/mvt.js
Normal file
507
test/acceptance/mvt.js
Normal file
@@ -0,0 +1,507 @@
|
||||
require('../support/test_helper');
|
||||
|
||||
var assert = require('../support/assert');
|
||||
var TestClient = require('../support/test-client');
|
||||
var serverOptions = require('../../lib/cartodb/server_options');
|
||||
|
||||
function createMapConfig(sql = TestClient.SQL.ONE_POINT) {
|
||||
return {
|
||||
version: '1.6.0',
|
||||
layers: [{
|
||||
type: "cartodb",
|
||||
options: {
|
||||
sql: sql,
|
||||
cartocss: TestClient.CARTOCSS.POINTS,
|
||||
cartocss_version: '2.3.0',
|
||||
interactivity: 'cartodb_id'
|
||||
}
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
describe('mvt (mapnik)', mvt(false));
|
||||
if (process.env.POSTGIS_VERSION === '2.4') {
|
||||
describe('mvt (postgis)', mvt(true));
|
||||
}
|
||||
|
||||
function mvt(usePostGIS) {
|
||||
return function () {
|
||||
const originalUsePostGIS = serverOptions.renderer.mvt.usePostGIS;
|
||||
before(function () {
|
||||
serverOptions.renderer.mvt.usePostGIS = usePostGIS;
|
||||
});
|
||||
after(function (){
|
||||
serverOptions.renderer.mvt.usePostGIS = originalUsePostGIS;
|
||||
});
|
||||
|
||||
describe('analysis-layers-dataviews-mvt', function () {
|
||||
|
||||
function createMapConfig(layers, dataviews, analysis) {
|
||||
return {
|
||||
version: '1.5.0',
|
||||
layers: layers,
|
||||
dataviews: dataviews || {},
|
||||
analyses: analysis || []
|
||||
};
|
||||
}
|
||||
|
||||
var CARTOCSS = [
|
||||
"#points {",
|
||||
" marker-fill-opacity: 1.0;",
|
||||
" marker-line-color: #FFF;",
|
||||
" marker-line-width: 0.5;",
|
||||
" marker-line-opacity: 1.0;",
|
||||
" marker-placement: point;",
|
||||
" marker-type: ellipse;",
|
||||
" marker-width: 8;",
|
||||
" marker-fill: red;",
|
||||
" marker-allow-overlap: true;",
|
||||
"}"
|
||||
].join('\n');
|
||||
|
||||
var mapConfig = createMapConfig(
|
||||
[
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"source": {
|
||||
"id": "2570e105-7b37-40d2-bdf4-1af889598745"
|
||||
},
|
||||
"cartocss": CARTOCSS,
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
{
|
||||
pop_max_histogram: {
|
||||
source: {
|
||||
id: '2570e105-7b37-40d2-bdf4-1af889598745'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'pop_max'
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
{
|
||||
"id": "2570e105-7b37-40d2-bdf4-1af889598745",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from populated_places_simple_reduced"
|
||||
}
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
it('should get pop_max column from dataview', function (done) {
|
||||
var testClient = new TestClient(mapConfig);
|
||||
|
||||
testClient.getTile(0, 0, 0, { format: 'mvt', layers: 0 }, function (err, res, MVT) {
|
||||
var geojsonTile = JSON.parse(MVT.toGeoJSONSync(0));
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.ok(Array.isArray(geojsonTile.features));
|
||||
assert.ok(geojsonTile.features.length > 0);
|
||||
var feature = geojsonTile.features[0];
|
||||
assert.ok(feature.properties.hasOwnProperty('pop_max'), 'Missing pop_max property');
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
const testCases = [
|
||||
{
|
||||
desc: 'should get empty mvt with code 204 (no content)',
|
||||
coords: { z: 0, x: 0, y: 0 },
|
||||
format: 'mvt',
|
||||
response: {
|
||||
status: 204,
|
||||
headers: {
|
||||
'Content-Type': undefined
|
||||
}
|
||||
},
|
||||
mapConfig: createMapConfig(TestClient.SQL.EMPTY)
|
||||
},
|
||||
{
|
||||
desc: 'should get mvt tile with code 200 (ok)',
|
||||
coords: { z: 0, x: 0, y: 0 },
|
||||
format: 'mvt',
|
||||
response: {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-protobuf'
|
||||
}
|
||||
},
|
||||
mapConfig: createMapConfig()
|
||||
}
|
||||
];
|
||||
|
||||
testCases.forEach(function (test) {
|
||||
it(test.desc, done => {
|
||||
var testClient = new TestClient(test.mapConfig);
|
||||
const { z, x, y } = test.coords;
|
||||
const { format, response } = test;
|
||||
|
||||
testClient.getTile(z, x, y, { format, response }, err => {
|
||||
assert.ifError(err);
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
if (usePostGIS){
|
||||
describe('use only needed columns', onlyNeededColumns);
|
||||
}else{
|
||||
describe.skip('use only needed columns', onlyNeededColumns);
|
||||
}
|
||||
|
||||
function onlyNeededColumns() {
|
||||
function getFeatureByCartodbId(features, cartodbId) {
|
||||
for (var i = 0, len = features.length; i < len; i++) {
|
||||
if (features[i].properties.cartodb_id === cartodbId) {
|
||||
return features[i];
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
var options = { format: 'mvt', layer: 0 };
|
||||
|
||||
afterEach(function (done) {
|
||||
if (this.testClient) {
|
||||
this.testClient.drain(done);
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('with aggregation widget, interactivity and cartocss columns', function (done) {
|
||||
var widgetMapConfig = {
|
||||
version: '1.5.0',
|
||||
layers: [{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from populated_places_simple_reduced',
|
||||
cartocss:
|
||||
'#layer0 { marker-fill: red; marker-width: 10; [name="Madrid"] { marker-fill: green; } }',
|
||||
cartocss_version: '2.0.1',
|
||||
widgets: {
|
||||
adm0name: {
|
||||
type: 'aggregation',
|
||||
options: {
|
||||
column: 'adm0name',
|
||||
aggregation: 'sum',
|
||||
aggregationColumn: 'pop_max'
|
||||
}
|
||||
}
|
||||
},
|
||||
interactivity: "cartodb_id,pop_min"
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(widgetMapConfig);
|
||||
this.testClient.getTile(0, 0, 0, options, function (err, res, MVT) {
|
||||
var geojsonTile = JSON.parse(MVT.toGeoJSONSync(0));
|
||||
assert.ok(!err, err);
|
||||
assert.deepEqual(getFeatureByCartodbId(geojsonTile.features, 1109).properties, {
|
||||
cartodb_id: 1109,
|
||||
name: 'Mardin',
|
||||
adm0name: 'Turkey',
|
||||
pop_max: 71373,
|
||||
pop_min: 57586
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should not duplicate columns', function (done) {
|
||||
var widgetMapConfig = {
|
||||
version: '1.5.0',
|
||||
layers: [{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from populated_places_simple_reduced',
|
||||
cartocss: ['#layer0 {',
|
||||
'marker-fill: red;',
|
||||
'marker-width: 10;',
|
||||
'[name="Madrid"] { marker-fill: green; } ',
|
||||
'[pop_max>100000] { marker-fill: black; } ',
|
||||
'}'].join('\n'),
|
||||
cartocss_version: '2.3.0',
|
||||
widgets: {
|
||||
adm0name: {
|
||||
type: 'aggregation',
|
||||
options: {
|
||||
column: 'adm0name',
|
||||
aggregation: 'sum',
|
||||
aggregationColumn: 'pop_max'
|
||||
}
|
||||
}
|
||||
},
|
||||
interactivity: "cartodb_id,pop_max"
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(widgetMapConfig);
|
||||
this.testClient.getTile(0, 0, 0, options, function (err, res, MVT) {
|
||||
var geojsonTile = JSON.parse(MVT.toGeoJSONSync(0));
|
||||
assert.ok(!err, err);
|
||||
assert.deepEqual(getFeatureByCartodbId(geojsonTile.features, 1109).properties, {
|
||||
cartodb_id: 1109,
|
||||
name: 'Mardin',
|
||||
adm0name: 'Turkey',
|
||||
pop_max: 71373
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('with formula widget, no interactivity and no cartocss columns', function (done) {
|
||||
var formulaWidgetMapConfig = {
|
||||
version: '1.5.0',
|
||||
layers: [{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from populated_places_simple_reduced where pop_max > 0 and pop_max < 600000',
|
||||
cartocss: '#layer0 { marker-fill: red; marker-width: 10; }',
|
||||
cartocss_version: '2.0.1',
|
||||
interactivity: 'cartodb_id',
|
||||
widgets: {
|
||||
pop_max_f: {
|
||||
type: 'formula',
|
||||
options: {
|
||||
column: 'pop_max',
|
||||
operation: 'count'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}]
|
||||
};
|
||||
this.testClient = new TestClient(formulaWidgetMapConfig);
|
||||
this.testClient.getTile(0, 0, 0, options, function (err, res, MVT) {
|
||||
var geojsonTile = JSON.parse(MVT.toGeoJSONSync(0));
|
||||
assert.ok(!err, err);
|
||||
assert.deepEqual(getFeatureByCartodbId(geojsonTile.features, 1109).properties, {
|
||||
cartodb_id: 1109,
|
||||
pop_max: 71373
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('with cartocss with multiple expressions', function (done) {
|
||||
var formulaWidgetMapConfig = {
|
||||
version: '1.5.0',
|
||||
layers: [{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from populated_places_simple_reduced where pop_max > 0 and pop_max < 600000',
|
||||
cartocss: '#layer0 { marker-fill: red; marker-width: 10; }' +
|
||||
'#layer0 { marker-width: 14; [name="Madrid"] { marker-width: 20; } }' +
|
||||
'#layer0[pop_max>1000] { marker-width: 14; [name="Madrid"] { marker-width: 20; } }' +
|
||||
'#layer0[adm0name=~".*Turkey*"] { marker-width: 14; [name="Madrid"] { marker-width: 20; } }',
|
||||
cartocss_version: '2.0.1',
|
||||
interactivity: 'cartodb_id'
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(formulaWidgetMapConfig);
|
||||
this.testClient.getTile(0, 0, 0, options, function (err, res, MVT) {
|
||||
var geojsonTile = JSON.parse(MVT.toGeoJSONSync(0));
|
||||
assert.ok(!err, err);
|
||||
assert.deepEqual(getFeatureByCartodbId(geojsonTile.features, 1109).properties, {
|
||||
cartodb_id: 1109,
|
||||
pop_max: 71373,
|
||||
name: "Mardin",
|
||||
adm0name: "Turkey"
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
var skipOnPostGIS = usePostGIS ? it.skip: it;
|
||||
skipOnPostGIS('should work with mapnik substitution tokens', function (done) {
|
||||
var cartocss = [
|
||||
"#layer {",
|
||||
" line-width: 2;",
|
||||
" line-color: #3B3B58;",
|
||||
" line-opacity: 1;",
|
||||
" polygon-opacity: 0.7;",
|
||||
" polygon-fill: ramp([points_count], (#E5F5F9,#99D8C9,#2CA25F))",
|
||||
"}"
|
||||
].join('\n');
|
||||
|
||||
var sql = [
|
||||
'WITH hgrid AS (',
|
||||
' SELECT CDB_HexagonGrid(',
|
||||
' ST_Expand(!bbox!, greatest(!pixel_width!,!pixel_height!) * 100),',
|
||||
' greatest(!pixel_width!,!pixel_height!) * 100',
|
||||
' ) as cell',
|
||||
')',
|
||||
'SELECT',
|
||||
' hgrid.cell as the_geom_webmercator,',
|
||||
' count(1) as points_count,',
|
||||
' count(1)/power(100 * CDB_XYZ_Resolution(CDB_ZoomFromScale(!scale_denominator!)), 2) as points_density,',
|
||||
' 1 as cartodb_id',
|
||||
'FROM hgrid, (SELECT * FROM populated_places_simple_reduced) i',
|
||||
'where ST_Intersects(i.the_geom_webmercator, hgrid.cell)',
|
||||
'GROUP BY hgrid.cell'
|
||||
].join('\n');
|
||||
|
||||
var mapConfig = {
|
||||
"version": "1.4.0",
|
||||
"layers": [
|
||||
{
|
||||
"type": 'mapnik',
|
||||
"options": {
|
||||
"cartocss_version": '2.3.0',
|
||||
"sql": sql,
|
||||
"cartocss": cartocss
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(mapConfig);
|
||||
this.testClient.getTile(0, 0, 0, options, function (err, res, MVT) {
|
||||
var geojsonTile = JSON.parse(MVT.toGeoJSONSync(0));
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.ok(geojsonTile);
|
||||
assert.equal(geojsonTile.features.length, 5);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip empty and null columns for geojson tiles', function (done) {
|
||||
|
||||
var mapConfig = {
|
||||
"analyses": [
|
||||
{
|
||||
"id": "a0",
|
||||
"params": {
|
||||
"query": "SELECT * FROM test_table"
|
||||
},
|
||||
"type": "source"
|
||||
}
|
||||
],
|
||||
"dataviews": {
|
||||
"4e7b0e07-6d21-4b83-9adb-6d7e17eea6ca": {
|
||||
"options": {
|
||||
"aggregationColumn": null,
|
||||
"column": "cartodb_id",
|
||||
"operation": "avg"
|
||||
},
|
||||
"source": {
|
||||
"id": "a0"
|
||||
},
|
||||
"type": "formula"
|
||||
},
|
||||
"74f590f8-625c-4e95-922f-34ad3e9919c0": {
|
||||
"options": {
|
||||
"aggregation": "sum",
|
||||
"aggregationColumn": "cartodb_id",
|
||||
"column": "name"
|
||||
},
|
||||
"source": {
|
||||
"id": "a0"
|
||||
},
|
||||
"type": "aggregation"
|
||||
},
|
||||
"98a75757-3006-400a-b028-fb613a6c0b69": {
|
||||
"options": {
|
||||
"aggregationColumn": null,
|
||||
"column": "cartodb_id",
|
||||
"operation": "sum"
|
||||
},
|
||||
"source": {
|
||||
"id": "a0"
|
||||
},
|
||||
"type": "formula"
|
||||
},
|
||||
"ebbc97b2-87d2-4895-9e1f-2f012df3679d": {
|
||||
"options": {
|
||||
"aggregationColumn": null,
|
||||
"bins": "12",
|
||||
"column": "cartodb_id"
|
||||
},
|
||||
"source": {
|
||||
"id": "a0"
|
||||
},
|
||||
"type": "histogram"
|
||||
},
|
||||
"ebc0653f-3581-469c-8b31-c969e440a865": {
|
||||
"options": {
|
||||
"aggregationColumn": null,
|
||||
"column": "cartodb_id",
|
||||
"operation": "avg"
|
||||
},
|
||||
"source": {
|
||||
"id": "a0"
|
||||
},
|
||||
"type": "formula"
|
||||
}
|
||||
},
|
||||
"layers": [
|
||||
{
|
||||
"options": {
|
||||
"subdomains": "abcd",
|
||||
"urlTemplate": "http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png"
|
||||
},
|
||||
"type": "http"
|
||||
},
|
||||
{
|
||||
"options": {
|
||||
"attributes": {
|
||||
"columns": [
|
||||
"name",
|
||||
"address"
|
||||
],
|
||||
"id": "cartodb_id"
|
||||
},
|
||||
"cartocss": "#layer { marker-width: 10; marker-fill: red; }",
|
||||
"cartocss_version": "2.3.0",
|
||||
"interactivity": "cartodb_id",
|
||||
"layer_name": "wadus",
|
||||
"source": {
|
||||
"id": "a0"
|
||||
}
|
||||
},
|
||||
"type": "cartodb"
|
||||
},
|
||||
{
|
||||
"options": {
|
||||
"subdomains": "abcd",
|
||||
"urlTemplate": "http://{s}.basemaps.cartocdn.com/light_only_labels/{z}/{x}/{y}.png"
|
||||
},
|
||||
"type": "http"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
this.testClient = new TestClient(mapConfig);
|
||||
this.testClient.getTile(0, 0, 0, options, function (err, res, MVT) {
|
||||
var geojsonTile = JSON.parse(MVT.toGeoJSONSync(0));
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.ok(geojsonTile);
|
||||
assert.equal(geojsonTile.features.length, 5);
|
||||
|
||||
assert.deepEqual(Object.keys(geojsonTile.features[0].properties), ['cartodb_id', 'name']);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -5,7 +5,7 @@ var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/server');
|
||||
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options');
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
|
||||
var LayergroupToken = require('../support/layergroup-token');
|
||||
var LayergroupToken = require('../../lib/cartodb/models/layergroup-token');
|
||||
|
||||
var RedisPool = require('redis-mpool');
|
||||
var TemplateMaps = require('../../lib/cartodb/backends/template_maps.js');
|
||||
|
||||
@@ -192,12 +192,36 @@ describe('named maps static view', function() {
|
||||
}
|
||||
getStaticMap({ zoom: 3 }, function(err, img) {
|
||||
assert.ok(!err);
|
||||
img.save('/tmp/static.png');
|
||||
assert.imageIsSimilarToFile(img, previewFixture('override-zoom'), IMAGE_TOLERANCE, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return override bbox', function (done) {
|
||||
var view = {
|
||||
bounds: {
|
||||
west: 0,
|
||||
south: 0,
|
||||
east: 45,
|
||||
north: 45
|
||||
},
|
||||
zoom: 4,
|
||||
center: {
|
||||
lng: 40,
|
||||
lat: 20
|
||||
}
|
||||
};
|
||||
templateMaps.addTemplate(username, createTemplate(view), function (err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
getStaticMap({ bbox: '0,45,90,45' }, function(err, img) {
|
||||
assert.ok(!err);
|
||||
assert.imageIsSimilarToFile(img, previewFixture('override-bbox'), IMAGE_TOLERANCE, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow to select the layers to render', function (done) {
|
||||
var view = {
|
||||
bounds: {
|
||||
|
||||
@@ -5,7 +5,7 @@ var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/server');
|
||||
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options');
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
|
||||
var LayergroupToken = require('../support/layergroup-token');
|
||||
var LayergroupToken = require('../../lib/cartodb/models/layergroup-token');
|
||||
|
||||
var RedisPool = require('redis-mpool');
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/server');
|
||||
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options');
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
|
||||
var LayergroupToken = require('../support/layergroup-token');
|
||||
var LayergroupToken = require('../../lib/cartodb/models/layergroup-token');
|
||||
|
||||
var RedisPool = require('redis-mpool');
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ var assert = require('../support/assert');
|
||||
var cartodbServer = require('../../lib/cartodb/server');
|
||||
var ServerOptions = require('./ported/support/ported_server_options');
|
||||
var testClient = require('./ported/support/test_client');
|
||||
var BaseController = require('../../lib/cartodb/controllers/base');
|
||||
|
||||
describe('overviews_queries', function() {
|
||||
|
||||
@@ -13,15 +12,7 @@ describe('overviews_queries', function() {
|
||||
|
||||
var IMAGE_EQUALS_TOLERANCE_PER_MIL = 2;
|
||||
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = ServerOptions.req2params;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
|
||||
testHelper.rmdirRecursiveSync(global.environment.millstone.cache_basedir);
|
||||
});
|
||||
|
||||
|
||||
@@ -4,13 +4,10 @@ var assert = require('../../support/assert');
|
||||
var step = require('step');
|
||||
var cartodbServer = require('../../../lib/cartodb/server');
|
||||
var PortedServerOptions = require('./support/ported_server_options');
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
|
||||
var LayergroupToken = require('../../support/layergroup-token');
|
||||
|
||||
var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token');
|
||||
|
||||
describe('attributes', function() {
|
||||
|
||||
var server = cartodbServer(PortedServerOptions);
|
||||
server.setMaxListeners(0);
|
||||
|
||||
@@ -49,16 +46,6 @@ describe('attributes', function() {
|
||||
testHelper.deleteRedisKeys(keysToDelete, done);
|
||||
});
|
||||
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = PortedServerOptions.req2params;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
});
|
||||
|
||||
it("can only be fetched from layer having an attributes spec", function(done) {
|
||||
|
||||
var expected_token;
|
||||
|
||||
@@ -3,21 +3,7 @@ require('../../support/test_helper');
|
||||
var assert = require('../../support/assert');
|
||||
var testClient = require('./support/test_client');
|
||||
|
||||
var PortedServerOptions = require('./support/ported_server_options');
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
|
||||
describe('blend png renderer', function() {
|
||||
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = PortedServerOptions.req2params;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
});
|
||||
|
||||
var IMAGE_TOLERANCE_PER_MIL = 20;
|
||||
|
||||
function plainTorqueMapConfig(plainColor) {
|
||||
|
||||
@@ -5,20 +5,13 @@ var testClient = require('./support/test_client');
|
||||
var fs = require('fs');
|
||||
var http = require('http');
|
||||
|
||||
var PortedServerOptions = require('./support/ported_server_options');
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
|
||||
describe('blend layer filtering', function() {
|
||||
|
||||
var IMG_TOLERANCE_PER_MIL = 20;
|
||||
|
||||
var httpRendererResourcesServer;
|
||||
|
||||
var req2paramsFn;
|
||||
before(function(done) {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = PortedServerOptions.req2params;
|
||||
|
||||
// Start a server to test external resources
|
||||
httpRendererResourcesServer = http.createServer( function(request, response) {
|
||||
var filename = __dirname + '/../../fixtures/http/light_nolabels-1-0-0.png';
|
||||
@@ -32,7 +25,6 @@ describe('blend layer filtering', function() {
|
||||
});
|
||||
|
||||
after(function(done) {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
httpRendererResourcesServer.close(done);
|
||||
});
|
||||
|
||||
|
||||
@@ -5,19 +5,13 @@ var testClient = require('./support/test_client');
|
||||
var fs = require('fs');
|
||||
var http = require('http');
|
||||
|
||||
var PortedServerOptions = require('./support/ported_server_options');
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
|
||||
describe('blend http fallback', function() {
|
||||
|
||||
var IMG_TOLERANCE_PER_MIL = 20;
|
||||
|
||||
var httpRendererResourcesServer;
|
||||
|
||||
var req2paramsFn;
|
||||
before(function(done) {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = PortedServerOptions.req2params;
|
||||
// Start a server to test external resources
|
||||
httpRendererResourcesServer = http.createServer( function(request, response) {
|
||||
if (request.url.match(/^\/error404\//)) {
|
||||
@@ -39,7 +33,6 @@ describe('blend http fallback', function() {
|
||||
});
|
||||
|
||||
after(function(done) {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
httpRendererResourcesServer.close(done);
|
||||
});
|
||||
|
||||
|
||||
@@ -6,21 +6,7 @@ var serverOptions = require('./support/ported_server_options');
|
||||
var fs = require('fs');
|
||||
var http = require('http');
|
||||
|
||||
var PortedServerOptions = require('./support/ported_server_options');
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
|
||||
describe.skip('blend http client timeout', function() {
|
||||
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = PortedServerOptions.req2params;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
});
|
||||
|
||||
var mapConfig = {
|
||||
version: '1.3.0',
|
||||
layers: [
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
var testHelper = require('../../support/test_helper');
|
||||
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var fs = require('fs');
|
||||
var PortedServerOptions = require('./support/ported_server_options');
|
||||
var http = require('http');
|
||||
var testClient = require('./support/test_client');
|
||||
|
||||
var nock = require('nock');
|
||||
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
|
||||
describe('external resources', function() {
|
||||
|
||||
var res_serv; // resources server
|
||||
@@ -19,12 +15,8 @@ describe('external resources', function() {
|
||||
|
||||
var IMAGE_EQUALS_TOLERANCE_PER_MIL = 25;
|
||||
|
||||
var req2paramsFn;
|
||||
before(function(done) {
|
||||
nock.enableNetConnect('127.0.0.1');
|
||||
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = PortedServerOptions.req2params;
|
||||
// Start a server to test external resources
|
||||
res_serv = http.createServer( function(request, response) {
|
||||
++res_serv_status.numrequests;
|
||||
@@ -44,8 +36,6 @@ describe('external resources', function() {
|
||||
});
|
||||
|
||||
after(function(done) {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
|
||||
testHelper.rmdirRecursiveSync(global.environment.millstone.cache_basedir);
|
||||
|
||||
// Close the resources server
|
||||
|
||||
@@ -6,19 +6,14 @@ var assert = require('../../support/assert');
|
||||
var testClient = require('./support/test_client');
|
||||
var serverOptions = require('./support/ported_server_options');
|
||||
|
||||
var PortedServerOptions = require('./support/ported_server_options');
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
|
||||
describe.skip('render limits', function() {
|
||||
|
||||
var IMAGE_EQUALS_TOLERANCE_PER_MIL = 25;
|
||||
|
||||
var limitsConfig;
|
||||
var onTileErrorStrategy;
|
||||
var req2paramsFn;
|
||||
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = PortedServerOptions.req2params;
|
||||
limitsConfig = serverOptions.renderer.mapnik.limits;
|
||||
serverOptions.renderer.mapnik.limits = {
|
||||
render: 50,
|
||||
@@ -31,7 +26,6 @@ describe.skip('render limits', function() {
|
||||
});
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
serverOptions.renderer.mapnik.limits = limitsConfig;
|
||||
serverOptions.renderer.onTileErrorStrategy = onTileErrorStrategy;
|
||||
});
|
||||
|
||||
@@ -7,8 +7,7 @@ var step = require('step');
|
||||
var mapnik = require('windshaft').mapnik;
|
||||
var cartodbServer = require('../../../lib/cartodb/server');
|
||||
var ServerOptions = require('./support/ported_server_options');
|
||||
var LayergroupToken = require('../../support/layergroup-token');
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token');
|
||||
|
||||
describe('multilayer', function() {
|
||||
|
||||
@@ -24,16 +23,6 @@ describe('multilayer', function() {
|
||||
assert.equal(res.headers['access-control-allow-origin'], '*');
|
||||
}
|
||||
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = ServerOptions.req2params;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
});
|
||||
|
||||
// See https://github.com/Vizzuality/Windshaft/issues/70
|
||||
it("post layergroup with encoding in content-type", function(done) {
|
||||
var layergroup = {
|
||||
|
||||
@@ -7,23 +7,11 @@ var ServerOptions = require('./support/ported_server_options');
|
||||
var testClient = require('./support/test_client');
|
||||
var TestClient = require('../../support/test-client');
|
||||
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
|
||||
describe('multilayer error cases', function() {
|
||||
|
||||
var server = cartodbServer(ServerOptions);
|
||||
server.setMaxListeners(0);
|
||||
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = ServerOptions.req2params;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
});
|
||||
|
||||
// var client = null;
|
||||
afterEach(function(done) {
|
||||
if (this.client) {
|
||||
@@ -40,7 +28,7 @@ describe('multilayer error cases', function() {
|
||||
}, {}, function(res) {
|
||||
assert.equal(res.statusCode, 400, res.body);
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
assert.deepEqual(parsedBody.errors, ["layergroup POST data must be of type application/json"]);
|
||||
assert.deepEqual(parsedBody.errors, ["POST data must be of type application/json"]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,25 +5,13 @@ var _ = require('underscore');
|
||||
var cartodbServer = require('../../../lib/cartodb/server');
|
||||
var getLayerTypeFn = require('windshaft').model.MapConfig.prototype.getType;
|
||||
var PortedServerOptions = require('./support/ported_server_options');
|
||||
var LayergroupToken = require('../../support/layergroup-token');
|
||||
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token');
|
||||
|
||||
describe('multilayer interactivity and layers order', function() {
|
||||
|
||||
var server = cartodbServer(PortedServerOptions);
|
||||
server.setMaxListeners(0);
|
||||
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = PortedServerOptions.req2params;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
});
|
||||
|
||||
function layerType(layer) {
|
||||
return layer.type || 'undefined';
|
||||
}
|
||||
|
||||
@@ -4,9 +4,7 @@ var assert = require('../../support/assert');
|
||||
var step = require('step');
|
||||
var cartodbServer = require('../../../lib/cartodb/server');
|
||||
var ServerOptions = require('./support/ported_server_options');
|
||||
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
var LayergroupToken = require('../../support/layergroup-token');
|
||||
var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token');
|
||||
|
||||
describe('raster', function() {
|
||||
|
||||
@@ -20,18 +18,7 @@ describe('raster', function() {
|
||||
|
||||
var IMAGE_EQUALS_TOLERANCE_PER_MIL = 2;
|
||||
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = ServerOptions.req2params;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
});
|
||||
|
||||
it("can render raster for valid mapconfig", function(done) {
|
||||
|
||||
var mapconfig = {
|
||||
version: '1.2.0',
|
||||
layers: [
|
||||
|
||||
@@ -1,22 +1,10 @@
|
||||
var testHelper = require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var ServerOptions = require('./support/ported_server_options');
|
||||
var testClient = require('./support/test_client');
|
||||
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
|
||||
describe('regressions', function() {
|
||||
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = ServerOptions.req2params;
|
||||
});
|
||||
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
testHelper.rmdirRecursiveSync(global.environment.millstone.cache_basedir);
|
||||
});
|
||||
|
||||
|
||||
@@ -5,8 +5,7 @@ var mapnik = require('windshaft').mapnik;
|
||||
var cartodbServer = require('../../../lib/cartodb/server');
|
||||
var ServerOptions = require('./support/ported_server_options');
|
||||
|
||||
var BaseController = require('../../../lib/cartodb/controllers/base');
|
||||
var LayergroupToken = require('../../support/layergroup-token');
|
||||
var LayergroupToken = require('../../../lib/cartodb/models/layergroup-token');
|
||||
|
||||
describe('retina support', function() {
|
||||
|
||||
@@ -15,15 +14,6 @@ describe('retina support', function() {
|
||||
var server = cartodbServer(ServerOptions);
|
||||
server.setMaxListeners(0);
|
||||
|
||||
var req2paramsFn;
|
||||
before(function() {
|
||||
req2paramsFn = BaseController.prototype.req2params;
|
||||
BaseController.prototype.req2params = ServerOptions.req2params;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
BaseController.prototype.req2params = req2paramsFn;
|
||||
});
|
||||
|
||||
var keysToDelete;
|
||||
beforeEach(function(done) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user