Compare commits
1045 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
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 | ||
|
|
dbd0398d9b | ||
|
|
4f7ec0dd4d | ||
|
|
5a1623b667 | ||
|
|
2c509d97b1 | ||
|
|
da3f30dd9f | ||
|
|
440953b1cd | ||
|
|
45471597bb | ||
|
|
882aeacac2 | ||
|
|
248adab05b | ||
|
|
f4e99629f6 | ||
|
|
df08463dcf | ||
|
|
b84bb469e5 | ||
|
|
26e4d19635 | ||
|
|
69984ed895 | ||
|
|
d471905358 | ||
|
|
e5223980cf | ||
|
|
1f65968de3 | ||
|
|
51c11aff03 | ||
|
|
87e6e64d42 | ||
|
|
7dfb7c605e | ||
|
|
8b0964ad7e | ||
|
|
5135b6e14a | ||
|
|
dcb26560e3 | ||
|
|
921468668b | ||
|
|
76c2f8dc0e | ||
|
|
7efb196247 | ||
|
|
95c324ddd1 | ||
|
|
0a3d1fbdf9 | ||
|
|
45c1ccab9e | ||
|
|
cc2a96579b | ||
|
|
61b19856e9 | ||
|
|
67aa2d1a00 | ||
|
|
c6ee2eac62 | ||
|
|
3978d58d66 | ||
|
|
cd86387fa7 | ||
|
|
3ce38d7081 | ||
|
|
e9112da305 | ||
|
|
c9e6e921cb | ||
|
|
4e715f6ba4 | ||
|
|
8f156b9f13 | ||
|
|
954876f738 | ||
|
|
fd178bcf71 | ||
|
|
acaff98da5 | ||
|
|
ed56094be2 | ||
|
|
c65518cf41 | ||
|
|
fb4ee61b83 | ||
|
|
808c729a0e | ||
|
|
4602fb3ecf | ||
|
|
c59996303d | ||
|
|
13b1978d49 | ||
|
|
e13ae8d5af | ||
|
|
0f16c0e396 | ||
|
|
29361f5392 | ||
|
|
422867762b | ||
|
|
5969c99e8a | ||
|
|
5417933ecc | ||
|
|
59585b5cd9 | ||
|
|
522c86e6f2 | ||
|
|
1a7fd9bf31 | ||
|
|
7596df96ed | ||
|
|
44cca38538 | ||
|
|
f6fff6953e | ||
|
|
35df0c3a68 | ||
|
|
f9c8178d99 | ||
|
|
787ca1607a | ||
|
|
7179c0a5f1 | ||
|
|
b739db1023 | ||
|
|
66a898cdc2 | ||
|
|
61f9ea6e86 | ||
|
|
5a44d6c547 | ||
|
|
53d1b2fbbf | ||
|
|
2c9d30e042 | ||
|
|
968677e275 | ||
|
|
daf19c5e27 | ||
|
|
ac94118798 | ||
|
|
7d5b6b0820 | ||
|
|
b87e442801 | ||
|
|
1a197bb9cf | ||
|
|
5b96db2ba2 | ||
|
|
3b687ce09a | ||
|
|
7bb039b13c | ||
|
|
474d68687c | ||
|
|
b25540720c | ||
|
|
759d28f12f | ||
|
|
15c68711aa | ||
|
|
568d6b5458 | ||
|
|
525c0f2afa | ||
|
|
3f6c8fa51c | ||
|
|
0ac53db73a | ||
|
|
36e9239056 | ||
|
|
4e6e267f10 | ||
|
|
2c235b6629 | ||
|
|
6bd7537467 | ||
|
|
55a351d751 | ||
|
|
05d3b3bf66 | ||
|
|
e97466378e | ||
|
|
8426dd00f1 | ||
|
|
b2b6cf1f02 | ||
|
|
c9af38ecd0 | ||
|
|
be58adb1b9 | ||
|
|
bfb283c5ba | ||
|
|
332a56b736 | ||
|
|
2f4e4246a4 | ||
|
|
c481d6473c | ||
|
|
40c0e306af | ||
|
|
0d840e6daf | ||
|
|
07e507e1aa | ||
|
|
7ea7a991aa | ||
|
|
0577fa5308 | ||
|
|
f29ee1b4ac | ||
|
|
0c08713521 | ||
|
|
567928a7f5 | ||
|
|
ae9e211f30 | ||
|
|
b5b75df91a | ||
|
|
8ddccc0b0c | ||
|
|
383a1a330a | ||
|
|
95195fff6f | ||
|
|
93b77dc4c1 | ||
|
|
4aee7fb1b8 | ||
|
|
a6d68dba5e | ||
|
|
109c550187 | ||
|
|
06353941e6 | ||
|
|
fed953d195 | ||
|
|
883f87c7c8 | ||
|
|
14d37268d6 | ||
|
|
4b6181039d | ||
|
|
47944671c6 | ||
|
|
f33a7dd665 | ||
|
|
781e5a71bf | ||
|
|
c4ff884ad0 | ||
|
|
02b9f85b16 | ||
|
|
2756252368 | ||
|
|
a386abf5a5 | ||
|
|
e5c2c35a81 | ||
|
|
227112c7aa | ||
|
|
a4ed37bdfc | ||
|
|
c6a62cee61 | ||
|
|
891bc818b2 | ||
|
|
ebe25d6f20 | ||
|
|
92ec17218b | ||
|
|
e8a0f6b7b6 | ||
|
|
125c39967c | ||
|
|
4132bc755d | ||
|
|
9707881bf9 | ||
|
|
fa6493ae44 | ||
|
|
0c387cf6d9 | ||
|
|
5e4d1d5c1c | ||
|
|
4d82fd65f6 | ||
|
|
6d3644f13b | ||
|
|
7a5aa7ba35 | ||
|
|
9c9609eb2b | ||
|
|
418d3c074f | ||
|
|
6bbda3d41e | ||
|
|
25669bb3f2 | ||
|
|
508d495a23 | ||
|
|
06427dc009 | ||
|
|
c325df1414 | ||
|
|
07447160e3 | ||
|
|
ededc73fd7 | ||
|
|
cad02bfad7 | ||
|
|
94299f0452 | ||
|
|
ae5d82c41d | ||
|
|
6468822295 | ||
|
|
777ae31426 | ||
|
|
1ca56fb81c | ||
|
|
5d74e1eafe | ||
|
|
f3fdd7ff25 | ||
|
|
fbbe69dac0 | ||
|
|
ac54179f14 | ||
|
|
50d296e46c | ||
|
|
616ba6500c | ||
|
|
d9968f2c91 | ||
|
|
8ca9c5bcf7 | ||
|
|
a7b0618f91 | ||
|
|
e9896e34e1 | ||
|
|
28bd03765a | ||
|
|
24a86ae8df | ||
|
|
f5c349e105 | ||
|
|
e8d2e28dba | ||
|
|
e0c2423ace | ||
|
|
5e429ba71f | ||
|
|
64dfdba94d | ||
|
|
3866413504 | ||
|
|
2da834784f | ||
|
|
d6181da32b | ||
|
|
8287b94a25 | ||
|
|
bc633301fe | ||
|
|
ed94fb4a66 | ||
|
|
fc27086052 | ||
|
|
de1d1961e3 | ||
|
|
a90a9383b4 | ||
|
|
fd244287d5 | ||
|
|
fafe9e7e8a | ||
|
|
db37513206 | ||
|
|
c023088a3f | ||
|
|
59f6217c4f | ||
|
|
0e43fbbb34 | ||
|
|
1720f22247 | ||
|
|
3f791d25b5 | ||
|
|
acd3047500 | ||
|
|
7b3a4aa2a8 | ||
|
|
4bfaeeb44b | ||
|
|
a094ae7197 | ||
|
|
ef0362d118 | ||
|
|
5a5763684d | ||
|
|
6e575300e3 | ||
|
|
8109fc4d46 | ||
|
|
e0519e7851 | ||
|
|
6334df5f5f | ||
|
|
38294d29f5 | ||
|
|
5b131cc8a7 | ||
|
|
5dee654132 | ||
|
|
d902476780 | ||
|
|
bc5dabef3c | ||
|
|
024f1e4851 | ||
|
|
5f87417d9e | ||
|
|
fa94550261 | ||
|
|
11efbf034e | ||
|
|
c839a0b0a3 | ||
|
|
420b657db8 | ||
|
|
2656a26272 | ||
|
|
8694c120bc | ||
|
|
992b2b6ba7 | ||
|
|
924f009390 | ||
|
|
48a1244fa0 | ||
|
|
8789a959e5 | ||
|
|
5765ac59cc | ||
|
|
3b9bf96431 | ||
|
|
a5fe5e7052 | ||
|
|
0f96b5a4a5 | ||
|
|
25babeae56 | ||
|
|
1951e79962 | ||
|
|
1e0e31cc1c | ||
|
|
8d35f72fcb | ||
|
|
5f3e515131 | ||
|
|
49236fce86 | ||
|
|
9d2dde7a5a | ||
|
|
c3e703237c | ||
|
|
8868066445 | ||
|
|
b446c31cbc | ||
|
|
2d6e7070a6 | ||
|
|
473e0cb902 | ||
|
|
1a8fca0534 | ||
|
|
e24bc12fc9 | ||
|
|
e77c9141ed | ||
|
|
321157b17b | ||
|
|
6ac6574b4c | ||
|
|
70ff0a9b8f | ||
|
|
15bdb57a22 | ||
|
|
bb54e5520c | ||
|
|
933d486a57 | ||
|
|
8a1c7f5b52 | ||
|
|
822954be5d | ||
|
|
57dc17518c | ||
|
|
3df8d4844e | ||
|
|
5a0443618f | ||
|
|
8a76cd506f | ||
|
|
50d05eae47 | ||
|
|
ca41b3b600 | ||
|
|
43a17ddc7d | ||
|
|
dfa347f860 | ||
|
|
6033027812 | ||
|
|
9ee6f7fbb8 | ||
|
|
60c0754800 | ||
|
|
a9251c5e71 | ||
|
|
7daeddc946 | ||
|
|
dbdb00070e | ||
|
|
e5c3c282ef | ||
|
|
caba79b5e2 | ||
|
|
d359ea7fa6 | ||
|
|
c0abbe570f | ||
|
|
229a2c0c3c | ||
|
|
3f185c9c69 | ||
|
|
f86f72ab27 | ||
|
|
74af17cc65 | ||
|
|
aaa3e34c7f | ||
|
|
a053f198f5 | ||
|
|
852ba68895 | ||
|
|
1b22d176d6 | ||
|
|
0ccbedf551 | ||
|
|
28f1179336 | ||
|
|
de4d9e285e | ||
|
|
e0faaac822 | ||
|
|
c84f27dd3f | ||
|
|
12279d5c00 | ||
|
|
281588abd2 | ||
|
|
7e206b84aa | ||
|
|
f69f999694 | ||
|
|
c0c062592f | ||
|
|
06885e2ba3 | ||
|
|
89a268d087 | ||
|
|
34424e713c | ||
|
|
89f381439f | ||
|
|
6a80be9df3 | ||
|
|
fde1923acb | ||
|
|
d486e1d34f | ||
|
|
3648b8b0b1 | ||
|
|
83301238d2 | ||
|
|
a4a1fb930a | ||
|
|
6555353e0e | ||
|
|
f5f0601e53 | ||
|
|
2598595e42 | ||
|
|
49b78a85c9 | ||
|
|
35b12ebd6c | ||
|
|
0918c8e68c | ||
|
|
1603a07de1 | ||
|
|
0a37aa4ba1 | ||
|
|
b721a80fcc | ||
|
|
01365d035e | ||
|
|
a4f059e20f | ||
|
|
eb758bbf36 | ||
|
|
bc2441e66a | ||
|
|
7c1792bbd2 | ||
|
|
2fdbc3e61c | ||
|
|
2ace705122 | ||
|
|
4b817062d8 | ||
|
|
79c35118d7 | ||
|
|
6a4f5d52ec | ||
|
|
ccaae2dd66 | ||
|
|
d335e64f88 | ||
|
|
177d7ed07a | ||
|
|
85a1e15b58 | ||
|
|
432b58a078 | ||
|
|
75e3c5daef | ||
|
|
deb71c27b0 | ||
|
|
8f5e1de6d8 | ||
|
|
4836d62d7a | ||
|
|
d27b0617b2 | ||
|
|
28a2c29a39 | ||
|
|
fcb6478407 | ||
|
|
6d72afe40e | ||
|
|
e775266c64 | ||
|
|
12f25b38c0 | ||
|
|
c67a1107cb | ||
|
|
34bfb0d62c | ||
|
|
ede45cad1f | ||
|
|
75fe4c8aed | ||
|
|
12e272a7e5 | ||
|
|
cfcba4e578 | ||
|
|
37ab898426 | ||
|
|
68865ea929 | ||
|
|
86674faa22 | ||
|
|
f07947ce45 | ||
|
|
6a50f59e25 | ||
|
|
bfacd56800 | ||
|
|
45dea8b0c1 | ||
|
|
2f7f8cf2d8 | ||
|
|
31611b6a28 | ||
|
|
d1cd4b0c2b | ||
|
|
c8ba1c3e7c | ||
|
|
fbc8fe4c2d | ||
|
|
54ec9b48db | ||
|
|
488698d5e2 | ||
|
|
58c407aabb | ||
|
|
fe750f23bc | ||
|
|
87a01a5cfd | ||
|
|
74dd669bb0 | ||
|
|
36a50389f5 | ||
|
|
4f2d7434c7 | ||
|
|
b0a0848476 | ||
|
|
9fcd897e54 | ||
|
|
daa8fff21e | ||
|
|
785229ddea | ||
|
|
8bb11bf1d4 | ||
|
|
1f975e15c1 | ||
|
|
6c69ba54db | ||
|
|
49f9904d00 | ||
|
|
7afd0dfa4e | ||
|
|
b1b6a437a7 | ||
|
|
e4d5006591 | ||
|
|
627b3771d3 | ||
|
|
f4758e84e8 | ||
|
|
8dfe2098ed | ||
|
|
c56a4ee036 | ||
|
|
c32623b821 | ||
|
|
3cd0a947f7 | ||
|
|
8eea1cf4e7 | ||
|
|
b5fccd5bbe | ||
|
|
e74ce9dfd8 | ||
|
|
3743365a83 | ||
|
|
8aeb2173d1 | ||
|
|
9a2b17d952 | ||
|
|
6f54cce01a | ||
|
|
6901b2049e | ||
|
|
d0dcc027df | ||
|
|
b693005118 | ||
|
|
ab4a0e836f | ||
|
|
abe02db6c6 | ||
|
|
a2cd5dd32d | ||
|
|
49b46a6096 | ||
|
|
94f420ca3f | ||
|
|
5e530105df | ||
|
|
2f82d34c4b | ||
|
|
81fd01d0ac | ||
|
|
9faac9f9fe | ||
|
|
d04787a60c | ||
|
|
f5dbf94b52 | ||
|
|
5bec2d9b15 | ||
|
|
fe64f0c63c | ||
|
|
c20fd9691a | ||
|
|
eb323fbff9 | ||
|
|
211f6b9a74 | ||
|
|
b6c003ec63 | ||
|
|
93d4bf2a72 | ||
|
|
c6cb573383 | ||
|
|
f4ce671ea4 | ||
|
|
147f7cbabb | ||
|
|
b05d5a141e | ||
|
|
d34e0306f8 | ||
|
|
bd9f48dd24 | ||
|
|
9805990d79 | ||
|
|
dbbe60967c | ||
|
|
0ef91c1904 | ||
|
|
376573459c | ||
|
|
9c6d7c0ff9 | ||
|
|
30a95b7da3 | ||
|
|
e6a60aef9a | ||
|
|
5c2024581f | ||
|
|
f7ea2bb51e | ||
|
|
3e4da8ab57 | ||
|
|
7352a28908 | ||
|
|
d1928ee578 | ||
|
|
cd978d7384 | ||
|
|
cde0d8f5e2 | ||
|
|
7bacfcc2e4 | ||
|
|
241fe36103 | ||
|
|
441714a656 | ||
|
|
bd3fdb7f16 | ||
|
|
775af6feee | ||
|
|
adf5c17e0d | ||
|
|
beb2d96a32 | ||
|
|
2a4ae88bc0 | ||
|
|
b76098ba45 | ||
|
|
c095027f8e | ||
|
|
9d1db19907 | ||
|
|
260e321537 | ||
|
|
073603b527 | ||
|
|
17b259cf31 | ||
|
|
8f0f0026e9 | ||
|
|
59dae2b545 | ||
|
|
4670f69ead | ||
|
|
16fbd25a34 | ||
|
|
2d75985cb3 | ||
|
|
f963fb321e | ||
|
|
10feea0d48 | ||
|
|
b6b9b0ac36 | ||
|
|
5551e85853 | ||
|
|
1f0fa5031b | ||
|
|
263294a3f5 | ||
|
|
f9df30f70b | ||
|
|
61d31ec054 | ||
|
|
c8917bfc4c | ||
|
|
36b69a05e5 | ||
|
|
c8d2f66467 | ||
|
|
7416bb0e56 | ||
|
|
9182d0132d | ||
|
|
9be9357ade | ||
|
|
7f414f8adf | ||
|
|
3af9549939 | ||
|
|
41f248d731 | ||
|
|
18a517b7bf | ||
|
|
5150204389 | ||
|
|
3b16e7729d | ||
|
|
76d27c9fce | ||
|
|
4ce6e41000 | ||
|
|
33260cdbd9 | ||
|
|
85d81ba7fd | ||
|
|
7e8a3ca21f | ||
|
|
e9e4dc1f5c | ||
|
|
17c30e165a | ||
|
|
c45c6ceb15 | ||
|
|
d73c2c465f | ||
|
|
d4fc53939b | ||
|
|
4becb65bec | ||
|
|
f64e16c790 | ||
|
|
1772011627 | ||
|
|
5b8f785e2b | ||
|
|
a0e3b77006 | ||
|
|
908070ecd7 | ||
|
|
7c6a58cd30 | ||
|
|
b0990a1132 | ||
|
|
c6988cdb88 | ||
|
|
e0d304b033 | ||
|
|
e4a9f2d64c | ||
|
|
0236fe3ca9 | ||
|
|
1bed8623a2 | ||
|
|
df7d957914 | ||
|
|
30c4b00f33 | ||
|
|
ab27886460 | ||
|
|
31e18d04d7 | ||
|
|
8155484510 | ||
|
|
b61f1d2b53 | ||
|
|
2e274b936a | ||
|
|
bf3e311b57 | ||
|
|
6a7613de6b | ||
|
|
ee46549e04 | ||
|
|
377f3d4aff | ||
|
|
752d47d71e | ||
|
|
367157b80c | ||
|
|
53542f1cd6 | ||
|
|
7a8f156abf | ||
|
|
c60cc57a0d | ||
|
|
8de6ec9a21 | ||
|
|
44b6f4be7e | ||
|
|
280be1751c | ||
|
|
701a73a2c5 | ||
|
|
b578eada07 | ||
|
|
8100f155dc | ||
|
|
9f1a014004 | ||
|
|
e35e0e157c | ||
|
|
3aff328af3 | ||
|
|
ffb086045a | ||
|
|
c0786dfa6f | ||
|
|
ddc33fa52b | ||
|
|
9f2d6a5d41 | ||
|
|
64e884a092 | ||
|
|
17ec174683 | ||
|
|
3666cbee94 | ||
|
|
25de018f7d | ||
|
|
6597851b48 | ||
|
|
0399131968 | ||
|
|
86836e7f89 | ||
|
|
df346b11d3 | ||
|
|
27f74b3fe2 | ||
|
|
87dec64ad1 | ||
|
|
54c787162a | ||
|
|
6e92e699dc | ||
|
|
7950f43db3 | ||
|
|
d300677315 | ||
|
|
bd4d29dd14 | ||
|
|
18a84433f4 | ||
|
|
768ebf0ef2 | ||
|
|
1c20cb5478 | ||
|
|
279587ea11 | ||
|
|
25df193390 | ||
|
|
9ce81693bd | ||
|
|
16765e092f | ||
|
|
237e1257c4 | ||
|
|
665859b17d | ||
|
|
b5a6d6974c | ||
|
|
26bab029f4 | ||
|
|
ed7bb07b03 | ||
|
|
c87277ad01 | ||
|
|
62be259a90 | ||
|
|
80798f984b | ||
|
|
e32409880c | ||
|
|
7b6eb2940e | ||
|
|
87ad8df22f | ||
|
|
9fe20036a1 | ||
|
|
2b0d8d43bd | ||
|
|
3af340d384 | ||
|
|
20c1ad8d87 | ||
|
|
8c351c7c46 | ||
|
|
6647e986d9 | ||
|
|
77f691520c | ||
|
|
dfaa6ec024 | ||
|
|
41c574f5df | ||
|
|
10e901dcaa | ||
|
|
1d3626b4e1 | ||
|
|
bc419a51d6 | ||
|
|
d0980a2872 | ||
|
|
74719e48d9 | ||
|
|
d7c6b45438 | ||
|
|
551cfd87ee | ||
|
|
ba29873a8e | ||
|
|
d9e2bb4537 | ||
|
|
16d7b15d67 | ||
|
|
b9da97fedd | ||
|
|
bdf3b0393a | ||
|
|
a18f701466 | ||
|
|
71c7d8a90c | ||
|
|
99e766d952 | ||
|
|
fc78a0ed36 | ||
|
|
d4d398f583 | ||
|
|
be766ec803 | ||
|
|
57bb8dbbe3 | ||
|
|
c539d4fbbd | ||
|
|
a7dddcebe8 | ||
|
|
a9275845ff | ||
|
|
e5bf9efdb9 | ||
|
|
85073345ec | ||
|
|
80d5b29902 | ||
|
|
f55b748d20 | ||
|
|
870468ddf7 | ||
|
|
b3107916ce | ||
|
|
9cf856ab78 | ||
|
|
bbd047a940 | ||
|
|
d3dfb0a7ff | ||
|
|
5a8f9db79c | ||
|
|
fbe20386b6 | ||
|
|
6245b40015 | ||
|
|
3e959d8dc0 | ||
|
|
564df920d1 | ||
|
|
7e2c467a4f | ||
|
|
3a42305408 | ||
|
|
09616777e6 | ||
|
|
c2a5569b8c | ||
|
|
38bea2108b | ||
|
|
ab0777b45f | ||
|
|
962f94387b | ||
|
|
de5600b4fd | ||
|
|
188a202f02 | ||
|
|
19f39b87f5 | ||
|
|
c759f314f9 | ||
|
|
6c98f14c64 | ||
|
|
f2348e1b24 | ||
|
|
4b5a10fe61 | ||
|
|
163fa58b5a | ||
|
|
2bb03225cb | ||
|
|
b3bbb6af01 | ||
|
|
28d711e1f4 | ||
|
|
410fdb8343 | ||
|
|
21c2f3bdd1 | ||
|
|
baae080318 | ||
|
|
81c0796056 | ||
|
|
6ea5c5f414 | ||
|
|
7e37705843 | ||
|
|
425b5e6b4a | ||
|
|
8942c72fb2 | ||
|
|
e9359fdd73 | ||
|
|
5c4308abc1 | ||
|
|
61576b671b | ||
|
|
1e4d6bb942 | ||
|
|
ff6c0addb4 | ||
|
|
98cd524c07 | ||
|
|
746d57ff42 | ||
|
|
7319822419 | ||
|
|
58bcde3818 | ||
|
|
b57d08f38e | ||
|
|
739a8cef32 | ||
|
|
8a6e31e025 | ||
|
|
616aac9771 | ||
|
|
23a1b7484e | ||
|
|
9624ee1c76 | ||
|
|
4c557be2c2 | ||
|
|
7577ee8015 | ||
|
|
8c73914da4 | ||
|
|
604ba300aa | ||
|
|
876166ab74 | ||
|
|
1bf8fda770 | ||
|
|
cd32218cea | ||
|
|
0fd0974738 | ||
|
|
ed7f95a1a7 | ||
|
|
a36c1c52ae | ||
|
|
226d948c4d | ||
|
|
84c67f977e | ||
|
|
c09cda84a3 | ||
|
|
bd36ea1829 | ||
|
|
01a47925e0 | ||
|
|
dd8a70eb95 | ||
|
|
013bdba4ff | ||
|
|
c1acc54d55 | ||
|
|
5f3fb6e5f7 | ||
|
|
e3fac9c161 | ||
|
|
accab9e78a | ||
|
|
cb53d140e3 | ||
|
|
72986d1946 | ||
|
|
2195c55518 | ||
|
|
c418ba1908 | ||
|
|
934356e5cc | ||
|
|
c40235a910 | ||
|
|
cd8338196e | ||
|
|
2143e87401 | ||
|
|
f0a536ee1e | ||
|
|
dde4b63c6b | ||
|
|
0e7bcc4b56 | ||
|
|
e4816b4322 | ||
|
|
af4f29c538 | ||
|
|
016adb64ef | ||
|
|
7cedccedcd | ||
|
|
a9c12d4534 | ||
|
|
77f71b1978 | ||
|
|
1c029fbc7b | ||
|
|
2bc0d8d145 | ||
|
|
4c2af88f92 | ||
|
|
ddd5d2a0b0 | ||
|
|
d5cb59dc84 | ||
|
|
f21581630a | ||
|
|
3fef37d06b | ||
|
|
a8b93896ed | ||
|
|
6c1e9bf0ca | ||
|
|
1d8947d404 | ||
|
|
834377b342 | ||
|
|
c2e0eb05e5 | ||
|
|
256032ca4a | ||
|
|
d80f2b9566 | ||
|
|
9e2f0371ba | ||
|
|
a2e74a3e1b | ||
|
|
f04a5a1ab9 | ||
|
|
7114311b75 | ||
|
|
1e9e092dc3 | ||
|
|
98b3a5ba23 | ||
|
|
200966e806 | ||
|
|
407430b81e | ||
|
|
c4b1fc039c | ||
|
|
d00379af6b | ||
|
|
7cee0f3ee3 | ||
|
|
2588346f1b | ||
|
|
863128013d | ||
|
|
51eb8eb67f | ||
|
|
28e01fd8ac | ||
|
|
726e153ad5 | ||
|
|
e8df09c85b | ||
|
|
2b4fb2971d | ||
|
|
933b36cca0 | ||
|
|
37a7cfb6ba | ||
|
|
8a1cda159c | ||
|
|
403dcbebcd | ||
|
|
373ad69306 | ||
|
|
b2029e09f5 | ||
|
|
4f37d2d0c2 | ||
|
|
a5b07bc2a8 | ||
|
|
1544a5622d | ||
|
|
d49a877771 | ||
|
|
0f4747743c | ||
|
|
368e4522e7 | ||
|
|
cb1d1bb115 | ||
|
|
612cc3dd41 | ||
|
|
bff082e577 | ||
|
|
ea41750a14 | ||
|
|
458376a665 | ||
|
|
f0284907c4 | ||
|
|
ad0385ccf7 | ||
|
|
4350fc3c65 | ||
|
|
fc3422b9e5 | ||
|
|
9703c19fb4 | ||
|
|
d36f2fb354 | ||
|
|
c837785314 | ||
|
|
33014a9f45 | ||
|
|
c16d0b8605 | ||
|
|
c88e4c5173 | ||
|
|
0540696c3e | ||
|
|
5f59a97a02 | ||
|
|
d3b815c3c7 | ||
|
|
d2a8dcbede | ||
|
|
4854e879a6 | ||
|
|
ddc99cebff | ||
|
|
47470d4f2b | ||
|
|
d9297d54de | ||
|
|
2d821f957e | ||
|
|
c0ce6e7a8a | ||
|
|
3f620c6cdd | ||
|
|
9160d8018d | ||
|
|
51307bcc69 | ||
|
|
d29651ee80 | ||
|
|
18640077aa | ||
|
|
59563c893b | ||
|
|
90fd1786e1 | ||
|
|
4a11115dd0 | ||
|
|
baf3e774c5 | ||
|
|
ac296411d5 | ||
|
|
09cea4d6d4 | ||
|
|
382ff2416f | ||
|
|
27036379dd | ||
|
|
f4e6e140e0 | ||
|
|
b4e5cb88d9 | ||
|
|
3269fef845 | ||
|
|
e797719b41 | ||
|
|
284a8f2465 | ||
|
|
54ea656da2 | ||
|
|
b4aaadf40b | ||
|
|
74d2e3ef75 | ||
|
|
b10e4c11d9 | ||
|
|
075e141a9c | ||
|
|
04acf895f0 | ||
|
|
21608bf2e2 | ||
|
|
653beb1952 | ||
|
|
050d33ff14 | ||
|
|
1ae86e039b | ||
|
|
f75cadf6ba | ||
|
|
9acb980b82 | ||
|
|
93d0fe9176 | ||
|
|
7d623faf4b | ||
|
|
0f2401b0cc |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -2,10 +2,12 @@ node_modules*
|
||||
config.status*
|
||||
config/environments/*.js
|
||||
.idea
|
||||
.vscode
|
||||
.nvmrc
|
||||
tools/munin/windshaft.conf
|
||||
logs/
|
||||
pids/
|
||||
redis.pid
|
||||
test.log
|
||||
npm-debug.log
|
||||
*.log
|
||||
coverage/
|
||||
.DS_Store
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"debug" : false, // true: Allow debugger statements e.g. browser breakpoints.
|
||||
// "eqnull" : false, // true: Tolerate use of `== null`
|
||||
// "es5" : false, // true: Allow ES5 syntax (ex: getters and setters)
|
||||
// "esnext" : false, // true: Allow ES.next (ES6) syntax (ex: `const`)
|
||||
"esnext" : true, // true: Allow ES.next (ES6) syntax (ex: `const`)
|
||||
// "moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features)
|
||||
// // (ex: `for each`, multiple try/catch, function expression…)
|
||||
// "evil" : false, // true: Tolerate use of `eval` and `new Function()`
|
||||
|
||||
23
.travis.yml
23
.travis.yml
@@ -1,30 +1,27 @@
|
||||
sudo: false
|
||||
|
||||
dist: trusty
|
||||
addons:
|
||||
postgresql: "9.4"
|
||||
postgresql: "9.5"
|
||||
apt:
|
||||
sources:
|
||||
- ubuntu-toolchain-r-test
|
||||
packages:
|
||||
- postgresql-plpython-9.4
|
||||
- postgresql-9.5-postgis-2.3
|
||||
- postgresql-plpython-9.5
|
||||
- pkg-config
|
||||
- libcairo2-dev
|
||||
- libjpeg8-dev
|
||||
- libgif-dev
|
||||
- libpango1.0-dev
|
||||
- g++-4.9
|
||||
|
||||
before_install:
|
||||
- npm install -g npm@2
|
||||
- createdb template_postgis
|
||||
- createuser publicuser
|
||||
- psql -c "CREATE EXTENSION postgis" template_postgis
|
||||
|
||||
env:
|
||||
- NPROCS=1 JOBS=1 PGUSER=postgres
|
||||
- NPROCS=1 JOBS=1 PGUSER=postgres CXX=g++-4.9
|
||||
|
||||
language: node_js
|
||||
node_js:
|
||||
- "0.10"
|
||||
|
||||
notifications:
|
||||
irc:
|
||||
channels:
|
||||
- "irc.freenode.org#cartodb"
|
||||
use_notice: true
|
||||
- "6"
|
||||
|
||||
@@ -8,4 +8,4 @@ We love pull requests from everyone, see [Contributing to Open Source on GitHub]
|
||||
|
||||
## Submitting Contributions
|
||||
|
||||
* You will need to sign a Contributor License Agreement (CLA) before making a submission. [Learn more here](https://cartodb.com/contributing).
|
||||
* You will need to sign a Contributor License Agreement (CLA) before making a submission. [Learn more here](https://carto.com/contributions).
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
1. Test (make clean all check), fix if broken before proceeding
|
||||
2. Ensure proper version in package.json
|
||||
2. Ensure proper version in package.json
|
||||
3. Ensure NEWS section exists for the new version, review it, add release date
|
||||
4. Recreate npm-shrinkwrap.json with: `npm install --no-shrinkwrap && npm shrinkwrap`
|
||||
5. Commit package.json, npm-shrinwrap.json, NEWS
|
||||
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. Announce on cartodb@googlegroups.com
|
||||
8. Stub NEWS/package for next version
|
||||
7. Stub NEWS/package for next version
|
||||
|
||||
Versions:
|
||||
|
||||
|
||||
12
INSTALL.md
12
INSTALL.md
@@ -4,11 +4,11 @@
|
||||
Make sure that you have the requirements needed. These are
|
||||
|
||||
- Core
|
||||
- Node.js >=0.8
|
||||
- npm >=1.2.1 <2.0.0
|
||||
- Node.js >=6.9.x
|
||||
- yarn >=0.21.3
|
||||
- PostgreSQL >8.3.x, PostGIS >1.5.x
|
||||
- Redis >2.4.0 (http://www.redis.io)
|
||||
- Mapnik 2.0.1, 2.0.2, 2.1.0, 2.2.0, 2.3.0. See [Installing Mapnik](https://github.com/CartoDB/Windshaft#installing-mapnik).
|
||||
- Mapnik >3.x. See [Installing Mapnik](https://github.com/CartoDB/Windshaft#installing-mapnik).
|
||||
- Windshaft: check [Windshaft dependencies and installation notes](https://github.com/CartoDB/Windshaft#dependencies)
|
||||
- libcairo2-dev, libpango1.0-dev, libjpeg8-dev and libgif-dev for server side canvas support
|
||||
|
||||
@@ -43,11 +43,11 @@ psql -d template_postgis -c 'CREATE EXTENSION postgis;'
|
||||
To fetch and build all node-based dependencies, run:
|
||||
|
||||
```
|
||||
npm install
|
||||
yarn
|
||||
```
|
||||
|
||||
Note that the ```npm install``` step will populate the node_modules/
|
||||
Note that the ```yarn``` step will populate the node_modules/
|
||||
directory with modules, some of which being compiled on demand. If you
|
||||
happen to have startup errors you may need to force rebuilding those
|
||||
modules. At any time just wipe out the node_modules/ directory and run
|
||||
```npm install``` again.
|
||||
```yarn``` again.
|
||||
|
||||
2
Makefile
2
Makefile
@@ -7,7 +7,7 @@ all:
|
||||
@$(SHELL) ./scripts/install.sh
|
||||
|
||||
clean:
|
||||
rm -rf node_modules/*
|
||||
rm -rf node_modules/
|
||||
|
||||
distclean: clean
|
||||
rm config.status*
|
||||
|
||||
958
NEWS.md
958
NEWS.md
@@ -1,5 +1,963 @@
|
||||
# Changelog
|
||||
|
||||
## 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
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [3.2.1](https://github.com/CartoDB/windshaft/releases/tag/3.2.1).
|
||||
- Add support to retrieve info about layer stats in map instantiation.
|
||||
- Upgrades camshaft to [0.55.2](https://github.com/CartoDB/camshaft/releases/tag/0.55.2).
|
||||
- Remove promise polyfill from turbo-carto adapter
|
||||
|
||||
|
||||
## 3.8.0
|
||||
Released 2017-05-22
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.55.0](https://github.com/CartoDB/camshaft/releases/tag/0.55.0).
|
||||
- Upgrades turbo-carto to [0.19.1](https://github.com/CartoDB/turbo-carto/releases/tag/0.19.1)
|
||||
|
||||
|
||||
## 3.7.1
|
||||
Released 2017-05-18
|
||||
|
||||
Bug fixes:
|
||||
- Fix buffersize assignment when is not defined in requested mapconfig.
|
||||
|
||||
|
||||
## 3.7.0
|
||||
Released 2017-05-18
|
||||
|
||||
Announcements:
|
||||
- Manage multiple values of buffer-size for different formats
|
||||
- Upgrades windshaft to [3.2.0](https://github.com/CartoDB/windshaft/releases/tag/3.2.0).
|
||||
|
||||
|
||||
## 3.6.6
|
||||
Released 2017-05-11
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.54.4](https://github.com/CartoDB/camshaft/releases/tag/0.54.4).
|
||||
|
||||
|
||||
## 3.6.5
|
||||
Released 2017-05-09
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.54.3](https://github.com/CartoDB/camshaft/releases/tag/0.54.3).
|
||||
|
||||
|
||||
## 3.6.4
|
||||
Released 2017-05-05
|
||||
|
||||
Announcements:
|
||||
- Upgrade cartodb-psql to [0.8.0](https://github.com/CartoDB/node-cartodb-psql/releases/tag/0.8.0).
|
||||
- Upgrades camshaft to [0.54.2](https://github.com/CartoDB/camshaft/releases/tag/0.54.2).
|
||||
- Upgrades windshaft to [3.1.2](https://github.com/CartoDB/windshaft/releases/tag/3.1.2).
|
||||
|
||||
|
||||
## 3.6.3
|
||||
Released 2017-04-25
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [3.1.1](https://github.com/CartoDB/windshaft/releases/tag/3.1.1).
|
||||
|
||||
|
||||
## 3.6.2
|
||||
Released 2017-04-24
|
||||
|
||||
Announcements:
|
||||
- Upgrades grainstore to [1.6.3](https://github.com/CartoDB/grainstore/releases/tag/1.6.3).
|
||||
|
||||
|
||||
## 3.6.1
|
||||
Released 2017-04-24
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.54.1](https://github.com/CartoDB/camshaft/releases/tag/0.54.1).
|
||||
|
||||
|
||||
## 3.6.0
|
||||
Released 2017-04-20
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.54.0](https://github.com/CartoDB/camshaft/releases/tag/0.54.0).
|
||||
|
||||
|
||||
## 3.5.1
|
||||
Released 2017-04-11
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.53.1](https://github.com/CartoDB/camshaft/releases/tag/0.53.1).
|
||||
|
||||
|
||||
## 3.5.0
|
||||
Released 2017-04-10
|
||||
|
||||
Bug fixes:
|
||||
- Fix invalidation of cache for maps with analyses #638.
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.53.0](https://github.com/CartoDB/camshaft/releases/tag/0.53.0).
|
||||
|
||||
|
||||
## 3.4.0
|
||||
Released 2017-04-03
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.51.0](https://github.com/CartoDB/camshaft/releases/tag/0.51.0).
|
||||
|
||||
|
||||
## 3.3.0
|
||||
Released 2017-04-03
|
||||
|
||||
New features:
|
||||
- Static map endpoints allow specifying the layers to render #653.
|
||||
|
||||
|
||||
## 3.2.0
|
||||
Released 2017-03-30
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [3.1.0](https://github.com/CartoDB/windshaft/releases/tag/3.1.0).
|
||||
- Active GC interval.
|
||||
|
||||
|
||||
## 3.1.1
|
||||
Released 2017-03-23
|
||||
|
||||
Bug fixes:
|
||||
- Use crc32 instead of md5 for computing subdomain candidate #642
|
||||
|
||||
|
||||
## 3.1.0
|
||||
Released 2017-03-22
|
||||
|
||||
Features:
|
||||
- Generate URLs for resources based on CDN and template rules
|
||||
|
||||
|
||||
## 3.0.2
|
||||
Released 2017-03-22
|
||||
|
||||
Bug fixes:
|
||||
- Upgrade dependencies
|
||||
- Improve docs: remove mentions to NPM and use yarn instead
|
||||
- Remove script to generate npm-shrinkwrap file
|
||||
|
||||
|
||||
## 3.0.1
|
||||
Released 2017-03-21
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [3.0.1](https://github.com/CartoDB/windshaft/releases/tag/3.0.1).
|
||||
|
||||
|
||||
## 3.0.0
|
||||
Released 2017-03-21
|
||||
|
||||
Announcements:
|
||||
- Supports Node v6.9.x
|
||||
- Drops support for Node v0.10.x
|
||||
- Upgrades windshaft to 3.0.0
|
||||
- Upgrades cartodb-query-tables to 0.2.0
|
||||
- Upgrades cartodb-redis to 0.13.2
|
||||
- Upgrades redis-mpool to 0.4.1
|
||||
|
||||
**Note**: Due to this [issue](https://github.com/npm/npm/issues/15713), Windshaft-cartodb must be installed with `yarn` instead of `npm` providing just a `yarn.lock` to get consistent installs across machines.
|
||||
|
||||
## 2.89.0
|
||||
Released 2017-03-17
|
||||
|
||||
**Deprecation warning**: v2.89.0 is the last release that supports Node v0.10.x. Next mayor release will support Node v6.9.x and further versions.
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [2.8.0](https://github.com/CartoDB/windshaft/releases/tag/2.8.0).
|
||||
|
||||
Bug fixes:
|
||||
- Histogram column type discovery query uses non-filtered query #637
|
||||
|
||||
|
||||
## 2.88.4
|
||||
Released 2017-03-10
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.50.3](https://github.com/CartoDB/camshaft/releases/tag/0.50.3).
|
||||
|
||||
|
||||
## 2.88.3
|
||||
Released 2017-03-02
|
||||
|
||||
Bug fixes:
|
||||
- Category dataviews now uses the proper aggregation function for the 'Other' category. See https://github.com/CartoDB/Windshaft-cartodb/issues/628
|
||||
|
||||
## 2.88.2
|
||||
Released 2017-02-23
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.50.2](https://github.com/CartoDB/camshaft/releases/tag/0.50.2).
|
||||
|
||||
|
||||
## 2.88.1
|
||||
Released 2017-02-21
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.50.1](https://github.com/CartoDB/camshaft/releases/tag/0.50.1)
|
||||
|
||||
|
||||
## 2.88.0
|
||||
Released 2017-02-21
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.50.0](https://github.com/CartoDB/camshaft/releases/tag/0.50.0).
|
||||
- Upgrades cartodb-psql to [0.7.1](https://github.com/CartoDB/node-cartodb-psql/releases/tag/0.7.1).
|
||||
- Upgrades windshaft to [2.7.0](https://github.com/CartoDB/windshaft/releases/tag/2.7.0).
|
||||
|
||||
|
||||
## 2.87.5
|
||||
Released 2017-02-02
|
||||
|
||||
Bug fixes:
|
||||
- Cast dataview override values to Number or throw error.
|
||||
|
||||
|
||||
## 2.87.4
|
||||
Released 2017-01-20
|
||||
|
||||
Bug fixes:
|
||||
- Be able to not compute NULL categories and null values in aggregation dataviews #617.
|
||||
|
||||
|
||||
## 2.87.3
|
||||
Released 2016-12-19
|
||||
|
||||
Bug fixes:
|
||||
- Fix overviews-related dataviews problems. See https://github.com/CartoDB/Windshaft-cartodb/pull/604
|
||||
|
||||
|
||||
## 2.87.2
|
||||
Released 2016-12-19
|
||||
|
||||
- Use exception safe Dataservices API functions. See https://github.com/CartoDB/dataservices-api/issues/314 and https://github.com/CartoDB/camshaft/issues/242
|
||||
|
||||
|
||||
## 2.87.1
|
||||
Released 2016-12-13
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [2.6.4](https://github.com/CartoDB/Windshaft/releases/tag/2.6.4).
|
||||
- Upgrades request dependency.
|
||||
- Regenerate npm-shrinkwrap.json: missing dependency updates.
|
||||
|
||||
|
||||
## 2.87.0
|
||||
Released 2016-12-12
|
||||
|
||||
Enhancements:
|
||||
- Upgrade turbo-carto dependency to version 0.19.0.
|
||||
|
||||
## 2.86.1
|
||||
Released 2016-12-02
|
||||
|
||||
Bug fixes:
|
||||
- Maps with analyses and `sql_wrap` were broken #599.
|
||||
|
||||
|
||||
## 2.86.0
|
||||
Released 2016-12-02
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [2.6.3](https://github.com/CartoDB/Windshaft/releases/tag/2.6.3).
|
||||
|
||||
|
||||
## 2.85.1
|
||||
Released 2016-11-30
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.48.4](https://github.com/CartoDB/camshaft/releases/tag/0.48.4).
|
||||
|
||||
|
||||
## 2.85.0
|
||||
Released 2016-11-24
|
||||
|
||||
New features:
|
||||
- Allow to set resource URL templates with substitution tokens #594.
|
||||
|
||||
|
||||
## 2.84.2
|
||||
Released 2016-11-23
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.48.3](https://github.com/CartoDB/camshaft/releases/tag/0.48.3).
|
||||
|
||||
|
||||
## 2.84.1
|
||||
Released 2016-11-23
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.48.2](https://github.com/CartoDB/camshaft/releases/tag/0.48.2).
|
||||
|
||||
|
||||
## 2.84.0
|
||||
Released 2016-11-11
|
||||
|
||||
New features:
|
||||
- Analyses limit configuration allows to set other limits than timeout.
|
||||
|
||||
|
||||
## 2.83.1
|
||||
Released 2016-11-10
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.48.1](https://github.com/CartoDB/camshaft/releases/tag/0.48.1).
|
||||
|
||||
|
||||
## 2.83.0
|
||||
Released 2016-11-10
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.48.0](https://github.com/CartoDB/camshaft/releases/tag/0.48.0).
|
||||
|
||||
|
||||
## 2.82.0
|
||||
Released 2016-11-08
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.47.0](https://github.com/CartoDB/camshaft/releases/tag/0.47.0).
|
||||
|
||||
|
||||
## 2.81.1
|
||||
Released 2016-11-05
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [2.6.2](https://github.com/CartoDB/windshaft/releases/tag/2.6.2).
|
||||
- Upgrades camshaft to [0.46.3](https://github.com/CartoDB/camshaft/releases/tag/0.46.3).
|
||||
|
||||
|
||||
## 2.81.0
|
||||
Released 2016-11-02
|
||||
|
||||
Enhancements:
|
||||
- Returns errors with context when query layer does not retrieve geometry column
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [2.6.1](https://github.com/CartoDB/windshaft/releases/tag/2.6.1).
|
||||
- Upgrades camshaft to [0.46.2](https://github.com/CartoDB/camshaft/releases/tag/0.46.2).
|
||||
|
||||
|
||||
## 2.80.2
|
||||
Released 2016-10-26
|
||||
|
||||
Bug fixes:
|
||||
- Fix order in categories query to get ramps
|
||||
|
||||
|
||||
## 2.80.1
|
||||
Released 2016-10-25
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.46.1](https://github.com/CartoDB/camshaft/releases/tag/0.46.1).
|
||||
|
||||
|
||||
## 2.80.0
|
||||
Released 2016-10-20
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.46.0](https://github.com/CartoDB/camshaft/releases/tag/0.46.0).
|
||||
|
||||
New features:
|
||||
- Default analyses limits can be defined in configuration.
|
||||
|
||||
|
||||
## 2.79.0
|
||||
Released 2016-10-11
|
||||
|
||||
New features:
|
||||
- Retrieve analysis limits and pass them into camshaft.
|
||||
|
||||
Announcements:
|
||||
- Upgrades turbo-carto to [0.18.0](https://github.com/CartoDB/turbo-carto/releases/tag/0.18.0).
|
||||
- Upgrades camshaft to [0.45.0](https://github.com/CartoDB/camshaft/releases/tag/0.45.0).
|
||||
|
||||
|
||||
## 2.78.1
|
||||
Released 2016-09-30
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.44.2](https://github.com/CartoDB/camshaft/releases/tag/0.44.2).
|
||||
|
||||
|
||||
## 2.78.0
|
||||
Released 2016-09-29
|
||||
|
||||
New features:
|
||||
- Add metadata about processed turbo-carto rules.
|
||||
|
||||
Announcements:
|
||||
- Upgrades turbo-carto to [0.17.1](https://github.com/CartoDB/turbo-carto/releases/tag/0.17.1).
|
||||
|
||||
|
||||
## 2.77.1
|
||||
|
||||
Released 2016-09-28
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.44.1](https://github.com/CartoDB/camshaft/releases/tag/0.44.1).
|
||||
|
||||
|
||||
## 2.77.0
|
||||
|
||||
Released 2016-09-26
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.44.0](https://github.com/CartoDB/camshaft/releases/tag/0.44.0).
|
||||
- Adds a new configuration for camshaft: logger stream.
|
||||
|
||||
|
||||
## 2.76.0
|
||||
|
||||
Released 2016-09-15
|
||||
|
||||
New features:
|
||||
- Allow to use `--config /path/to/config.js` to specify configuration file.
|
||||
- Environment will be loaded from config file if `environment` key is present, otherwise it keeps current behaviour.
|
||||
|
||||
Bug fixes:
|
||||
- Allow to use absolute paths for log files #570.
|
||||
|
||||
|
||||
## 2.75.0
|
||||
|
||||
Released 2016-09-14
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.43.0](https://github.com/CartoDB/camshaft/releases/tag/0.43.0).
|
||||
|
||||
|
||||
## 2.74.1
|
||||
|
||||
Released 2016-09-07
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.42.1](https://github.com/CartoDB/camshaft/releases/tag/0.42.1).
|
||||
|
||||
|
||||
## 2.74.0
|
||||
|
||||
Released 2016-09-06
|
||||
|
||||
Enhancements:
|
||||
- Layers in previews can be shown or hidden using `preview_layers` property in template map
|
||||
|
||||
|
||||
## 2.73.1
|
||||
|
||||
Released 2016-09-06
|
||||
|
||||
Bug fixes:
|
||||
- Fixes missing column in fixture table `cdb_analysis_catalog`.
|
||||
|
||||
|
||||
## 2.73.0
|
||||
|
||||
Released 2016-09-06
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.42.0](https://github.com/CartoDB/camshaft/releases/tag/0.42.0).
|
||||
|
||||
|
||||
## 2.72.0
|
||||
|
||||
Released 2016-08-23
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.41.0](https://github.com/CartoDB/camshaft/releases/tag/0.41.0).
|
||||
|
||||
|
||||
## 2.71.0
|
||||
|
||||
Released 2016-08-17
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [2.5.0](https://github.com/CartoDB/windshaft/releases/tag/2.5.0).
|
||||
|
||||
|
||||
## 2.70.0
|
||||
|
||||
Released 2016-08-16
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.40.0](https://github.com/CartoDB/camshaft/releases/tag/0.40.0).
|
||||
|
||||
|
||||
## 2.69.1
|
||||
|
||||
Released 2016-08-12
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [2.4.2](https://github.com/CartoDB/windshaft/releases/tag/2.4.2).
|
||||
|
||||
|
||||
## 2.69.0
|
||||
|
||||
Released 2016-08-11
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.39.0](https://github.com/CartoDB/camshaft/releases/tag/0.39.0).
|
||||
|
||||
|
||||
## 2.68.0
|
||||
|
||||
Released 2016-07-21
|
||||
|
||||
Announcements:
|
||||
- Upgrades turbo-carto to [0.16.0](https://github.com/CartoDB/turbo-carto/releases/tag/0.16.0).
|
||||
|
||||
|
||||
## 2.67.1
|
||||
|
||||
Released 2016-07-21
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.38.1](https://github.com/CartoDB/camshaft/releases/tag/0.38.1).
|
||||
|
||||
|
||||
## 2.67.0
|
||||
|
||||
Released 2016-07-21
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.38.0](https://github.com/CartoDB/camshaft/releases/tag/0.38.0).
|
||||
|
||||
|
||||
## 2.66.2
|
||||
|
||||
Released 2016-07-20
|
||||
|
||||
Announcements:
|
||||
- Upgrades turbo-carto to [0.15.1](https://github.com/CartoDB/turbo-carto/releases/tag/0.15.1).
|
||||
|
||||
|
||||
## 2.66.1
|
||||
|
||||
Released 2016-07-20
|
||||
|
||||
Announcements:
|
||||
- Upgrades turbo-carto to [0.15.0](https://github.com/CartoDB/turbo-carto/releases/tag/0.15.0).
|
||||
|
||||
|
||||
## 2.66.0
|
||||
|
||||
Released 2016-07-18
|
||||
|
||||
Announcements:
|
||||
- Available new endpoint to check user analyses.
|
||||
- Upgrades camshaft to [0.37.1](https://github.com/CartoDB/camshaft/releases/tag/0.37.1).
|
||||
|
||||
|
||||
## 2.65.0
|
||||
|
||||
Released 2016-07-15
|
||||
|
||||
Announcements:
|
||||
- Upgrades cartodb-redis to 0.13.1.
|
||||
- Upgrades camshaft to [0.37.0](https://github.com/CartoDB/camshaft/releases/tag/0.37.0).
|
||||
|
||||
|
||||
## 2.64.0
|
||||
|
||||
Released 2016-07-12
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.36.0](https://github.com/CartoDB/camshaft/releases/tag/0.36.0).
|
||||
|
||||
|
||||
## 2.63.0
|
||||
|
||||
Released 2016-07-11
|
||||
|
||||
Enhancements:
|
||||
- Return last error message for failed nodes on map creation.
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.35.0](https://github.com/CartoDB/camshaft/releases/tag/0.35.0).
|
||||
- Upgrades lzma to 2.3.2.
|
||||
|
||||
|
||||
## 2.62.0
|
||||
|
||||
Released 2016-07-07
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.34.0](https://github.com/CartoDB/camshaft/releases/tag/0.34.0).
|
||||
|
||||
|
||||
## 2.61.2
|
||||
|
||||
Released 2016-07-07
|
||||
|
||||
Announcements:
|
||||
- Limit analysis creation concurrency.
|
||||
- Upgrades camshaft to [0.33.3](https://github.com/CartoDB/camshaft/releases/tag/0.33.3).
|
||||
|
||||
|
||||
## 2.61.1
|
||||
|
||||
Released 2016-07-06
|
||||
|
||||
Enhancements:
|
||||
- Dataviews use mapconfig to store/retrieve their queries instead of instantiating analyses again.
|
||||
|
||||
|
||||
## 2.61.0
|
||||
|
||||
Released 2016-07-06
|
||||
|
||||
Enhancements:
|
||||
- More clear turbo-carto error messages: no context in message.
|
||||
- Return multiple turbo-carto errors #541.
|
||||
|
||||
Announcements:
|
||||
- Upgrades turbo-carto to [0.14.0](https://github.com/CartoDB/turbo-carto/releases/tag/0.14.0).
|
||||
- Upgrades camshaft to [0.33.2](https://github.com/CartoDB/camshaft/releases/tag/0.33.2).
|
||||
|
||||
|
||||
## 2.60.0
|
||||
|
||||
Released 2016-07-05
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.32.0](https://github.com/CartoDB/camshaft/releases/tag/0.32.0).
|
||||
|
||||
|
||||
## 2.59.1
|
||||
|
||||
Released 2016-07-05
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.31.0](https://github.com/CartoDB/camshaft/releases/tag/0.31.0).
|
||||
|
||||
|
||||
## 2.59.0
|
||||
|
||||
Released 2016-07-05
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.30.0](https://github.com/CartoDB/camshaft/releases/tag/0.30.0).
|
||||
|
||||
|
||||
## 2.58.0
|
||||
|
||||
Released 2016-07-05
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.29.2](https://github.com/CartoDB/camshaft/releases/tag/0.29.2).
|
||||
|
||||
Bug fixes:
|
||||
- Return full list of nodes in response metadata.
|
||||
|
||||
|
||||
## 2.57.0
|
||||
|
||||
Released 2016-07-04
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.28.1](https://github.com/CartoDB/camshaft/releases/tag/0.28.1).
|
||||
|
||||
|
||||
## 2.56.0
|
||||
|
||||
Released 2016-07-04
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.27.0](https://github.com/CartoDB/camshaft/releases/tag/0.27.0).
|
||||
|
||||
|
||||
## 2.55.0
|
||||
|
||||
Released 2016-07-04
|
||||
|
||||
Enhancements:
|
||||
- Skip null values for quantification methods generating null values.
|
||||
|
||||
Announcements:
|
||||
- Uses new configuration for camshaft: analysis node has an associated user/owner.
|
||||
- Upgrades camshaft to [0.26.0](https://github.com/CartoDB/camshaft/releases/tag/0.26.0).
|
||||
|
||||
|
||||
## 2.54.0
|
||||
|
||||
Released 2016-06-30
|
||||
|
||||
Improvements:
|
||||
- Errors with context: replaced `turbo-carto` error type by `layer` type.
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.23.0](https://github.com/CartoDB/camshaft/releases/tag/0.23.0)
|
||||
|
||||
|
||||
## 2.53.5
|
||||
|
||||
Released 2016-06-29
|
||||
|
||||
Bug fixes:
|
||||
- Uses node list so identical nodes are not de-duplicated and can be used with different ids #528.
|
||||
|
||||
|
||||
## 2.53.4
|
||||
|
||||
Released 2016-06-28
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.22.4](https://github.com/CartoDB/camshaft/releases/tag/0.22.4)
|
||||
|
||||
|
||||
## 2.53.3
|
||||
|
||||
Released 2016-06-28
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.22.3](https://github.com/CartoDB/camshaft/releases/tag/0.22.3)
|
||||
|
||||
|
||||
## 2.53.2
|
||||
|
||||
Released 2016-06-28
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.22.2](https://github.com/CartoDB/camshaft/releases/tag/0.22.2)
|
||||
|
||||
|
||||
## 2.53.1
|
||||
|
||||
Released 2016-06-28
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.22.1](https://github.com/CartoDB/camshaft/releases/tag/0.22.1)
|
||||
|
||||
|
||||
## 2.53.0
|
||||
|
||||
Released 2016-06-24
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.22.0](https://github.com/CartoDB/camshaft/releases/tag/0.22.0)
|
||||
|
||||
|
||||
## 2.52.0
|
||||
|
||||
Released 2016-06-23
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.21.0](https://github.com/CartoDB/camshaft/releases/tag/0.21.0)
|
||||
|
||||
|
||||
## 2.51.0
|
||||
|
||||
Released 2016-06-21
|
||||
|
||||
Enhancements:
|
||||
- Split turbo-carto adapter substitutions tokens query.
|
||||
- Now errors with context have the same schema. #519
|
||||
- Responses with error now return the layer-id to give more info to the user.
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.20.0](https://github.com/CartoDB/camshaft/releases/tag/0.20.0)
|
||||
|
||||
|
||||
## 2.50.0
|
||||
|
||||
Released 2016-06-21
|
||||
|
||||
Bug fixes:
|
||||
- Pixel size query for turbo-carto adapter using radians and degrees instead of meters.
|
||||
|
||||
New features:
|
||||
- Add support for min, max, and avg operations in aggregation dataview #513.
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.19.0](https://github.com/CartoDB/camshaft/releases/tag/0.19.0)
|
||||
|
||||
|
||||
## 2.49.1
|
||||
|
||||
Released 2016-06-20
|
||||
|
||||
Announcements:
|
||||
- Upgrades turbo-carto to [0.12.1](https://github.com/CartoDB/turbo-carto/releases/tag/0.12.1).
|
||||
|
||||
Bug fixes:
|
||||
- Use an empty array as default value for falsy ramps #512.
|
||||
- Use the_geom for intermediate dataviews #511.
|
||||
- Pick last update time for layergroupid from analyses results #510.
|
||||
|
||||
|
||||
## 2.49.0
|
||||
|
||||
Released 2016-06-15
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.17.1](https://github.com/CartoDB/camshaft/releases/tag/0.17.1)
|
||||
|
||||
|
||||
## 2.48.0
|
||||
|
||||
Released 2016-06-14
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.15.1](https://github.com/CartoDB/camshaft/releases/tag/0.15.1)
|
||||
- Responses with more context info if analysis or turbo-carto fails during map creation.
|
||||
|
||||
## 2.47.1
|
||||
|
||||
Released 2016-06-13
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.14.1](https://github.com/CartoDB/camshaft/releases/tag/0.14.1)
|
||||
|
||||
|
||||
## 2.47.0
|
||||
|
||||
Released 2016-06-10
|
||||
|
||||
Announcements:
|
||||
- Upgrades camshaft to [0.14.0](https://github.com/CartoDB/camshaft/releases/tag/0.14.0)
|
||||
|
||||
|
||||
## 2.46.0
|
||||
|
||||
Released 2016-06-09
|
||||
|
||||
Improvements:
|
||||
- Support for substitution tokens in geojson tiles
|
||||
- Warn on application start about non-matching dependencies
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to [2.3.0](https://github.com/CartoDB/camshaft/releases/tag/2.3.0)
|
||||
- Upgrades camshaft to [0.13.0](https://github.com/CartoDB/camshaft/releases/tag/0.13.0)
|
||||
- Upgrades turbo-carto to [0.11.0](https://github.com/CartoDB/turbo-carto/releases/tag/0.11.0)
|
||||
|
||||
Bug fixes:
|
||||
- Column provided for geojson renderer should not be null #476
|
||||
- Dataviews/widgets adapter working with non sql, non source, and non widgets layers
|
||||
|
||||
|
||||
## 2.45.0
|
||||
|
||||
Released 2016-06-02
|
||||
|
||||
12
README.md
12
README.md
@@ -32,14 +32,14 @@ Upgrading
|
||||
Checkout your commit/branch. If you need to reinstall dependencies (you can check [NEWS](NEWS.md)) do the following:
|
||||
|
||||
```
|
||||
rm -rf node_modules; npm install
|
||||
rm -rf node_modules; yarn
|
||||
```
|
||||
|
||||
Run
|
||||
---
|
||||
|
||||
```
|
||||
node app.js <env>
|
||||
node app.js <env>
|
||||
```
|
||||
|
||||
Where <env> is the name of a configuration file under config/environments/.
|
||||
@@ -71,12 +71,12 @@ See [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
### Developing with a custom windshaft version
|
||||
|
||||
If you plan or want to use a custom / not released yet version of windshaft (or any other dependency) the best option is
|
||||
to use `npm link`. You can read more about it at [npm-link: Symlink a package folder](https://docs.npmjs.com/cli/link).
|
||||
to use `yarn link`. You can read more about it at [yarn-link: Symlink a package folder](https://yarnpkg.com/en/docs/cli/link).
|
||||
|
||||
**Quick start**:
|
||||
|
||||
```shell
|
||||
~/windshaft-directory $ npm install
|
||||
~/windshaft-directory $ npm link
|
||||
~/windshaft-cartodb-directory $ npm link windshaft
|
||||
~/windshaft-directory $ yarn
|
||||
~/windshaft-directory $ yarn link
|
||||
~/windshaft-cartodb-directory $ yarn link windshaft
|
||||
```
|
||||
|
||||
97
app.js
97
app.js
@@ -5,20 +5,35 @@ var fs = require('fs');
|
||||
|
||||
var _ = require('underscore');
|
||||
|
||||
var ENVIRONMENT;
|
||||
if ( process.argv[2] ) {
|
||||
ENVIRONMENT = process.argv[2];
|
||||
} else if ( process.env.NODE_ENV ) {
|
||||
ENVIRONMENT = process.env.NODE_ENV;
|
||||
} else {
|
||||
ENVIRONMENT = 'development';
|
||||
}
|
||||
|
||||
// jshint undef:false
|
||||
var log = console.log.bind(console);
|
||||
var logError = console.error.bind(console);
|
||||
// jshint undef:true
|
||||
|
||||
var argv = require('yargs')
|
||||
.usage('Usage: $0 <environment> [options]')
|
||||
.help('h')
|
||||
.example(
|
||||
'$0 production -c /etc/sql-api/config.js',
|
||||
'start server in production environment with /etc/sql-api/config.js as config file'
|
||||
)
|
||||
.alias('h', 'help')
|
||||
.alias('c', 'config')
|
||||
.nargs('c', 1)
|
||||
.describe('c', 'Load configuration from path')
|
||||
.argv;
|
||||
|
||||
var environmentArg = argv._[0] || process.env.NODE_ENV || 'development';
|
||||
var configurationFile = path.resolve(argv.config || './config/environments/' + environmentArg + '.js');
|
||||
if (!fs.existsSync(configurationFile)) {
|
||||
logError('Configuration file "%s" does not exist', configurationFile);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
global.environment = require(configurationFile);
|
||||
var ENVIRONMENT = argv._[0] || process.env.NODE_ENV || global.environment.environment;
|
||||
process.env.NODE_ENV = ENVIRONMENT;
|
||||
|
||||
var availableEnvironments = {
|
||||
production: true,
|
||||
staging: true,
|
||||
@@ -33,16 +48,6 @@ if (!availableEnvironments[ENVIRONMENT]){
|
||||
}
|
||||
|
||||
process.env.NODE_ENV = ENVIRONMENT;
|
||||
|
||||
// set environment specific variables
|
||||
global.environment = require('./config/environments/' + ENVIRONMENT);
|
||||
|
||||
global.log4js = require('log4js');
|
||||
var log4js_config = {
|
||||
appenders: [],
|
||||
replaceConsole: true
|
||||
};
|
||||
|
||||
if (global.environment.uv_threadpool_size) {
|
||||
process.env.UV_THREADPOOL_SIZE = global.environment.uv_threadpool_size;
|
||||
}
|
||||
@@ -58,25 +63,31 @@ var agentOptions = _.defaults(global.environment.httpAgent || {}, {
|
||||
http.globalAgent = new http.Agent(agentOptions);
|
||||
https.globalAgent = new https.Agent(agentOptions);
|
||||
|
||||
|
||||
global.log4js = require('log4js');
|
||||
var log4jsConfig = {
|
||||
appenders: [],
|
||||
replaceConsole: true
|
||||
};
|
||||
|
||||
if ( global.environment.log_filename ) {
|
||||
var logdir = path.dirname(global.environment.log_filename);
|
||||
// See cwd inlog4js.configure call below
|
||||
logdir = path.resolve(__dirname, logdir);
|
||||
if ( ! fs.existsSync(logdir) ) {
|
||||
logError("Log filename directory does not exist: " + logdir);
|
||||
process.exit(1);
|
||||
}
|
||||
log("Logs will be written to " + global.environment.log_filename);
|
||||
log4js_config.appenders.push(
|
||||
{ type: "file", filename: global.environment.log_filename }
|
||||
);
|
||||
var logFilename = path.resolve(global.environment.log_filename);
|
||||
var logDirectory = path.dirname(logFilename);
|
||||
if (!fs.existsSync(logDirectory)) {
|
||||
logError("Log filename directory does not exist: " + logDirectory);
|
||||
process.exit(1);
|
||||
}
|
||||
log("Logs will be written to " + logFilename);
|
||||
log4jsConfig.appenders.push(
|
||||
{ type: "file", absolute: true, filename: logFilename }
|
||||
);
|
||||
} else {
|
||||
log4js_config.appenders.push(
|
||||
{ type: "console", layout: { type:'basic' } }
|
||||
);
|
||||
log4jsConfig.appenders.push(
|
||||
{ type: "console", layout: { type:'basic' } }
|
||||
);
|
||||
}
|
||||
|
||||
global.log4js.configure(log4js_config, { cwd: __dirname });
|
||||
global.log4js.configure(log4jsConfig);
|
||||
global.logger = global.log4js.getLogger();
|
||||
|
||||
global.environment.api_hostname = require('os').hostname().split('.')[0];
|
||||
@@ -99,6 +110,8 @@ var listener = server.listen(serverOptions.bind.port, serverOptions.bind.host, b
|
||||
var version = require("./package").version;
|
||||
|
||||
listener.on('listening', function() {
|
||||
log("Using Node.js %s", process.version);
|
||||
log('Using configuration file "%s"', configurationFile);
|
||||
log(
|
||||
"Windshaft tileserver %s started on %s:%s PID=%d (%s)",
|
||||
version, serverOptions.bind.host, serverOptions.bind.port, process.pid, ENVIRONMENT
|
||||
@@ -114,7 +127,7 @@ setInterval(function() {
|
||||
|
||||
process.on('SIGHUP', function() {
|
||||
global.log4js.clearAndShutdownAppenders(function() {
|
||||
global.log4js.configure(log4js_config);
|
||||
global.log4js.configure(log4jsConfig);
|
||||
global.logger = global.log4js.getLogger();
|
||||
log('Log files reloaded');
|
||||
});
|
||||
@@ -123,3 +136,17 @@ process.on('SIGHUP', function() {
|
||||
process.on('uncaughtException', function(err) {
|
||||
global.logger.error('Uncaught exception: ' + err.stack);
|
||||
});
|
||||
|
||||
if (global.gc) {
|
||||
var gcInterval = Number.isFinite(global.environment.gc_interval) ?
|
||||
global.environment.gc_interval :
|
||||
10000;
|
||||
|
||||
if (gcInterval > 0) {
|
||||
setInterval(function gcForcedCycle() {
|
||||
var start = Date.now();
|
||||
global.gc();
|
||||
global.statsClient.timing('windshaft.gc', Date.now() - start);
|
||||
}, gcInterval);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@ var config = {
|
||||
// Its default size is 4, but it can be changed at startup time (the absolute maximum is 128).
|
||||
// See http://docs.libuv.org/en/latest/threadpool.html
|
||||
,uv_threadpool_size: undefined
|
||||
// Time in milliseconds to force GC cycle.
|
||||
// Disable by using <=0 value.
|
||||
,gc_interval: 10000
|
||||
// Regular expression pattern to extract username
|
||||
// from hostname. Must have a single grabbing block.
|
||||
,user_from_host: '^(.*)\\.localhost'
|
||||
@@ -23,6 +26,21 @@ var config = {
|
||||
// "tiles/layergroup" is for compatibility with versions up to 1.6.x
|
||||
,base_url_detached: '(?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)'
|
||||
|
||||
// Resource URLs expose endpoints to request/retrieve metadata associated to Maps: dataviews, analysis node status.
|
||||
//
|
||||
// This URLs depend on how `base_url_detached` and `user_from_host` are configured: the application can be
|
||||
// configured to accept request with the {user} in the header host or in the request path.
|
||||
// It also might depend on the configured cdn_url via `serverMetadata.cdn_url`.
|
||||
//
|
||||
// This template allows to make the endpoints generation more flexible, the template exposes the following params:
|
||||
// 1. {{=it.cdn_url}}: will be used when `serverMetadata.cdn_url` exists.
|
||||
// 2. {{=it.user}}: will use the username as extraced from `user_from_host` or `base_url_detached`.
|
||||
// 3. {{=it.port}}: will use the `port` from this very same configuration file.
|
||||
,resources_url_templates: {
|
||||
http: 'http://{{=it.user}}.localhost.lan:{{=it.port}}/api/v1/map',
|
||||
https: 'http://localhost.lan:{{=it.port}}/user/{{=it.user}}/api/v1/map'
|
||||
}
|
||||
|
||||
// Maximum number of connections for one process
|
||||
// 128 is a good value with a limit of 1024 open file descriptors
|
||||
,maxConnections:128
|
||||
@@ -62,6 +80,7 @@ var config = {
|
||||
extent: "-180,-90,180,90",
|
||||
srid: 4326,
|
||||
*/
|
||||
// max number of rows to return when querying data, 0 means no limit
|
||||
row_limit: 65535,
|
||||
simplify_geometries: true,
|
||||
use_overviews: true, // use overviews to retrieve raster
|
||||
@@ -95,6 +114,10 @@ var config = {
|
||||
// Important: check the configuration of uv_threadpool_size to use suitable value
|
||||
poolSize: 8,
|
||||
|
||||
// Whether grainstore will use a child process or not to transform CartoCSS into Mapnik XML.
|
||||
// This will prevent blocking the main thread.
|
||||
useCartocssWorkers: false,
|
||||
|
||||
// Metatile is the number of tiles-per-side that are going
|
||||
// to be rendered at once. If all of them will be requested
|
||||
// we'd have saved time. If only one will be used, we'd have
|
||||
@@ -209,6 +232,18 @@ var config = {
|
||||
endpoint: 'http://127.0.0.1:8080/api/v2/sql/job',
|
||||
// the template to use for adding the host header in the batch api requests
|
||||
hostHeaderTemplate: '{{=it.username}}.localhost.lan'
|
||||
},
|
||||
logger: {
|
||||
// If filename is given logs comming from analysis client will be written
|
||||
// there, in append mode. Otherwise 'log_filename' is used. Otherwise stdout is used (default).
|
||||
// Log file will be re-opened on receiving the HUP signal
|
||||
filename: '/tmp/analysis.log'
|
||||
},
|
||||
// Define max execution time in ms for analyses or tags
|
||||
// If analysis or tag are not found in redis this values will be used as default.
|
||||
limits: {
|
||||
moran: { timeout: 120000, maxNumberOfRows: 1e5 },
|
||||
cpu2x: { timeout: 60000 }
|
||||
}
|
||||
}
|
||||
,millstone: {
|
||||
@@ -289,8 +324,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
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ var config = {
|
||||
// Its default size is 4, but it can be changed at startup time (the absolute maximum is 128).
|
||||
// See http://docs.libuv.org/en/latest/threadpool.html
|
||||
,uv_threadpool_size: undefined
|
||||
// Time in milliseconds to force GC cycle.
|
||||
// Disable by using <=0 value.
|
||||
,gc_interval: 10000
|
||||
// Regular expression pattern to extract username
|
||||
// from hostname. Must have a single grabbing block.
|
||||
,user_from_host: '^(.*)\\.cartodb\\.com$'
|
||||
@@ -23,6 +26,21 @@ var config = {
|
||||
// "tiles/layergroup" is for compatibility with versions up to 1.6.x
|
||||
,base_url_detached: '(?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)'
|
||||
|
||||
// Resource URLs expose endpoints to request/retrieve metadata associated to Maps: dataviews, analysis node status.
|
||||
//
|
||||
// This URLs depend on how `base_url_detached` and `user_from_host` are configured: the application can be
|
||||
// configured to accept request with the {user} in the header host or in the request path.
|
||||
// It also might depend on the configured cdn_url via `serverMetadata.cdn_url`.
|
||||
//
|
||||
// This template allows to make the endpoints generation more flexible, the template exposes the following params:
|
||||
// 1. {{=it.cdn_url}}: will be used when `serverMetadata.cdn_url` exists.
|
||||
// 2. {{=it.user}}: will use the username as extraced from `user_from_host` or `base_url_detached`.
|
||||
// 3. {{=it.port}}: will use the `port` from this very same configuration file.
|
||||
,resources_url_templates: {
|
||||
http: 'http://{{=it.cdn_url}}/{{=it.user}}/api/v1/map',
|
||||
https: 'https://{{=it.cdn_url}}/{{=it.user}}/api/v1/map'
|
||||
}
|
||||
|
||||
// Maximum number of connections for one process
|
||||
// 128 is a good value with a limit of 1024 open file descriptors
|
||||
,maxConnections:128
|
||||
@@ -56,6 +74,7 @@ var config = {
|
||||
host: '127.0.0.1',
|
||||
port: 6432,
|
||||
extent: "-20037508.3,-20037508.3,20037508.3,20037508.3",
|
||||
// max number of rows to return when querying data, 0 means no limit
|
||||
row_limit: 65535,
|
||||
/*
|
||||
* Set persist_connection to false if you want
|
||||
@@ -89,6 +108,10 @@ var config = {
|
||||
// Important: check the configuration of uv_threadpool_size to use suitable value
|
||||
poolSize: 8,
|
||||
|
||||
// Whether grainstore will use a child process or not to transform CartoCSS into Mapnik XML.
|
||||
// This will prevent blocking the main thread.
|
||||
useCartocssWorkers: false,
|
||||
|
||||
// Metatile is the number of tiles-per-side that are going
|
||||
// to be rendered at once. If all of them will be requested
|
||||
// we'd have saved time. If only one will be used, we'd have
|
||||
@@ -203,6 +226,18 @@ var config = {
|
||||
endpoint: 'http://127.0.0.1:8080/api/v2/sql/job',
|
||||
// the template to use for adding the host header in the batch api requests
|
||||
hostHeaderTemplate: '{{=it.username}}.localhost.lan'
|
||||
},
|
||||
logger: {
|
||||
// If filename is given logs comming from analysis client will be written
|
||||
// there, in append mode. Otherwise 'log_filename' is used. Otherwise stdout is used (default).
|
||||
// Log file will be re-opened on receiving the HUP signal
|
||||
filename: 'logs/analysis.log'
|
||||
},
|
||||
// Define max execution time in ms for analyses or tags
|
||||
// If analysis or tag are not found in redis this values will be used as default.
|
||||
limits: {
|
||||
moran: { timeout: 120000, maxNumberOfRows: 1e5 },
|
||||
cpu2x: { timeout: 60000 }
|
||||
}
|
||||
}
|
||||
,millstone: {
|
||||
@@ -289,7 +324,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
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ var config = {
|
||||
// Its default size is 4, but it can be changed at startup time (the absolute maximum is 128).
|
||||
// See http://docs.libuv.org/en/latest/threadpool.html
|
||||
,uv_threadpool_size: undefined
|
||||
// Time in milliseconds to force GC cycle.
|
||||
// Disable by using <=0 value.
|
||||
,gc_interval: 10000
|
||||
// Regular expression pattern to extract username
|
||||
// from hostname. Must have a single grabbing block.
|
||||
,user_from_host: '^(.*)\\.cartodb\\.com$'
|
||||
@@ -23,6 +26,21 @@ var config = {
|
||||
// "/tiles/layergroup" is for compatibility with versions up to 1.6.x
|
||||
,base_url_detached: '(?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)'
|
||||
|
||||
// Resource URLs expose endpoints to request/retrieve metadata associated to Maps: dataviews, analysis node status.
|
||||
//
|
||||
// This URLs depend on how `base_url_detached` and `user_from_host` are configured: the application can be
|
||||
// configured to accept request with the {user} in the header host or in the request path.
|
||||
// It also might depend on the configured cdn_url via `serverMetadata.cdn_url`.
|
||||
//
|
||||
// This template allows to make the endpoints generation more flexible, the template exposes the following params:
|
||||
// 1. {{=it.cdn_url}}: will be used when `serverMetadata.cdn_url` exists.
|
||||
// 2. {{=it.user}}: will use the username as extraced from `user_from_host` or `base_url_detached`.
|
||||
// 3. {{=it.port}}: will use the `port` from this very same configuration file.
|
||||
,resources_url_templates: {
|
||||
http: 'http://{{=it.user}}.localhost.lan:{{=it.port}}/api/v1/map',
|
||||
https: 'https://{{=it.cdn_url}}/{{=it.user}}/api/v1/map'
|
||||
}
|
||||
|
||||
// Maximum number of connections for one process
|
||||
// 128 is a good value with a limit of 1024 open file descriptors
|
||||
,maxConnections:128
|
||||
@@ -56,6 +74,7 @@ var config = {
|
||||
host: '127.0.0.1',
|
||||
port: 6432,
|
||||
extent: "-20037508.3,-20037508.3,20037508.3,20037508.3",
|
||||
// max number of rows to return when querying data, 0 means no limit
|
||||
row_limit: 65535,
|
||||
simplify_geometries: true,
|
||||
use_overviews: true, // use overviews to retrieve raster
|
||||
@@ -89,6 +108,10 @@ var config = {
|
||||
// Important: check the configuration of uv_threadpool_size to use suitable value
|
||||
poolSize: 8,
|
||||
|
||||
// Whether grainstore will use a child process or not to transform CartoCSS into Mapnik XML.
|
||||
// This will prevent blocking the main thread.
|
||||
useCartocssWorkers: false,
|
||||
|
||||
// Metatile is the number of tiles-per-side that are going
|
||||
// to be rendered at once. If all of them will be requested
|
||||
// we'd have saved time. If only one will be used, we'd have
|
||||
@@ -203,6 +226,18 @@ var config = {
|
||||
endpoint: 'http://127.0.0.1:8080/api/v2/sql/job',
|
||||
// the template to use for adding the host header in the batch api requests
|
||||
hostHeaderTemplate: '{{=it.username}}.localhost.lan'
|
||||
},
|
||||
logger: {
|
||||
// If filename is given logs comming from analysis client will be written
|
||||
// there, in append mode. Otherwise 'log_filename' is used. Otherwise stdout is used (default).
|
||||
// Log file will be re-opened on receiving the HUP signal
|
||||
filename: 'logs/analysis.log'
|
||||
},
|
||||
// Define max execution time in ms for analyses or tags
|
||||
// If analysis or tag are not found in redis this values will be used as default.
|
||||
limits: {
|
||||
moran: { timeout: 120000, maxNumberOfRows: 1e5 },
|
||||
cpu2x: { timeout: 60000 }
|
||||
}
|
||||
}
|
||||
,millstone: {
|
||||
@@ -289,7 +324,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
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ var config = {
|
||||
// Its default size is 4, but it can be changed at startup time (the absolute maximum is 128).
|
||||
// See http://docs.libuv.org/en/latest/threadpool.html
|
||||
,uv_threadpool_size: undefined
|
||||
// Time in milliseconds to force GC cycle.
|
||||
// Disable by using <=0 value.
|
||||
,gc_interval: 10000
|
||||
// Regular expression pattern to extract username
|
||||
// from hostname. Must have a single grabbing block.
|
||||
,user_from_host: '(.*)'
|
||||
@@ -23,6 +26,20 @@ var config = {
|
||||
// "tiles/layergroup" is for compatibility with versions up to 1.6.x
|
||||
,base_url_detached: '(?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)'
|
||||
|
||||
// Resource URLs expose endpoints to request/retrieve metadata associated to Maps: dataviews, analysis node status.
|
||||
//
|
||||
// This URLs depend on how `base_url_detached` and `user_from_host` are configured: the application can be
|
||||
// configured to accept request with the {user} in the header host or in the request path.
|
||||
// It also might depend on the configured cdn_url via `serverMetadata.cdn_url`.
|
||||
//
|
||||
// This template allows to make the endpoints generation more flexible, the template exposes the following params:
|
||||
// 1. {{=it.cdn_url}}: will be used when `serverMetadata.cdn_url` exists.
|
||||
// 2. {{=it.user}}: will use the username as extraced from `user_from_host` or `base_url_detached`.
|
||||
// 3. {{=it.port}}: will use the `port` from this very same configuration file.
|
||||
,resources_url_templates: {
|
||||
http: 'http://{{=it.user}}.localhost.lan:{{=it.port}}/api/v1/map'
|
||||
}
|
||||
|
||||
// Maximum number of connections for one process
|
||||
// 128 is a good value with a limit of 1024 open file descriptors
|
||||
,maxConnections:128
|
||||
@@ -56,6 +73,7 @@ var config = {
|
||||
host: '127.0.0.1',
|
||||
port: 5432,
|
||||
extent: "-20037508.3,-20037508.3,20037508.3,20037508.3",
|
||||
// max number of rows to return when querying data, 0 means no limit
|
||||
row_limit: 65535,
|
||||
simplify_geometries: true,
|
||||
use_overviews: true, // use overviews to retrieve raster
|
||||
@@ -89,6 +107,10 @@ var config = {
|
||||
// Important: check the configuration of uv_threadpool_size to use suitable value
|
||||
poolSize: 8,
|
||||
|
||||
// Whether grainstore will use a child process or not to transform CartoCSS into Mapnik XML.
|
||||
// This will prevent blocking the main thread.
|
||||
useCartocssWorkers: false,
|
||||
|
||||
// Metatile is the number of tiles-per-side that are going
|
||||
// to be rendered at once. If all of them will be requested
|
||||
// we'd have saved time. If only one will be used, we'd have
|
||||
@@ -204,6 +226,18 @@ var config = {
|
||||
endpoint: 'http://127.0.0.1:8080/api/v2/sql/job',
|
||||
// the template to use for adding the host header in the batch api requests
|
||||
hostHeaderTemplate: '{{=it.username}}.localhost.lan'
|
||||
},
|
||||
logger: {
|
||||
// If filename is given logs comming from analysis client will be written
|
||||
// there, in append mode. Otherwise 'log_filename' is used. Otherwise stdout is used (default).
|
||||
// Log file will be re-opened on receiving the HUP signal
|
||||
filename: 'node-windshaft.log'
|
||||
},
|
||||
// Define max execution time in ms for analyses or tags
|
||||
// If analysis or tag are not found in redis this values will be used as default.
|
||||
limits: {
|
||||
moran: { timeout: 120000, maxNumberOfRows: 1e5 },
|
||||
cpu2x: { timeout: 60000 }
|
||||
}
|
||||
}
|
||||
,millstone: {
|
||||
@@ -284,7 +318,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
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# Maps API
|
||||
|
||||
The CartoDB Maps API allows you to generate maps based on data hosted in your CartoDB account and apply custom SQL and CartoCSS to the data. The API generates a XYZ-based URL to fetch Web Mercator projected tiles, using web clients such as [Leaflet](http://leafletjs.com), [Google Maps](https://developers.google.com/maps/), or [OpenLayers](http://openlayers.org/).
|
||||
The CARTO Maps API allows you to generate maps based on data hosted in your CARTO account and apply custom SQL and CartoCSS to the data. The API generates a XYZ-based URL to fetch Web Mercator projected tiles, using web clients such as [Leaflet](http://leafletjs.com), [Google Maps](https://developers.google.com/maps/), or [OpenLayers](http://openlayers.org/).
|
||||
|
||||
You can create two types of maps with the Maps API:
|
||||
|
||||
- **Anonymous Maps**
|
||||
You can create maps using your CartoDB public data. Any client can change the read-only SQL and CartoCSS parameters that generate the map tiles. These maps can be created from a JavaScript application alone and no authenticated calls are needed. See [this CartoDB.js example](/cartodb-platform/cartodb-js/getting-started/).
|
||||
You can create maps using your CARTO public data. Any client can change the read-only SQL and CartoCSS parameters that generate the map tiles. These maps can be created from a JavaScript application alone and no authenticated calls are needed. See [this CARTO.js example](/carto-engine/carto-js/getting-started/).
|
||||
|
||||
- **Named Maps**
|
||||
There are also maps that have access to your private data. These maps require an owner to setup and modify any SQL and CartoCSS parameters and are not modifiable without new setup calls.
|
||||
|
||||
@@ -28,7 +28,7 @@ POST /api/v1/map
|
||||
}
|
||||
```
|
||||
|
||||
See [MapConfig File Formats](http://docs.cartodb.com/cartodb-platform/maps-api/mapconfig/) for details.
|
||||
See [MapConfig File Formats](http://docs.carto.com/carto-engine/maps-api/mapconfig/) for details.
|
||||
|
||||
#### Response
|
||||
|
||||
@@ -36,7 +36,7 @@ The response includes:
|
||||
|
||||
Attributes | Description
|
||||
--- | ---
|
||||
layergroupid | The ID for that map, used to compose the URL for the tiles. The final URL is: `https://{username}.cartodb.com/api/v1/map/{layergroupid}/{z}/{x}/{y}.png`
|
||||
layergroupid | The ID for that map, used to compose the URL for the tiles. The final URL is: `https://{username}.carto.com/api/v1/map/{layergroupid}/{z}/{x}/{y}.png`
|
||||
updated_at | The ISO date of the last time the data involved in the query was updated.
|
||||
metadata | Includes information about the layers.
|
||||
cdn_url | URLs to fetch the data using the best CDN for your zone.
|
||||
@@ -46,7 +46,7 @@ cdn_url | URLs to fetch the data using the best CDN for your zone.
|
||||
#### Call
|
||||
|
||||
```bash
|
||||
curl 'https://{username}.cartodb.com/api/v1/map' -H 'Content-Type: application/json' -d @mapconfig.json
|
||||
curl 'https://{username}.carto.com/api/v1/map' -H 'Content-Type: application/json' -d @mapconfig.json
|
||||
```
|
||||
|
||||
#### Response
|
||||
@@ -79,7 +79,7 @@ When you have a layergroup, there are several resources for retrieving layergoup
|
||||
These tiles will get just the Mapnik layers. To get individual layers, see the following section.
|
||||
|
||||
```bash
|
||||
https://{username}.cartodb.com/api/v1/map/{layergroupid}/{z}/{x}/{y}.png
|
||||
https://{username}.carto.com/api/v1/map/{layergroupid}/{z}/{x}/{y}.png
|
||||
```
|
||||
|
||||
#### Individual layers
|
||||
@@ -89,7 +89,7 @@ The MapConfig specification holds the layers definition in a 0-based index. Laye
|
||||
Individual layers can be accessed using that 0-based index. For UTF grid tiles:
|
||||
|
||||
```bash
|
||||
https://{username}.cartodb.com/api/v1/map/{layergroupid}/{layer}/{z}/{x}/{y}.grid.json
|
||||
https://{username}.carto.com/api/v1/map/{layergroupid}/{layer}/{z}/{x}/{y}.grid.json
|
||||
```
|
||||
|
||||
In this case, `layer` as 0 returns the UTF grid tiles/attributes for layer 0, the only layer in the example MapConfig.
|
||||
@@ -97,13 +97,13 @@ In this case, `layer` as 0 returns the UTF grid tiles/attributes for layer 0, th
|
||||
If the MapConfig had a Torque layer at index 1 it could be possible to request it with:
|
||||
|
||||
```bash
|
||||
https://{username}.cartodb.com/api/v1/map/{layergroupid}/1/{z}/{x}/{y}.torque.json
|
||||
https://{username}.carto.com/api/v1/map/{layergroupid}/1/{z}/{x}/{y}.torque.json
|
||||
```
|
||||
|
||||
#### Attributes defined in `attributes` section
|
||||
|
||||
```bash
|
||||
https://{username}.cartodb.com/api/v1/map/{layergroupid}/{layer}/attributes/{feature_id}
|
||||
https://{username}.carto.com/api/v1/map/{layergroupid}/{layer}/attributes/{feature_id}
|
||||
```
|
||||
|
||||
Which returns JSON with the attributes defined, like:
|
||||
@@ -115,7 +115,7 @@ Which returns JSON with the attributes defined, like:
|
||||
#### Blending and layer selection
|
||||
|
||||
```bash
|
||||
https://{username}.cartodb.com/api/v1/map/{layergroupid}/{layer_filter}/{z}/{x}/{y}.png
|
||||
https://{username}.carto.com/api/v1/map/{layergroupid}/{layer_filter}/{z}/{x}/{y}.png
|
||||
```
|
||||
|
||||
Note: currently format is limited to `png`.
|
||||
@@ -127,7 +127,7 @@ Note: currently format is limited to `png`.
|
||||
Using `all` as `layer_filter` will blend all layers in the layergroup
|
||||
|
||||
```bash
|
||||
https://{username}.cartodb.com/api/v1/map/{layergroupid}/all/{z}/{x}/{y}.png
|
||||
https://{username}.carto.com/api/v1/map/{layergroupid}/all/{z}/{x}/{y}.png
|
||||
```
|
||||
|
||||
- Filter by layer index
|
||||
@@ -135,15 +135,12 @@ https://{username}.cartodb.com/api/v1/map/{layergroupid}/all/{z}/{x}/{y}.png
|
||||
A list of comma separated layer indexes can be used to just render a subset of layers. For example `0,3,4` will filter and blend layers with indexes 0, 3, and 4.
|
||||
|
||||
```bash
|
||||
https://{username}.cartodb.com/api/v1/map/{layergroupid}/0,3,4/{z}/{x}/{y}.png
|
||||
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.
|
||||
- Once a Mapnik layer is selected, all Mapnik layers will get blended. As this may change in the future **it is
|
||||
recommended** to always select all Mapnik layers if you want to select at least one 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 you will get a
|
||||
consistent behavior in the future.
|
||||
@@ -172,7 +169,7 @@ callback | JSON callback name.
|
||||
#### Call
|
||||
|
||||
```bash
|
||||
curl "https://{username}.cartodb.com/api/v1/map?callback=callback&config=%7B%22version%22%3A%221.0.1%22%2C%22layers%22%3A%5B%7B%22type%22%3A%22cartodb%22%2C%22options%22%3A%7B%22sql%22%3A%22select+%2A+from+european_countries_e%22%2C%22cartocss%22%3A%22%23european_countries_e%7B+polygon-fill%3A+%23FF6600%3B+%7D%22%2C%22cartocss_version%22%3A%222.3.0%22%2C%22interactivity%22%3A%5B%22cartodb_id%22%5D%7D%7D%5D%7D"
|
||||
curl "https://{username}.carto.com/api/v1/map?callback=callback&config=%7B%22version%22%3A%221.0.1%22%2C%22layers%22%3A%5B%7B%22type%22%3A%22cartodb%22%2C%22options%22%3A%7B%22sql%22%3A%22select+%2A+from+european_countries_e%22%2C%22cartocss%22%3A%22%23european_countries_e%7B+polygon-fill%3A+%23FF6600%3B+%7D%22%2C%22cartocss_version%22%3A%222.3.0%22%2C%22interactivity%22%3A%5B%22cartodb_id%22%5D%7D%7D%5D%7D"
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
@@ -4,7 +4,7 @@ The following concepts are the same for every endpoint in the API except when it
|
||||
|
||||
## Auth
|
||||
|
||||
By default, users do not have access to private tables in CartoDB. In order to instantiate a map from private table data an API Key is required. Additionally, to include some endpoints, an API Key must be included (e.g. creating a Named Map).
|
||||
By default, users do not have access to private tables in CARTO. In order to instantiate a map from private table data an API Key is required. Additionally, to include some endpoints, an API Key must be included (e.g. creating a Named Map).
|
||||
|
||||
To execute an authorized request, `api_key=YOURAPIKEY` should be added to the request URL. The param can be also passed as POST param. Using HTTPS is mandatory when you are performing requests that include your `api_key`.
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
# Named Maps
|
||||
|
||||
Named Maps are essentially the same as Anonymous Maps except the MapConfig is stored on the server, and the map is given a unique name. You can create Named Maps from private data, and users without an API Key can view your Named Map (while keeping your data private).
|
||||
Named Maps are essentially the same as Anonymous Maps except the MapConfig is stored on the server, and the map is given a unique name. You can create Named Maps from private data, and users without an API Key can view your Named Map (while keeping your data private).
|
||||
|
||||
The Named Map workflow consists of uploading a MapConfig file to CartoDB servers, to select data from your CartoDB user database by using SQL, and specifying the CartoCSS for your map.
|
||||
The Named Map workflow consists of uploading a MapConfig file to CARTO servers, to select data from your CARTO user database by using SQL, and specifying the CartoCSS for your map.
|
||||
|
||||
The response back from the API provides the template_id of your Named Map as the `name` (the identifier of your Named Map), which is the name that you specified in the MapConfig. You can which you can then use to create your Named Map details, or [fetch XYZ tiles](#fetching-xyz-tiles-for-named-maps) directly for Named Maps.
|
||||
The response back from the API provides the template_id of your Named Map as the `name` (the identifier of your Named Map), which is the name that you specified in the MapConfig. You can which you can then use to create your Named Map details, or [fetch XYZ tiles](#fetching-xyz-tiles-for-named-maps) directly for Named Maps.
|
||||
|
||||
**Tip:** You can also use a Named Map that you created (which is defined by its `name`), to create a map using CartoDB.js. This is achieved by adding the [`namedmap` type](http://docs.cartodb.com/cartodb-platform/cartodb-js/layer-source-object/#named-maps-layer-source-object-type-namedmap) layer source object to draw the Named Map.
|
||||
**Tip:** You can also use a Named Map that you created (which is defined by its `name`), to create a map using CARTO.js. This is achieved by adding the [`namedmap` type](http://docs.carto.com/carto-engine/carto-js/layer-source-object/#named-maps-layer-source-object-type-namedmap) layer source object to draw the Named Map.
|
||||
|
||||
The main differences, compared to Anonymous Maps, is that Named Maps include:
|
||||
|
||||
@@ -14,11 +14,11 @@ The main differences, compared to Anonymous Maps, is that Named Maps include:
|
||||
This allows you to control who is able to see the map based on an auth token, and create a secure Named Map with password-protection.
|
||||
|
||||
- **template map**
|
||||
The template map is static and may contain placeholders, enabling you to modify your maps appearance by using variables. Templates maps are persistent with no preset expiration. They can only be created, or deleted, by a CartoDB user with a valid API KEY (See [auth argument](#arguments)).
|
||||
The template map is static and may contain placeholders, enabling you to modify your maps appearance by using variables. Templates maps are persistent with no preset expiration. They can only be created, or deleted, by a CARTO user with a valid API KEY (See [auth argument](#arguments)).
|
||||
|
||||
Uploading a MapConfig creates a Named Map. MapConfigs are uploaded to the server by sending the server a "template".json file, which contain the [MapConfig specifications](http://docs.cartodb.com/cartodb-platform/maps-api/mapconfig/).
|
||||
Uploading a MapConfig creates a Named Map. MapConfigs are uploaded to the server by sending the server a "template".json file, which contain the [MapConfig specifications](http://docs.carto.com/carto-engine/maps-api/mapconfig/).
|
||||
|
||||
**Note:** There is a limit of 4,096 Named Maps allowed per account. If you need to create more Named Maps, it is recommended to use a single Named Map and change the variables using [placeholders](#placeholder-format), instead of uploading multiple [Named Map MapConfigs](http://docs.cartodb.com/cartodb-platform/maps-api/mapconfig/#named-map-layer-options).
|
||||
**Note:** There is a limit of 4,096 Named Maps allowed per account. If you need to create more Named Maps, it is recommended to use a single Named Map and change the variables using [placeholders](#placeholder-format), instead of uploading multiple [Named Map MapConfigs](http://docs.carto.com/carto-engine/maps-api/mapconfig/#named-map-layer-options).
|
||||
|
||||
## Create
|
||||
|
||||
@@ -33,7 +33,7 @@ POST /api/v1/map/named
|
||||
Params | Description
|
||||
--- | ---
|
||||
api_key | is required
|
||||
MapConfig | a [Named Map MapConfig](http://docs.cartodb.com/cartodb-platform/maps-api/mapconfig/#named-map-layer-options) is required to create a Named Map
|
||||
MapConfig | a [Named Map MapConfig](http://docs.carto.com/carto-engine/maps-api/mapconfig/#named-map-layer-options) is required to create a Named Map
|
||||
|
||||
#### template.json
|
||||
|
||||
@@ -84,6 +84,10 @@ The `name` argument defines how to name this "template_name".json. Note that the
|
||||
"south": -45,
|
||||
"east": 45,
|
||||
"north": 45
|
||||
},
|
||||
"preview_layers": {
|
||||
"0": true,
|
||||
"layer1": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,22 +99,22 @@ Params | Description
|
||||
--- | ---
|
||||
name | There can only be _one_ template with the same name for any user. Valid names start with a letter or a number, and only contain letters, numbers, dashes (-), or underscores (_). _This is specific to the name of your Named Map that is specified in the `name` property of the template file_.
|
||||
|
||||
auth |
|
||||
auth |
|
||||
--- | ---
|
||||
|_ method | `"token"` or `"open"` (`"open"` is the default if no method is specified. Use `"token"` to password-protect your map)
|
||||
|_ valid_tokens | when `"method"` is set to `"token"`, the values listed here allow you to instantiate the Named Map. See this [example](http://docs.cartodb.com/faqs/manipulating-your-data/#how-to-create-a-password-protected-named-map) for how to create a password-protected map.
|
||||
|_ valid_tokens | when `"method"` is set to `"token"`, the values listed here allow you to instantiate the Named Map. See this [example](http://docs.carto.com/faqs/manipulating-your-data/#how-to-create-a-password-protected-named-map) for how to create a password-protected map.
|
||||
placeholders | Placeholders are variables that can be placed in your template.json file's SQL or CartoCSS.
|
||||
layergroup | the layergroup configurations, as specified in the template. See [MapConfig File Format](http://docs.cartodb.com/cartodb-platform/maps-api/mapconfig/) for more information.
|
||||
view (optional) | extra keys to specify the view area for the map. It can be used to have a static preview of a Named Map without having to instantiate it. It is possible to specify it with `center` + `zoom` or with a bounding box `bbox`. Center+zoom takes precedence over bounding box.
|
||||
layergroup | the layergroup configurations, as specified in the template. See [MapConfig File Format](http://docs.carto.com/carto-engine/maps-api/mapconfig/) for more information.
|
||||
view (optional) | extra keys to specify the view area for the map. It can be used to have a static preview of a Named Map without having to instantiate it. It is possible to specify it with `center` + `zoom` or with a bounding box `bbox`. Center+zoom takes precedence over bounding box. Also it is possible to choose which layers are visible or not with `preview_layers` indicating its visibility by layer index or id (visible by default).
|
||||
--- | ---
|
||||
|_ zoom | The zoom level to use
|
||||
|
||||
|_ center |
|
||||
|_ center |
|
||||
--- | ---
|
||||
|_ |_ lng | The longitude to use for the center
|
||||
|_ |_ lat | The latitude to use for the center
|
||||
|
||||
|_ bounds |
|
||||
|_ bounds |
|
||||
--- | ---
|
||||
|_ |_ west | LowerCorner longitude for the bounding box, in decimal degrees (aka most western)
|
||||
|_ |_ south | LowerCorner latitude for the bounding box, in decimal degrees (aka most southern)
|
||||
@@ -120,7 +124,7 @@ view (optional) | extra keys to specify the view area for the map. It can be use
|
||||
|
||||
### Placeholder Format
|
||||
|
||||
Placeholders are variables that can be placed in your template.json file. Placeholders need to be defined with a `type` and a default value for MapConfigs. See details about defining a MapConfig `type` for [Layergoup configurations](http://docs.cartodb.com/cartodb-platform/maps-api/mapconfig/#layergroup-configurations).
|
||||
Placeholders are variables that can be placed in your template.json file. Placeholders need to be defined with a `type` and a default value for MapConfigs. See details about defining a MapConfig `type` for [Layergroup configurations](http://docs.carto.com/carto-engine/maps-api/mapconfig/#layergroup-configurations).
|
||||
|
||||
Valid placeholder names start with a letter and can only contain letters, numbers, or underscores. They have to be written between the `<%=` and `%>` strings in order to be replaced inside the Named Maps API.
|
||||
|
||||
@@ -155,12 +159,12 @@ This is the call for creating the Named Map. It is sending the template.json fil
|
||||
curl -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d @template.json \
|
||||
'https://{username}.cartodb.com/api/v1/map/named?api_key={api_key}'
|
||||
'https://{username}.carto.com/api/v1/map/named?api_key={api_key}'
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
The response back from the API provides the name of your MapConfig as a template, enabling you to edit the Named Map details by inserting your variables into the template where placeholders are defined, and create custom queries using SQL.
|
||||
The response back from the API provides the name of your MapConfig as a template, enabling you to edit the Named Map details by inserting your variables into the template where placeholders are defined, and create custom queries using SQL.
|
||||
|
||||
```javascript
|
||||
{
|
||||
@@ -170,7 +174,7 @@ The response back from the API provides the name of your MapConfig as a template
|
||||
|
||||
## Instantiate
|
||||
|
||||
Instantiating a Named Map allows you to fetch the map tiles. You can use the Maps API to instantiate, or use the CartoDB.js `createLayer()` function. The result is an Anonymous Map.
|
||||
Instantiating a Named Map allows you to fetch the map tiles. You can use the Maps API to instantiate, or use the CARTO.js `createLayer()` function. The result is an Anonymous Map.
|
||||
|
||||
#### Definition
|
||||
|
||||
@@ -209,7 +213,7 @@ Valid auth token will be needed, if required by the template.
|
||||
curl -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d @params.json \
|
||||
'https://{username}.cartodb.com/api/v1/map/named/{template_name}?auth_token={auth_token}'
|
||||
'https://{username}.carto.com/api/v1/map/named/{template_name}?auth_token={auth_token}'
|
||||
```
|
||||
|
||||
#### Response
|
||||
@@ -229,7 +233,7 @@ curl -X POST \
|
||||
}
|
||||
```
|
||||
|
||||
You can then use the `layergroupid` for fetching tiles and grids as you would normally (see [Anonymous Maps](http://docs.cartodb.com/cartodb-platform/maps-api/anonymous-maps/)).
|
||||
You can then use the `layergroupid` for fetching tiles and grids as you would normally (see [Anonymous Maps](http://docs.carto.com/carto-engine/maps-api/anonymous-maps/)).
|
||||
|
||||
## Update
|
||||
|
||||
@@ -261,7 +265,7 @@ Updating a Named Map removes all the Named Map instances, so they need to be ini
|
||||
curl -X PUT \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d @template.json \
|
||||
'https://{username}.cartodb.com/api/v1/map/named/{template_name}?api_key={api_key}'
|
||||
'https://{username}.carto.com/api/v1/map/named/{template_name}?api_key={api_key}'
|
||||
```
|
||||
|
||||
#### Response
|
||||
@@ -303,7 +307,7 @@ api_key | is required
|
||||
#### Call
|
||||
|
||||
```bash
|
||||
curl -X DELETE 'https://{username}.cartodb.com/api/v1/map/named/{template_name}?api_key={api_key}'
|
||||
curl -X DELETE 'https://{username}.carto.com/api/v1/map/named/{template_name}?api_key={api_key}'
|
||||
```
|
||||
|
||||
#### Response
|
||||
@@ -337,7 +341,7 @@ api_key | is required
|
||||
#### Call
|
||||
|
||||
```bash
|
||||
curl -X GET 'https://{username}.cartodb.com/api/v1/map/named?api_key={api_key}'
|
||||
curl -X GET 'https://{username}.carto.com/api/v1/map/named?api_key={api_key}'
|
||||
```
|
||||
|
||||
#### Response
|
||||
@@ -377,7 +381,7 @@ api_key | is required
|
||||
#### Call
|
||||
|
||||
```bash
|
||||
curl -X GET 'https://{username}.cartodb.com/api/v1/map/named/{template_name}?api_key={api_key}'
|
||||
curl -X GET 'https://{username}.carto.com/api/v1/map/named/{template_name}?api_key={api_key}'
|
||||
```
|
||||
|
||||
#### Response
|
||||
@@ -418,7 +422,7 @@ callback | JSON callback name
|
||||
#### Call
|
||||
|
||||
```bash
|
||||
curl 'https://{username}.cartodb.com/api/v1/map/named/{template_name}/jsonp?auth_token={auth_token}&callback=callback&config=template_params_json'
|
||||
curl 'https://{username}.carto.com/api/v1/map/named/{template_name}/jsonp?auth_token={auth_token}&callback=callback&config=template_params_json'
|
||||
```
|
||||
|
||||
#### Response
|
||||
@@ -450,9 +454,9 @@ callback({
|
||||
})
|
||||
```
|
||||
|
||||
## CartoDB.js for Named Maps
|
||||
## CARTO.js for Named Maps
|
||||
|
||||
You can use a Named Map that you created (which is defined by its `name`), to create a map using CartoDB.js. This is achieved by adding the [`namedmap` type](http://docs.cartodb.com/cartodb-platform/cartodb-js/layer-source-object/#named-maps-layer-source-object-type-namedmap) layer source object to draw the Named Map.
|
||||
You can use a Named Map that you created (which is defined by its `name`), to create a map using CARTO.js. This is achieved by adding the [`namedmap` type](http://docs.carto.com/carto-engine/carto-js/layer-source-object/#named-maps-layer-source-object-type-namedmap) layer source object to draw the Named Map.
|
||||
|
||||
```javascript
|
||||
{
|
||||
@@ -482,17 +486,17 @@ You can use a Named Map that you created (which is defined by its `name`), to cr
|
||||
|
||||
**Note:** Instantiating a Named Map over a `createLayer` does not require an API Key and by default, does not include auth tokens. _If_ you defined auth tokens for the Named Map configuration, then you will have to include them.
|
||||
|
||||
[CartoDB.js](http://docs.cartodb.com/cartodb-platform/cartodb-js/) has methods for accessing your Named Maps.
|
||||
[CARTO.js](http://docs.carto.com/carto-engine/carto-js/) has methods for accessing your Named Maps.
|
||||
|
||||
1. [layer.setParams()](http://docs.cartodb.com/cartodb-platform/cartodb-js/api-methods/#layersetparamskey-value) allows you to change the template variables (in the placeholders object) via JavaScript
|
||||
1. [layer.setParams()](http://docs.carto.com/carto-engine/carto-js/api-methods/#layersetparamskey-value) allows you to change the template variables (in the placeholders object) via JavaScript
|
||||
|
||||
**Note:** The CartoDB.js `layer.setParams()` function is not supported when using Named Maps for Torque. Alternatively, you can create a [Torque layer in a Named Map](http://bl.ocks.org/iriberri/de37be6406f9cc7cfe5a)
|
||||
**Note:** The CARTO.js `layer.setParams()` function is not supported when using Named Maps for Torque. Alternatively, you can create a [Torque layer in a Named Map](http://bl.ocks.org/iriberri/de37be6406f9cc7cfe5a)
|
||||
|
||||
2. [layer.setAuthToken()](http://docs.cartodb.com/cartodb-platform/cartodb-js/api-methods/#layersetauthtokenauthtoken) allows you to set the auth tokens to create the layer
|
||||
2. [layer.setAuthToken()](http://docs.carto.com/carto-engine/carto-js/api-methods/#layersetauthtokenauthtoken) allows you to set the auth tokens to create the layer
|
||||
|
||||
### Torque Layer in a Named Map
|
||||
|
||||
If you are creating a Torque layer in a Named Map without using the Torque.js library, you can apply the Torque layer by applying the following code with CartoDBjs:
|
||||
If you are creating a Torque layer in a Named Map without using the Torque.js library, you can apply the Torque layer by applying the following code with CARTO.js:
|
||||
|
||||
```javascript
|
||||
// add cartodb layer with one sublayer
|
||||
@@ -516,18 +520,18 @@ If you are creating a Torque layer in a Named Map without using the Torque.js li
|
||||
})
|
||||
.addTo(map)
|
||||
.done(function(layer) {
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
#### Examples of Named Maps created with CartoDB.js
|
||||
#### Examples of Named Maps created with CARTO.js
|
||||
|
||||
- [Named Map selectors with interaction](http://bl.ocks.org/ohasselblad/515a8af1f99d5e690484)
|
||||
- [Named Map selectors with interaction](http://bl.ocks.org/andy-esch/515a8af1f99d5e690484)
|
||||
|
||||
- [Named Map with interactivity](http://bl.ocks.org/ohasselblad/d1a45b8ff5e7bd90cd68)
|
||||
- [Named Map with interactivity](http://bl.ocks.org/andy-esch/d1a45b8ff5e7bd90cd68)
|
||||
|
||||
- [Toggling sublayers in a Named Map](http://bl.ocks.org/ohasselblad/c1a0f4913610eec53cd3)
|
||||
- [Toggling sublayers in a Named Map](http://bl.ocks.org/andy-esch/c1a0f4913610eec53cd3)
|
||||
|
||||
## Fetching XYZ Tiles for Named Maps
|
||||
|
||||
@@ -535,7 +539,7 @@ Optionally, authenticated users can fetch projected tiles (XYZ tiles or Mapnik R
|
||||
|
||||
### Fetch XYZ Tiles Directly with a URL
|
||||
|
||||
Authenticated users, with an auth token, can use XYZ-based URLs to fetch tiles directly, and instantiate the Named Map as part of the request to your application. You do not have to do any other steps to initialize your map.
|
||||
Authenticated users, with an auth token, can use XYZ-based URLs to fetch tiles directly, and instantiate the Named Map as part of the request to your application. You do not have to do any other steps to initialize your map.
|
||||
|
||||
To call a template_id in a URL:
|
||||
|
||||
@@ -543,21 +547,21 @@ To call a template_id in a URL:
|
||||
|
||||
For example, a complete URL might appear as:
|
||||
|
||||
"https://{username}.cartodb.com/api/v1/map/named/{template_id}/{layer}/{z}/{x}/{y}.png"
|
||||
"https://{username}.carto.com/api/v1/map/named/{template_id}/{layer}/{z}/{x}/{y}.png"
|
||||
|
||||
The placeholders indicate the following:
|
||||
|
||||
- [`template_id`](http://docs.cartodb.com/cartodb-platform/maps-api/named-maps/#response) is the response of your Named Map.
|
||||
- [`template_id`](http://docs.carto.com/carto-engine/maps-api/named-maps/#response) is the response of your Named Map.
|
||||
- layers can be a number (referring to the # layer of your map), all layers of your map, or a list of layers.
|
||||
- To show just the basemap layer, enter the number value `0` in the layer placeholder "https://{username}.cartodb.com/api/v1/map/named/{template_id}/0/{z}/{x}/{y}.png"
|
||||
- To show the first layer, enter the number value `1` in the layer placeholder "https://{username}.cartodb.com/api/v1/map/named/{template_id}/1/{z}/{x}/{y}.png"
|
||||
- To show all layers, enter the value `all` for the layer placeholder "https://{username}.cartodb.com/api/v1/map/named/{template_id}/all/{z}/{x}/{y}.png"
|
||||
- To show a [list of layers](http://docs.cartodb.com/cartodb-platform/maps-api/anonymous-maps/#blending-and-layer-selection), enter the comma separated layer value as 0,1,2 in the layer placeholder. For example, to show the basemap and the first layer, "https://{username}.cartodb.com/api/v1/map/named/{template_id}/0,1/{z}/{x}/{y}.png"
|
||||
- To show just the basemap layer, enter the number value `0` in the layer placeholder "https://{username}.carto.com/api/v1/map/named/{template_id}/0/{z}/{x}/{y}.png"
|
||||
- To show the first layer, enter the number value `1` in the layer placeholder "https://{username}.carto.com/api/v1/map/named/{template_id}/1/{z}/{x}/{y}.png"
|
||||
- To show all layers, enter the value `all` for the layer placeholder "https://{username}.carto.com/api/v1/map/named/{template_id}/all/{z}/{x}/{y}.png"
|
||||
- To show a [list of layers](http://docs.carto.com/carto-engine/maps-api/anonymous-maps/#blending-and-layer-selection), enter the comma separated layer value as 0,1,2 in the layer placeholder. For example, to show the basemap and the first layer, "https://{username}.carto.com/api/v1/map/named/{template_id}/0,1/{z}/{x}/{y}.png"
|
||||
|
||||
|
||||
### Get Mapnik Retina Tiles
|
||||
|
||||
Mapnik Retina tiles are not directly supported for Named Maps, so you cannot use the Named Map template_id. To fetch Mapnik Retina tiles, get the [layergroupid](http://docs.cartodb.com/cartodb-platform/maps-api/named-maps/#response-1) to initialize the map.
|
||||
Mapnik Retina tiles are not directly supported for Named Maps, so you cannot use the Named Map template_id. To fetch Mapnik Retina tiles, get the [layergroupid](http://docs.carto.com/carto-engine/maps-api/named-maps/#response-1) to initialize the map.
|
||||
|
||||
Instantiate the map by using your `layergroupid` in the token placeholder:
|
||||
|
||||
|
||||
@@ -22,10 +22,10 @@ $.ajax({
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
url: 'https://{username}.cartodb.com/api/v1/map',
|
||||
url: 'https://{username}.carto.com/api/v1/map',
|
||||
data: JSON.stringify(mapconfig),
|
||||
success: function(data) {
|
||||
var templateUrl = 'https://{username}.cartodb.com/api/v1/map/' + data.layergroupid + '/{z}/{x}/{y}.png'
|
||||
var templateUrl = 'https://{username}.carto.com/api/v1/map/' + data.layergroupid + '/{z}/{x}/{y}.png'
|
||||
console.log(templateUrl);
|
||||
}
|
||||
})
|
||||
@@ -33,7 +33,7 @@ $.ajax({
|
||||
|
||||
## Named Maps
|
||||
|
||||
Let's create a Named Map using some private tables in a CartoDB account.
|
||||
Let's create a Named Map using some private tables in a CARTO account.
|
||||
The following map config sets up a map of European countries that have a white fill color:
|
||||
|
||||
```javascript
|
||||
@@ -56,12 +56,12 @@ The following map config sets up a map of European countries that have a white f
|
||||
}
|
||||
```
|
||||
|
||||
The MapConfig needs to be sent to CartoDB's Map API using an authenticated call. Here we will use a command line tool called `curl`. For more info about this tool, see [this blog post](http://quickleft.com/blog/command-line-tutorials-curl), or type `man curl` in bash. Using `curl`, and storing the config from above in a file `MapConfig.json`, the call would look like:
|
||||
The MapConfig needs to be sent to CARTO's Map API using an authenticated call. Here we will use a command line tool called `curl`. For more info about this tool, see [this blog post](http://quickleft.com/blog/command-line-tutorials-curl), or type `man curl` in bash. Using `curl`, and storing the config from above in a file `MapConfig.json`, the call would look like:
|
||||
|
||||
#### Call
|
||||
|
||||
```bash
|
||||
curl 'https://{username}.cartodb.com/api/v1/map/named?api_key={api_key}' -H 'Content-Type: application/json' -d @mapconfig.json
|
||||
curl 'https://{username}.carto.com/api/v1/map/named?api_key={api_key}' -H 'Content-Type: application/json' -d @mapconfig.json
|
||||
```
|
||||
|
||||
To get the `URL` to fetch the tiles you need to instantiate the map, where `template_id` is the template name from the previous response.
|
||||
@@ -69,7 +69,7 @@ To get the `URL` to fetch the tiles you need to instantiate the map, where `temp
|
||||
#### Call
|
||||
|
||||
```bash
|
||||
curl -X POST 'https://{username}.cartodb.com/api/v1/map/named/{template_id}' -H 'Content-Type: application/json'
|
||||
curl -X POST 'https://{username}.carto.com/api/v1/map/named/{template_id}' -H 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
The response will return JSON with properties for the `layergroupid`, the timestamp (`last_updated`) of the last data modification and some key/value pairs with `metadata` for the `layers`.
|
||||
@@ -96,5 +96,5 @@ Note: all `layers` in `metadata` will always have a `type` string and a `meta` d
|
||||
You can use the `layergroupid` to instantiate a URL template for accessing tiles on the client. Here we use the `layergroupid` from the example response above in this URL template:
|
||||
|
||||
```bash
|
||||
https://{username}.cartodb.com/api/v1/map/{layergroupid}/{z}/{x}/{y}.png
|
||||
https://{username}.carto.com/api/v1/map/{layergroupid}/{z}/{x}/{y}.png
|
||||
```
|
||||
|
||||
@@ -78,7 +78,7 @@ format | the format for the image, supported types: `png`, `jpg`
|
||||
--- | ---
|
||||
|_ jpg | will have a default quality of 85.
|
||||
|
||||
A Named Maps static image will get its constraints from the [`view` argument of the Create Named Map function](http://docs.cartodb.com/cartodb-platform/maps-api/named-maps/#arguments). If `view` is not defined, it will estimate the extent based on the involved tables, otherwise it fallbacks to `"zoom": 1`, `"lng": 0` and `"lat": 0`.
|
||||
A Named Maps static image will get its constraints from the [`view` argument of the Create Named Map function](http://docs.carto.com/carto-engine/maps-api/named-maps/#arguments). If `view` is not defined, it will estimate the extent based on the involved tables, otherwise it fallbacks to `"zoom": 1`, `"lng": 0` and `"lat": 0`.
|
||||
|
||||
#### Layers
|
||||
|
||||
@@ -122,9 +122,9 @@ By manipulating the `"urlTemplate"` custom basemaps can be used in generating st
|
||||
},
|
||||
```
|
||||
|
||||
**CartoDB**
|
||||
**CARTO**
|
||||
|
||||
As described in the [MapConfig File Format](http://docs.cartodb.com/cartodb-platform/maps-api/mapconfig/), a "cartodb" type layer is now just an alias to a "mapnik" type layer as above, intended for backwards compatibility.
|
||||
As described in the [MapConfig File Format](http://docs.carto.com/carto-engine/maps-api/mapconfig/), a "cartodb" type layer is now just an alias to a "mapnik" type layer as above, intended for backwards compatibility.
|
||||
|
||||
```javascript
|
||||
{
|
||||
@@ -142,18 +142,22 @@ Additionally, static images from Torque maps and other map layers can be used to
|
||||
|
||||
### Caching
|
||||
|
||||
It is important to note that generated images are cached from the live data referenced with the `layergroupid token` on the specified CartoDB account. This means that if the data changes, the cached image will also change. When linking dynamically, it is important to take into consideration the state of the data and longevity of the static image to avoid broken images or changes in how the image is displayed. To obtain a static snapshot of the map as it is today and preserve the image long-term regardless of changes in data, the image must be saved and stored locally.
|
||||
It is important to note that generated images are cached from the live data referenced with the `layergroupid token` on the specified CARTO account. This means that if the data changes, the cached image will also change. When linking dynamically, it is important to take into consideration the state of the data and longevity of the static image to avoid broken images or changes in how the image is displayed. To obtain a static snapshot of the map as it is today and preserve the image long-term regardless of changes in data, the image must be saved and stored locally.
|
||||
|
||||
### Limits
|
||||
|
||||
* While images can encompass an entirety of a map, the default limit for pixel range is 8192 x 8192.
|
||||
* Image resolution by default is set to 72 DPI
|
||||
* JPEG quality by default is 85%
|
||||
* Timeout limits for generating static maps are the same across the CartoDB Editor and Platform. It is important to ensure timely processing of queries.
|
||||
* While images can encompass an entirety of a map, the limit for pixel range is 8192 x 8192.
|
||||
* Image resolution is set to 72 DPI
|
||||
* JPEG quality is 85%
|
||||
* 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>
|
||||
{% endhighlight %}
|
||||
|
||||
## Examples
|
||||
|
||||
After instantiating a map from a CartoDB account:
|
||||
After instantiating a map from a CARTO account:
|
||||
|
||||
#### Call
|
||||
|
||||
|
||||
@@ -95,9 +95,7 @@ AuthApi.prototype.authorize = function(req, callback) {
|
||||
self.authorizedByAPIKey(user, req, this);
|
||||
},
|
||||
function checkApiKey(err, authorized){
|
||||
if (req.profiler) {
|
||||
req.profiler.done('authorizedByAPIKey');
|
||||
}
|
||||
req.profiler.done('authorizedByAPIKey');
|
||||
assert.ifError(err);
|
||||
|
||||
// if not authorized by api_key, continue
|
||||
@@ -131,9 +129,7 @@ AuthApi.prototype.authorize = function(req, callback) {
|
||||
}
|
||||
|
||||
self.pgConnection.setDBAuth(user, req.params, function(err) {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('setDBAuth');
|
||||
}
|
||||
req.profiler.done('setDBAuth');
|
||||
callback(err, true); // authorized (or error)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
var _ = require('underscore');
|
||||
var step = require('step');
|
||||
var CamshaftFilter = require('../models/filter/camshaft');
|
||||
var AnalysisFilter = require('../models/filter/analysis');
|
||||
|
||||
function FilterStatsApi(pgQueryRunner) {
|
||||
this.pgQueryRunner = pgQueryRunner;
|
||||
@@ -40,8 +40,8 @@ FilterStatsApi.prototype.getFilterStats = function (username, unfiltered_query,
|
||||
},
|
||||
function getFilteredRows() {
|
||||
if ( filters && !_.isEmpty(filters)) {
|
||||
var camshaftFilter = new CamshaftFilter(filters);
|
||||
var query = camshaftFilter.sql(unfiltered_query);
|
||||
var analysisFilter = new AnalysisFilter(filters);
|
||||
var query = analysisFilter.sql(unfiltered_query);
|
||||
getEstimatedRows(self.pgQueryRunner, username, query, this);
|
||||
} else {
|
||||
this(null, null);
|
||||
|
||||
@@ -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
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,7 +9,10 @@ module.exports = AnalysisStatusBackend;
|
||||
AnalysisStatusBackend.prototype.getNodeStatus = function (params, callback) {
|
||||
var nodeId = params.nodeId;
|
||||
|
||||
var statusQuery = 'SELECT node_id, status, updated_at FROM cdb_analysis_catalog where node_id = \'' + nodeId + '\'';
|
||||
var statusQuery = [
|
||||
'SELECT node_id, status, updated_at, last_error_message as error_message',
|
||||
'FROM cdb_analysis_catalog where node_id = \'' + nodeId + '\''
|
||||
].join(' ');
|
||||
|
||||
var pg = new PSQL(dbParamsFromReqParams(params));
|
||||
pg.query(statusQuery, function(err, result) {
|
||||
@@ -21,10 +24,16 @@ AnalysisStatusBackend.prototype.getNodeStatus = function (params, callback) {
|
||||
|
||||
var rows = result.rows || [];
|
||||
|
||||
return callback(null, rows[0] || {
|
||||
var statusResponse = rows[0] || {
|
||||
node_id: nodeId,
|
||||
status: 'unknown'
|
||||
});
|
||||
};
|
||||
|
||||
if (statusResponse.status !== 'failed') {
|
||||
delete statusResponse.error_message;
|
||||
}
|
||||
|
||||
return callback(null, statusResponse);
|
||||
}, true); // use read-only transaction
|
||||
};
|
||||
|
||||
|
||||
@@ -1,19 +1,93 @@
|
||||
var camshaft = require('camshaft');
|
||||
'use strict';
|
||||
|
||||
function AnalysisBackend(options) {
|
||||
var batchConfig = options.batch || {};
|
||||
var _ = require('underscore');
|
||||
var camshaft = require('camshaft');
|
||||
var fs = require('fs');
|
||||
|
||||
var REDIS_LIMITS = {
|
||||
DB: 5,
|
||||
PREFIX: 'limits:analyses:' // + username
|
||||
};
|
||||
|
||||
function AnalysisBackend (metadataBackend, options) {
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.options = options || {};
|
||||
this.options.limits = this.options.limits || {};
|
||||
this.setBatchConfig(this.options.batch);
|
||||
this.setLoggerConfig(this.options.logger);
|
||||
}
|
||||
|
||||
module.exports = AnalysisBackend;
|
||||
|
||||
AnalysisBackend.prototype.setBatchConfig = function (options) {
|
||||
var batchConfig = options || {};
|
||||
batchConfig.endpoint = batchConfig.endpoint || 'http://127.0.0.1:8080/api/v1/sql/job';
|
||||
batchConfig.inlineExecution = batchConfig.inlineExecution || false;
|
||||
batchConfig.hostHeaderTemplate = batchConfig.hostHeaderTemplate || '{{=it.username}}.localhost.lan';
|
||||
this.batchConfig = batchConfig;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = AnalysisBackend;
|
||||
AnalysisBackend.prototype.setLoggerConfig = function (options) {
|
||||
this.loggerConfig = options || {};
|
||||
|
||||
if (this.loggerConfig.filename) {
|
||||
this.stream = fs.createWriteStream(this.loggerConfig.filename, { flags: 'a', encoding: 'utf8' });
|
||||
|
||||
process.on('SIGHUP', function () {
|
||||
if (this.stream) {
|
||||
this.stream.destroy();
|
||||
}
|
||||
|
||||
this.stream = fs.createWriteStream(this.loggerConfig.filename, { flags: 'a', encoding: 'utf8' });
|
||||
}.bind(this));
|
||||
}
|
||||
};
|
||||
|
||||
AnalysisBackend.prototype.create = function(analysisConfiguration, analysisDefinition, callback) {
|
||||
analysisConfiguration.batch.endpoint = this.batchConfig.endpoint;
|
||||
analysisConfiguration.batch.inlineExecution = this.batchConfig.inlineExecution;
|
||||
analysisConfiguration.batch.hostHeaderTemplate = this.batchConfig.hostHeaderTemplate;
|
||||
|
||||
camshaft.create(analysisConfiguration, analysisDefinition, callback);
|
||||
analysisConfiguration.logger = {
|
||||
stream: this.stream ? this.stream : process.stdout
|
||||
};
|
||||
|
||||
this.getAnalysesLimits(analysisConfiguration.user, function(err, limits) {
|
||||
analysisConfiguration.limits = limits || {};
|
||||
camshaft.create(analysisConfiguration, analysisDefinition, callback);
|
||||
});
|
||||
};
|
||||
|
||||
AnalysisBackend.prototype.getAnalysesLimits = function(username, callback) {
|
||||
var self = this;
|
||||
|
||||
var analysesLimits = {
|
||||
analyses: {
|
||||
// buffer: {
|
||||
// timeout: 1000,
|
||||
// maxNumberOfRows: 1e6
|
||||
// }
|
||||
}
|
||||
};
|
||||
|
||||
Object.keys(self.options.limits).forEach(function(analysisTypeOrTag) {
|
||||
analysesLimits.analyses[analysisTypeOrTag] = _.extend({}, self.options.limits[analysisTypeOrTag]);
|
||||
});
|
||||
|
||||
var analysesLimitsKey = REDIS_LIMITS.PREFIX + username;
|
||||
this.metadataBackend.redisCmd(REDIS_LIMITS.DB, 'HGETALL', [analysesLimitsKey], function(err, analysesTimeouts) {
|
||||
// analysesTimeouts wil be something like: { moran: 3000, intersection: 5000 }
|
||||
analysesTimeouts = analysesTimeouts || {};
|
||||
|
||||
Object.keys(analysesTimeouts).forEach(function(analysisType) {
|
||||
analysesLimits.analyses[analysisType] = _.defaults(
|
||||
{
|
||||
timeout: Number.isFinite(+analysesTimeouts[analysisType]) ? +analysesTimeouts[analysisType] : 0
|
||||
},
|
||||
analysesLimits.analyses[analysisType]
|
||||
);
|
||||
});
|
||||
|
||||
return callback(null, analysesLimits);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -2,11 +2,8 @@ var assert = require('assert');
|
||||
|
||||
var _ = require('underscore');
|
||||
var PSQL = require('cartodb-psql');
|
||||
var camshaft = require('camshaft');
|
||||
var step = require('step');
|
||||
|
||||
var Timer = require('../stats/timer');
|
||||
|
||||
var BBoxFilter = require('../models/filter/bbox');
|
||||
|
||||
var DataviewFactory = require('../models/dataview/factory');
|
||||
@@ -16,6 +13,9 @@ var overviewsQueryRewriter = new OverviewsQueryRewriter({
|
||||
zoom_level: 'CDB_ZoomFromScale(!scale_denominator!)'
|
||||
});
|
||||
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
|
||||
function DataviewBackend(analysisBackend) {
|
||||
this.analysisBackend = analysisBackend;
|
||||
}
|
||||
@@ -23,251 +23,117 @@ function DataviewBackend(analysisBackend) {
|
||||
module.exports = DataviewBackend;
|
||||
|
||||
DataviewBackend.prototype.getDataview = function (mapConfigProvider, user, params, callback) {
|
||||
var self = this;
|
||||
|
||||
var timer = new Timer();
|
||||
|
||||
var dataviewName = params.dataviewName;
|
||||
|
||||
var mapConfig;
|
||||
var dataviewDefinition;
|
||||
step(
|
||||
function getMapConfig() {
|
||||
mapConfigProvider.getMapConfig(this);
|
||||
},
|
||||
function _getDataviewDefinition(err, _mapConfig) {
|
||||
function runDataviewQuery(err, mapConfig) {
|
||||
assert.ifError(err);
|
||||
|
||||
mapConfig = _mapConfig;
|
||||
|
||||
var _dataviewDefinition = getDataviewDefinition(mapConfig.obj(), dataviewName);
|
||||
if (!_dataviewDefinition) {
|
||||
var dataviewDefinition = getDataviewDefinition(mapConfig.obj(), dataviewName);
|
||||
if (!dataviewDefinition) {
|
||||
throw new Error("Dataview '" + dataviewName + "' does not exists");
|
||||
}
|
||||
|
||||
dataviewDefinition = _dataviewDefinition;
|
||||
|
||||
return dataviewDefinition;
|
||||
},
|
||||
function loadAnalysis(err) {
|
||||
assert.ifError(err);
|
||||
|
||||
var analysisConfiguration = {
|
||||
db: {
|
||||
host: params.dbhost,
|
||||
port: params.dbport,
|
||||
dbname: params.dbname,
|
||||
user: params.dbuser,
|
||||
pass: params.dbpassword
|
||||
},
|
||||
batch: {
|
||||
username: user,
|
||||
apiKey: params.api_key
|
||||
}
|
||||
};
|
||||
|
||||
var sourceId = dataviewDefinition.source.id;
|
||||
var analysisDefinition = getAnalysisDefinition(mapConfig.obj().analyses, sourceId);
|
||||
|
||||
var next = this;
|
||||
|
||||
self.analysisBackend.create(analysisConfiguration, analysisDefinition, function(err, analysis) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
var sourceId2Node = {};
|
||||
var rootNode = analysis.getRoot();
|
||||
if (rootNode.params && rootNode.params.id) {
|
||||
sourceId2Node[rootNode.params.id] = rootNode;
|
||||
}
|
||||
|
||||
analysis.getSortedNodes().forEach(function(node) {
|
||||
if (node.params && node.params.id) {
|
||||
sourceId2Node[node.params.id] = node;
|
||||
}
|
||||
});
|
||||
|
||||
var node = sourceId2Node[sourceId];
|
||||
|
||||
if (!node) {
|
||||
return next(new Error('Analysis node not found for dataview'));
|
||||
}
|
||||
|
||||
return next(null, node);
|
||||
});
|
||||
},
|
||||
function runDataviewQuery(err, node) {
|
||||
assert.ifError(err);
|
||||
|
||||
var pg = new PSQL(dbParamsFromReqParams(params));
|
||||
|
||||
var ownFilter = +params.own_filter;
|
||||
ownFilter = !!ownFilter;
|
||||
|
||||
var query;
|
||||
|
||||
if (ownFilter) {
|
||||
query = node.getQuery();
|
||||
} else {
|
||||
var applyFilters = {};
|
||||
applyFilters[dataviewName] = false;
|
||||
query = node.getQuery(applyFilters);
|
||||
}
|
||||
|
||||
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 ) {
|
||||
if ( node.type === 'source' ) {
|
||||
var filters = node.getFilters();
|
||||
var filters_disabler = Object.keys(filters).reduce(
|
||||
function(disabler, filter_id){ disabler[filter_id] = false; return disabler; },
|
||||
{}
|
||||
);
|
||||
var unfiltered_query = node.getQuery(filters_disabler);
|
||||
queryRewriteData = _.extend(
|
||||
{},
|
||||
queryRewriteData, { filters: filters, unfiltered_query: unfiltered_query }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
var query = (ownFilter) ? dataviewDefinition.sql.own_filter_on : dataviewDefinition.sql.own_filter_off;
|
||||
if (params.bbox) {
|
||||
var bboxFilter = new BBoxFilter({column: 'the_geom', srid: 4326}, {bbox: 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',
|
||||
srid: 4326,
|
||||
},
|
||||
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) {
|
||||
overrides[k] = Number.isFinite(+val) ? +val : 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, timer.getTimes());
|
||||
return callback(err, result);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
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'),
|
||||
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, params, callback) {
|
||||
var self = this;
|
||||
|
||||
var timer = new Timer();
|
||||
|
||||
var dataviewName = params.dataviewName;
|
||||
|
||||
var mapConfig;
|
||||
var dataviewDefinition;
|
||||
step(
|
||||
function getMapConfig() {
|
||||
mapConfigProvider.getMapConfig(this);
|
||||
},
|
||||
function _getDataviewDefinition(err, _mapConfig) {
|
||||
function runDataviewSearchQuery(err, mapConfig) {
|
||||
assert.ifError(err);
|
||||
|
||||
mapConfig = _mapConfig;
|
||||
|
||||
var _dataviewDefinition = getDataviewDefinition(mapConfig.obj(), dataviewName);
|
||||
if (!_dataviewDefinition) {
|
||||
var dataviewDefinition = getDataviewDefinition(mapConfig.obj(), dataviewName);
|
||||
if (!dataviewDefinition) {
|
||||
throw new Error("Dataview '" + dataviewName + "' does not exists");
|
||||
}
|
||||
|
||||
dataviewDefinition = _dataviewDefinition;
|
||||
|
||||
return dataviewDefinition;
|
||||
},
|
||||
function loadAnalysis(err) {
|
||||
assert.ifError(err);
|
||||
|
||||
var analysisConfiguration = {
|
||||
db: {
|
||||
host: params.dbhost,
|
||||
port: params.dbport,
|
||||
dbname: params.dbname,
|
||||
user: params.dbuser,
|
||||
pass: params.dbpassword
|
||||
},
|
||||
batch: {
|
||||
// TODO load this from configuration
|
||||
endpoint: 'http://127.0.0.1:8080/api/v1/sql/job',
|
||||
username: user,
|
||||
apiKey: params.api_key
|
||||
}
|
||||
};
|
||||
|
||||
var sourceId = dataviewDefinition.source.id;
|
||||
var analysisDefinition = getAnalysisDefinition(mapConfig.obj().analyses, sourceId);
|
||||
|
||||
var next = this;
|
||||
|
||||
self.analysisBackend.create(analysisConfiguration, analysisDefinition, function(err, analysis) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
var sourceId2Node = {};
|
||||
var rootNode = analysis.getRoot();
|
||||
if (rootNode.params && rootNode.params.id) {
|
||||
sourceId2Node[rootNode.params.id] = rootNode;
|
||||
}
|
||||
|
||||
analysis.getSortedNodes().forEach(function(node) {
|
||||
if (node.params && node.params.id) {
|
||||
sourceId2Node[node.params.id] = node;
|
||||
}
|
||||
});
|
||||
|
||||
var node = sourceId2Node[sourceId];
|
||||
|
||||
if (!node) {
|
||||
return next(new Error('Analysis node not found for dataview'));
|
||||
}
|
||||
|
||||
return next(null, node);
|
||||
});
|
||||
},
|
||||
function runDataviewQuery(err, node) {
|
||||
assert.ifError(err);
|
||||
|
||||
var pg = new PSQL(dbParamsFromReqParams(params));
|
||||
|
||||
var ownFilter = +params.own_filter;
|
||||
ownFilter = !!ownFilter;
|
||||
|
||||
var query;
|
||||
if (ownFilter) {
|
||||
query = node.getQuery();
|
||||
} else {
|
||||
var applyFilters = {};
|
||||
applyFilters[dataviewName] = false;
|
||||
query = node.getQuery(applyFilters);
|
||||
}
|
||||
var query = (ownFilter) ? dataviewDefinition.sql.own_filter_on : dataviewDefinition.sql.own_filter_off;
|
||||
|
||||
if (params.bbox) {
|
||||
var bboxFilter = new BBoxFilter({column: 'the_geom', srid: 4326}, {bbox: params.bbox});
|
||||
@@ -280,23 +146,11 @@ DataviewBackend.prototype.search = function (mapConfigProvider, user, params, ca
|
||||
dataview.search(pg, userQuery, this);
|
||||
},
|
||||
function returnCallback(err, result) {
|
||||
return callback(err, result, timer.getTimes());
|
||||
return callback(err, result);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
function getAnalysisDefinition(mapConfigAnalyses, sourceId) {
|
||||
mapConfigAnalyses = mapConfigAnalyses || [];
|
||||
for (var i = 0; i < mapConfigAnalyses.length; i++) {
|
||||
var analysisGraph = new camshaft.reference.AnalysisGraph(mapConfigAnalyses[i]);
|
||||
var nodes = analysisGraph.getNodesWithId();
|
||||
if (nodes.hasOwnProperty(sourceId)) {
|
||||
return mapConfigAnalyses[i];
|
||||
}
|
||||
}
|
||||
throw new Error('There is no associated analysis for the dataview source id');
|
||||
}
|
||||
|
||||
function getDataviewDefinition(mapConfig, dataviewName) {
|
||||
var dataviews = mapConfig.dataviews || {};
|
||||
return dataviews[dataviewName];
|
||||
|
||||
16
lib/cartodb/backends/layer-stats/empty-layer-stats.js
Normal file
16
lib/cartodb/backends/layer-stats/empty-layer-stats.js
Normal file
@@ -0,0 +1,16 @@
|
||||
function EmptyLayerStats(types) {
|
||||
this._types = types || {};
|
||||
}
|
||||
|
||||
EmptyLayerStats.prototype.is = function (type) {
|
||||
return this._types[type] ? this._types[type] : false;
|
||||
};
|
||||
|
||||
EmptyLayerStats.prototype.getStats =
|
||||
function (layer, dbConnection, callback) {
|
||||
setImmediate(function() {
|
||||
callback(null, {});
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = EmptyLayerStats;
|
||||
23
lib/cartodb/backends/layer-stats/factory.js
Normal file
23
lib/cartodb/backends/layer-stats/factory.js
Normal file
@@ -0,0 +1,23 @@
|
||||
var LayerStats = require('./layer-stats');
|
||||
var EmptyLayerStats = require('./empty-layer-stats');
|
||||
var MapnikLayerStats = require('./mapnik-layer-stats');
|
||||
var TorqueLayerStats = require('./torque-layer-stats');
|
||||
|
||||
module.exports = function LayerStatsFactory(type) {
|
||||
var layerStatsIterator = [];
|
||||
var selectedType = type || 'ALL';
|
||||
|
||||
if (selectedType === 'ALL') {
|
||||
layerStatsIterator.push(new EmptyLayerStats({ http: true, plain: true }));
|
||||
layerStatsIterator.push(new MapnikLayerStats());
|
||||
layerStatsIterator.push(new TorqueLayerStats());
|
||||
} else if (selectedType === 'mapnik') {
|
||||
layerStatsIterator.push(new EmptyLayerStats({ http: true, plain: true, torque: true }));
|
||||
layerStatsIterator.push(new MapnikLayerStats());
|
||||
} else if (selectedType === 'torque') {
|
||||
layerStatsIterator.push(new EmptyLayerStats({ http: true, plain: true, mapnik: true }));
|
||||
layerStatsIterator.push(new TorqueLayerStats());
|
||||
}
|
||||
|
||||
return new LayerStats(layerStatsIterator);
|
||||
};
|
||||
45
lib/cartodb/backends/layer-stats/layer-stats.js
Normal file
45
lib/cartodb/backends/layer-stats/layer-stats.js
Normal file
@@ -0,0 +1,45 @@
|
||||
var queue = require('queue-async');
|
||||
|
||||
function LayerStats(layerStatsIterator) {
|
||||
this.layerStatsIterator = layerStatsIterator;
|
||||
}
|
||||
|
||||
LayerStats.prototype.getStats = function (mapConfig, dbConnection, callback) {
|
||||
var self = this;
|
||||
var stats = [];
|
||||
|
||||
if (!mapConfig.getLayers().length) {
|
||||
return callback(null, stats);
|
||||
}
|
||||
var metaQueue = queue(mapConfig.getLayers().length);
|
||||
mapConfig.getLayers().forEach(function (layer, layerId) {
|
||||
var layerType = mapConfig.layerType(layerId);
|
||||
|
||||
for (var i = 0; i < self.layerStatsIterator.length; i++) {
|
||||
if (self.layerStatsIterator[i].is(layerType)) {
|
||||
var getStats = self.layerStatsIterator[i].getStats.bind(self.layerStatsIterator[i]);
|
||||
metaQueue.defer(getStats, layer, dbConnection);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
metaQueue.awaitAll(function (err, results) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!results) {
|
||||
return callback(null, null);
|
||||
}
|
||||
|
||||
mapConfig.getLayers().forEach(function (layer, layerIndex) {
|
||||
stats[layerIndex] = results[layerIndex];
|
||||
});
|
||||
|
||||
return callback(err, stats);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
module.exports = LayerStats;
|
||||
28
lib/cartodb/backends/layer-stats/mapnik-layer-stats.js
Normal file
28
lib/cartodb/backends/layer-stats/mapnik-layer-stats.js
Normal file
@@ -0,0 +1,28 @@
|
||||
var queryUtils = require('../../utils/query-utils');
|
||||
|
||||
function MapnikLayerStats () {
|
||||
this._types = {
|
||||
mapnik: true,
|
||||
cartodb: true
|
||||
};
|
||||
}
|
||||
|
||||
MapnikLayerStats.prototype.is = function (type) {
|
||||
return this._types[type] ? this._types[type] : false;
|
||||
};
|
||||
|
||||
MapnikLayerStats.prototype.getStats =
|
||||
function (layer, dbConnection, callback) {
|
||||
var queryRowCountSql = queryUtils.getQueryRowCount(layer.options.sql);
|
||||
// This query would gather stats for postgresql table if not exists
|
||||
dbConnection.query(queryRowCountSql, function (err, res) {
|
||||
if (err) {
|
||||
return callback(null, {estimatedFeatureCount: -1});
|
||||
} else {
|
||||
// We decided that the relation is 1 row == 1 feature
|
||||
return callback(null, {estimatedFeatureCount: res.rows[0].rows});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = MapnikLayerStats;
|
||||
16
lib/cartodb/backends/layer-stats/torque-layer-stats.js
Normal file
16
lib/cartodb/backends/layer-stats/torque-layer-stats.js
Normal file
@@ -0,0 +1,16 @@
|
||||
function TorqueLayerStats() {
|
||||
this._types = {
|
||||
torque: true
|
||||
};
|
||||
}
|
||||
|
||||
TorqueLayerStats.prototype.is = function (type) {
|
||||
return this._types[type] ? this._types[type] : false;
|
||||
};
|
||||
|
||||
TorqueLayerStats.prototype.getStats =
|
||||
function (layer, dbConnection, callback) {
|
||||
return callback(null, {});
|
||||
};
|
||||
|
||||
module.exports = TorqueLayerStats;
|
||||
16
lib/cartodb/backends/stats.js
Normal file
16
lib/cartodb/backends/stats.js
Normal file
@@ -0,0 +1,16 @@
|
||||
var layerStats = require('./layer-stats/factory');
|
||||
|
||||
function StatsBackend() {
|
||||
}
|
||||
|
||||
module.exports = StatsBackend;
|
||||
|
||||
StatsBackend.prototype.getStats = function(mapConfig, dbConnection, callback) {
|
||||
var enabledFeatures = global.environment.enabledFeatures;
|
||||
var layerStatsEnabled = enabledFeatures ? enabledFeatures.layerStats: false;
|
||||
if (layerStatsEnabled) {
|
||||
layerStats().getStats(mapConfig, dbConnection, callback);
|
||||
} else {
|
||||
return callback(null, []);
|
||||
}
|
||||
};
|
||||
@@ -55,11 +55,9 @@ util.inherits(TemplateMaps, EventEmitter);
|
||||
module.exports = TemplateMaps;
|
||||
|
||||
|
||||
var o = TemplateMaps.prototype;
|
||||
|
||||
//--------------- PRIVATE METHODS --------------------------------
|
||||
|
||||
o._userTemplateLimit = function() {
|
||||
TemplateMaps.prototype._userTemplateLimit = function() {
|
||||
return this.opts.max_user_templates || 0;
|
||||
};
|
||||
|
||||
@@ -70,7 +68,7 @@ o._userTemplateLimit = function() {
|
||||
* @param redisArgs - the arguments for the redis function in an array
|
||||
* @param callback - function to pass results too.
|
||||
*/
|
||||
o._redisCmd = function(redisFunc, redisArgs, callback) {
|
||||
TemplateMaps.prototype._redisCmd = function(redisFunc, redisArgs, callback) {
|
||||
var redisClient;
|
||||
var that = this;
|
||||
var db = that.db_signatures;
|
||||
@@ -97,7 +95,7 @@ o._redisCmd = function(redisFunc, redisArgs, callback) {
|
||||
var _reValidNameIdentifier = /^[a-z0-9][0-9a-z_\-]*$/i;
|
||||
var _reValidPlaceholderIdentifier = /^[a-z][0-9a-z_]*$/i;
|
||||
// jshint maxcomplexity:15
|
||||
o._checkInvalidTemplate = function(template) {
|
||||
TemplateMaps.prototype._checkInvalidTemplate = function(template) {
|
||||
if ( template.version !== '0.0.1' ) {
|
||||
return new Error("Unsupported template version " + template.version);
|
||||
}
|
||||
@@ -200,7 +198,7 @@ function templateDefaults(template) {
|
||||
// @param callback function(err, tpl_id)
|
||||
// Return template identifier (only valid for given user)
|
||||
//
|
||||
o.addTemplate = function(owner, template, callback) {
|
||||
TemplateMaps.prototype.addTemplate = function(owner, template, callback) {
|
||||
var self = this;
|
||||
|
||||
template = templateDefaults(template);
|
||||
@@ -258,7 +256,7 @@ o.addTemplate = function(owner, template, callback) {
|
||||
//
|
||||
// @param callback function(err)
|
||||
//
|
||||
o.delTemplate = function(owner, tpl_id, callback) {
|
||||
TemplateMaps.prototype.delTemplate = function(owner, tpl_id, callback) {
|
||||
var self = this;
|
||||
step(
|
||||
function deleteTemplate() {
|
||||
@@ -297,7 +295,8 @@ o.delTemplate = function(owner, tpl_id, callback) {
|
||||
//
|
||||
// @param callback function(err)
|
||||
//
|
||||
o.updTemplate = function(owner, tpl_id, template, callback) {
|
||||
TemplateMaps.prototype.updTemplate = function(owner, tpl_id, template, callback) {
|
||||
|
||||
var self = this;
|
||||
|
||||
template = templateDefaults(template);
|
||||
@@ -356,7 +355,7 @@ o.updTemplate = function(owner, tpl_id, template, callback) {
|
||||
// @param callback function(err, tpl_id_list)
|
||||
// Returns a list of template identifiers
|
||||
//
|
||||
o.listTemplates = function(owner, callback) {
|
||||
TemplateMaps.prototype.listTemplates = function(owner, callback) {
|
||||
this._redisCmd('HKEYS', [ this.key_usr_tpl({owner:owner}) ], callback);
|
||||
};
|
||||
|
||||
@@ -370,7 +369,7 @@ o.listTemplates = function(owner, callback) {
|
||||
// @param callback function(err, template)
|
||||
// Return full template definition
|
||||
//
|
||||
o.getTemplate = function(owner, tpl_id, callback) {
|
||||
TemplateMaps.prototype.getTemplate = function(owner, tpl_id, callback) {
|
||||
var self = this;
|
||||
step(
|
||||
function getTemplate() {
|
||||
@@ -386,7 +385,7 @@ o.getTemplate = function(owner, tpl_id, callback) {
|
||||
);
|
||||
};
|
||||
|
||||
o.isAuthorized = function(template, authTokens) {
|
||||
TemplateMaps.prototype.isAuthorized = function(template, authTokens) {
|
||||
if (!template) {
|
||||
return false;
|
||||
}
|
||||
@@ -431,14 +430,18 @@ var _reNumber = /^([-+]?[\d\.]?\d+([eE][+-]?\d+)?)$/,
|
||||
_reCSSColorVal = /^#[0-9a-fA-F]{3,6}$/;
|
||||
|
||||
function _replaceVars (str, params) {
|
||||
//return _.template(str, params); // lazy way, possibly dangerous
|
||||
// Construct regular expressions for each param
|
||||
// Construct regular expressions for each param
|
||||
Object.keys(params).forEach(function(k) {
|
||||
str = str.replace(new RegExp("<%=\\s*" + k + "\\s*%>", "g"), params[k]);
|
||||
});
|
||||
return str;
|
||||
}
|
||||
o.instance = function(template, params) {
|
||||
|
||||
function isObject(val) {
|
||||
return ( _.isObject(val) && !_.isArray(val) && !_.isFunction(val));
|
||||
}
|
||||
|
||||
TemplateMaps.prototype.instance = function(template, params) {
|
||||
var all_params = {};
|
||||
var phold = template.placeholders || {};
|
||||
Object.keys(phold).forEach(function(k) {
|
||||
@@ -475,6 +478,13 @@ o.instance = function(template, params) {
|
||||
|
||||
// NOTE: we're deep-cloning the layergroup here
|
||||
var layergroup = JSON.parse(JSON.stringify(template.layergroup));
|
||||
|
||||
if (layergroup.buffersize && isObject(layergroup.buffersize)) {
|
||||
Object.keys(layergroup.buffersize).forEach(function(k) {
|
||||
layergroup.buffersize[k] = parseInt(_replaceVars(layergroup.buffersize[k], all_params), 10);
|
||||
});
|
||||
}
|
||||
|
||||
for (var i=0; i<layergroup.layers.length; ++i) {
|
||||
var lyropt = layergroup.layers[i].options;
|
||||
|
||||
@@ -500,7 +510,7 @@ o.instance = function(template, params) {
|
||||
};
|
||||
|
||||
// Return a fingerPrint of the object
|
||||
o.fingerPrint = function(template) {
|
||||
TemplateMaps.prototype.fingerPrint = function(template) {
|
||||
return crypto.createHash('md5')
|
||||
.update(JSON.stringify(template))
|
||||
.digest('hex')
|
||||
|
||||
@@ -6,8 +6,17 @@ dot.templateSettings.strip = false;
|
||||
function createTemplate(method) {
|
||||
return dot.template([
|
||||
'SELECT',
|
||||
'min({{=it._column}}) min_val,',
|
||||
'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'));
|
||||
}
|
||||
|
||||
@@ -29,17 +38,12 @@ methodTemplates.category = dot.template([
|
||||
' SELECT {{=it._column}} AS category, count(1) AS value, row_number() OVER (ORDER BY count(1) desc) as rank',
|
||||
' FROM ({{=it._sql}}) _cdb_aggregation_all',
|
||||
' GROUP BY {{=it._column}}',
|
||||
' ORDER BY 2 DESC',
|
||||
' ORDER BY 2 DESC, 1 ASC',
|
||||
'),',
|
||||
'agg_categories AS (',
|
||||
' SELECT \'__other\' category',
|
||||
' SELECT category',
|
||||
' FROM categories',
|
||||
' WHERE rank >= {{=it._buckets}}',
|
||||
' GROUP BY 1',
|
||||
' UNION ALL',
|
||||
' SELECT CAST(category AS text)',
|
||||
' FROM categories',
|
||||
' WHERE rank < {{=it._buckets}}',
|
||||
' WHERE rank <= {{=it._buckets}}',
|
||||
')',
|
||||
'SELECT array_agg(category) AS category FROM agg_categories'
|
||||
].join('\n'));
|
||||
@@ -54,9 +58,8 @@ var method2strategy = {
|
||||
category: STRATEGY.EXACT
|
||||
};
|
||||
|
||||
function PostgresDatasource (pgQueryRunner, username, query) {
|
||||
this.pgQueryRunner = pgQueryRunner;
|
||||
this.username = username;
|
||||
function PostgresDatasource (psql, query) {
|
||||
this.psql = psql;
|
||||
this.query = query;
|
||||
}
|
||||
|
||||
@@ -75,21 +78,39 @@ PostgresDatasource.prototype.getRamp = function (column, buckets, method, callba
|
||||
|
||||
var query = template({ _column: column, _sql: this.query, _buckets: buckets });
|
||||
|
||||
this.pgQueryRunner.run(this.username, query, function (err, result) {
|
||||
this.psql.query(query, function (err, resultSet) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var result = getResult(resultSet);
|
||||
var strategy = method2strategy[methodName];
|
||||
var ramp = result[0][methodName];
|
||||
var ramp = result[methodName] || [];
|
||||
var stats = {
|
||||
min_val: result.min_val,
|
||||
max_val: result.max_val,
|
||||
avg_val: result.avg_val
|
||||
};
|
||||
// Skip null values from ramp
|
||||
// Generated turbo-carto won't be correct, but better to keep it working than failing
|
||||
// TODO fix cartodb-postgres extension quantification functions
|
||||
ramp = ramp.filter(function(value) { return value !== null; });
|
||||
if (strategy !== STRATEGY.EXACT) {
|
||||
ramp = ramp.sort(function(a, b) {
|
||||
return a - b;
|
||||
});
|
||||
}
|
||||
|
||||
return callback(null, { ramp: ramp, strategy: strategy });
|
||||
});
|
||||
return callback(null, { ramp: ramp, strategy: strategy, stats: stats });
|
||||
}, true); // use read-only transaction
|
||||
};
|
||||
|
||||
function getResult(resultSet) {
|
||||
resultSet = resultSet || {};
|
||||
var result = resultSet.rows || [];
|
||||
result = result[0] || {};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = PostgresDatasource;
|
||||
169
lib/cartodb/controllers/analyses.js
Normal file
169
lib/cartodb/controllers/analyses.js
Normal file
@@ -0,0 +1,169 @@
|
||||
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);
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
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: []
|
||||
};
|
||||
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) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
return callback(null, results);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function dbParamsFromReqParams(params) {
|
||||
var dbParams = {};
|
||||
if ( params.dbuser ) {
|
||||
dbParams.user = params.dbuser;
|
||||
}
|
||||
if ( params.dbpassword ) {
|
||||
dbParams.pass = params.dbpassword;
|
||||
}
|
||||
if ( params.dbhost ) {
|
||||
dbParams.host = params.dbhost;
|
||||
}
|
||||
if ( params.dbport ) {
|
||||
dbParams.port = params.dbport;
|
||||
}
|
||||
if ( params.dbname ) {
|
||||
dbParams.dbname = params.dbname;
|
||||
}
|
||||
return dbParams;
|
||||
}
|
||||
@@ -17,16 +17,8 @@ var REQUEST_QUERY_PARAMS_WHITELIST = [
|
||||
'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'
|
||||
// analysis
|
||||
'filters' // json
|
||||
];
|
||||
|
||||
function BaseController(authApi, pgConnection) {
|
||||
@@ -36,7 +28,7 @@ function BaseController(authApi, pgConnection) {
|
||||
|
||||
module.exports = BaseController;
|
||||
|
||||
// jshint maxcomplexity:9
|
||||
// jshint maxcomplexity:10
|
||||
/**
|
||||
* Whitelist input and get database name & default geometry type from
|
||||
* subdomain/user metadata held in CartoDB Redis
|
||||
@@ -61,9 +53,7 @@ BaseController.prototype.req2params = function(req, callback){
|
||||
lzmaWorker.decompress(
|
||||
lzma,
|
||||
function(result) {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('lzma');
|
||||
}
|
||||
req.profiler.done('lzma');
|
||||
try {
|
||||
delete req.query.lzma;
|
||||
_.extend(req.query, JSON.parse(result));
|
||||
@@ -77,7 +67,11 @@ BaseController.prototype.req2params = function(req, callback){
|
||||
return;
|
||||
}
|
||||
|
||||
req.query = _.pick(req.query, REQUEST_QUERY_PARAMS_WHITELIST);
|
||||
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;
|
||||
@@ -115,18 +109,14 @@ BaseController.prototype.req2params = function(req, callback){
|
||||
// bring all query values onto req.params object
|
||||
_.extend(req.params, req.query);
|
||||
|
||||
if (req.profiler) {
|
||||
req.profiler.done('req2params.setup');
|
||||
}
|
||||
req.profiler.done('req2params.setup');
|
||||
|
||||
step(
|
||||
function getPrivacy(){
|
||||
self.authApi.authorize(req, this);
|
||||
},
|
||||
function validateAuthorization(err, authorized) {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('authorize');
|
||||
}
|
||||
req.profiler.done('authorize');
|
||||
assert.ifError(err);
|
||||
if(!authorized) {
|
||||
err = new Error("Sorry, you are unauthorized (permission denied)");
|
||||
@@ -167,9 +157,7 @@ BaseController.prototype.send = function(req, res, body, status, headers) {
|
||||
res.set('X-Served-By-DB-Host', req.params.dbhost);
|
||||
}
|
||||
|
||||
if (req.profiler) {
|
||||
res.set('X-Tiler-Profiler', req.profiler.toJSONString());
|
||||
}
|
||||
res.set('X-Tiler-Profiler', req.profiler.toJSONString());
|
||||
|
||||
if (headers) {
|
||||
res.set(headers);
|
||||
@@ -187,26 +175,31 @@ BaseController.prototype.send = function(req, res, body, status, headers) {
|
||||
res.send(body);
|
||||
}
|
||||
|
||||
if (req.profiler) {
|
||||
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);
|
||||
}
|
||||
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];
|
||||
|
||||
allErrors = populateTimeoutErrors(allErrors);
|
||||
|
||||
label = label || 'UNKNOWN';
|
||||
err = allErrors[0] || new Error(label);
|
||||
allErrors[0] = err;
|
||||
|
||||
var statusCode = findStatusCode(err);
|
||||
|
||||
if (err.message === 'Tile does not exist' && req.params.format === 'mvt') {
|
||||
statusCode = 204;
|
||||
}
|
||||
|
||||
debug('[%s ERROR] -- %d: %s, %s', label, statusCode, err, err.stack);
|
||||
|
||||
// If a callback was requested, force status to 200
|
||||
@@ -214,15 +207,15 @@ BaseController.prototype.sendError = function(req, res, err, label) {
|
||||
statusCode = 200;
|
||||
}
|
||||
|
||||
var errorResponseBody = { errors: allErrors.map(errorMessage) };
|
||||
var errorResponseBody = {
|
||||
errors: allErrors.map(errorMessage),
|
||||
errors_with_context: allErrors.map(errorMessageWithContext)
|
||||
};
|
||||
|
||||
this.send(req, res, errorResponseBody, statusCode);
|
||||
};
|
||||
|
||||
function errorMessage(err) {
|
||||
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/68
|
||||
var message = (_.isString(err) ? err : err.message) || 'Unknown error';
|
||||
|
||||
function stripConnectionInfo(message) {
|
||||
// Strip connection info, if any
|
||||
return message
|
||||
// See https://github.com/CartoDB/Windshaft/issues/173
|
||||
@@ -230,6 +223,44 @@ function errorMessage(err) {
|
||||
// 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 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) && shouldBeExposed(prop)) {
|
||||
error[prop] = err[prop];
|
||||
}
|
||||
}
|
||||
|
||||
return error;
|
||||
}
|
||||
module.exports.errorMessage = errorMessage;
|
||||
|
||||
function findStatusCode(err) {
|
||||
@@ -265,5 +296,38 @@ function statusFromErrorMessage(errMsg) {
|
||||
statusCode = 404;
|
||||
}
|
||||
}
|
||||
|
||||
return statusCode;
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
module.exports = {
|
||||
Analyses: require('./analyses'),
|
||||
Layergroup: require('./layergroup'),
|
||||
Map: require('./map'),
|
||||
NamedMaps: require('./named_maps'),
|
||||
|
||||
@@ -6,6 +6,7 @@ var BaseController = require('./base');
|
||||
|
||||
var cors = require('../middleware/cors');
|
||||
var userMiddleware = require('../middleware/user');
|
||||
var allowQueryParams = require('../middleware/allow-query-params');
|
||||
|
||||
var DataviewBackend = require('../backends/dataview');
|
||||
var AnalysisStatusBackend = require('../backends/analysis-status');
|
||||
@@ -67,28 +68,62 @@ LayergroupController.prototype.register = function(app) {
|
||||
this.attributes.bind(this));
|
||||
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/static/center/:token/:z/:lat/:lng/:width/:height.:format', cors(), userMiddleware,
|
||||
'/static/center/:token/:z/:lat/:lng/:width/:height.:format',
|
||||
cors(), userMiddleware, allowQueryParams(['layer']),
|
||||
this.center.bind(this));
|
||||
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/static/bbox/:token/:west,:south,:east,:north/:width/:height.:format', cors(), userMiddleware,
|
||||
'/static/bbox/:token/:west,:south,:east,:north/:width/:height.:format',
|
||||
cors(), userMiddleware, allowQueryParams(['layer']),
|
||||
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
|
||||
];
|
||||
|
||||
app.get(
|
||||
app.base_url_mapconfig + '/:token/dataview/:dataviewName',
|
||||
cors(),
|
||||
userMiddleware,
|
||||
allowQueryParams(allowedDataviewQueryParams),
|
||||
this.dataview.bind(this)
|
||||
);
|
||||
|
||||
app.get(
|
||||
app.base_url_mapconfig + '/:token/:layer/widget/:dataviewName',
|
||||
cors(),
|
||||
userMiddleware,
|
||||
allowQueryParams(allowedDataviewQueryParams),
|
||||
this.dataview.bind(this)
|
||||
);
|
||||
|
||||
app.get(
|
||||
app.base_url_mapconfig + '/:token/dataview/:dataviewName/search',
|
||||
cors(),
|
||||
userMiddleware,
|
||||
allowQueryParams(allowedDataviewQueryParams),
|
||||
this.dataviewSearch.bind(this)
|
||||
);
|
||||
|
||||
app.get(
|
||||
app.base_url_mapconfig + '/:token/:layer/widget/:dataviewName/search',
|
||||
cors(),
|
||||
userMiddleware,
|
||||
allowQueryParams(allowedDataviewQueryParams),
|
||||
this.dataviewSearch.bind(this)
|
||||
);
|
||||
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/:token/analysis/node/:nodeId', cors(), userMiddleware,
|
||||
@@ -250,7 +285,9 @@ LayergroupController.prototype.finalizeGetTileOrGrid = function(err, req, res, t
|
||||
grid_json: true,
|
||||
json_torque: true,
|
||||
torque_json: true,
|
||||
png: true
|
||||
png: true,
|
||||
png32: true,
|
||||
mvt: true
|
||||
};
|
||||
|
||||
var formatStat = 'invalid';
|
||||
@@ -338,6 +375,8 @@ LayergroupController.prototype.staticMap = function(req, res, width, height, zoo
|
||||
LayergroupController.prototype.sendResponse = function(req, res, body, status, headers) {
|
||||
var self = this;
|
||||
|
||||
req.profiler.done('res');
|
||||
|
||||
res.set('Cache-Control', 'public,max-age=31536000');
|
||||
|
||||
// Set Last-Modified header
|
||||
@@ -386,13 +425,15 @@ LayergroupController.prototype.getAffectedTables = function(user, dbName, layerg
|
||||
function getSQL(err, mapConfig) {
|
||||
assert.ifError(err);
|
||||
|
||||
var queries = mapConfig.getLayers()
|
||||
.map(function(lyr) {
|
||||
return lyr.options.sql;
|
||||
})
|
||||
.filter(function(sql) {
|
||||
return !!sql;
|
||||
});
|
||||
var queries = [];
|
||||
mapConfig.getLayers().forEach(function(layer) {
|
||||
queries.push(layer.options.sql);
|
||||
if (layer.options.affected_tables) {
|
||||
layer.options.affected_tables.map(function(table) {
|
||||
queries.push('SELECT * FROM ' + table + ' LIMIT 0');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return queries.length ? queries.join(';') : null;
|
||||
},
|
||||
|
||||
@@ -4,6 +4,8 @@ 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');
|
||||
|
||||
@@ -18,6 +20,7 @@ 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
|
||||
@@ -28,10 +31,12 @@ var CreateLayergroupMapConfigProvider = require('../models/mapconfig/provider/cr
|
||||
* @param {UserLimitsApi} userLimitsApi
|
||||
* @param {LayergroupAffectedTables} layergroupAffectedTables
|
||||
* @param {MapConfigAdapter} mapConfigAdapter
|
||||
* @param {StatsBackend} statsBackend
|
||||
* @constructor
|
||||
*/
|
||||
function MapController(authApi, pgConnection, templateMaps, mapBackend, metadataBackend,
|
||||
surrogateKeysCache, userLimitsApi, layergroupAffectedTables, mapConfigAdapter) {
|
||||
surrogateKeysCache, userLimitsApi, layergroupAffectedTables, mapConfigAdapter,
|
||||
statsBackend) {
|
||||
|
||||
BaseController.call(this, authApi, pgConnection);
|
||||
|
||||
@@ -44,6 +49,9 @@ function MapController(authApi, pgConnection, templateMaps, mapBackend, metadata
|
||||
this.layergroupAffectedTables = layergroupAffectedTables;
|
||||
|
||||
this.mapConfigAdapter = mapConfigAdapter;
|
||||
this.resourceLocator = new ResourceLocator(global.environment);
|
||||
|
||||
this.statsBackend = statsBackend;
|
||||
}
|
||||
|
||||
util.inherits(MapController, BaseController);
|
||||
@@ -84,9 +92,7 @@ MapController.prototype.createPost = function(req, res) {
|
||||
};
|
||||
|
||||
MapController.prototype.instantiate = function(req, res) {
|
||||
if (req.profiler) {
|
||||
req.profiler.start('windshaft-cartodb.instance_template_post');
|
||||
}
|
||||
req.profiler.start('windshaft-cartodb.instance_template_post');
|
||||
|
||||
this.instantiateTemplate(req, res, function prepareTemplateParams(callback) {
|
||||
if (!req.is('application/json')) {
|
||||
@@ -97,9 +103,7 @@ MapController.prototype.instantiate = function(req, res) {
|
||||
};
|
||||
|
||||
MapController.prototype.jsonp = function(req, res) {
|
||||
if (req.profiler) {
|
||||
req.profiler.start('windshaft-cartodb.instance_template_get');
|
||||
}
|
||||
req.profiler.start('windshaft-cartodb.instance_template_get');
|
||||
|
||||
this.instantiateTemplate(req, res, function prepareJsonTemplateParams(callback) {
|
||||
var err = null;
|
||||
@@ -135,6 +139,7 @@ MapController.prototype.create = function(req, res, prepareConfigFn) {
|
||||
function prepareAdapterMapConfig(err, requestMapConfig) {
|
||||
assert.ifError(err);
|
||||
context.analysisConfiguration = {
|
||||
user: req.context.user,
|
||||
db: {
|
||||
host: req.params.dbhost,
|
||||
port: req.params.dbport,
|
||||
@@ -161,15 +166,34 @@ MapController.prototype.create = function(req, res, prepareConfigFn) {
|
||||
},
|
||||
function afterLayergroupCreate(err, layergroup) {
|
||||
assert.ifError(err);
|
||||
self.afterLayergroupCreate(req, res, mapConfig, layergroup, this);
|
||||
self.afterLayergroupCreate(req, res, mapConfig, layergroup, context.analysesResults, this);
|
||||
},
|
||||
function finish(err, layergroup) {
|
||||
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 || [];
|
||||
addDataviewsAndWidgetsUrls(req.context.user, layergroup, mapConfig.obj());
|
||||
addAnalysesMetadata(req.context.user, layergroup, analysesResults, true);
|
||||
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);
|
||||
}
|
||||
@@ -177,6 +201,17 @@ MapController.prototype.create = function(req, res, prepareConfigFn) {
|
||||
);
|
||||
};
|
||||
|
||||
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.instantiateTemplate = function(req, res, prepareParamsFn) {
|
||||
var self = this;
|
||||
|
||||
@@ -184,7 +219,6 @@ MapController.prototype.instantiateTemplate = function(req, res, prepareParamsFn
|
||||
|
||||
var mapConfigProvider;
|
||||
var mapConfig;
|
||||
|
||||
step(
|
||||
function setupParams(){
|
||||
self.req2params(req, this);
|
||||
@@ -219,7 +253,9 @@ MapController.prototype.instantiateTemplate = function(req, res, prepareParamsFn
|
||||
},
|
||||
function afterLayergroupCreate(err, layergroup) {
|
||||
assert.ifError(err);
|
||||
self.afterLayergroupCreate(req, res, mapConfig, layergroup, this);
|
||||
self.afterLayergroupCreate(req, res, mapConfig, layergroup,
|
||||
mapConfigProvider.analysesResults,
|
||||
this);
|
||||
},
|
||||
function finishTemplateInstantiation(err, layergroup) {
|
||||
if (err) {
|
||||
@@ -228,8 +264,10 @@ MapController.prototype.instantiateTemplate = function(req, res, prepareParamsFn
|
||||
var templateHash = self.templateMaps.fingerPrint(mapConfigProvider.template).substring(0, 8);
|
||||
layergroup.layergroupid = cdbuser + '@' + templateHash + '@' + layergroup.layergroupid;
|
||||
|
||||
addDataviewsAndWidgetsUrls(cdbuser, layergroup, mapConfig.obj());
|
||||
addAnalysesMetadata(cdbuser, layergroup, mapConfigProvider.analysesResults);
|
||||
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()));
|
||||
@@ -240,7 +278,8 @@ MapController.prototype.instantiateTemplate = function(req, res, prepareParamsFn
|
||||
);
|
||||
};
|
||||
|
||||
MapController.prototype.afterLayergroupCreate = function(req, res, mapconfig, layergroup, callback) {
|
||||
MapController.prototype.afterLayergroupCreate =
|
||||
function(req, res, mapconfig, layergroup, analysesResults, callback) {
|
||||
var self = this;
|
||||
|
||||
var username = req.context.user;
|
||||
@@ -267,21 +306,26 @@ MapController.prototype.afterLayergroupCreate = function(req, res, mapconfig, la
|
||||
// take place before proceeding. Error will be logged
|
||||
// asynchronously
|
||||
this.metadataBackend.incMapviewCount(username, mapconfig.obj().stat_tag, function(err) {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('incMapviewCount');
|
||||
}
|
||||
req.profiler.done('incMapviewCount');
|
||||
if ( err ) {
|
||||
global.logger.log("ERROR: failed to increment mapview count for user '" + username + "': " + err);
|
||||
}
|
||||
done();
|
||||
});
|
||||
|
||||
var sql = mapconfig.getLayers().map(function(layer) {
|
||||
return layer.options.sql;
|
||||
}).join(';');
|
||||
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() {
|
||||
@@ -289,19 +333,21 @@ MapController.prototype.afterLayergroupCreate = function(req, res, mapconfig, la
|
||||
},
|
||||
function getAffectedTablesAndLastUpdatedTime(err, connection) {
|
||||
assert.ifError(err);
|
||||
QueryTables.getAffectedTablesFromQuery(connection, sql, this);
|
||||
dbConnection = connection;
|
||||
QueryTables.getAffectedTablesFromQuery(dbConnection, sql.join(';'), this);
|
||||
},
|
||||
function handleAffectedTablesAndLastUpdatedTime(err, result) {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('queryTablesAndLastUpdated');
|
||||
}
|
||||
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 + ':' + result.getLastUpdatedAt();
|
||||
layergroup.last_updated = new Date(result.getLastUpdatedAt()).toISOString();
|
||||
layergroup.layergroupid = layergroup.layergroupid + ':' + lastUpdateTime;
|
||||
layergroup.last_updated = new Date(lastUpdateTime).toISOString();
|
||||
|
||||
if (req.method === 'GET') {
|
||||
var ttl = global.environment.varnish.layergroupTtl || 86400;
|
||||
@@ -315,57 +361,89 @@ MapController.prototype.afterLayergroupCreate = function(req, res, mapconfig, la
|
||||
|
||||
return null;
|
||||
},
|
||||
function fetchLayersStats(err) {
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
self.statsBackend.getStats(mapconfig, dbConnection, function(err, layersStats) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
if (layersStats.length > 0) {
|
||||
layergroup.metadata.layers.forEach(function (layer, index) {
|
||||
layer.meta.stats = layersStats[index];
|
||||
});
|
||||
}
|
||||
return next();
|
||||
});
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
function addAnalysesMetadata(username, layergroup, analysesResults, includeQuery) {
|
||||
function getLastUpdatedTime(analysesResults, lastUpdateTime) {
|
||||
if (!Array.isArray(analysesResults)) {
|
||||
return lastUpdateTime;
|
||||
}
|
||||
return analysesResults.reduce(function(lastUpdateTime, analysis) {
|
||||
return analysis.getNodes().reduce(function(lastNodeUpdatedAtTime, node) {
|
||||
var nodeUpdatedAtDate = node.getUpdatedAt();
|
||||
var nodeUpdatedTimeAt = (nodeUpdatedAtDate && nodeUpdatedAtDate.getTime()) || 0;
|
||||
return nodeUpdatedTimeAt > lastNodeUpdatedAtTime ? nodeUpdatedTimeAt : lastNodeUpdatedAtTime;
|
||||
}, lastUpdateTime);
|
||||
}, lastUpdateTime);
|
||||
}
|
||||
|
||||
MapController.prototype.addAnalysesMetadata = function(username, layergroup, analysesResults, includeQuery) {
|
||||
includeQuery = includeQuery || false;
|
||||
analysesResults = analysesResults || [];
|
||||
layergroup.metadata.analyses = [];
|
||||
|
||||
analysesResults.forEach(function(analysis) {
|
||||
var nodes = analysis.getSortedNodes();
|
||||
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();
|
||||
nodesIdMap[node.params.id] = {
|
||||
var nodeRepr = {
|
||||
status: node.getStatus(),
|
||||
url: getUrls(username, nodeResource)
|
||||
url: this.resourceLocator.getUrls(username, nodeResource)
|
||||
};
|
||||
if (includeQuery) {
|
||||
nodesIdMap[node.params.id].query = node.getQuery();
|
||||
nodeRepr.query = node.getQuery();
|
||||
}
|
||||
if (node.getStatus() === 'failed') {
|
||||
nodeRepr.error_message = node.getErrorMessage();
|
||||
}
|
||||
nodesIdMap[node.params.id] = nodeRepr;
|
||||
}
|
||||
|
||||
return nodesIdMap;
|
||||
}, {})
|
||||
}.bind(this), {})
|
||||
});
|
||||
});
|
||||
}
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
// TODO this should take into account several URL patterns
|
||||
function addDataviewsAndWidgetsUrls(username, layergroup, mapConfig) {
|
||||
addDataviewsUrls(username, layergroup, mapConfig);
|
||||
addWidgetsUrl(username, layergroup, mapConfig);
|
||||
}
|
||||
MapController.prototype.addDataviewsAndWidgetsUrls = function(username, layergroup, mapConfig) {
|
||||
this.addDataviewsUrls(username, layergroup, mapConfig);
|
||||
this.addWidgetsUrl(username, layergroup, mapConfig);
|
||||
};
|
||||
|
||||
function addDataviewsUrls(username, layergroup, mapConfig) {
|
||||
MapController.prototype.addDataviewsUrls = function(username, layergroup, mapConfig) {
|
||||
layergroup.metadata.dataviews = layergroup.metadata.dataviews || {};
|
||||
var dataviews = mapConfig.dataviews || {};
|
||||
|
||||
Object.keys(dataviews).forEach(function(dataviewName) {
|
||||
var resource = layergroup.layergroupid + '/dataview/' + dataviewName;
|
||||
layergroup.metadata.dataviews[dataviewName] = {
|
||||
url: getUrls(username, resource)
|
||||
url: this.resourceLocator.getUrls(username, resource)
|
||||
};
|
||||
});
|
||||
}
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
function addWidgetsUrl(username, layergroup, mapConfig) {
|
||||
MapController.prototype.addWidgetsUrl = function(username, layergroup, mapConfig) {
|
||||
if (layergroup.metadata && Array.isArray(layergroup.metadata.layers) && Array.isArray(mapConfig.layers)) {
|
||||
layergroup.metadata.layers = layergroup.metadata.layers.map(function(layer, layerIndex) {
|
||||
var mapConfigLayer = mapConfig.layers[layerIndex];
|
||||
@@ -375,26 +453,11 @@ function addWidgetsUrl(username, layergroup, mapConfig) {
|
||||
var resource = layergroup.layergroupid + '/' + layerIndex + '/widget/' + widgetName;
|
||||
layer.widgets[widgetName] = {
|
||||
type: mapConfigLayer.options.widgets[widgetName].type,
|
||||
url: getUrls(username, resource)
|
||||
url: this.resourceLocator.getUrls(username, resource)
|
||||
};
|
||||
});
|
||||
}.bind(this));
|
||||
}
|
||||
return layer;
|
||||
});
|
||||
}.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
function getUrls(username, resource) {
|
||||
var cdnUrl = global.environment.serverMetadata && global.environment.serverMetadata.cdn_url;
|
||||
if (cdnUrl) {
|
||||
return {
|
||||
http: 'http://' + cdnUrl.http + '/' + username + '/api/v1/map/' + resource,
|
||||
https: 'https://' + cdnUrl.https + '/' + username + '/api/v1/map/' + resource
|
||||
};
|
||||
} else {
|
||||
var port = global.environment.port;
|
||||
return {
|
||||
http: 'http://' + username + '.' + 'localhost.lan:' + port + '/api/v1/map/' + resource
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ var BaseController = require('./base');
|
||||
|
||||
var cors = require('../middleware/cors');
|
||||
var userMiddleware = require('../middleware/user');
|
||||
var allowQueryParams = require('../middleware/allow-query-params');
|
||||
|
||||
function NamedMapsController(authApi, pgConnection, namedMapProviderCache, tileBackend, previewBackend,
|
||||
surrogateKeysCache, tablesExtentApi, metadataBackend) {
|
||||
@@ -31,7 +32,7 @@ NamedMapsController.prototype.register = function(app) {
|
||||
this.tile.bind(this));
|
||||
|
||||
app.get(app.base_url_mapconfig +
|
||||
'/static/named/:template_id/:width/:height.:format', cors(), userMiddleware,
|
||||
'/static/named/:template_id/:width/:height.:format', cors(), userMiddleware, allowQueryParams(['layer']),
|
||||
this.staticMap.bind(this));
|
||||
};
|
||||
|
||||
@@ -100,9 +101,7 @@ NamedMapsController.prototype.tile = function(req, res) {
|
||||
self.tileBackend.getTile(namedMapProvider, req.params, this);
|
||||
},
|
||||
function handleImage(err, tile, headers, stats) {
|
||||
if (req.profiler) {
|
||||
req.profiler.add(stats);
|
||||
}
|
||||
req.profiler.add(stats);
|
||||
if (err) {
|
||||
self.sendError(req, res, err, 'NAMED_MAP_TILE');
|
||||
} else {
|
||||
@@ -137,9 +136,15 @@ NamedMapsController.prototype.staticMap = function(req, res) {
|
||||
this
|
||||
);
|
||||
},
|
||||
function prepareImageOptions(err, _namedMapProvider) {
|
||||
function prepareLayerVisibility(err, _namedMapProvider) {
|
||||
assert.ifError(err);
|
||||
|
||||
namedMapProvider = _namedMapProvider;
|
||||
|
||||
self.prepareLayerFilterFromPreviewLayers(cdbUser, req, namedMapProvider, this);
|
||||
},
|
||||
function prepareImageOptions(err) {
|
||||
assert.ifError(err);
|
||||
self.getStaticImageOptions(cdbUser, req.params, namedMapProvider, this);
|
||||
},
|
||||
function getImage(err, imageOpts) {
|
||||
@@ -170,10 +175,8 @@ NamedMapsController.prototype.staticMap = function(req, res) {
|
||||
});
|
||||
},
|
||||
function handleImage(err, image, headers, stats) {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('render-' + format);
|
||||
req.profiler.add(stats || {});
|
||||
}
|
||||
req.profiler.done('render-' + format);
|
||||
req.profiler.add(stats || {});
|
||||
|
||||
if (err) {
|
||||
self.sendError(req, res, err, 'STATIC_VIZ_MAP');
|
||||
@@ -184,6 +187,45 @@ NamedMapsController.prototype.staticMap = function(req, res) {
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.prepareLayerFilterFromPreviewLayers = function (user, req, namedMapProvider, callback) {
|
||||
var self = this;
|
||||
namedMapProvider.getTemplate(function (err, template) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!template || !template.view || !template.view.preview_layers) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
var previewLayers = template.view.preview_layers;
|
||||
var layerVisibilityFilter = [];
|
||||
|
||||
template.layergroup.layers.forEach(function (layer, index) {
|
||||
if (previewLayers[''+index] !== false && previewLayers[layer.id] !== false) {
|
||||
layerVisibilityFilter.push(''+index);
|
||||
}
|
||||
});
|
||||
|
||||
if (!layerVisibilityFilter.length) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
// overwrites 'all' default filter
|
||||
req.params.layer = layerVisibilityFilter.join(',');
|
||||
|
||||
// recreates the provider
|
||||
self.namedMapProviderCache.get(
|
||||
user,
|
||||
req.params.template_id,
|
||||
req.query.config,
|
||||
req.query.auth_token,
|
||||
req.params,
|
||||
callback
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
var DEFAULT_ZOOM_CENTER = {
|
||||
zoom: 1,
|
||||
center: {
|
||||
|
||||
@@ -91,9 +91,7 @@ NamedMapsAdminController.prototype.update = function(req, res) {
|
||||
NamedMapsAdminController.prototype.retrieve = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
if (req.profiler) {
|
||||
req.profiler.start('windshaft-cartodb.get_template');
|
||||
}
|
||||
req.profiler.start('windshaft-cartodb.get_template');
|
||||
|
||||
var cdbuser = req.context.user;
|
||||
var tpl_id;
|
||||
@@ -127,9 +125,7 @@ NamedMapsAdminController.prototype.retrieve = function(req, res) {
|
||||
NamedMapsAdminController.prototype.destroy = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
if (req.profiler) {
|
||||
req.profiler.start('windshaft-cartodb.delete_template');
|
||||
}
|
||||
req.profiler.start('windshaft-cartodb.delete_template');
|
||||
|
||||
var cdbuser = req.context.user;
|
||||
var tpl_id;
|
||||
@@ -154,9 +150,7 @@ NamedMapsAdminController.prototype.destroy = function(req, res) {
|
||||
|
||||
NamedMapsAdminController.prototype.list = function(req, res) {
|
||||
var self = this;
|
||||
if ( req.profiler ) {
|
||||
req.profiler.start('windshaft-cartodb.get_template_list');
|
||||
}
|
||||
req.profiler.start('windshaft-cartodb.get_template_list');
|
||||
|
||||
var cdbuser = req.context.user;
|
||||
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
var windshaft = require('windshaft');
|
||||
var HealthCheck = require('../monitoring/health_check');
|
||||
|
||||
var WELCOME_MSG = "This is the CartoDB Maps API, " +
|
||||
"see the documentation at http://docs.cartodb.com/cartodb-platform/maps-api.html";
|
||||
|
||||
|
||||
var versions = {
|
||||
windshaft: windshaft.version,
|
||||
grainstore: windshaft.grainstore.version(),
|
||||
node_mapnik: windshaft.mapnik.version,
|
||||
mapnik: windshaft.mapnik.versions.mapnik,
|
||||
windshaft_cartodb: require('../../../package.json').version
|
||||
};
|
||||
|
||||
function ServerInfoController() {
|
||||
function ServerInfoController(versions) {
|
||||
this.healthConfig = global.environment.health || {};
|
||||
this.healthCheck = new HealthCheck(global.environment.disabled_file);
|
||||
this.versions = versions || {};
|
||||
}
|
||||
|
||||
module.exports = ServerInfoController;
|
||||
@@ -31,7 +23,7 @@ ServerInfoController.prototype.welcome = function(req, res) {
|
||||
};
|
||||
|
||||
ServerInfoController.prototype.version = function(req, res) {
|
||||
res.status(200).send(versions);
|
||||
res.status(200).send(this.versions);
|
||||
};
|
||||
|
||||
ServerInfoController.prototype.health = function(req, res) {
|
||||
|
||||
9
lib/cartodb/middleware/allow-query-params.js
Normal file
9
lib/cartodb/middleware/allow-query-params.js
Normal file
@@ -0,0 +1,9 @@
|
||||
module.exports = function allowQueryParams(params) {
|
||||
if (!Array.isArray(params)) {
|
||||
throw new Error('allowQueryParams must receive an Array of params');
|
||||
}
|
||||
return function allowQueryParamsMiddleware(req, res, next) {
|
||||
req.context.allowedQueryParams = params;
|
||||
next();
|
||||
};
|
||||
};
|
||||
@@ -5,11 +5,32 @@ var debug = require('debug')('windshaft:widget:aggregation');
|
||||
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',
|
||||
' count(1) 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'));
|
||||
@@ -18,35 +39,51 @@ 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',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var categoriesSummaryQueryTpl = dot.template([
|
||||
'categories_summary AS(',
|
||||
' SELECT count(1) categories_count, max(value) max_val, min(value) min_val',
|
||||
var categoriesSummaryMinMaxQueryTpl = dot.template([
|
||||
'categories_summary_min_max AS(',
|
||||
' SELECT max(value) max_val, min(value) min_val',
|
||||
' FROM categories',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
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'));
|
||||
|
||||
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',
|
||||
'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',
|
||||
' FROM categories, summary, categories_summary',
|
||||
'SELECT \'Other\' category, {{=it._aggregationFn}}(value) as 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',
|
||||
'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 ({{=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{{?it._isFloatColumn}}, nans_count, infinities_count{{?}}',
|
||||
'ORDER BY value DESC'
|
||||
].join('\n'));
|
||||
|
||||
@@ -54,7 +91,10 @@ var CATEGORIES_LIMIT = 6;
|
||||
|
||||
var VALID_OPERATIONS = {
|
||||
count: [],
|
||||
sum: ['aggregationColumn']
|
||||
sum: ['aggregationColumn'],
|
||||
avg: ['aggregationColumn'],
|
||||
min: ['aggregationColumn'],
|
||||
max: ['aggregationColumn']
|
||||
};
|
||||
|
||||
var TYPE = 'aggregation';
|
||||
@@ -68,7 +108,7 @@ var TYPE = 'aggregation';
|
||||
}
|
||||
}
|
||||
*/
|
||||
function Aggregation(query, options) {
|
||||
function Aggregation(query, options, queries) {
|
||||
if (!_.isString(options.column)) {
|
||||
throw new Error('Aggregation expects `column` in widget options');
|
||||
}
|
||||
@@ -92,9 +132,11 @@ function Aggregation(query, options) {
|
||||
BaseWidget.apply(this);
|
||||
|
||||
this.query = query;
|
||||
this.queries = queries;
|
||||
this.column = options.column;
|
||||
this.aggregation = options.aggregation;
|
||||
this.aggregationColumn = options.aggregationColumn;
|
||||
this._isFloatColumn = null;
|
||||
}
|
||||
|
||||
Aggregation.prototype = new BaseWidget();
|
||||
@@ -103,33 +145,39 @@ Aggregation.prototype.constructor = Aggregation;
|
||||
module.exports = Aggregation;
|
||||
|
||||
Aggregation.prototype.sql = function(psql, override, callback) {
|
||||
var self = this;
|
||||
|
||||
if (!callback) {
|
||||
callback = override;
|
||||
override = {};
|
||||
}
|
||||
|
||||
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 _query = this.query;
|
||||
|
||||
var aggregationSql;
|
||||
|
||||
if (!!override.ownFilter) {
|
||||
aggregationSql = [
|
||||
"WITH",
|
||||
[
|
||||
summaryQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column
|
||||
}),
|
||||
rankedCategoriesQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column,
|
||||
_aggregation: this.getAggregationSql()
|
||||
}),
|
||||
categoriesSummaryQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column
|
||||
})
|
||||
].join(',\n'),
|
||||
this.getCategoriesCTESql(
|
||||
_query,
|
||||
this.column,
|
||||
this.aggregation,
|
||||
this.aggregationColumn,
|
||||
this._isFloatColumn
|
||||
),
|
||||
aggregationQueryTpl({
|
||||
_isFloatColumn: this._isFloatColumn,
|
||||
_query: _query,
|
||||
_column: this.column,
|
||||
_aggregation: this.getAggregationSql(),
|
||||
@@ -138,25 +186,18 @@ Aggregation.prototype.sql = function(psql, override, callback) {
|
||||
].join('\n');
|
||||
} else {
|
||||
aggregationSql = [
|
||||
"WITH",
|
||||
[
|
||||
summaryQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column
|
||||
}),
|
||||
rankedCategoriesQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column,
|
||||
_aggregation: this.getAggregationSql()
|
||||
}),
|
||||
categoriesSummaryQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column
|
||||
})
|
||||
].join(',\n'),
|
||||
this.getCategoriesCTESql(
|
||||
_query,
|
||||
this.column,
|
||||
this.aggregation,
|
||||
this.aggregationColumn,
|
||||
this._isFloatColumn
|
||||
),
|
||||
rankedAggregationQueryTpl({
|
||||
_isFloatColumn: this._isFloatColumn,
|
||||
_query: _query,
|
||||
_column: this.column,
|
||||
_aggregationFn: this.aggregation !== 'count' ? this.aggregation : 'sum',
|
||||
_limit: CATEGORIES_LIMIT
|
||||
})
|
||||
].join('\n');
|
||||
@@ -167,6 +208,40 @@ Aggregation.prototype.sql = function(psql, override, callback) {
|
||||
return callback(null, aggregationSql);
|
||||
};
|
||||
|
||||
Aggregation.prototype.getCategoriesCTESql = function(query, column, aggregation, aggregationColumn, isFloatColumn) {
|
||||
return [
|
||||
"WITH",
|
||||
[
|
||||
filteredQueryTpl({
|
||||
_isFloatColumn: isFloatColumn,
|
||||
_query: this.query,
|
||||
_column: this.column,
|
||||
_aggregationColumn: aggregation !== 'count' ? aggregationColumn : null
|
||||
}),
|
||||
summaryQueryTpl({
|
||||
_isFloatColumn: isFloatColumn,
|
||||
_query: query,
|
||||
_column: column,
|
||||
_aggregationColumn: aggregation !== 'count' ? aggregationColumn : null
|
||||
}),
|
||||
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({
|
||||
@@ -179,6 +254,8 @@ Aggregation.prototype.format = function(result) {
|
||||
var categories = [];
|
||||
var count = 0;
|
||||
var nulls = 0;
|
||||
var nans = 0;
|
||||
var infinities = 0;
|
||||
var minValue = 0;
|
||||
var maxValue = 0;
|
||||
var categoriesCount = 0;
|
||||
@@ -188,18 +265,24 @@ Aggregation.prototype.format = function(result) {
|
||||
var firstRow = result.rows[0];
|
||||
count = firstRow.count;
|
||||
nulls = firstRow.nulls_count;
|
||||
nans = firstRow.nans_count;
|
||||
infinities = firstRow.infinities_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'));
|
||||
categories.push(_.omit(row, 'count', 'nulls_count', 'min_val',
|
||||
'max_val', 'categories_count', 'nans_count', 'infinities_count'));
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
aggregation: this.aggregation,
|
||||
count: count,
|
||||
nulls: nulls,
|
||||
nans: nans,
|
||||
infinities: infinities,
|
||||
min: minValue,
|
||||
max: maxValue,
|
||||
categoriesCount: categoriesCount,
|
||||
@@ -238,6 +321,8 @@ Aggregation.prototype.search = function(psql, userQuery, callback) {
|
||||
var self = this;
|
||||
|
||||
var _userQuery = psql.escapeLiteral('%' + userQuery + '%');
|
||||
var _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
|
||||
var query = searchQueryTpl({
|
||||
@@ -250,7 +335,7 @@ Aggregation.prototype.search = function(psql, userQuery, callback) {
|
||||
_searchFiltered: filterCategoriesQueryTpl({
|
||||
_query: this.query,
|
||||
_column: this.column,
|
||||
_value: 'count(1)',
|
||||
_value: _value,
|
||||
_userQuery: _userQuery
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
|
||||
function BaseDataview() {}
|
||||
|
||||
module.exports = BaseDataview;
|
||||
@@ -5,8 +8,11 @@ module.exports = BaseDataview;
|
||||
BaseDataview.prototype.getResult = function(psql, override, callback) {
|
||||
var self = this;
|
||||
this.sql(psql, override, function(err, query) {
|
||||
psql.query(query, function(err, result) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
psql.query(query, function(err, result) {
|
||||
if (err) {
|
||||
return callback(err, result);
|
||||
}
|
||||
@@ -24,3 +30,42 @@ BaseDataview.prototype.getResult = function(psql, override, callback) {
|
||||
BaseDataview.prototype.search = function(psql, userQuery, callback) {
|
||||
return callback(null, this.format({ rows: [] }));
|
||||
};
|
||||
|
||||
var FLOAT_OIDS = {
|
||||
700: true,
|
||||
701: true,
|
||||
1700: true
|
||||
};
|
||||
|
||||
var DATE_OIDS = {
|
||||
1082: true,
|
||||
1114: true,
|
||||
1184: true
|
||||
};
|
||||
|
||||
var columnTypeQueryTpl = dot.template(
|
||||
'SELECT pg_typeof({{=it.column}})::oid FROM ({{=it.query}}) _cdb_column_type limit 1'
|
||||
);
|
||||
|
||||
BaseDataview.prototype.getColumnType = function (psql, column, query, callback) {
|
||||
var readOnlyTransaction = true;
|
||||
|
||||
var columnTypeQuery = columnTypeQueryTpl({
|
||||
column: column, query: query
|
||||
});
|
||||
|
||||
psql.query(columnTypeQuery, function(err, result) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
var pgType = result.rows[0].pg_typeof;
|
||||
callback(null, getPGTypeName(pgType));
|
||||
}, readOnlyTransaction);
|
||||
};
|
||||
|
||||
function getPGTypeName (pgType) {
|
||||
return {
|
||||
float: FLOAT_OIDS.hasOwnProperty(pgType),
|
||||
date: DATE_OIDS.hasOwnProperty(pgType)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ var DataviewFactory = {
|
||||
if (!this.dataviews[type]) {
|
||||
throw new Error('Invalid dataview type: "' + type + '"');
|
||||
}
|
||||
return new this.dataviews[type](query, dataviewDefinition.options);
|
||||
return new this.dataviews[type](query, dataviewDefinition.options, dataviewDefinition.sql);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -7,9 +7,19 @@ dot.templateSettings.strip = false;
|
||||
|
||||
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'
|
||||
' {{=it._operation}}({{=it._column}}) 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_nulls',
|
||||
' WHERE {{=it._column}} = \'infinity\'::float OR {{=it._column}} = \'-infinity\'::float) AS infinities_count',
|
||||
' ,(SELECT count(1) FROM ({{=it._query}}) _cdb_formula_nulls',
|
||||
' WHERE {{=it._column}} = \'NaN\'::float) AS nans_count{{?}}',
|
||||
'FROM ({{=it._query}}) _cdb_formula',
|
||||
'{{?it._isFloatColumn && it._operation !== \'count\'}}WHERE',
|
||||
' {{=it._column}} != \'infinity\'::float',
|
||||
'AND',
|
||||
' {{=it._column}} != \'-infinity\'::float',
|
||||
'AND',
|
||||
' {{=it._column}} != \'NaN\'::float{{?}}'
|
||||
].join('\n'));
|
||||
|
||||
var VALID_OPERATIONS = {
|
||||
@@ -31,7 +41,7 @@ var TYPE = 'formula';
|
||||
}
|
||||
}
|
||||
*/
|
||||
function Formula(query, options) {
|
||||
function Formula(query, options, queries) {
|
||||
if (!_.isString(options.operation)) {
|
||||
throw new Error('Formula expects `operation` in widget options');
|
||||
}
|
||||
@@ -47,8 +57,10 @@ function Formula(query, options) {
|
||||
BaseWidget.apply(this);
|
||||
|
||||
this.query = query;
|
||||
this.queries = queries;
|
||||
this.column = options.column || '1';
|
||||
this.operation = options.operation;
|
||||
this._isFloatColumn = null;
|
||||
}
|
||||
|
||||
Formula.prototype = new BaseWidget();
|
||||
@@ -57,14 +69,27 @@ Formula.prototype.constructor = Formula;
|
||||
module.exports = Formula;
|
||||
|
||||
Formula.prototype.sql = function(psql, override, callback) {
|
||||
var self = this;
|
||||
|
||||
if (!callback) {
|
||||
callback = override;
|
||||
override = {};
|
||||
}
|
||||
|
||||
var _query = this.query;
|
||||
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: _query,
|
||||
_isFloatColumn: this._isFloatColumn,
|
||||
_query: this.query,
|
||||
_operation: this.operation,
|
||||
_column: this.column
|
||||
});
|
||||
@@ -78,13 +103,17 @@ Formula.prototype.format = function(result) {
|
||||
var formattedResult = {
|
||||
operation: this.operation,
|
||||
result: 0,
|
||||
nulls: 0
|
||||
nulls: 0,
|
||||
nans: 0,
|
||||
infinities: 0
|
||||
};
|
||||
|
||||
if (result.rows.length) {
|
||||
formattedResult.operation = this.operation;
|
||||
formattedResult.result = result.rows[0].result;
|
||||
formattedResult.nulls = result.rows[0].nulls_count;
|
||||
formattedResult.nans = result.rows[0].nans_count;
|
||||
formattedResult.infinities = result.rows[0].infinities_count;
|
||||
}
|
||||
|
||||
return formattedResult;
|
||||
|
||||
@@ -5,118 +5,302 @@ var debug = require('debug')('windshaft:dataview:histogram');
|
||||
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 columnCastTpl = dot.template("date_part('epoch', {{=it.column}})");
|
||||
|
||||
var dateIntervalQueryTpl = dot.template([
|
||||
'WITH',
|
||||
'__cdb_dates AS (',
|
||||
' SELECT',
|
||||
' MAX({{=it.column}}::timestamp) AS __cdb_end,',
|
||||
' MIN({{=it.column}}::timestamp) AS __cdb_start',
|
||||
' FROM ({{=it.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'
|
||||
].join('\n'));
|
||||
|
||||
var MAX_INTERVAL_VALUE = 366;
|
||||
var BIN_MIN_NUMBER = 6;
|
||||
var BIN_MAX_NUMBER = 48;
|
||||
|
||||
var filteredQueryTpl = dot.template([
|
||||
'__cdb_filtered_source AS (',
|
||||
' SELECT *',
|
||||
' FROM ({{=it._query}}) __cdb_filtered_source_query',
|
||||
' 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 (',
|
||||
'__cdb_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',
|
||||
' max({{=it._column}}) AS __cdb_max_val, min({{=it._column}}) AS __cdb_min_val,',
|
||||
' avg({{=it._column}}) AS __cdb_avg_val, count(1) AS __cdb_total_rows',
|
||||
' FROM __cdb_filtered_source',
|
||||
')'
|
||||
].join(' \n'));
|
||||
|
||||
var overrideBasicsQueryTpl = dot.template([
|
||||
'basics AS (',
|
||||
'__cdb_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',
|
||||
' max({{=it._end}}) AS __cdb_max_val, min({{=it._start}}) AS __cdb_min_val,',
|
||||
' avg({{=it._column}}) AS __cdb_avg_val, count(1) AS __cdb_total_rows',
|
||||
' FROM __cdb_filtered_source',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var iqrQueryTpl = dot.template([
|
||||
'iqrange AS (',
|
||||
' SELECT max(quartile_max) - min(quartile_max) AS iqr',
|
||||
'__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 {{=it._column}} AS _cdb_iqr_column, ntile(4) over (order by {{=it._column}}',
|
||||
' ) AS quartile',
|
||||
' FROM ({{=it._query}}) _cdb_rank) _cdb_quartiles',
|
||||
' FROM __cdb_filtered_source) _cdb_quartiles',
|
||||
' WHERE quartile = 1 or quartile = 3',
|
||||
' GROUP BY quartile',
|
||||
' ) _cdb_iqr',
|
||||
' ) __cdb_iqr',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var binsQueryTpl = dot.template([
|
||||
'bins AS (',
|
||||
' SELECT CASE WHEN total_rows = 0 OR iqr = 0',
|
||||
'__cdb_bins AS (',
|
||||
' SELECT CASE WHEN __cdb_total_rows = 0 OR __cdb_iqr = 0',
|
||||
' THEN 1',
|
||||
' ELSE GREATEST(',
|
||||
' LEAST({{=it._minBins}}, CAST(total_rows AS INT)),',
|
||||
' LEAST({{=it._minBins}}, CAST(__cdb_total_rows AS INT)),',
|
||||
' LEAST(',
|
||||
' CAST(((max_val - min_val) / (2 * iqr * power(total_rows, 1/3))) AS INT),',
|
||||
' CAST(((__cdb_max_val - __cdb_min_val) / (2 * __cdb_iqr * power(__cdb_total_rows, 1/3))) AS INT),',
|
||||
' {{=it._maxBins}}',
|
||||
' )',
|
||||
' )',
|
||||
' END AS bins_number',
|
||||
' FROM basics, iqrange, ({{=it._query}}) _cdb_bins',
|
||||
' END AS __cdb_bins_number',
|
||||
' FROM __cdb_basics, __cdb_iqrange, __cdb_filtered_source',
|
||||
' LIMIT 1',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var overrideBinsQueryTpl = dot.template([
|
||||
'bins AS (',
|
||||
' SELECT {{=it._bins}} AS bins_number',
|
||||
'__cdb_bins AS (',
|
||||
' SELECT {{=it._bins}} AS __cdb_bins_number',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var nullsQueryTpl = dot.template([
|
||||
'nulls AS (',
|
||||
'__cdb_nulls AS (',
|
||||
' SELECT',
|
||||
' count(*) AS nulls_count',
|
||||
' FROM ({{=it._query}}) _cdb_histogram_nulls',
|
||||
' count(*) AS __cdb_nulls_count',
|
||||
' FROM ({{=it._query}}) __cdb_histogram_nulls',
|
||||
' WHERE {{=it._column}} IS NULL',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var infinitiesQueryTpl = dot.template([
|
||||
'__cdb_infinities AS (',
|
||||
' SELECT',
|
||||
' count(*) AS __cdb_infinities_count',
|
||||
' FROM ({{=it._query}}) __cdb_infinities_query',
|
||||
' WHERE',
|
||||
' {{=it._column}} = \'infinity\'::float',
|
||||
' OR',
|
||||
' {{=it._column}} = \'-infinity\'::float',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var nansQueryTpl = dot.template([
|
||||
'__cdb_nans AS (',
|
||||
' SELECT',
|
||||
' count(*) AS __cdb_nans_count',
|
||||
' FROM ({{=it._query}}) __cdb_nans_query',
|
||||
' 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,',
|
||||
' avg_val,',
|
||||
' CASE WHEN min_val = max_val',
|
||||
' (__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,',
|
||||
' {{?it._isFloatColumn}}__cdb_infinities_count AS infinities_count,',
|
||||
' __cdb_nans_count AS nans_count,{{?}}',
|
||||
' __cdb_avg_val AS avg_val,',
|
||||
' CASE WHEN __cdb_min_val = __cdb_max_val',
|
||||
' THEN 0',
|
||||
' ELSE GREATEST(1, LEAST(WIDTH_BUCKET({{=it._column}}, min_val, max_val, bins_number), bins_number)) - 1',
|
||||
' ELSE GREATEST(',
|
||||
' 1,',
|
||||
' LEAST(',
|
||||
' WIDTH_BUCKET({{=it._column}}, __cdb_min_val, __cdb_max_val, __cdb_bins_number),',
|
||||
' __cdb_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',
|
||||
'FROM __cdb_filtered_source, __cdb_basics, __cdb_nulls,',
|
||||
' __cdb_bins{{?it._isFloatColumn}}, __cdb_infinities, __cdb_nans{{?}}',
|
||||
'GROUP BY bin, bins_number, bin_width, nulls_count,',
|
||||
' avg_val{{?it._isFloatColumn}}, infinities_count, nans_count{{?}}',
|
||||
'ORDER BY bin'
|
||||
].join('\n'));
|
||||
|
||||
var dateBasicsQueryTpl = dot.template([
|
||||
'__cdb_basics AS (',
|
||||
' SELECT',
|
||||
' max(date_part(\'epoch\', {{=it._column}})) AS __cdb_max_val,',
|
||||
' min(date_part(\'epoch\', {{=it._column}})) AS __cdb_min_val,',
|
||||
' avg(date_part(\'epoch\', {{=it._column}})) AS __cdb_avg_val,',
|
||||
' min(date_trunc(',
|
||||
' \'{{=it._aggregation}}\', {{=it._column}}::timestamp AT TIME ZONE \'{{=it._offset}}\'',
|
||||
' )) AS __cdb_start_date,',
|
||||
' max({{=it._column}}::timestamp AT TIME ZONE \'{{=it._offset}}\') AS __cdb_end_date,',
|
||||
' count(1) AS __cdb_total_rows',
|
||||
' FROM ({{=it._query}}) __cdb_basics_query',
|
||||
')'
|
||||
].join(' \n'));
|
||||
|
||||
var dateOverrideBasicsQueryTpl = dot.template([
|
||||
'__cdb_basics AS (',
|
||||
' SELECT',
|
||||
' max({{=it._end}}) AS __cdb_max_val,',
|
||||
' min({{=it._start}}) AS __cdb_min_val,',
|
||||
' avg(date_part(\'epoch\', {{=it._column}})) AS __cdb_avg_val,',
|
||||
' min(',
|
||||
' date_trunc(',
|
||||
' \'{{=it._aggregation}}\',',
|
||||
' TO_TIMESTAMP({{=it._start}})::timestamp AT TIME ZONE \'{{=it._offset}}\'',
|
||||
' )',
|
||||
' ) AS __cdb_start_date,',
|
||||
' max(',
|
||||
' TO_TIMESTAMP({{=it._end}})::timestamp AT TIME ZONE \'{{=it._offset}}\'',
|
||||
' ) AS __cdb_end_date,',
|
||||
' count(1) AS __cdb_total_rows',
|
||||
' FROM ({{=it._query}}) __cdb_basics_query',
|
||||
')'
|
||||
].join(' \n'));
|
||||
|
||||
var dateBinsQueryTpl = dot.template([
|
||||
'__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,',
|
||||
' {{?it._aggregation==="quarter"}}\'3 month\'{{??}}\'1 {{=it._aggregation}}\'{{?}}::interval',
|
||||
' )',
|
||||
' ) AS __cdb_bins_array',
|
||||
' FROM __cdb_basics',
|
||||
' ) __cdb_bins_array_query',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var dateHistogramQueryTpl = dot.template([
|
||||
'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(',
|
||||
' {{=it._column}}::timestamp AT TIME ZONE \'{{=it._offset}}\',',
|
||||
' __cdb_bins_array',
|
||||
' ),',
|
||||
' __cdb_bins_number',
|
||||
' )) - 1',
|
||||
' END AS bin,',
|
||||
' min(',
|
||||
' date_part(',
|
||||
' \'epoch\', ',
|
||||
' date_trunc(',
|
||||
' \'{{=it._aggregation}}\', {{=it._column}}::timestamp AT TIME ZONE \'{{=it._offset}}\'',
|
||||
' ) AT TIME ZONE \'{{=it._offset}}\'',
|
||||
' )',
|
||||
' )::numeric AS timestamp,',
|
||||
' date_part(\'epoch\', __cdb_start_date)::numeric AS timestamp_start,',
|
||||
' min(date_part(\'epoch\', {{=it._column}}))::numeric AS min,',
|
||||
' max(date_part(\'epoch\', {{=it._column}}))::numeric AS max,',
|
||||
' avg(date_part(\'epoch\', {{=it._column}}))::numeric AS avg,',
|
||||
' count(*) AS freq',
|
||||
'FROM ({{=it._query}}) __cdb_histogram, __cdb_basics, __cdb_bins, __cdb_nulls',
|
||||
'WHERE date_part(\'epoch\', {{=it._column}}) IS NOT NULL',
|
||||
'GROUP BY bin, bins_number, bin_width, nulls_count, timestamp_start',
|
||||
'ORDER BY bin'
|
||||
].join('\n'));
|
||||
|
||||
var TYPE = 'histogram';
|
||||
|
||||
/**
|
||||
{
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'name',
|
||||
bins: 10 // OPTIONAL
|
||||
}
|
||||
Numeric histogram:
|
||||
{
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'name', // column data type: numeric
|
||||
bins: 10 // OPTIONAL
|
||||
}
|
||||
}
|
||||
|
||||
Time series:
|
||||
{
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'date', // column data type: date
|
||||
aggregation: 'day' // OPTIONAL (if undefined then it'll be built as numeric)
|
||||
offset: -7200 // OPTIONAL (UTC offset in seconds)
|
||||
}
|
||||
}
|
||||
*/
|
||||
function Histogram(query, options) {
|
||||
function Histogram(query, options, queries) {
|
||||
if (!_.isString(options.column)) {
|
||||
throw new Error('Histogram expects `column` in widget options');
|
||||
}
|
||||
|
||||
this.query = query;
|
||||
this.queries = queries;
|
||||
this.column = options.column;
|
||||
this.bins = options.bins;
|
||||
this.aggregation = options.aggregation;
|
||||
this.offset = options.offset;
|
||||
|
||||
this._columnType = null;
|
||||
}
|
||||
@@ -126,56 +310,61 @@ 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.query
|
||||
});
|
||||
|
||||
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
|
||||
return null;
|
||||
}
|
||||
|
||||
this._buildQuery(psql, override, callback);
|
||||
};
|
||||
|
||||
Histogram.prototype.isDateHistogram = function (override) {
|
||||
return this._columnType === 'date' && (this.aggregation !== undefined || override.aggregation !== undefined);
|
||||
};
|
||||
|
||||
Histogram.prototype._buildQuery = function (psql, override, callback) {
|
||||
var filteredQuery, basicsQuery, binsQuery;
|
||||
var _column = this.column;
|
||||
var _query = this.query;
|
||||
|
||||
if (this.isDateHistogram(override)) {
|
||||
return this._buildDateHistogramQuery(psql, override, callback);
|
||||
}
|
||||
|
||||
if (this._columnType === 'date') {
|
||||
_column = columnCastTpl({column: _column});
|
||||
}
|
||||
|
||||
var _query = this.query;
|
||||
filteredQuery = filteredQueryTpl({
|
||||
_isFloatColumn: this._columnType === 'float',
|
||||
_query: _query,
|
||||
_column: _column
|
||||
});
|
||||
|
||||
var basicsQuery, binsQuery;
|
||||
|
||||
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,
|
||||
_start: override.start,
|
||||
_end: override.end
|
||||
_start: getBinStart(override),
|
||||
_end: getBinEnd(override)
|
||||
});
|
||||
|
||||
binsQuery = [
|
||||
@@ -189,7 +378,7 @@ Histogram.prototype.sql = function(psql, override, callback) {
|
||||
_column: _column
|
||||
});
|
||||
|
||||
if (override && _.has(override, 'bins')) {
|
||||
if (this._shouldOverrideBins(override)) {
|
||||
binsQuery = [
|
||||
overrideBinsQueryTpl({
|
||||
_bins: override.bins
|
||||
@@ -210,18 +399,34 @@ 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
|
||||
})
|
||||
@@ -232,6 +437,143 @@ Histogram.prototype.sql = function(psql, override, callback) {
|
||||
return callback(null, 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');
|
||||
};
|
||||
|
||||
var DATE_AGGREGATIONS = {
|
||||
'auto': true,
|
||||
'minute': true,
|
||||
'hour': true,
|
||||
'day': true,
|
||||
'week': true,
|
||||
'month': true,
|
||||
'quarter': true,
|
||||
'year': true
|
||||
};
|
||||
|
||||
Histogram.prototype._buildDateHistogramQuery = function (psql, override, callback) {
|
||||
var _column = this.column;
|
||||
var _query = this.query;
|
||||
var _aggregation = override && override.aggregation ? override.aggregation : this.aggregation;
|
||||
var _offset = override && Number.isFinite(override.offset) ? override.offset : this.offset;
|
||||
|
||||
if (!DATE_AGGREGATIONS.hasOwnProperty(_aggregation)) {
|
||||
return callback(new Error('Invalid aggregation value. Valid ones: ' +
|
||||
Object.keys(DATE_AGGREGATIONS).join(', ')
|
||||
));
|
||||
}
|
||||
|
||||
if (_aggregation === 'auto') {
|
||||
this.getAutomaticAggregation(psql, function (err, aggregation) {
|
||||
if (err || aggregation === 'none') {
|
||||
this.aggregation = 'day';
|
||||
} else {
|
||||
this.aggregation = aggregation;
|
||||
}
|
||||
override.aggregation = this.aggregation;
|
||||
this._buildDateHistogramQuery(psql, override, callback);
|
||||
}.bind(this));
|
||||
return null;
|
||||
}
|
||||
|
||||
var dateBasicsQuery;
|
||||
|
||||
if (override && _.has(override, 'start') && _.has(override, 'end')) {
|
||||
dateBasicsQuery = dateOverrideBasicsQueryTpl({
|
||||
_query: _query,
|
||||
_column: _column,
|
||||
_aggregation: _aggregation,
|
||||
_start: getBinStart(override),
|
||||
_end: getBinEnd(override),
|
||||
_offset: parseOffset(_offset, _aggregation)
|
||||
});
|
||||
} else {
|
||||
dateBasicsQuery = dateBasicsQueryTpl({
|
||||
_query: _query,
|
||||
_column: _column,
|
||||
_aggregation: _aggregation,
|
||||
_offset: parseOffset(_offset, _aggregation)
|
||||
});
|
||||
}
|
||||
|
||||
var dateBinsQuery = [
|
||||
dateBinsQueryTpl({
|
||||
_aggregation: _aggregation
|
||||
})
|
||||
].join(',\n');
|
||||
|
||||
var nullsQuery = nullsQueryTpl({
|
||||
_query: _query,
|
||||
_column: _column
|
||||
});
|
||||
|
||||
var dateHistogramQuery = dateHistogramQueryTpl({
|
||||
_query: _query,
|
||||
_column: _column,
|
||||
_aggregation: _aggregation,
|
||||
_offset: parseOffset(_offset, _aggregation)
|
||||
});
|
||||
|
||||
var histogramSql = [
|
||||
"WITH",
|
||||
[
|
||||
dateBasicsQuery,
|
||||
dateBinsQuery,
|
||||
nullsQuery
|
||||
].join(',\n'),
|
||||
dateHistogramQuery
|
||||
].join('\n');
|
||||
|
||||
debug(histogramSql);
|
||||
|
||||
return callback(null, histogramSql);
|
||||
};
|
||||
|
||||
Histogram.prototype.getAutomaticAggregation = function (psql, callback) {
|
||||
var dateIntervalQuery = dateIntervalQueryTpl({
|
||||
query: this.query,
|
||||
column: this.column
|
||||
});
|
||||
|
||||
debug(dateIntervalQuery);
|
||||
|
||||
psql.query(dateIntervalQuery, function (err, result) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var aggegations = result.rows[0];
|
||||
var aggregation = Object.keys(aggegations)
|
||||
.map(function (key) {
|
||||
return {
|
||||
name: key,
|
||||
value: aggegations[key]
|
||||
};
|
||||
})
|
||||
.reduce(function (closer, current) {
|
||||
if (current.value > MAX_INTERVAL_VALUE) {
|
||||
return closer;
|
||||
}
|
||||
|
||||
var closerDiff = MAX_INTERVAL_VALUE - closer.value;
|
||||
var 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);
|
||||
});
|
||||
};
|
||||
|
||||
Histogram.prototype.format = function(result, override) {
|
||||
override = override || {};
|
||||
var buckets = [];
|
||||
@@ -240,7 +582,12 @@ Histogram.prototype.format = function(result, override) {
|
||||
var width = getWidth(override);
|
||||
var binsStart = getBinStart(override);
|
||||
var nulls = 0;
|
||||
var infinities = 0;
|
||||
var nans = 0;
|
||||
var avg;
|
||||
var timestampStart;
|
||||
var aggregation;
|
||||
var offset;
|
||||
|
||||
if (result.rows.length) {
|
||||
var firstRow = result.rows[0];
|
||||
@@ -248,27 +595,74 @@ Histogram.prototype.format = function(result, override) {
|
||||
width = firstRow.bin_width || width;
|
||||
avg = firstRow.avg_val;
|
||||
nulls = firstRow.nulls_count;
|
||||
binsStart = override.hasOwnProperty('start') ? override.start : firstRow.min;
|
||||
timestampStart = firstRow.timestamp_start;
|
||||
infinities = firstRow.infinities_count;
|
||||
nans = firstRow.nans_count;
|
||||
binsStart = populateBinStart(override, firstRow);
|
||||
|
||||
if (Number.isFinite(timestampStart)) {
|
||||
aggregation = getAggregation(override, this.aggregation);
|
||||
offset = getOffset(override, this.offset);
|
||||
}
|
||||
|
||||
buckets = result.rows.map(function(row) {
|
||||
return _.omit(row, 'bins_number', 'bin_width', 'nulls_count', 'avg_val');
|
||||
return _.omit(
|
||||
row,
|
||||
'bins_number',
|
||||
'bin_width',
|
||||
'nulls_count',
|
||||
'infinities_count',
|
||||
'nans_count',
|
||||
'avg_val',
|
||||
'timestamp_start'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
aggregation: aggregation,
|
||||
offset: offset,
|
||||
timestamp_start: timestampStart,
|
||||
bin_width: width,
|
||||
bins_count: binsCount,
|
||||
bins_start: binsStart,
|
||||
nulls: nulls,
|
||||
infinities: infinities,
|
||||
nans: nans,
|
||||
avg: avg,
|
||||
bins: buckets
|
||||
};
|
||||
};
|
||||
|
||||
function getAggregation(override, aggregation) {
|
||||
return override && override.aggregation ? override.aggregation : aggregation;
|
||||
}
|
||||
|
||||
function getOffset(override, offset) {
|
||||
if (override && override.offset) {
|
||||
return override.offset;
|
||||
}
|
||||
if (offset) {
|
||||
return offset;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -284,6 +678,32 @@ function getWidth(override) {
|
||||
return width;
|
||||
}
|
||||
|
||||
function parseOffset(offset, aggregation) {
|
||||
if (!offset) {
|
||||
return '0';
|
||||
}
|
||||
if (aggregation === 'hour' || aggregation === 'minute') {
|
||||
return '0';
|
||||
}
|
||||
|
||||
var offsetInHours = Math.ceil(offset / 3600);
|
||||
return '' + offsetInHours;
|
||||
}
|
||||
|
||||
function populateBinStart(override, firstRow) {
|
||||
var binStart;
|
||||
|
||||
if (firstRow.hasOwnProperty('timestamp')) {
|
||||
binStart = firstRow.timestamp;
|
||||
} else if (override.hasOwnProperty('start')) {
|
||||
binStart = getBinStart(override);
|
||||
} else {
|
||||
binStart = firstRow.min;
|
||||
}
|
||||
|
||||
return binStart;
|
||||
}
|
||||
|
||||
Histogram.prototype.getType = function() {
|
||||
return TYPE;
|
||||
};
|
||||
|
||||
@@ -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,47 +39,65 @@ 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',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var categoriesSummaryQueryTpl = dot.template([
|
||||
'categories_summary AS(',
|
||||
' SELECT count(1) categories_count, max(value) max_val, min(value) min_val',
|
||||
var categoriesSummaryMinMaxQueryTpl = dot.template([
|
||||
'categories_summary_min_max AS(',
|
||||
' SELECT max(value) max_val, min(value) min_val',
|
||||
' FROM categories',
|
||||
')'
|
||||
].join('\n'));
|
||||
|
||||
var categoriesSummaryCountQueryTpl = dot.template([
|
||||
'categories_summary_count AS(',
|
||||
' SELECT count(1) AS categories_count',
|
||||
' FROM (',
|
||||
' SELECT {{=it._column}} AS category',
|
||||
' 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',
|
||||
' FROM categories, summary, categories_summary',
|
||||
'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',
|
||||
' FROM categories, summary, categories_summary',
|
||||
'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',
|
||||
'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);
|
||||
@@ -65,34 +105,62 @@ Aggregation.prototype.constructor = Aggregation;
|
||||
|
||||
module.exports = Aggregation;
|
||||
|
||||
Aggregation.prototype.sql = function(psql, filters, override, callback) {
|
||||
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()
|
||||
_aggregation: this.getAggregationSql(),
|
||||
_aggregationColumn: _aggregationColumn
|
||||
}),
|
||||
categoriesSummaryQueryTpl({
|
||||
categoriesSummaryMinMaxQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column
|
||||
}),
|
||||
categoriesSummaryCountQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column
|
||||
})
|
||||
].join(',\n'),
|
||||
aggregationQueryTpl({
|
||||
_isFloatColumn: this._isFloatColumn,
|
||||
_query: _query,
|
||||
_column: this.column,
|
||||
_aggregation: this.getAggregationSql(),
|
||||
@@ -103,21 +171,35 @@ Aggregation.prototype.sql = function(psql, filters, 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()
|
||||
_aggregation: this.getAggregationSql(),
|
||||
_aggregationColumn: _aggregationColumn
|
||||
}),
|
||||
categoriesSummaryQueryTpl({
|
||||
categoriesSummaryMinMaxQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column
|
||||
}),
|
||||
categoriesSummaryCountQueryTpl({
|
||||
_query: _query,
|
||||
_column: this.column
|
||||
})
|
||||
].join(',\n'),
|
||||
rankedAggregationQueryTpl({
|
||||
_isFloatColumn: this._isFloatColumn,
|
||||
_query: _query,
|
||||
_column: this.column,
|
||||
_limit: CATEGORIES_LIMIT
|
||||
@@ -125,6 +207,8 @@ Aggregation.prototype.sql = function(psql, filters, 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;
|
||||
@@ -55,20 +56,20 @@ BaseOverviewsDataview.prototype.rewrittenQuery = function(query) {
|
||||
};
|
||||
|
||||
// Default behaviour
|
||||
BaseOverviewsDataview.prototype.defaultSql = function(psql, filters, override, callback) {
|
||||
BaseOverviewsDataview.prototype.defaultSql = function(psql, override, callback) {
|
||||
var query = this.query;
|
||||
var dataview = this.baseDataview;
|
||||
if ( SETTINGS.defaultOverviews ) {
|
||||
query = this.rewrittenQuery(query);
|
||||
dataview = new this.BaseDataview(query, this.queryOptions);
|
||||
}
|
||||
return dataview.sql(psql, filters, override, callback);
|
||||
return dataview.sql(psql, override, callback);
|
||||
};
|
||||
|
||||
// default implementation that can be override in derived classes:
|
||||
|
||||
BaseOverviewsDataview.prototype.sql = function(psql, filters, override, callback) {
|
||||
return this.defaultSql(psql, filters, override, callback);
|
||||
BaseOverviewsDataview.prototype.sql = function(psql, override, callback) {
|
||||
return this.defaultSql(psql, override, callback);
|
||||
};
|
||||
|
||||
BaseOverviewsDataview.prototype.search = function(psql, userQuery, callback) {
|
||||
|
||||
@@ -14,7 +14,8 @@ OverviewsDataviewFactory.prototype.getDataview = function(query, dataviewDefinit
|
||||
return parentFactory.getDataview(query, dataviewDefinition);
|
||||
}
|
||||
return new dataviews[type](
|
||||
query, dataviewDefinition.options, this.queryRewriter, this.queryRewriteData, this.options
|
||||
query, dataviewDefinition.options, this.queryRewriter, this.queryRewriteData, this.options,
|
||||
dataviewDefinition.sql
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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, filters, 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, filters, override, callback);
|
||||
return this.defaultSql(psql, override, callback);
|
||||
};
|
||||
|
||||
@@ -1,24 +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 columnCastTpl = dot.template("date_part('epoch', {{=it.column}})");
|
||||
|
||||
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'));
|
||||
|
||||
@@ -27,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'));
|
||||
|
||||
@@ -38,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',
|
||||
@@ -57,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'));
|
||||
@@ -77,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',
|
||||
@@ -91,16 +125,17 @@ 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) {
|
||||
BaseOverviewsDataview.call(this, query, options, BaseDataview, queryRewriter, queryRewriteData, params);
|
||||
function Histogram(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.bins = options.bins;
|
||||
|
||||
@@ -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.query)
|
||||
});
|
||||
|
||||
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
|
||||
@@ -149,14 +171,29 @@ Histogram.prototype.sql = function(psql, override, callback) {
|
||||
}
|
||||
|
||||
if (this._columnType === 'date') {
|
||||
_column = columnCastTpl({column: _column});
|
||||
// overviews currently aggregate dates to NULL
|
||||
// to avoid problem we don't use overviews for histograms of date columns
|
||||
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,
|
||||
@@ -175,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
|
||||
@@ -196,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,8 +1,8 @@
|
||||
var BaseOverviewsDataview = require('./base');
|
||||
var BaseDataview = require('../list');
|
||||
|
||||
function List(query, options, queryRewriter, queryRewriteData, params) {
|
||||
BaseOverviewsDataview.call(this, query, options, BaseDataview, queryRewriter, queryRewriteData, params);
|
||||
function List(query, options, queryRewriter, queryRewriteData, params, queries) {
|
||||
BaseOverviewsDataview.call(this, query, options, BaseDataview, queryRewriter, queryRewriteData, params, queries);
|
||||
}
|
||||
|
||||
List.prototype = Object.create(BaseOverviewsDataview.prototype);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
var filters = {
|
||||
category: require('./camshaft/category'),
|
||||
range: require('./camshaft/range')
|
||||
category: require('./analysis/category'),
|
||||
range: require('./analysis/range')
|
||||
};
|
||||
|
||||
function createFilter(filterDefinition) {
|
||||
@@ -11,11 +11,11 @@ function createFilter(filterDefinition) {
|
||||
return new filters[filterType](filterDefinition.column, filterDefinition.params);
|
||||
}
|
||||
|
||||
function CamshaftFilters(filters) {
|
||||
function AnalysisFilters(filters) {
|
||||
this.filters = filters;
|
||||
}
|
||||
|
||||
CamshaftFilters.prototype.sql = function(rawSql) {
|
||||
AnalysisFilters.prototype.sql = function(rawSql) {
|
||||
var filters = this.filters || {};
|
||||
var applyFilters = {};
|
||||
|
||||
@@ -32,4 +32,4 @@ CamshaftFilters.prototype.sql = function(rawSql) {
|
||||
}, rawSql);
|
||||
};
|
||||
|
||||
module.exports = CamshaftFilters;
|
||||
module.exports = AnalysisFilters;
|
||||
@@ -6,7 +6,7 @@ dot.templateSettings.strip = false;
|
||||
|
||||
var filterQueryTpl = dot.template([
|
||||
'SELECT *',
|
||||
'FROM ({{=it._sql}}) _camshaft_category_filter',
|
||||
'FROM ({{=it._sql}}) _analysis_category_filter',
|
||||
'WHERE {{=it._filters}}'
|
||||
].join('\n'));
|
||||
var escapeStringTpl = dot.template('$escape_{{=it._i}}${{=it._value}}$escape_{{=it._i}}$');
|
||||
@@ -6,7 +6,7 @@ dot.templateSettings.strip = false;
|
||||
var betweenFilterTpl = dot.template('{{=it._column}} BETWEEN {{=it._min}} AND {{=it._max}}');
|
||||
var minFilterTpl = dot.template('{{=it._column}} >= {{=it._min}}');
|
||||
var maxFilterTpl = dot.template('{{=it._column}} <= {{=it._max}}');
|
||||
var filterQueryTpl = dot.template('SELECT * FROM ({{=it._sql}}) _camshaft_range_filter WHERE {{=it._filter}}');
|
||||
var filterQueryTpl = dot.template('SELECT * FROM ({{=it._sql}}) _analysis_range_filter WHERE {{=it._filter}}');
|
||||
|
||||
function Range(column, filterParams) {
|
||||
this.column = column;
|
||||
@@ -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;
|
||||
|
||||
@@ -59,10 +59,23 @@ AnalysisMapConfigAdapter.prototype.getMapConfig = function(user, requestMapConfi
|
||||
requestMapConfig = appendFiltersToNodes(requestMapConfig, dataviewsFiltersBySourceId);
|
||||
|
||||
function createAnalysis(analysisDefinition, done) {
|
||||
self.analysisBackend.create(analysisConfiguration, analysisDefinition, done);
|
||||
self.analysisBackend.create(analysisConfiguration, analysisDefinition, function (err, analysis) {
|
||||
if (err) {
|
||||
var error = new Error(err.message);
|
||||
error.type = 'analysis';
|
||||
error.analysis = {
|
||||
id: analysisDefinition.id,
|
||||
node_id: err.node_id,
|
||||
type: analysisDefinition.type
|
||||
};
|
||||
return done(error);
|
||||
}
|
||||
|
||||
done(null, analysis);
|
||||
});
|
||||
}
|
||||
|
||||
var analysesQueue = queue(requestMapConfig.analyses.length);
|
||||
var analysesQueue = queue(1);
|
||||
requestMapConfig.analyses.forEach(function(analysis) {
|
||||
analysesQueue.defer(createAnalysis, analysis);
|
||||
});
|
||||
@@ -78,7 +91,7 @@ AnalysisMapConfigAdapter.prototype.getMapConfig = function(user, requestMapConfi
|
||||
sourceId2Query[rootNode.params.id] = rootNode;
|
||||
}
|
||||
|
||||
analysis.getSortedNodes().forEach(function(node) {
|
||||
analysis.getNodes().forEach(function(node) {
|
||||
if (node.params && node.params.id) {
|
||||
sourceId2Query[node.params.id] = node;
|
||||
}
|
||||
@@ -101,10 +114,8 @@ AnalysisMapConfigAdapter.prototype.getMapConfig = function(user, requestMapConfi
|
||||
analysisSql = sqlQueryWrap.replace(/<%=\s*sql\s*%>/g, analysisSql);
|
||||
}
|
||||
layer.options.sql = analysisSql;
|
||||
var layerDataviews = getLayerDataviews(layer, dataviews);
|
||||
layer.options.columns = layerDataviews.reduce(function(columns, dataview) {
|
||||
return columns.concat(getDataviewColumns(dataview));
|
||||
}, []);
|
||||
layer.options.columns = getDataviewsColumns(getLayerDataviews(layer, dataviews));
|
||||
layer.options.affected_tables = getAllAffectedTablesFromSourceNodes(layerNode);
|
||||
} else {
|
||||
missingNodesErrors.push(
|
||||
new Error('Missing analysis node.id="' + layerSourceId +'" for layer='+layerIndex)
|
||||
@@ -114,12 +125,37 @@ AnalysisMapConfigAdapter.prototype.getMapConfig = function(user, requestMapConfi
|
||||
return layer;
|
||||
});
|
||||
|
||||
debug('mapconfig output', JSON.stringify(requestMapConfig, null, 4));
|
||||
|
||||
if (missingNodesErrors.length > 0) {
|
||||
return callback(missingNodesErrors);
|
||||
var missingDataviewsNodesErrors = getMissingDataviewsSourceIds(dataviews, sourceId2Node);
|
||||
if (missingNodesErrors.length > 0 || missingDataviewsNodesErrors.length > 0) {
|
||||
return callback(missingNodesErrors.concat(missingDataviewsNodesErrors));
|
||||
}
|
||||
|
||||
// Augment dataviews with sql from analyses
|
||||
Object.keys(dataviews).forEach(function(dataviewName) {
|
||||
var dataview = requestMapConfig.dataviews[dataviewName];
|
||||
var dataviewSourceId = dataview.source.id;
|
||||
var dataviewNode = sourceId2Node[dataviewSourceId];
|
||||
dataview.node = {
|
||||
type: dataviewNode.type,
|
||||
filters: dataviewNode.getFilters()
|
||||
};
|
||||
dataview.sql = {
|
||||
own_filter_on: dataviewQuery(dataviewNode, dataviewName, true),
|
||||
own_filter_off: dataviewQuery(dataviewNode, dataviewName, false),
|
||||
no_filters: dataviewNode.getQuery(Object.keys(dataviewNode.getFilters())
|
||||
.reduce(function(applyFilters, filterId) {
|
||||
applyFilters[filterId] = false;
|
||||
return applyFilters;
|
||||
}, {})
|
||||
)
|
||||
};
|
||||
});
|
||||
if (Object.keys(dataviews).length > 0) {
|
||||
requestMapConfig.dataviews = dataviews;
|
||||
}
|
||||
|
||||
debug('mapconfig output', JSON.stringify(requestMapConfig, null, 4));
|
||||
|
||||
context.analysesResults = analysesResults;
|
||||
|
||||
return callback(null, requestMapConfig);
|
||||
@@ -136,7 +172,7 @@ function skipColumns(columnNames) {
|
||||
.filter(function(columnName) { return !SKIP_COLUMNS[columnName]; });
|
||||
}
|
||||
|
||||
var layerQueryTemplate = dot.template([
|
||||
var wrappedQueryTpl = dot.template([
|
||||
'SELECT {{=it._columns}}',
|
||||
'FROM ({{=it._query}}) _cdb_analysis_query'
|
||||
].join('\n'));
|
||||
@@ -146,7 +182,20 @@ function layerQuery(node) {
|
||||
return node.getQuery();
|
||||
}
|
||||
var _columns = ['ST_Transform(the_geom, 3857) the_geom_webmercator'].concat(skipColumns(node.getColumns()));
|
||||
return layerQueryTemplate({ _query: node.getQuery(), _columns: _columns.join(', ') });
|
||||
return wrappedQueryTpl({ _query: node.getQuery(), _columns: _columns.join(', ') });
|
||||
}
|
||||
|
||||
function dataviewQuery(node, dataviewName, ownFilter) {
|
||||
var applyFilters = {};
|
||||
if (!ownFilter) {
|
||||
applyFilters[dataviewName] = false;
|
||||
}
|
||||
|
||||
if (node.type === 'source') {
|
||||
return node.getQuery(applyFilters);
|
||||
}
|
||||
var _columns = ['ST_Transform(the_geom, 3857) the_geom_webmercator'].concat(skipColumns(node.getColumns()));
|
||||
return wrappedQueryTpl({ _query: node.getQuery(applyFilters), _columns: _columns.join(', ') });
|
||||
}
|
||||
|
||||
function appendFiltersToNodes(requestMapConfig, dataviewsFiltersBySourceId) {
|
||||
@@ -166,8 +215,9 @@ function appendFiltersToNodes(requestMapConfig, dataviewsFiltersBySourceId) {
|
||||
}
|
||||
|
||||
function shouldAdaptLayers(requestMapConfig) {
|
||||
return Array.isArray(requestMapConfig.layers) &&
|
||||
Array.isArray(requestMapConfig.analyses) && requestMapConfig.analyses.length > 0;
|
||||
return Array.isArray(requestMapConfig.layers) && requestMapConfig.layers.some(getLayerSourceId) ||
|
||||
(Array.isArray(requestMapConfig.analyses) && requestMapConfig.analyses.length > 0) ||
|
||||
requestMapConfig.dataviews;
|
||||
}
|
||||
|
||||
var DATAVIEW_TYPE_2_FILTER_TYPE = {
|
||||
@@ -208,11 +258,22 @@ function getLayerDataviews(layer, dataviews) {
|
||||
return layerDataviews;
|
||||
}
|
||||
|
||||
function getDataviewsColumns(dataviews) {
|
||||
return Object.keys(dataviews.reduce(function(columnsDict, dataview) {
|
||||
getDataviewColumns(dataview).forEach(function(columnName) {
|
||||
if (!!columnName) {
|
||||
columnsDict[columnName] = true;
|
||||
}
|
||||
});
|
||||
return columnsDict;
|
||||
}, {}));
|
||||
}
|
||||
|
||||
function getDataviewColumns(dataview) {
|
||||
var columns = [];
|
||||
var options = dataview.options;
|
||||
['column', 'aggregationColumn'].forEach(function(opt) {
|
||||
if (options.hasOwnProperty(opt)) {
|
||||
if (options.hasOwnProperty(opt) && !!options[opt]) {
|
||||
columns.push(options[opt]);
|
||||
}
|
||||
});
|
||||
@@ -224,6 +285,15 @@ function getDataviewsList(dataviews) {
|
||||
}
|
||||
|
||||
function getDataviewsErrors(dataviews) {
|
||||
var dataviewType = typeof dataviews;
|
||||
if (dataviewType !== 'object') {
|
||||
return [new Error('"dataviews" must be a valid JSON object: "' + dataviewType + '" type found')];
|
||||
}
|
||||
|
||||
if (Array.isArray(dataviews)) {
|
||||
return [new Error('"dataviews" must be a valid JSON object: "array" type found')];
|
||||
}
|
||||
|
||||
var errors = [];
|
||||
|
||||
Object.keys(dataviews).forEach(function(dataviewName) {
|
||||
@@ -239,3 +309,35 @@ function getDataviewsErrors(dataviews) {
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
function getMissingDataviewsSourceIds(dataviews, sourceId2Node) {
|
||||
var missingDataviewsSourceIds = [];
|
||||
Object.keys(dataviews).forEach(function(dataviewName) {
|
||||
var dataview = dataviews[dataviewName];
|
||||
var dataviewSourceId = getDataviewSourceId(dataview);
|
||||
if (!sourceId2Node.hasOwnProperty(dataviewSourceId)) {
|
||||
missingDataviewsSourceIds.push(new AnalysisError('Node with `source.id="' + dataviewSourceId +'"`' +
|
||||
' not found in analyses for dataview "' + dataviewName + '"'));
|
||||
}
|
||||
});
|
||||
|
||||
return missingDataviewsSourceIds;
|
||||
}
|
||||
|
||||
function AnalysisError(message) {
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
this.name = this.constructor.name;
|
||||
this.type = 'analysis';
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
function getAllAffectedTablesFromSourceNodes(node) {
|
||||
var affectedTables = node.getAllInputNodes(function (node) {
|
||||
return node.getType() === 'source';
|
||||
}).reduce(function(list, node) {
|
||||
return list.concat(node.getAffectedTables());
|
||||
},[]);
|
||||
return affectedTables;
|
||||
}
|
||||
|
||||
require('util').inherits(AnalysisError, Error);
|
||||
|
||||
@@ -15,6 +15,15 @@ DataviewsWidgetsMapConfigAdapter.prototype.getMapConfig = function(user, request
|
||||
|
||||
requestMapConfig.layers.forEach(function(layer, index) {
|
||||
var layerSourceId = getLayerSourceId(layer);
|
||||
|
||||
if (!layer.options.widgets) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!layerSourceId && !layer.options.sql) {
|
||||
return;
|
||||
}
|
||||
|
||||
var dataviewSourceId = layerSourceId || 'cdb-layer-source-' + index;
|
||||
// Append a new analysis if layer has no source id but sql.
|
||||
if (!layerSourceId) {
|
||||
@@ -29,7 +38,7 @@ DataviewsWidgetsMapConfigAdapter.prototype.getMapConfig = function(user, request
|
||||
);
|
||||
}
|
||||
var source = { id: dataviewSourceId };
|
||||
var layerWidgets = layer.options.widgets;
|
||||
var layerWidgets = layer.options.widgets || {};
|
||||
Object.keys(layerWidgets).forEach(function(widgetId) {
|
||||
var dataview = layerWidgets[widgetId];
|
||||
requestMapConfig.dataviews[widgetId] = {
|
||||
@@ -67,7 +76,6 @@ DataviewsWidgetsMapConfigAdapter.prototype.getMapConfig = function(user, request
|
||||
};
|
||||
|
||||
function shouldAdapt(requestMapConfig) {
|
||||
// return false;
|
||||
return Array.isArray(requestMapConfig.layers) && requestMapConfig.layers.some(function hasWidgets(layer) {
|
||||
return layer.options && layer.options.widgets && Object.keys(layer.options.widgets).length > 0;
|
||||
});
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
function MapConfigBufferSizeAdapter() {
|
||||
this.formats = ['png', 'png32', 'mvt', 'grid.json'];
|
||||
}
|
||||
|
||||
module.exports = MapConfigBufferSizeAdapter;
|
||||
|
||||
MapConfigBufferSizeAdapter.prototype.getMapConfig = function (user, requestMapConfig, params, context, callback) {
|
||||
if (!context.templateParams || !context.templateParams.buffersize) {
|
||||
return callback(null, requestMapConfig);
|
||||
}
|
||||
|
||||
this.formats.forEach(function (format) {
|
||||
if (Number.isFinite(context.templateParams.buffersize[format])) {
|
||||
if (requestMapConfig.buffersize === undefined) {
|
||||
requestMapConfig.buffersize = {};
|
||||
}
|
||||
|
||||
requestMapConfig.buffersize[format] = context.templateParams.buffersize[format];
|
||||
}
|
||||
});
|
||||
|
||||
setImmediate(function () {
|
||||
callback(null, requestMapConfig);
|
||||
});
|
||||
};
|
||||
@@ -43,7 +43,6 @@ MapConfigNamedLayersAdapter.prototype.getMapConfig = function (user, requestMapC
|
||||
|
||||
if (nestedNamedLayers.length > 0) {
|
||||
var nestedNamedMapsError = new Error('Nested named layers are not allowed');
|
||||
// nestedNamedMapsError.http_status = 400;
|
||||
return done(nestedNamedMapsError);
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ MapConfigOverviewsAdapter.prototype.getMapConfig = function(user, requestMapConf
|
||||
step(
|
||||
function collectFiltersData() {
|
||||
var filters, unfiltered_query;
|
||||
if ( layer.options.source && analysesResults ) {
|
||||
if ( layer.options.source && analysesResults && !layer.options.sql_wrap) {
|
||||
var sourceId = layer.options.source.id;
|
||||
var node = _.find(analysesResults, function(a){ return a.rootNode.params.id === sourceId; });
|
||||
if ( node ) {
|
||||
|
||||
@@ -3,10 +3,15 @@
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
var queue = require('queue-async');
|
||||
var SubstitutionTokens = require('../../../utils/substitution-tokens');
|
||||
var PSQL = require('cartodb-psql');
|
||||
var turboCarto = require('turbo-carto');
|
||||
|
||||
function TurboCartoAdapter(turboCartoParser) {
|
||||
this.turboCartoParser = turboCartoParser;
|
||||
var SubstitutionTokens = require('../../../utils/substitution-tokens');
|
||||
var PostgresDatasource = require('../../../backends/turbo-carto-postgres-datasource');
|
||||
|
||||
var MapConfig = require('windshaft').model.MapConfig;
|
||||
|
||||
function TurboCartoAdapter() {
|
||||
}
|
||||
|
||||
module.exports = TurboCartoAdapter;
|
||||
@@ -22,38 +27,116 @@ TurboCartoAdapter.prototype.getMapConfig = function (user, requestMapConfig, par
|
||||
|
||||
var parseCartoQueue = queue(layers.length);
|
||||
|
||||
layers.forEach(function(layer) {
|
||||
parseCartoQueue.defer(self._parseCartoCss.bind(self), user, layer);
|
||||
layers.forEach(function(layer, index) {
|
||||
var layerId = MapConfig.getLayerId(requestMapConfig, index);
|
||||
parseCartoQueue.defer(self._parseCartoCss.bind(self), user, params, layer, index, layerId);
|
||||
});
|
||||
|
||||
parseCartoQueue.awaitAll(function (err, layers) {
|
||||
parseCartoQueue.awaitAll(function (err, results) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
requestMapConfig.layers = layers;
|
||||
var errors = results.reduce(function(errors, result) {
|
||||
if (result.error) {
|
||||
errors.push(result.error);
|
||||
}
|
||||
return errors;
|
||||
}, []);
|
||||
if (errors.length > 0) {
|
||||
return callback(errors);
|
||||
}
|
||||
|
||||
requestMapConfig.layers = results.map(function(result) { return result.layer; });
|
||||
context.turboCarto = {
|
||||
layers: results.map(function(result) {
|
||||
return result.meta;
|
||||
})
|
||||
};
|
||||
|
||||
return callback(null, requestMapConfig);
|
||||
});
|
||||
};
|
||||
|
||||
var bboxTemplate = dot.template('(select ST_SetSRID(st_extent(the_geom_webmercator), 3857) from ({{=it._sql}}) __c)');
|
||||
var zoomTemplate = dot.template([
|
||||
'GREATEST(',
|
||||
'ceil(log(40075017000 / 256 / GREATEST(',
|
||||
' st_xmax({{=it._bbox}}) - st_xmin({{=it._bbox}}),',
|
||||
' st_ymax({{=it._bbox}}) - st_ymin({{=it._bbox}})',
|
||||
'))/log(2)),',
|
||||
'0',
|
||||
')'
|
||||
var tokensQueryTpl = dot.template([
|
||||
'WITH input_query AS (',
|
||||
' {{=it._sql}}',
|
||||
'),',
|
||||
'bbox_query AS (',
|
||||
' SELECT ST_SetSRID(ST_Extent(the_geom_webmercator), 3857) as bbox from input_query',
|
||||
'),',
|
||||
'zoom_query as (',
|
||||
' SELECT GREATEST(',
|
||||
' ceil(log(40075017000 / 256 / GREATEST(ST_XMax(bbox) - ST_XMin(bbox), ST_YMax(bbox) - ST_YMin(bbox)))/log(2)),',
|
||||
' 0) as zoom',
|
||||
' FROM bbox_query',
|
||||
'),',
|
||||
'pixel_size_query as (',
|
||||
' SELECT 40075017 * cos(radians(ST_Y(ST_Transform(ST_Centroid(bbox), 4326)))) / 2 ^ ((zoom) + 8) as pixel_size',
|
||||
' FROM bbox_query, zoom_query',
|
||||
'),',
|
||||
'scale_denominator_query as (',
|
||||
' SELECT (pixel_size / 0.00028)::numeric as scale_denominator',
|
||||
' FROM pixel_size_query',
|
||||
')',
|
||||
'select ST_AsText(bbox) bbox, pixel_size, scale_denominator, zoom',
|
||||
'from bbox_query, pixel_size_query, scale_denominator_query, zoom_query'
|
||||
].join('\n'));
|
||||
var pixelSizeTemplate = dot.template('40075017 * cos(ST_Y(ST_Centroid({{=it._bbox}}))) / 2 ^ (({{=it._zoom}}) + 8)');
|
||||
var scaleDenominatorTemplate = dot.template('({{=it._pixelSize}} / 0.00028)::numeric');
|
||||
|
||||
|
||||
TurboCartoAdapter.prototype._parseCartoCss = function (username, layer, callback) {
|
||||
TurboCartoAdapter.prototype._parseCartoCss = function (username, params, layer, layerIndex, layerId, callback) {
|
||||
if (!shouldParseLayerCartocss(layer)) {
|
||||
return callback(null, layer);
|
||||
return callback(null, { layer: layer });
|
||||
}
|
||||
|
||||
var pg = new PSQL(dbParamsFromReqParams(params));
|
||||
function processCallback(err, cartocss, meta) {
|
||||
// Only return turbo-carto errors
|
||||
if (err && err.name === 'TurboCartoError') {
|
||||
var error = new Error(err.message);
|
||||
error.http_status = 400;
|
||||
error.type = 'layer';
|
||||
error.subtype = 'turbo-carto';
|
||||
error.layer = {
|
||||
id: layerId,
|
||||
index: layerIndex,
|
||||
type: layer.type,
|
||||
context: err.context
|
||||
};
|
||||
|
||||
return callback(null, { error: error });
|
||||
}
|
||||
|
||||
// Try to continue in the rest of the cases
|
||||
if (cartocss) {
|
||||
layer.options.cartocss = cartocss;
|
||||
}
|
||||
return callback(null, { layer: layer, meta: meta });
|
||||
}
|
||||
|
||||
var layerSql = layer.options.sql;
|
||||
var layerRawSql = layer.options.sql_raw;
|
||||
if (SubstitutionTokens.hasTokens(layerSql) && layerRawSql) {
|
||||
var self = this;
|
||||
var tokensQuery = tokensQueryTpl({_sql: layerRawSql});
|
||||
return pg.query(tokensQuery, function(err, resultSet) {
|
||||
if (err) {
|
||||
return processCallback(err);
|
||||
}
|
||||
|
||||
resultSet = resultSet || {};
|
||||
var rows = resultSet.rows || [];
|
||||
var result = rows[0] || {};
|
||||
|
||||
var tokens = {
|
||||
bbox: 'ST_SetSRID(ST_GeomFromText(\'' + result.bbox + '\'), 3857)',
|
||||
scale_denominator: result.scale_denominator,
|
||||
pixel_width: result.pixel_size,
|
||||
pixel_height: result.pixel_size
|
||||
};
|
||||
|
||||
var sql = SubstitutionTokens.replace(layerSql, tokens);
|
||||
self.process(pg, layer.options.cartocss, sql, processCallback);
|
||||
}, true); // use read-only transaction
|
||||
}
|
||||
|
||||
var tokens = {
|
||||
@@ -63,40 +146,35 @@ TurboCartoAdapter.prototype._parseCartoCss = function (username, layer, callback
|
||||
pixel_height: '156412'
|
||||
};
|
||||
|
||||
var layerSql = layer.options.sql;
|
||||
var layerRawSql = layer.options.sql_raw;
|
||||
if (SubstitutionTokens.hasTokens(layerSql) && layerRawSql) {
|
||||
var bbox = bboxTemplate({ _sql: layerRawSql });
|
||||
var zoom = zoomTemplate({ _bbox: bbox });
|
||||
var pixelSize = pixelSizeTemplate({ _bbox: bbox, _zoom: zoom });
|
||||
var scaleDenominator = scaleDenominatorTemplate({ _pixelSize: pixelSize });
|
||||
|
||||
tokens = {
|
||||
bbox: bbox,
|
||||
scale_denominator: scaleDenominator,
|
||||
pixel_width: pixelSize,
|
||||
pixel_height: pixelSize
|
||||
};
|
||||
}
|
||||
|
||||
var sql = SubstitutionTokens.replace(layerSql, tokens);
|
||||
this.process(pg, layer.options.cartocss, sql, processCallback);
|
||||
};
|
||||
|
||||
this.turboCartoParser.process(username, layer.options.cartocss, sql, function (err, cartocss) {
|
||||
// Only return turbo-carto errors
|
||||
if (err && err.name === 'TurboCartoError') {
|
||||
err = new Error('turbo-carto: ' + err.message);
|
||||
err.http_status = 400;
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// Try to continue in the rest of the cases
|
||||
if (cartocss) {
|
||||
layer.options.cartocss = cartocss;
|
||||
}
|
||||
return callback(null, layer);
|
||||
});
|
||||
TurboCartoAdapter.prototype.process = function (psql, cartocss, sql, callback) {
|
||||
var datasource = new PostgresDatasource(psql, sql);
|
||||
turboCarto(cartocss, datasource, callback);
|
||||
};
|
||||
|
||||
function shouldParseLayerCartocss(layer) {
|
||||
return layer && layer.options && layer.options.cartocss && layer.options.sql;
|
||||
}
|
||||
|
||||
function dbParamsFromReqParams(params) {
|
||||
var dbParams = {};
|
||||
if ( params.dbuser ) {
|
||||
dbParams.user = params.dbuser;
|
||||
}
|
||||
if ( params.dbpassword ) {
|
||||
dbParams.pass = params.dbpassword;
|
||||
}
|
||||
if ( params.dbhost ) {
|
||||
dbParams.host = params.dbhost;
|
||||
}
|
||||
if ( params.dbport ) {
|
||||
dbParams.port = params.dbport;
|
||||
}
|
||||
if ( params.dbname ) {
|
||||
dbParams.dbname = params.dbname;
|
||||
}
|
||||
return dbParams;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -90,11 +90,13 @@ NamedMapMapConfigProvider.prototype.getMapConfig = function(callback) {
|
||||
},
|
||||
function instantiateTemplate(err, templateParams) {
|
||||
assert.ifError(err);
|
||||
context.templateParams = templateParams;
|
||||
return self.templateMaps.instance(self.template, templateParams);
|
||||
},
|
||||
function prepareAdapterMapConfig(err, requestMapConfig) {
|
||||
assert.ifError(err);
|
||||
context.analysisConfiguration = {
|
||||
user: self.owner,
|
||||
db: {
|
||||
host: rendererParams.dbhost,
|
||||
port: rendererParams.dbport,
|
||||
@@ -112,13 +114,14 @@ 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;
|
||||
self.mapConfig = (mapConfig === null) ? null : new MapConfig(mapConfig, context.datasource);
|
||||
self.analysesResults = context.analysesResults || [];
|
||||
self.rendererParams = rendererParams;
|
||||
self.context = context;
|
||||
self.context.limits = renderLimits || {};
|
||||
return callback(self.err, self.mapConfig, self.rendererParams, self.context);
|
||||
}
|
||||
|
||||
119
lib/cartodb/models/resource-locator.js
Normal file
119
lib/cartodb/models/resource-locator.js
Normal file
@@ -0,0 +1,119 @@
|
||||
var dot = require('dot');
|
||||
dot.templateSettings.strip = false;
|
||||
|
||||
function ResourceLocator(environment) {
|
||||
this.environment = environment;
|
||||
|
||||
this.resourcesUrlTemplates = null;
|
||||
if (this.environment.resources_url_templates) {
|
||||
var templates = environment.resources_url_templates;
|
||||
|
||||
if (templates.http) {
|
||||
this.resourcesUrlTemplates = this.resourcesUrlTemplates || {};
|
||||
this.resourcesUrlTemplates.http = dot.template(templates.http + '/{{=it.resource}}');
|
||||
}
|
||||
if (templates.https) {
|
||||
this.resourcesUrlTemplates = this.resourcesUrlTemplates || {};
|
||||
this.resourcesUrlTemplates.https = dot.template(templates.https + '/{{=it.resource}}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ResourceLocator;
|
||||
|
||||
ResourceLocator.prototype.getUrls = function(username, resource) {
|
||||
if (this.resourcesUrlTemplates) {
|
||||
return this.getUrlsFromTemplate(username, resource);
|
||||
}
|
||||
var cdnDomain = getCdnDomain(this.environment.serverMetadata, resource);
|
||||
if (cdnDomain) {
|
||||
return {
|
||||
http: 'http://' + cdnDomain.http + '/' + username + '/api/v1/map/' + resource,
|
||||
https: 'https://' + cdnDomain.https + '/' + username + '/api/v1/map/' + resource
|
||||
};
|
||||
} else {
|
||||
var port = this.environment.port;
|
||||
return {
|
||||
http: 'http://' + username + '.' + 'localhost.lan:' + port + '/api/v1/map/' + resource
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
ResourceLocator.prototype.getUrlsFromTemplate = function(username, resource) {
|
||||
var urls = {};
|
||||
var cdnDomain = getCdnDomain(this.environment.serverMetadata, resource) || {};
|
||||
|
||||
if (this.resourcesUrlTemplates.http) {
|
||||
urls.http = this.resourcesUrlTemplates.http({
|
||||
cdn_url: cdnDomain.http,
|
||||
user: username,
|
||||
port: this.environment.port,
|
||||
resource: resource
|
||||
});
|
||||
}
|
||||
|
||||
if (this.resourcesUrlTemplates.https) {
|
||||
urls.https = this.resourcesUrlTemplates.https({
|
||||
cdn_url: cdnDomain.https,
|
||||
user: username,
|
||||
port: this.environment.port,
|
||||
resource: resource
|
||||
});
|
||||
}
|
||||
|
||||
return urls;
|
||||
};
|
||||
|
||||
function getCdnDomain(serverMetadata, resource) {
|
||||
if (serverMetadata && serverMetadata.cdn_url) {
|
||||
var cdnUrl = serverMetadata.cdn_url;
|
||||
var http = cdnUrl.http;
|
||||
var https = cdnUrl.https;
|
||||
if (cdnUrl.templates) {
|
||||
var templates = cdnUrl.templates;
|
||||
var httpUrlTemplate = templates.http.url;
|
||||
var httpsUrlTemplate = templates.https.url;
|
||||
http = httpUrlTemplate
|
||||
.replace(/^(http[s]*:\/\/)/, '')
|
||||
.replace('{s}', subdomain(templates.http.subdomains, resource));
|
||||
https = httpsUrlTemplate
|
||||
.replace(/^(http[s]*:\/\/)/, '')
|
||||
.replace('{s}', subdomain(templates.https.subdomains, resource));
|
||||
}
|
||||
return {
|
||||
http: http,
|
||||
https: https,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ref https://jsperf.com/js-crc32
|
||||
function crcTable() {
|
||||
var c;
|
||||
var table = [];
|
||||
for (var n = 0; n < 256; n++) {
|
||||
c = n;
|
||||
for (var k = 0; k < 8; k++) {
|
||||
c = ((c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1));
|
||||
}
|
||||
table[n] = c;
|
||||
}
|
||||
return table;
|
||||
}
|
||||
var CRC_TABLE = crcTable();
|
||||
|
||||
function crc32(str) {
|
||||
var crc = 0 ^ (-1);
|
||||
for (var i = 0; i < str.length; i++) {
|
||||
crc = (crc >>> 8) ^ CRC_TABLE[(crc ^ str.charCodeAt(i)) & 0xFF];
|
||||
}
|
||||
return (crc ^ (-1)) >>> 0;
|
||||
}
|
||||
|
||||
function subdomain(subdomains, resource) {
|
||||
var index = crc32(resource) % subdomains.length;
|
||||
return subdomains[index];
|
||||
}
|
||||
module.exports.subdomain = subdomain;
|
||||
@@ -3,7 +3,6 @@ var bodyParser = require('body-parser');
|
||||
var RedisPool = require('redis-mpool');
|
||||
var cartodbRedis = require('cartodb-redis');
|
||||
var _ = require('underscore');
|
||||
var debug = require('debug')('windshaft:cartodb');
|
||||
|
||||
var controller = require('./controllers');
|
||||
|
||||
@@ -34,16 +33,17 @@ var AnalysisBackend = require('./backends/analysis');
|
||||
var timeoutErrorTilePath = __dirname + '/../../assets/render-timeout-fallback.png';
|
||||
var timeoutErrorTile = require('fs').readFileSync(timeoutErrorTilePath, {encoding: null});
|
||||
|
||||
var TurboCartoParser = require('./utils/style/turbo-carto-parser');
|
||||
|
||||
var SqlWrapMapConfigAdapter = require('./models/mapconfig/adapter/sql-wrap-mapconfig-adapter');
|
||||
var MapConfigNamedLayersAdapter = require('./models/mapconfig/adapter/mapconfig-named-layers-adapter');
|
||||
var MapConfigBufferSizeAdapter = require('./models/mapconfig/adapter/mapconfig-buffer-size-adapter');
|
||||
var AnalysisMapConfigAdapter = require('./models/mapconfig/adapter/analysis-mapconfig-adapter');
|
||||
var MapConfigOverviewsAdapter = require('./models/mapconfig/adapter/mapconfig-overviews-adapter');
|
||||
var TurboCartoAdapter = require('./models/mapconfig/adapter/turbo-carto-adapter');
|
||||
var DataviewsWidgetsAdapter = require('./models/mapconfig/adapter/dataviews-widgets-adapter');
|
||||
var MapConfigAdapter = require('./models/mapconfig/adapter');
|
||||
|
||||
var StatsBackend = require('./backends/stats');
|
||||
|
||||
module.exports = function(serverOptions) {
|
||||
// Make stats client globally accessible
|
||||
global.statsClient = StatsClient.getInstance(serverOptions.statsd);
|
||||
@@ -118,8 +118,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);
|
||||
}
|
||||
@@ -150,20 +169,22 @@ module.exports = function(serverOptions) {
|
||||
var tileBackend = new windshaft.backend.Tile(rendererCache);
|
||||
var mapValidatorBackend = new windshaft.backend.MapValidator(tileBackend, attributesBackend);
|
||||
var mapBackend = new windshaft.backend.Map(rendererCache, mapStore, mapValidatorBackend);
|
||||
var analysisBackend = new AnalysisBackend(serverOptions.analysis);
|
||||
|
||||
var analysisBackend = new AnalysisBackend(metadataBackend, serverOptions.analysis);
|
||||
|
||||
var statsBackend = new StatsBackend();
|
||||
|
||||
var layergroupAffectedTablesCache = new LayergroupAffectedTablesCache();
|
||||
app.layergroupAffectedTablesCache = layergroupAffectedTablesCache;
|
||||
|
||||
var turboCartoParser = new TurboCartoParser(pgQueryRunner);
|
||||
|
||||
var mapConfigAdapter = new MapConfigAdapter(
|
||||
new MapConfigNamedLayersAdapter(templateMaps, pgConnection),
|
||||
new MapConfigBufferSizeAdapter(),
|
||||
new SqlWrapMapConfigAdapter(),
|
||||
new DataviewsWidgetsAdapter(),
|
||||
new AnalysisMapConfigAdapter(analysisBackend),
|
||||
new MapConfigOverviewsAdapter(overviewsMetadataApi, filterStatsApi),
|
||||
new TurboCartoAdapter(turboCartoParser)
|
||||
new TurboCartoAdapter()
|
||||
);
|
||||
|
||||
var namedMapProviderCache = new NamedMapProviderCache(
|
||||
@@ -183,6 +204,8 @@ module.exports = function(serverOptions) {
|
||||
var TablesExtentApi = require('./api/tables_extent_api');
|
||||
var tablesExtentApi = new TablesExtentApi(pgQueryRunner);
|
||||
|
||||
var versions = getAndValidateVersions(serverOptions);
|
||||
|
||||
/*******************************************************************************************************************
|
||||
* Routing
|
||||
******************************************************************************************************************/
|
||||
@@ -209,7 +232,8 @@ module.exports = function(serverOptions) {
|
||||
surrogateKeysCache,
|
||||
userLimitsApi,
|
||||
layergroupAffectedTablesCache,
|
||||
mapConfigAdapter
|
||||
mapConfigAdapter,
|
||||
statsBackend
|
||||
).register(app);
|
||||
|
||||
new controller.NamedMaps(
|
||||
@@ -225,7 +249,9 @@ module.exports = function(serverOptions) {
|
||||
|
||||
new controller.NamedMapsAdmin(authApi, pgConnection, templateMaps).register(app);
|
||||
|
||||
new controller.ServerInfo().register(app);
|
||||
new controller.Analyses(authApi, pgConnection).register(app);
|
||||
|
||||
new controller.ServerInfo(versions).register(app);
|
||||
|
||||
/*******************************************************************************************************************
|
||||
* END Routing
|
||||
@@ -238,12 +264,45 @@ function validateOptions(opts) {
|
||||
if (!_.isString(opts.base_url) || !_.isString(opts.base_url_mapconfig) || !_.isString(opts.base_url_templated)) {
|
||||
throw new Error("Must initialise server with: 'base_url'/'base_url_mapconfig'/'base_url_templated' URLs");
|
||||
}
|
||||
}
|
||||
|
||||
// Be nice and warn if configured mapnik version is != instaled mapnik version
|
||||
if (mapnik.versions.mapnik !== opts.grainstore.mapnik_version) {
|
||||
debug('WARNING: detected mapnik version (' + mapnik.versions.mapnik + ')' +
|
||||
' != configured mapnik version (' + opts.grainstore.mapnik_version + ')');
|
||||
function getAndValidateVersions(options) {
|
||||
// jshint undef:false
|
||||
var warn = console.warn.bind(console);
|
||||
// jshint undef:true
|
||||
|
||||
var packageDefinition = require('../../package.json');
|
||||
|
||||
var declaredDependencies = packageDefinition.dependencies || {};
|
||||
var installedDependenciesVersions = {
|
||||
camshaft: require('camshaft').version,
|
||||
grainstore: windshaft.grainstore.version(),
|
||||
mapnik: windshaft.mapnik.versions.mapnik,
|
||||
node_mapnik: windshaft.mapnik.version,
|
||||
'turbo-carto': require('turbo-carto').version,
|
||||
windshaft: windshaft.version,
|
||||
windshaft_cartodb: packageDefinition.version
|
||||
};
|
||||
|
||||
var dependenciesToValidate = ['camshaft', 'turbo-carto', 'windshaft'];
|
||||
dependenciesToValidate.forEach(function(depName) {
|
||||
var declaredDependencyVersion = declaredDependencies[depName];
|
||||
var installedDependencyVersion = installedDependenciesVersions[depName];
|
||||
if (declaredDependencyVersion !== installedDependencyVersion) {
|
||||
warn(
|
||||
'Dependency="%s" installed version="%s" does not match declared version="%s". Check your installation.',
|
||||
depName, installedDependencyVersion, declaredDependencyVersion
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Be nice and warn if configured mapnik version is != installed mapnik version
|
||||
if (mapnik.versions.mapnik !== options.grainstore.mapnik_version) {
|
||||
warn('WARNING: detected mapnik version (' + mapnik.versions.mapnik + ')' +
|
||||
' != configured mapnik version (' + options.grainstore.mapnik_version + ')');
|
||||
}
|
||||
|
||||
return installedDependenciesVersions;
|
||||
}
|
||||
|
||||
function bootstrapFonts(opts) {
|
||||
@@ -270,6 +329,25 @@ 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) {
|
||||
|
||||
@@ -36,7 +36,11 @@ var analysisConfig = _.defaults(global.environment.analysis || {}, {
|
||||
inlineExecution: false,
|
||||
endpoint: 'http://127.0.0.1:8080/api/v2/sql/job',
|
||||
hostHeaderTemplate: '{{=it.username}}.localhost.lan'
|
||||
}
|
||||
},
|
||||
logger: {
|
||||
filename: undefined
|
||||
},
|
||||
limits: {}
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
@@ -66,6 +70,7 @@ module.exports = {
|
||||
},
|
||||
datasource: global.environment.postgres,
|
||||
cachedir: global.environment.millstone.cache_basedir,
|
||||
use_workers: rendererConfig.mapnik.useCartocssWorkers || false,
|
||||
mapnik_version: global.environment.mapnik_version,
|
||||
mapnik_tile_format: global.environment.mapnik_tile_format || 'png',
|
||||
default_layergroup_ttl: global.environment.mapConfigTTL || 7200
|
||||
@@ -95,7 +100,11 @@ module.exports = {
|
||||
inlineExecution: analysisConfig.batch.inlineExecution,
|
||||
endpoint: analysisConfig.batch.endpoint,
|
||||
hostHeaderTemplate: analysisConfig.batch.hostHeaderTemplate
|
||||
}
|
||||
},
|
||||
logger: {
|
||||
filename: analysisConfig.logger.filename
|
||||
},
|
||||
limits: analysisConfig.limits
|
||||
},
|
||||
// Do not send unwatch on release. See http://github.com/CartoDB/Windshaft-cartodb/issues/161
|
||||
redis: _.extend(global.environment.redis, {unwatchOnRelease: false}),
|
||||
|
||||
@@ -2,7 +2,7 @@ var _ = require('underscore');
|
||||
var TableNameParser = require('./table_name_parser');
|
||||
|
||||
var BBoxFilter = require('../models/filter/bbox');
|
||||
var CamshaftFilter = require('../models/filter/camshaft');
|
||||
var AnalysisFilter = require('../models/filter/analysis');
|
||||
|
||||
// Minimim number of filtered rows to use overviews
|
||||
var FILTER_MIN_ROWS = 65536;
|
||||
@@ -11,8 +11,8 @@ var FILTER_MAX_FRACTION = 0.2;
|
||||
|
||||
function apply_filters_to_query(query, filters, bbox_filter) {
|
||||
if ( filters && !_.isEmpty(filters)) {
|
||||
var camshaftFilter = new CamshaftFilter(filters);
|
||||
query = camshaftFilter.sql(query);
|
||||
var analysisFilter = new AnalysisFilter(filters);
|
||||
query = analysisFilter.sql(query);
|
||||
}
|
||||
if ( bbox_filter ) {
|
||||
var bboxFilter = new BBoxFilter(bbox_filter.options, bbox_filter.params);
|
||||
|
||||
26
lib/cartodb/utils/query-utils.js
Normal file
26
lib/cartodb/utils/query-utils.js
Normal file
@@ -0,0 +1,26 @@
|
||||
function prepareQuery(sql) {
|
||||
var affectedTableRegexCache = {
|
||||
bbox: /!bbox!/g,
|
||||
scale_denominator: /!scale_denominator!/g,
|
||||
pixel_width: /!pixel_width!/g,
|
||||
pixel_height: /!pixel_height!/g
|
||||
};
|
||||
|
||||
return sql
|
||||
.replace(affectedTableRegexCache.bbox, 'ST_MakeEnvelope(0,0,0,0)')
|
||||
.replace(affectedTableRegexCache.scale_denominator, '0')
|
||||
.replace(affectedTableRegexCache.pixel_width, '1')
|
||||
.replace(affectedTableRegexCache.pixel_height, '1');
|
||||
}
|
||||
|
||||
module.exports.extractTableNames = function extractTableNames(query) {
|
||||
return [
|
||||
'SELECT * FROM CDB_QueryTablesText($windshaft$',
|
||||
prepareQuery(query),
|
||||
'$windshaft$) as tablenames'
|
||||
].join('');
|
||||
};
|
||||
|
||||
module.exports.getQueryRowCount = function getQueryRowEstimation(query) {
|
||||
return 'select CDB_EstimateRowCount(\'' + query + '\') as rows';
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var turboCarto = require('turbo-carto');
|
||||
var PostgresDatasource = require('./postgres-datasource');
|
||||
|
||||
function TurboCartoParser (pgQueryRunner) {
|
||||
this.pgQueryRunner = pgQueryRunner;
|
||||
}
|
||||
|
||||
module.exports = TurboCartoParser;
|
||||
|
||||
TurboCartoParser.prototype.process = function (username, cartocss, sql, callback) {
|
||||
var datasource = new PostgresDatasource(this.pgQueryRunner, username, sql);
|
||||
turboCarto(cartocss, datasource, callback);
|
||||
};
|
||||
4049
npm-shrinkwrap.json
generated
4049
npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load Diff
38
package.json
38
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "windshaft-cartodb",
|
||||
"version": "2.45.0",
|
||||
"version": "3.12.4",
|
||||
"description": "A map tile server for CartoDB",
|
||||
"keywords": [
|
||||
"cartodb"
|
||||
@@ -16,46 +16,50 @@
|
||||
"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>"
|
||||
],
|
||||
"dependencies": {
|
||||
"body-parser": "~1.14.0",
|
||||
"camshaft": "0.12.1",
|
||||
"cartodb-psql": "~0.6.1",
|
||||
"cartodb-query-tables": "~0.1.0",
|
||||
"cartodb-redis": "~0.13.0",
|
||||
"camshaft": "0.56.0",
|
||||
"cartodb-psql": "0.10.1",
|
||||
"cartodb-query-tables": "0.2.0",
|
||||
"cartodb-redis": "0.14.0",
|
||||
"debug": "~2.2.0",
|
||||
"dot": "~1.0.2",
|
||||
"express": "~4.13.3",
|
||||
"fastly-purge": "~1.0.1",
|
||||
"log4js": "https://github.com/CartoDB/log4js-node/tarball/cdb",
|
||||
"log4js": "cartodb/log4js-node#cdb",
|
||||
"lru-cache": "2.6.5",
|
||||
"lzma": "~1.3.7",
|
||||
"lzma": "~2.3.2",
|
||||
"node-statsd": "~0.0.7",
|
||||
"queue-async": "~1.0.7",
|
||||
"redis-mpool": "~0.4.0",
|
||||
"request": "~2.62.0",
|
||||
"redis-mpool": "0.4.1",
|
||||
"request": "~2.79.0",
|
||||
"step": "~0.0.6",
|
||||
"step-profiler": "~0.3.0",
|
||||
"turbo-carto": "0.10.1",
|
||||
"turbo-carto": "0.19.2",
|
||||
"underscore": "~1.6.0",
|
||||
"windshaft": "2.0.1"
|
||||
"windshaft": "3.3.1",
|
||||
"yargs": "~5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"istanbul": "~0.4.3",
|
||||
"jshint": "~2.6.0",
|
||||
"mocha": "~1.21.4",
|
||||
"jshint": "~2.9.4",
|
||||
"mocha": "~3.4.1",
|
||||
"moment": "~2.18.1",
|
||||
"nock": "~2.11.0",
|
||||
"redis": "~0.8.6",
|
||||
"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"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8 <0.11",
|
||||
"npm": ">=2.14.16"
|
||||
"node": ">=6.9",
|
||||
"yarn": "^0.21.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,4 +4,4 @@ if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:/opt/X11/lib/pkgconfig
|
||||
fi
|
||||
|
||||
npm install
|
||||
yarn
|
||||
|
||||
@@ -202,7 +202,7 @@ describe('analysis-layers', function() {
|
||||
assert.equal(analyses.length, 1, 'Invalid number of analyses in metadata');
|
||||
var nodes = analyses[0].nodes;
|
||||
var nodesIds = Object.keys(nodes);
|
||||
assert.deepEqual(nodesIds, ['2570e105-7b37-40d2-bdf4-1af889598745', 'HEAD']);
|
||||
assert.deepEqual(nodesIds, ['HEAD', '2570e105-7b37-40d2-bdf4-1af889598745']);
|
||||
nodesIds.forEach(function(nodeId) {
|
||||
var node = nodes[nodeId];
|
||||
assert.ok(node.hasOwnProperty('url'), 'Missing "url" attribute in node');
|
||||
|
||||
@@ -20,6 +20,13 @@ describe('analysis-layers error cases', function() {
|
||||
}
|
||||
};
|
||||
|
||||
var AUTH_ERROR_RESPONSE = {
|
||||
status: 403,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
it('should handle missing analysis nodes for layers', function(done) {
|
||||
var mapConfig = createMapConfig(
|
||||
[
|
||||
@@ -64,4 +71,372 @@ describe('analysis-layers error cases', function() {
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle missing analyses when layers point to nonexistent one', function(done) {
|
||||
var mapConfig = createMapConfig(
|
||||
[
|
||||
{
|
||||
"type": "http",
|
||||
"options": {
|
||||
"urlTemplate": "http://{s}.basemaps.cartocdn.com/light_only_labels/{z}/{x}/{y}.png",
|
||||
"subdomains": "abcd"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"source": {
|
||||
"id": "ID-FOR-NONEXISTENT-ANALYSIS"
|
||||
},
|
||||
"cartocss": '#polygons { polygon-fill: red; }',
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
var testClient = new TestClient(mapConfig, 1234);
|
||||
|
||||
testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(layergroupResult.errors.length, 1);
|
||||
assert.equal(
|
||||
layergroupResult.errors[0],
|
||||
'Missing analysis node.id="ID-FOR-NONEXISTENT-ANALYSIS" for layer=1'
|
||||
);
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle missing analyses when dataviews point to nonexistent one', function(done) {
|
||||
var mapConfig = createMapConfig(
|
||||
[
|
||||
{
|
||||
"type": "http",
|
||||
"options": {
|
||||
"urlTemplate": "http://{s}.basemaps.cartocdn.com/light_only_labels/{z}/{x}/{y}.png",
|
||||
"subdomains": "abcd"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"sql": "select * from populated_places_simple_reduced",
|
||||
"cartocss": '#polygons { polygon-fill: red; }',
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
{
|
||||
pop_max_histogram: {
|
||||
source: {
|
||||
id: 'ID-FOR-NONEXISTENT-ANALYSIS'
|
||||
},
|
||||
type: 'histogram',
|
||||
options: {
|
||||
column: 'pop_max'
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
var testClient = new TestClient(mapConfig, 1234);
|
||||
|
||||
testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(layergroupResult.errors.length, 1);
|
||||
assert.equal(layergroupResult.errors[0], 'Node with `source.id="ID-FOR-NONEXISTENT-ANALYSIS"`' +
|
||||
' not found in analyses for dataview "pop_max_histogram"');
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('camshaft: should return error missing analysis nodes for layers with some context', function(done) {
|
||||
var mapConfig = createMapConfig(
|
||||
[
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"source": {
|
||||
"id": "HEAD"
|
||||
},
|
||||
"cartocss": '#polygons { polygon-fill: red; }',
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
{},
|
||||
[
|
||||
{
|
||||
"id": "HEAD",
|
||||
"type": "buffer",
|
||||
"params": {
|
||||
"source": {
|
||||
"id": "HEAD",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from populated_places_simple_reduced"
|
||||
}
|
||||
},
|
||||
"radius": 50000
|
||||
}
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
var testClient = new TestClient(mapConfig, 11111);
|
||||
|
||||
testClient.getLayergroup(AUTH_ERROR_RESPONSE, function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(layergroupResult.errors.length, 1);
|
||||
assert.equal(
|
||||
layergroupResult.errors[0],
|
||||
'Analysis requires authentication with API key: permission denied.'
|
||||
);
|
||||
|
||||
assert.equal(layergroupResult.errors_with_context[0].type, 'analysis');
|
||||
assert.equal(
|
||||
layergroupResult.errors_with_context[0].message,
|
||||
'Analysis requires authentication with API key: permission denied.'
|
||||
);
|
||||
assert.equal(layergroupResult.errors_with_context[0].analysis.id, 'HEAD');
|
||||
assert.equal(layergroupResult.errors_with_context[0].analysis.type, 'buffer');
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('camshaft: should return error: Missing required param "radius"; with context', function(done) {
|
||||
var mapConfig = createMapConfig(
|
||||
[
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"source": {
|
||||
"id": "HEAD"
|
||||
},
|
||||
"cartocss": '#polygons { polygon-fill: red; }',
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
{},
|
||||
[
|
||||
{
|
||||
"id": "HEAD",
|
||||
"type": "buffer",
|
||||
"params": {
|
||||
"source": {
|
||||
"id": "HEAD2",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from populated_places_simple_reduced"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
var testClient = new TestClient(mapConfig, 1234);
|
||||
|
||||
testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(layergroupResult.errors.length, 1);
|
||||
assert.equal(
|
||||
layergroupResult.errors[0],
|
||||
'Missing required param "radius"'
|
||||
);
|
||||
|
||||
assert.equal(layergroupResult.errors_with_context[0].type, 'analysis');
|
||||
assert.equal(layergroupResult.errors_with_context[0].message, 'Missing required param "radius"');
|
||||
assert.equal(layergroupResult.errors_with_context[0].analysis.id, 'HEAD');
|
||||
assert.equal(layergroupResult.errors_with_context[0].analysis.type, 'buffer');
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return missing param error of outer node indicating the node_id and context', function(done) {
|
||||
var mapConfig = createMapConfig([{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"source": {
|
||||
"id": "HEAD"
|
||||
},
|
||||
"cartocss": '#polygons { polygon-fill: red; }',
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}], {}, [{
|
||||
"id": "HEAD",
|
||||
"type": "buffer",
|
||||
"params": {
|
||||
"source": {
|
||||
"id": "HEAD2",
|
||||
"type": "buffer",
|
||||
"params": {
|
||||
"source": {
|
||||
"id": "HEAD3",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from populated_places_simple_reduced"
|
||||
}
|
||||
},
|
||||
"radius": 10
|
||||
}
|
||||
}
|
||||
}
|
||||
// radius: 'missing'
|
||||
}]);
|
||||
|
||||
var testClient = new TestClient(mapConfig, 1234);
|
||||
|
||||
testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(layergroupResult.errors.length, 1);
|
||||
assert.equal(
|
||||
layergroupResult.errors[0],
|
||||
'Missing required param "radius"'
|
||||
);
|
||||
|
||||
assert.equal(layergroupResult.errors_with_context[0].type, 'analysis');
|
||||
assert.equal(layergroupResult.errors_with_context[0].message, 'Missing required param "radius"');
|
||||
assert.equal(layergroupResult.errors_with_context[0].analysis.id, 'HEAD');
|
||||
assert.equal(layergroupResult.errors_with_context[0].analysis.type, 'buffer');
|
||||
assert.equal(layergroupResult.errors_with_context[0].analysis.node_id, 'HEAD');
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return invalid param type error of inner node indicating the node_id and context', function(done) {
|
||||
var mapConfig = createMapConfig([{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"source": {
|
||||
"id": "HEAD"
|
||||
},
|
||||
"cartocss": '#polygons { polygon-fill: red; }',
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}], {}, [{
|
||||
"id": "HEAD",
|
||||
"type": "buffer",
|
||||
"params": {
|
||||
"source": {
|
||||
"id": "HEAD2",
|
||||
"type": "buffer",
|
||||
"params": {
|
||||
"source": {
|
||||
"id": "HEAD3",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from populated_places_simple_reduced"
|
||||
}
|
||||
},
|
||||
"radius": 'invalid_radius'
|
||||
}
|
||||
},
|
||||
"radius": 10
|
||||
}
|
||||
}]);
|
||||
|
||||
var testClient = new TestClient(mapConfig, 1234);
|
||||
|
||||
testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(layergroupResult.errors.length, 1);
|
||||
assert.equal(
|
||||
layergroupResult.errors[0],
|
||||
'Invalid type for param "radius", expects "number" type, got `"invalid_radius"`'
|
||||
);
|
||||
|
||||
assert.equal(layergroupResult.errors_with_context[0].type, 'analysis');
|
||||
assert.equal(
|
||||
layergroupResult.errors_with_context[0].message,
|
||||
'Invalid type for param "radius", expects "number" type, got `"invalid_radius"`'
|
||||
);
|
||||
assert.equal(layergroupResult.errors_with_context[0].analysis.id, 'HEAD');
|
||||
assert.equal(layergroupResult.errors_with_context[0].analysis.type, 'buffer');
|
||||
assert.equal(layergroupResult.errors_with_context[0].analysis.node_id, 'HEAD2');
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return "function does not exist" indicating the node_id and context', function(done) {
|
||||
var mapConfig = createMapConfig([{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"source": {
|
||||
"id": "HEAD"
|
||||
},
|
||||
"cartocss": '#polygons { polygon-fill: red; }',
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}], {}, [{
|
||||
"id": "HEAD",
|
||||
"type": "buffer",
|
||||
"params": {
|
||||
"source": {
|
||||
"id": "HEAD2",
|
||||
"type": "buffer",
|
||||
"params": {
|
||||
"source": {
|
||||
"id": "HEAD3",
|
||||
"type": 'deprecated-sql-function',
|
||||
"params": {
|
||||
"id": "HEAD4",
|
||||
"function_name": 'DEP_EXT_does_not_exist_fn',
|
||||
"primary_source": {
|
||||
"type": 'source',
|
||||
"params": {
|
||||
"query": "select * from populated_places_simple_reduced"
|
||||
}
|
||||
},
|
||||
"function_args": ['wadus']
|
||||
}
|
||||
},
|
||||
"radius": 10
|
||||
}
|
||||
},
|
||||
"radius": 10
|
||||
}
|
||||
}]);
|
||||
|
||||
var testClient = new TestClient(mapConfig, 1234);
|
||||
|
||||
testClient.getLayergroup(ERROR_RESPONSE, function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.equal(layergroupResult.errors.length, 1);
|
||||
assert.equal(
|
||||
layergroupResult.errors[0],
|
||||
'function dep_ext_does_not_exist_fn(unknown, unknown, unknown, text[], unknown) does not exist'
|
||||
);
|
||||
|
||||
assert.equal(layergroupResult.errors_with_context[0].type, 'analysis');
|
||||
assert.equal(
|
||||
layergroupResult.errors_with_context[0].message,
|
||||
'function dep_ext_does_not_exist_fn(unknown, unknown, unknown, text[], unknown) does not exist'
|
||||
);
|
||||
assert.equal(layergroupResult.errors_with_context[0].analysis.id, 'HEAD');
|
||||
assert.equal(layergroupResult.errors_with_context[0].analysis.type, 'buffer');
|
||||
assert.equal(layergroupResult.errors_with_context[0].analysis.node_id, 'HEAD3');
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
});
|
||||
|
||||
@@ -141,7 +141,7 @@ describe('named-maps analysis', function() {
|
||||
assert.equal(analyses.length, 1, 'Invalid number of analyses in metadata');
|
||||
var nodes = analyses[0].nodes;
|
||||
var nodesIds = Object.keys(nodes);
|
||||
assert.deepEqual(nodesIds, ['2570e105-7b37-40d2-bdf4-1af889598745', 'HEAD']);
|
||||
assert.deepEqual(nodesIds, ['HEAD', '2570e105-7b37-40d2-bdf4-1af889598745']);
|
||||
nodesIds.forEach(function(nodeId) {
|
||||
var node = nodes[nodeId];
|
||||
assert.ok(node.hasOwnProperty('url'), 'Missing "url" attribute in node');
|
||||
|
||||
115
test/acceptance/analysis/regressions.js
Normal file
115
test/acceptance/analysis/regressions.js
Normal file
@@ -0,0 +1,115 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var TestClient = require('../../support/test-client');
|
||||
|
||||
describe('analysis-layers regressions', function() {
|
||||
it('should return a complete list of nodes from analysis', function(done) {
|
||||
var mapConfig = {
|
||||
"version": "1.5.0",
|
||||
"layers": [
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"cartocss": TestClient.CARTOCSS.POINTS,
|
||||
"cartocss_version": "2.1.1",
|
||||
"interactivity": [],
|
||||
"source": {
|
||||
"id": "a4"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"cartocss": TestClient.CARTOCSS.POINTS,
|
||||
"cartocss_version": "2.1.0",
|
||||
"interactivity": [],
|
||||
"source": {
|
||||
"id": "b1"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"dataviews": {
|
||||
"74493a30-4679-4b72-a60c-b6f808b57c98": {
|
||||
"type": "histogram",
|
||||
"source": {
|
||||
"id": "b0"
|
||||
},
|
||||
"options": {
|
||||
"column": "customer_value",
|
||||
"bins": 10
|
||||
}
|
||||
}
|
||||
},
|
||||
"analyses": [
|
||||
{
|
||||
"id": "a4",
|
||||
"type": "point-in-polygon",
|
||||
"params": {
|
||||
"polygons_source": {
|
||||
"id": "a3",
|
||||
"type": "buffer",
|
||||
"params": {
|
||||
"source": {
|
||||
"id": "a2",
|
||||
"type": "centroid",
|
||||
"params": {
|
||||
"source": {
|
||||
"id": "b1",
|
||||
"type": "kmeans",
|
||||
"params": {
|
||||
"source": {
|
||||
"id": "b0",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "SELECT * FROM populated_places_simple_reduced"
|
||||
}
|
||||
},
|
||||
"clusters": 5
|
||||
}
|
||||
},
|
||||
"category_column": "cluster_no"
|
||||
}
|
||||
},
|
||||
"radius": 200000
|
||||
}
|
||||
},
|
||||
"points_source": {
|
||||
"id": "customer_home_locations",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "SELECT * FROM populated_places_simple_reduced"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var testClient = new TestClient(mapConfig, 1234);
|
||||
|
||||
testClient.getLayergroup(function(err, layergroupResult) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.ok(layergroupResult);
|
||||
assert.ok(layergroupResult.metadata);
|
||||
var analyses = layergroupResult.metadata.analyses;
|
||||
assert.ok(analyses);
|
||||
assert.equal(analyses.length, 1);
|
||||
|
||||
var expectedIds = ['customer_home_locations', 'b0', 'b1', 'a2', 'a3', 'a4'];
|
||||
expectedIds.forEach(function(expectedId) {
|
||||
assert.ok(
|
||||
analyses[0].nodes.hasOwnProperty(expectedId),
|
||||
'Missing "' + expectedId + '" from node list.'
|
||||
);
|
||||
});
|
||||
assert.equal(Object.keys(analyses[0].nodes).length, expectedIds.length, Object.keys(analyses[0].nodes));
|
||||
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
441
test/acceptance/buffer-size-format.js
Normal file
441
test/acceptance/buffer-size-format.js
Normal file
@@ -0,0 +1,441 @@
|
||||
require('../support/test_helper');
|
||||
|
||||
var fs = require('fs');
|
||||
var assert = require('../support/assert');
|
||||
var TestClient = require('../support/test-client');
|
||||
var mapnik = require('windshaft').mapnik;
|
||||
var IMAGE_TOLERANCE_PER_MIL = 5;
|
||||
|
||||
var CARTOCSS_LABELS = [
|
||||
'#layer {',
|
||||
' polygon-fill: #374C70;',
|
||||
' polygon-opacity: 0.9;',
|
||||
' line-width: 1;',
|
||||
' line-color: #FFF;',
|
||||
' line-opacity: 0.5;',
|
||||
'}',
|
||||
'#layer::labels {',
|
||||
' text-name: [name];',
|
||||
' text-face-name: \'DejaVu Sans Book\';',
|
||||
' text-size: 20;',
|
||||
' text-fill: #FFFFFF;',
|
||||
' text-label-position-tolerance: 0;',
|
||||
' text-halo-radius: 1;',
|
||||
' text-halo-fill: #6F808D;',
|
||||
' text-dy: -10;',
|
||||
' text-allow-overlap: true;',
|
||||
' text-placement: point;',
|
||||
' text-placement-type: dummy;',
|
||||
'}'
|
||||
].join('\n');
|
||||
|
||||
function createMapConfig (bufferSize, cartocss) {
|
||||
cartocss = cartocss || CARTOCSS_LABELS;
|
||||
|
||||
return {
|
||||
version: '1.6.0',
|
||||
buffersize: bufferSize,
|
||||
layers: [{
|
||||
type: "cartodb",
|
||||
options: {
|
||||
sql: [
|
||||
'select',
|
||||
' *',
|
||||
'from',
|
||||
' populated_places_simple_reduced',
|
||||
].join('\n'),
|
||||
cartocss: cartocss,
|
||||
cartocss_version: '2.3.0',
|
||||
interactivity: 'cartodb_id'
|
||||
}
|
||||
}]
|
||||
};
|
||||
}
|
||||
|
||||
describe('buffer size per format', function () {
|
||||
var testCases = [
|
||||
{
|
||||
desc: 'should get png tile using buffer-size 0',
|
||||
coords: { z: 7, x: 64, y: 48 },
|
||||
format: 'png',
|
||||
fixturePath: './test/fixtures/buffer-size/tile-7.64.48-buffer-size-0.png',
|
||||
mapConfig: createMapConfig({ png: 0, 'grid.json': 0 }),
|
||||
assert: function (tile, callback) {
|
||||
assert.imageIsSimilarToFile(tile, this.fixturePath, IMAGE_TOLERANCE_PER_MIL, callback);
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'should get png tile using buffer-size 128',
|
||||
coords: { z: 7, x: 64, y: 48 },
|
||||
format: 'png',
|
||||
fixturePath: './test/fixtures/buffer-size/tile-7.64.48-buffer-size-128.png',
|
||||
mapConfig: createMapConfig({ png: 128, 'grid.json': 128 }),
|
||||
assert: function (tile, callback) {
|
||||
assert.imageIsSimilarToFile(tile, this.fixturePath, IMAGE_TOLERANCE_PER_MIL, callback);
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'should get mvt tile using buffer-size 0',
|
||||
coords: { z: 7, x: 64, y: 48 },
|
||||
format: 'mvt',
|
||||
fixturePath: './test/fixtures/buffer-size/tile-7.64.48-buffer-size-0.mvt',
|
||||
mapConfig: createMapConfig({ mvt: 0 }),
|
||||
assert: function (tile, callback) {
|
||||
var tileJSON = tile.toJSON();
|
||||
var features = tileJSON[0].features;
|
||||
assert.equal(features.length, 1);
|
||||
callback();
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'should get mvt tile using buffer-size 128',
|
||||
coords: { z: 7, x: 64, y: 48 },
|
||||
format: 'mvt',
|
||||
fixturePath: './test/fixtures/buffer-size/tile-7.64.48-buffer-size-128.mvt',
|
||||
mapConfig: createMapConfig({ mvt: 128 }),
|
||||
assert: function (tile, callback) {
|
||||
var tileJSON = tile.toJSON();
|
||||
var features = tileJSON[0].features;
|
||||
assert.equal(features.length, 9);
|
||||
callback();
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'should get grid.json tile using buffer-size 0 overriden by template params',
|
||||
coords: { z: 7, x: 64, y: 48 },
|
||||
format: 'grid.json',
|
||||
layers: [0],
|
||||
fixturePath: './test/fixtures/buffer-size/tile-grid.json.7.64.48-buffer-size-0.grid.json',
|
||||
mapConfig: createMapConfig({ 'grid.json': 0 }),
|
||||
assert: function (tile, callback) {
|
||||
assert.utfgridEqualsFile(tile, this.fixturePath, 2,callback);
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'should get grid.json tile using buffer-size 128 overriden by template params',
|
||||
coords: { z: 7, x: 64, y: 48 },
|
||||
format: 'grid.json',
|
||||
layers: [0],
|
||||
fixturePath: './test/fixtures/buffer-size/tile-7.64.48-buffer-size-128.grid.json',
|
||||
mapConfig: createMapConfig({ 'grid.json': 128 }),
|
||||
assert: function (tile, callback) {
|
||||
assert.utfgridEqualsFile(tile, this.fixturePath, 2, callback);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
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) {
|
||||
assert.ifError(err);
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createBufferSizeTemplate (name, buffersize, placeholders, cartocss) {
|
||||
cartocss = cartocss || CARTOCSS_LABELS;
|
||||
|
||||
return {
|
||||
"version": "0.0.1",
|
||||
"name": name,
|
||||
"placeholders": placeholders || {
|
||||
"buffersize": {
|
||||
"type": "number",
|
||||
"default": 0
|
||||
}
|
||||
},
|
||||
"layergroup": createMapConfig(buffersize)
|
||||
};
|
||||
}
|
||||
|
||||
describe('buffer size per format for named maps', function () {
|
||||
var testCases = [
|
||||
{
|
||||
desc: 'should get png tile using buffer-size 0 (default value in template)',
|
||||
coords: { z: 7, x: 64, y: 48 },
|
||||
format: 'png',
|
||||
fixturePath: './test/fixtures/buffer-size/tile-7.64.48-buffer-size-0.png',
|
||||
template: createBufferSizeTemplate('named-default-buffer-size', {png: '<%= buffersize %>'}),
|
||||
assert: function (tile, callback) {
|
||||
assert.imageIsSimilarToFile(tile, this.fixturePath, IMAGE_TOLERANCE_PER_MIL, callback);
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'should get png tile using buffer-size 128 (placehoder value)',
|
||||
coords: { z: 7, x: 64, y: 48 },
|
||||
format: 'png',
|
||||
placeholders: { buffersize: 128 },
|
||||
fixturePath: './test/fixtures/buffer-size/tile-7.64.48-buffer-size-128.png',
|
||||
template: createBufferSizeTemplate('named-custom-buffer-size', { png: '<%= buffersize %>'}),
|
||||
assert: function (tile, callback) {
|
||||
assert.imageIsSimilarToFile(tile, this.fixturePath, IMAGE_TOLERANCE_PER_MIL, callback);
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'should get png tile using buffer-size 0 (default value in template by format)',
|
||||
coords: { z: 7, x: 64, y: 48 },
|
||||
format: 'png',
|
||||
placeholders: { buffersize_png: 0 },
|
||||
fixturePath: './test/fixtures/buffer-size/tile-7.64.48-buffer-size-0.png',
|
||||
template: createBufferSizeTemplate('named-default-buffer-size-by-format', {
|
||||
png: '<%= buffersize_png %>'
|
||||
}, {
|
||||
"buffersize_png": {
|
||||
"type": "number",
|
||||
"default": "0"
|
||||
}
|
||||
}),
|
||||
assert: function (tile, callback) {
|
||||
assert.imageIsSimilarToFile(tile, this.fixturePath, IMAGE_TOLERANCE_PER_MIL, callback);
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'should get png tile using buffer-size 128 (placehoder value in template by format)',
|
||||
coords: { z: 7, x: 64, y: 48 },
|
||||
format: 'png',
|
||||
placeholders: { buffersize_png: 128 },
|
||||
fixturePath: './test/fixtures/buffer-size/tile-7.64.48-buffer-size-128.png',
|
||||
template: createBufferSizeTemplate('named-custom-buffer-size-by-format', {
|
||||
png: '<%= buffersize_png %>'
|
||||
}, {
|
||||
"buffersize_png": {
|
||||
"type": "number",
|
||||
"default": "0"
|
||||
}
|
||||
}),
|
||||
assert: function (tile, callback) {
|
||||
assert.imageIsSimilarToFile(tile, this.fixturePath, IMAGE_TOLERANCE_PER_MIL, callback);
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'should get grid.json tile using buffer-size 0 overriden by template params',
|
||||
coords: { z: 7, x: 64, y: 48 },
|
||||
format: 'grid.json',
|
||||
layers: [0],
|
||||
placeholders: { buffersize_gridjson: 0 },
|
||||
fixturePath: './test/fixtures/buffer-size/tile-grid.json.7.64.48-buffer-size-0.grid.json',
|
||||
template: createBufferSizeTemplate('named-default-buffer-size-by-format-gridjson', {
|
||||
'grid.json': '<%= buffersize_gridjson %>'
|
||||
}, {
|
||||
"buffersize_gridjson": {
|
||||
"type": "number",
|
||||
"default": "0"
|
||||
}
|
||||
}),
|
||||
assert: function (tile, callback) {
|
||||
assert.utfgridEqualsFile(tile, this.fixturePath, 2,callback);
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'should get grid.json tile using buffer-size 128 overriden by template params',
|
||||
coords: { z: 7, x: 64, y: 48 },
|
||||
format: 'grid.json',
|
||||
layers: [0],
|
||||
placeholders: { buffersize_gridjson: 128 },
|
||||
fixturePath: './test/fixtures/buffer-size/tile-7.64.48-buffer-size-128.grid.json',
|
||||
template: createBufferSizeTemplate('named-custom-buffer-size-by-format-gridjson', {
|
||||
'grid.json': '<%= buffersize_gridjson %>'
|
||||
}, {
|
||||
"buffersize_gridjson": {
|
||||
"type": "number",
|
||||
"default": "0"
|
||||
}
|
||||
}),
|
||||
assert: function (tile, callback) {
|
||||
assert.utfgridEqualsFile(tile, this.fixturePath, 2, callback);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
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/fixtures/buffer-size/tile-7.64.48-buffer-size-0-test.png');
|
||||
test.assert(tile, function (err) {
|
||||
assert.ifError(err);
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('buffer size per format for named maps w/o placeholders', function () {
|
||||
var testCases = [
|
||||
{
|
||||
desc: 'should get png tile using buffer-size 0 overriden by template params',
|
||||
coords: { z: 7, x: 64, y: 48 },
|
||||
format: 'png',
|
||||
placeholders: {
|
||||
buffersize: {
|
||||
png: 0
|
||||
}
|
||||
},
|
||||
fixturePath: './test/fixtures/buffer-size/tile-7.64.48-buffer-size-0.png',
|
||||
template: createBufferSizeTemplate('named-no-buffer-size-png-0', {}, {}),
|
||||
assert: function (tile, callback) {
|
||||
assert.imageIsSimilarToFile(tile, this.fixturePath, IMAGE_TOLERANCE_PER_MIL, callback);
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'should get png tile using buffer-size 128 overriden by template params',
|
||||
coords: { z: 7, x: 64, y: 48 },
|
||||
format: 'png',
|
||||
placeholders: {
|
||||
buffersize: {
|
||||
png: 128
|
||||
}
|
||||
},
|
||||
fixturePath: './test/fixtures/buffer-size/tile-7.64.48-buffer-size-128.png',
|
||||
template: createBufferSizeTemplate('named-no-buffer-size-png-128', {}, {}),
|
||||
assert: function (tile, callback) {
|
||||
assert.imageIsSimilarToFile(tile, this.fixturePath, IMAGE_TOLERANCE_PER_MIL, callback);
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'should get mvt tile using buffer-size 0 overriden by template params',
|
||||
coords: { z: 7, x: 64, y: 48 },
|
||||
format: 'mvt',
|
||||
placeholders: {
|
||||
buffersize: {
|
||||
mvt: 0
|
||||
}
|
||||
},
|
||||
fixturePath: './test/fixtures/buffer-size/tile-mvt-7.64.48-buffer-size-0.mvt',
|
||||
template: createBufferSizeTemplate('named-no-buffer-size-mvt', {}, {}),
|
||||
assert: function (tile, callback) {
|
||||
var tileJSON = tile.toJSON();
|
||||
var features = tileJSON[0].features;
|
||||
|
||||
var dataFixture = fs.readFileSync(this.fixturePath);
|
||||
var vtile = new mapnik.VectorTile(this.coords.z, this.coords.x, this.coords.y);
|
||||
vtile.setDataSync(dataFixture);
|
||||
var vtileJSON = vtile.toJSON();
|
||||
var vtileFeatures = vtileJSON[0].features;
|
||||
|
||||
assert.equal(features.length, vtileFeatures.length);
|
||||
callback();
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'should get mvt tile using buffer-size 128 overriden by template params',
|
||||
coords: { z: 7, x: 64, y: 48 },
|
||||
format: 'mvt',
|
||||
placeholders: {
|
||||
buffersize: {
|
||||
mvt: 128
|
||||
}
|
||||
},
|
||||
fixturePath: './test/fixtures/buffer-size/tile-mvt-7.64.48-buffer-size-128.mvt',
|
||||
template: createBufferSizeTemplate('named-no-buffer-size-mvt-128', {}, {}),
|
||||
assert: function (tile, callback) {
|
||||
var tileJSON = tile.toJSON();
|
||||
var features = tileJSON[0].features;
|
||||
|
||||
var dataFixture = fs.readFileSync(this.fixturePath);
|
||||
var vtile = new mapnik.VectorTile(this.coords.z, this.coords.x, this.coords.y);
|
||||
vtile.setDataSync(dataFixture);
|
||||
var vtileJSON = vtile.toJSON();
|
||||
var vtileFeatures = vtileJSON[0].features;
|
||||
|
||||
assert.equal(features.length, vtileFeatures.length);
|
||||
callback();
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'should get grid.json tile using buffer-size 0 overriden by template params',
|
||||
coords: { z: 7, x: 64, y: 48 },
|
||||
format: 'grid.json',
|
||||
layers: [0],
|
||||
placeholders: {
|
||||
buffersize: {
|
||||
'grid.json': 0
|
||||
}
|
||||
},
|
||||
fixturePath: './test/fixtures/buffer-size/tile-grid.json.7.64.48-buffer-size-0.grid.json',
|
||||
template: createBufferSizeTemplate('named-no-buffer-size-grid-json-0', {}, {}),
|
||||
assert: function (tile, callback) {
|
||||
assert.utfgridEqualsFile(tile, this.fixturePath, 2,callback);
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'should get grid.json tile using buffer-size 128 overriden by template params',
|
||||
coords: { z: 7, x: 64, y: 48 },
|
||||
format: 'grid.json',
|
||||
layers: [0],
|
||||
placeholders: {
|
||||
buffersize: {
|
||||
'grid.json': 128
|
||||
}
|
||||
},
|
||||
fixturePath: './test/fixtures/buffer-size/tile-7.64.48-buffer-size-128.grid.json',
|
||||
template: createBufferSizeTemplate('named-no-buffer-size-grid-json-128', {}, {}),
|
||||
assert: function (tile, callback) {
|
||||
assert.utfgridEqualsFile(tile, this.fixturePath, 2, callback);
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'should get png tile using buffer-size 0' +
|
||||
' overriden by template params with no buffersize in mapconfig',
|
||||
coords: { z: 7, x: 64, y: 48 },
|
||||
format: 'png',
|
||||
placeholders: {
|
||||
buffersize: {
|
||||
png: 0
|
||||
}
|
||||
},
|
||||
fixturePath: './test/fixtures/buffer-size/tile-7.64.48-buffer-size-0.png',
|
||||
template: createBufferSizeTemplate('named-no-buffer-size-mapconfig-png-0', undefined, {}),
|
||||
assert: function (tile, callback) {
|
||||
assert.imageIsSimilarToFile(tile, this.fixturePath, IMAGE_TOLERANCE_PER_MIL, callback);
|
||||
}
|
||||
},
|
||||
|
||||
];
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
393
test/acceptance/cache/cache_headers.js
vendored
Normal file
393
test/acceptance/cache/cache_headers.js
vendored
Normal file
@@ -0,0 +1,393 @@
|
||||
var testHelper = require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var qs = require('querystring');
|
||||
|
||||
var CartodbWindshaft = require('../../../lib/cartodb/server');
|
||||
var serverOptions = require('../../../lib/cartodb/server_options');
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
server.setMaxListeners(0);
|
||||
|
||||
var LayergroupToken = require('../../support/layergroup-token');
|
||||
|
||||
describe('get requests with cache headers', function() {
|
||||
|
||||
var keysToDelete;
|
||||
beforeEach(function() {
|
||||
keysToDelete = {};
|
||||
});
|
||||
|
||||
afterEach(function(done) {
|
||||
testHelper.deleteRedisKeys(keysToDelete, done);
|
||||
});
|
||||
|
||||
var statusOkResponse = {
|
||||
status: 200
|
||||
};
|
||||
|
||||
var mapConfigs = [
|
||||
{
|
||||
"description": "cache headers should be present",
|
||||
"cache_headers": {
|
||||
"x_cache_channel": {
|
||||
"db_name": "test_windshaft_cartodb_user_1_db",
|
||||
"tables": ["public.test_table"]
|
||||
},
|
||||
"surrogate_keys": "t:77pJnX"
|
||||
},
|
||||
"data":
|
||||
{
|
||||
version: '1.5.0',
|
||||
layers: [
|
||||
{
|
||||
options: {
|
||||
source: {
|
||||
id: "2570e105-7b37-40d2-bdf4-1af889598745"
|
||||
},
|
||||
sql: 'select * from test_table limit 2',
|
||||
cartocss: '#layer { marker-fill:red; }',
|
||||
cartocss_version: '2.3.0',
|
||||
attributes: {
|
||||
id:'cartodb_id',
|
||||
columns: [
|
||||
'name',
|
||||
'address'
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
analyses: [
|
||||
{
|
||||
"id": "2570e105-7b37-40d2-bdf4-1af889598745",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from test_table limit 2"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
},
|
||||
{
|
||||
"description": "cache headers should be present and be composed with source table name",
|
||||
"cache_headers": {
|
||||
"x_cache_channel": {
|
||||
"db_name": "test_windshaft_cartodb_user_1_db",
|
||||
"tables": ["public.analysis_2f13a3dbd7_9eb239903a1afd8a69130d1ece0fc8b38de8592d",
|
||||
"public.test_table"]
|
||||
},
|
||||
"surrogate_keys": "t:77pJnX t:iL4eth"
|
||||
},
|
||||
"data":
|
||||
{
|
||||
version: '1.5.0',
|
||||
layers: [
|
||||
{
|
||||
options: {
|
||||
source: {
|
||||
id: "2570e105-7b37-40d2-bdf4-1af889598745"
|
||||
},
|
||||
sql: 'select * from test_table limit 2',
|
||||
cartocss: '#layer { marker-fill:red; }',
|
||||
cartocss_version: '2.3.0',
|
||||
attributes: {
|
||||
id:'cartodb_id',
|
||||
columns: [
|
||||
'name',
|
||||
'address'
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
analyses: [
|
||||
{
|
||||
"id": "2570e105-7b37-40d2-bdf4-1af889598745",
|
||||
"type": "buffer",
|
||||
"params": {
|
||||
"source": {
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select * from test_table limit 2"
|
||||
}
|
||||
},
|
||||
"radius": 50000
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}];
|
||||
|
||||
var layergroupRequest = function(mapConfig) {
|
||||
return {
|
||||
url: '/api/v1/map?api_key=1234&config=' + encodeURIComponent(JSON.stringify(mapConfig)),
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
function getRequest(url, addApiKey, callbackName) {
|
||||
var params = {};
|
||||
if (!!addApiKey) {
|
||||
params.api_key = '1234';
|
||||
}
|
||||
if (!!callbackName) {
|
||||
params.callback = callbackName;
|
||||
}
|
||||
|
||||
return {
|
||||
url: url + '?' + qs.stringify(params),
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function validateCacheHeaders(done, expectedCacheHeaders) {
|
||||
return function(res, err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
assert.ok(res.headers['x-cache-channel']);
|
||||
assert.ok(res.headers['surrogate-key']);
|
||||
if (expectedCacheHeaders) {
|
||||
validateXChannelHeaders(res.headers, expectedCacheHeaders);
|
||||
assert.equal(res.headers['surrogate-key'], expectedCacheHeaders.surrogate_keys);
|
||||
}
|
||||
|
||||
done();
|
||||
};
|
||||
}
|
||||
|
||||
function validateXChannelHeaders(headers, expectedCacheHeaders) {
|
||||
var dbName = headers['x-cache-channel'].split(':')[0];
|
||||
var tables = headers['x-cache-channel'].split(':')[1].split(',').sort();
|
||||
assert.equal(dbName, expectedCacheHeaders.x_cache_channel.db_name);
|
||||
assert.deepEqual(tables, expectedCacheHeaders.x_cache_channel.tables.sort());
|
||||
}
|
||||
|
||||
function noCacheHeaders(done) {
|
||||
return function(res, err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
assert.ok(
|
||||
!res.headers['x-cache-channel'],
|
||||
'did not expect x-cache-channel header, got: `' + res.headers['x-cache-channel'] + '`'
|
||||
);
|
||||
assert.ok(
|
||||
!res.headers['surrogate-key'],
|
||||
'did not expect surrogate-key header, got: `' + res.headers['surrogate-key'] + '`'
|
||||
);
|
||||
done();
|
||||
};
|
||||
}
|
||||
|
||||
function withLayergroupId(mapConfig, callback) {
|
||||
assert.response(
|
||||
server,
|
||||
layergroupRequest(mapConfig),
|
||||
statusOkResponse,
|
||||
function(res, err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
var layergroupId = JSON.parse(res.body).layergroupid;
|
||||
keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
callback(null, layergroupId, res);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
mapConfigs.forEach(function(mapConfigData) {
|
||||
describe(mapConfigData.description, function() {
|
||||
var mapConfig = mapConfigData.data;
|
||||
var expectedCacheHeaders = mapConfigData.cache_headers;
|
||||
it('/api/v1/map Map instantiation', function(done) {
|
||||
var testFn = validateCacheHeaders(done, expectedCacheHeaders);
|
||||
withLayergroupId(mapConfig, function(err, layergroupId, res) {
|
||||
testFn(res);
|
||||
});
|
||||
});
|
||||
|
||||
it ('/api/v1/map/:token/:z/:x/:y@:scale_factor?x.:format Mapnik retina tiles', function(done) {
|
||||
withLayergroupId(mapConfig, function(err, layergroupId) {
|
||||
assert.response(
|
||||
server,
|
||||
getRequest('/api/v1/map/' + layergroupId + '/0/0/0@2x.png', true),
|
||||
validateCacheHeaders(done, expectedCacheHeaders)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it ('/api/v1/map/:token/:z/:x/:y@:scale_factor?x.:format Mapnik tiles', function(done) {
|
||||
withLayergroupId(mapConfig, function(err, layergroupId) {
|
||||
assert.response(
|
||||
server,
|
||||
getRequest('/api/v1/map/' + layergroupId + '/0/0/0.png', true),
|
||||
validateCacheHeaders(done, expectedCacheHeaders)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it ('/api/v1/map/:token/:layer/:z/:x/:y.(:format) Per :layer rendering', function(done) {
|
||||
withLayergroupId(mapConfig, function(err, layergroupId) {
|
||||
assert.response(
|
||||
server,
|
||||
getRequest('/api/v1/map/' + layergroupId + '/0/0/0/0.png', true),
|
||||
validateCacheHeaders(done, expectedCacheHeaders)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it ('/api/v1/map/:token/:layer/attributes/:fid endpoint for info windows', function(done) {
|
||||
withLayergroupId(mapConfig, function(err, layergroupId) {
|
||||
assert.response(
|
||||
server,
|
||||
getRequest('/api/v1/map/' + layergroupId + '/0/attributes/1', true),
|
||||
validateCacheHeaders(done, expectedCacheHeaders)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it ('/api/v1/map/static/center/:token/:z/:lat/:lng/:width/:height.:format static maps', function(done) {
|
||||
withLayergroupId(mapConfig, function(err, layergroupId) {
|
||||
assert.response(
|
||||
server,
|
||||
getRequest('/api/v1/map/static/center/' + layergroupId + '/0/0/0/400/300.png', true),
|
||||
validateCacheHeaders(done, expectedCacheHeaders)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it ('/api/v1/map/static/bbox/:token/:bbox/:width/:height.:format static maps', function(done) {
|
||||
withLayergroupId(mapConfig, function(err, layergroupId) {
|
||||
assert.response(
|
||||
server,
|
||||
getRequest('/api/v1/map/static/bbox/' + layergroupId + '/-45,-45,45,45/400/300.png', true),
|
||||
validateCacheHeaders(done, expectedCacheHeaders)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('cache headers should NOT be present', function() {
|
||||
|
||||
it('/', function(done) {
|
||||
assert.response(
|
||||
server,
|
||||
getRequest('/'),
|
||||
statusOkResponse,
|
||||
noCacheHeaders(done)
|
||||
);
|
||||
});
|
||||
|
||||
it('/version', function(done) {
|
||||
assert.response(
|
||||
server,
|
||||
getRequest('/version'),
|
||||
statusOkResponse,
|
||||
noCacheHeaders(done)
|
||||
);
|
||||
});
|
||||
|
||||
it('/health', function(done) {
|
||||
assert.response(
|
||||
server,
|
||||
getRequest('/health'),
|
||||
statusOkResponse,
|
||||
noCacheHeaders(done)
|
||||
);
|
||||
});
|
||||
|
||||
it('/api/v1/map/named list named maps', function(done) {
|
||||
assert.response(
|
||||
server,
|
||||
getRequest('/api/v1/map/named', true),
|
||||
statusOkResponse,
|
||||
noCacheHeaders(done)
|
||||
);
|
||||
});
|
||||
|
||||
describe('with named maps', function() {
|
||||
|
||||
var templateName = 'x_cache';
|
||||
|
||||
beforeEach(function(done) {
|
||||
var template = {
|
||||
version: '0.0.1',
|
||||
name: templateName,
|
||||
auth: {
|
||||
method: 'open'
|
||||
},
|
||||
layergroup: mapConfigs[0].data
|
||||
};
|
||||
|
||||
var namedMapRequest = {
|
||||
url: '/api/v1/map/named?api_key=1234',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(template)
|
||||
};
|
||||
|
||||
assert.response(
|
||||
server,
|
||||
namedMapRequest,
|
||||
statusOkResponse,
|
||||
function(res, err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(function(done) {
|
||||
assert.response(
|
||||
server,
|
||||
{
|
||||
url: '/api/v1/map/named/' + templateName + '?api_key=1234',
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
}
|
||||
},
|
||||
{
|
||||
status: 204
|
||||
},
|
||||
function(res, err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
it('/api/v1/map/named/:template_id Named map retrieval', function(done) {
|
||||
assert.response(
|
||||
server,
|
||||
getRequest('/api/v1/map/named/' + templateName, true),
|
||||
statusOkResponse,
|
||||
noCacheHeaders(done)
|
||||
);
|
||||
});
|
||||
|
||||
it('/api/v1/map/named/:template_id/jsonp Named map retrieval', function(done) {
|
||||
assert.response(
|
||||
server,
|
||||
getRequest('/api/v1/map/named/' + templateName, true, 'cb'),
|
||||
statusOkResponse,
|
||||
noCacheHeaders(done)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
326
test/acceptance/dataviews/aggregation.js
Normal file
326
test/acceptance/dataviews/aggregation.js
Normal file
@@ -0,0 +1,326 @@
|
||||
require('../../support/test_helper');
|
||||
var assert = require('../../support/assert');
|
||||
var TestClient = require('../../support/test-client');
|
||||
|
||||
describe('aggregations happy cases', function() {
|
||||
|
||||
afterEach(function(done) {
|
||||
if (this.testClient) {
|
||||
this.testClient.drain(done);
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
function aggregationOperationMapConfig(operation, query, column, aggregationColumn) {
|
||||
column = column || 'adm0name';
|
||||
aggregationColumn = aggregationColumn || 'pop_max';
|
||||
query = query || 'select * from populated_places_simple_reduced';
|
||||
|
||||
var mapConfig = {
|
||||
version: '1.5.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: query,
|
||||
cartocss: '#layer0 { marker-fill: red; marker-width: 10; }',
|
||||
cartocss_version: '2.0.1',
|
||||
widgets: {}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
mapConfig.layers[0].options.widgets[column] = {
|
||||
type: 'aggregation',
|
||||
options: {
|
||||
column: column,
|
||||
aggregation: operation,
|
||||
aggregationColumn: aggregationColumn
|
||||
}
|
||||
};
|
||||
|
||||
return mapConfig;
|
||||
}
|
||||
|
||||
var operations = ['count', 'sum', 'avg', 'max', 'min'];
|
||||
|
||||
operations.forEach(function(operation) {
|
||||
it('should be able to use "' + operation + '" as aggregation operation', function(done) {
|
||||
|
||||
this.testClient = new TestClient(aggregationOperationMapConfig(operation));
|
||||
this.testClient.getDataview('adm0name', { own_filter: 0 }, function (err, aggregation) {
|
||||
assert.ok(!err, err);
|
||||
assert.ok(aggregation);
|
||||
|
||||
assert.equal(aggregation.type, 'aggregation');
|
||||
assert.equal(aggregation.aggregation, operation);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
var query = [
|
||||
'select 1 as val, \'a\' as cat, ST_Transform(ST_SetSRID(ST_MakePoint(0,0),4326),3857) as the_geom_webmercator',
|
||||
'select null, \'b\', ST_Transform(ST_SetSRID(ST_MakePoint(0,1),4326),3857)',
|
||||
'select null, \'b\', ST_Transform(ST_SetSRID(ST_MakePoint(1,0),4326),3857)',
|
||||
'select null, null, ST_Transform(ST_SetSRID(ST_MakePoint(1,1),4326),3857)'
|
||||
].join(' UNION ALL ');
|
||||
|
||||
operations.forEach(function (operation) {
|
||||
var not = operation === 'count' ? ' not ' : ' ';
|
||||
var description = 'should' +
|
||||
not +
|
||||
'handle NULL values in category and aggregation columns using "' +
|
||||
operation +
|
||||
'" as aggregation operation';
|
||||
|
||||
it(description, function (done) {
|
||||
this.testClient = new TestClient(aggregationOperationMapConfig(operation, query, 'cat', 'val'));
|
||||
this.testClient.getDataview('cat', { own_filter: 0 }, function (err, aggregation) {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.ok(aggregation);
|
||||
assert.equal(aggregation.type, 'aggregation');
|
||||
assert.ok(aggregation.categories);
|
||||
assert.equal(aggregation.categoriesCount, 3);
|
||||
assert.equal(aggregation.count, 4);
|
||||
assert.equal(aggregation.nulls, 1);
|
||||
|
||||
var hasNullCategory = false;
|
||||
aggregation.categories.forEach(function (category) {
|
||||
if (category.category === null) {
|
||||
hasNullCategory = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (operation === 'count') {
|
||||
assert.ok(hasNullCategory, 'aggregation has not a category NULL');
|
||||
} else {
|
||||
assert.ok(!hasNullCategory, 'aggregation has category NULL');
|
||||
}
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
var operations_and_values = {'count': 9, 'sum': 45, 'avg': 5, 'max': 9, 'min': 1};
|
||||
|
||||
var query_other = [
|
||||
'select generate_series(1,3) as val, \'other_a\' as cat, NULL as the_geom_webmercator',
|
||||
'select generate_series(4,6) as val, \'other_b\' as cat, NULL as the_geom_webmercator',
|
||||
'select generate_series(7,9) as val, \'other_c\' as cat, NULL as the_geom_webmercator',
|
||||
'select generate_series(10,12) as val, \'category_1\' as cat, NULL as the_geom_webmercator',
|
||||
'select generate_series(10,12) as val, \'category_2\' as cat, NULL as the_geom_webmercator',
|
||||
'select generate_series(10,12) as val, \'category_3\' as cat, NULL as the_geom_webmercator',
|
||||
'select generate_series(10,12) as val, \'category_4\' as cat, NULL as the_geom_webmercator',
|
||||
'select generate_series(10,12) as val, \'category_5\' as cat, NULL as the_geom_webmercator'
|
||||
].join(' UNION ALL ');
|
||||
|
||||
Object.keys(operations_and_values).forEach(function (operation) {
|
||||
var description = 'should aggregate OTHER category using "' + operation + '"';
|
||||
|
||||
it(description, function (done) {
|
||||
this.testClient = new TestClient(aggregationOperationMapConfig(operation, query_other, 'cat', 'val'));
|
||||
this.testClient.getDataview('cat', { own_filter: 0 }, function (err, aggregation) {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.ok(aggregation);
|
||||
assert.equal(aggregation.type, 'aggregation');
|
||||
assert.ok(aggregation.categories);
|
||||
assert.equal(aggregation.categoriesCount, 8);
|
||||
assert.equal(aggregation.count, 24);
|
||||
assert.equal(aggregation.nulls, 0);
|
||||
|
||||
var aggregated_categories = aggregation.categories.filter( function(category) {
|
||||
return category.agg === true;
|
||||
});
|
||||
assert.equal(aggregated_categories.length, 1);
|
||||
assert.equal(aggregated_categories[0].value, operations_and_values[operation]);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
87
test/acceptance/dataviews/error-cases.js
Normal file
87
test/acceptance/dataviews/error-cases.js
Normal file
@@ -0,0 +1,87 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var TestClient = require('../../support/test-client');
|
||||
|
||||
describe('histogram-dataview', function() {
|
||||
|
||||
afterEach(function(done) {
|
||||
if (this.testClient) {
|
||||
this.testClient.drain(done);
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
var ERROR_RESPONSE = {
|
||||
status: 400,
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
function createMapConfig(dataviews) {
|
||||
return {
|
||||
version: '1.5.0',
|
||||
layers: [
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"source": {
|
||||
"id": "HEAD"
|
||||
},
|
||||
"cartocss": "#points { marker-width: 10; marker-fill: red; }",
|
||||
"cartocss_version": "2.3.0"
|
||||
}
|
||||
}
|
||||
],
|
||||
dataviews: dataviews,
|
||||
analyses: [
|
||||
{
|
||||
"id": "HEAD",
|
||||
"type": "source",
|
||||
"params": {
|
||||
"query": "select null::geometry the_geom_webmercator, x from generate_series(0,1000) x"
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
it('should fail when invalid dataviews object is provided, string case', function(done) {
|
||||
var mapConfig = createMapConfig("wadus-string");
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getLayergroup(ERROR_RESPONSE, function(err, errObj) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.deepEqual(errObj.errors, [ '"dataviews" must be a valid JSON object: "string" type found' ]);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail when invalid dataviews object is provided, array case', function(done) {
|
||||
var mapConfig = createMapConfig([]);
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getLayergroup(ERROR_RESPONSE, function(err, errObj) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.deepEqual(errObj.errors, [ '"dataviews" must be a valid JSON object: "array" type found' ]);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should work with empty but valid objects', function(done) {
|
||||
var mapConfig = createMapConfig({});
|
||||
this.testClient = new TestClient(mapConfig, 1234);
|
||||
this.testClient.getLayergroup(function(err, layergroup) {
|
||||
assert.ok(!err, err);
|
||||
|
||||
assert.ok(layergroup);
|
||||
assert.ok(layergroup.layergroupid);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
1177
test/acceptance/dataviews/histogram.js
Normal file
1177
test/acceptance/dataviews/histogram.js
Normal file
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,43 @@ 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'},
|
||||
options: {
|
||||
column: 'value',
|
||||
bins: 2
|
||||
}
|
||||
},
|
||||
test_histogram_date: {
|
||||
type: 'histogram',
|
||||
source: {id: 'data-source'},
|
||||
options: {
|
||||
column: 'updated_at',
|
||||
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'},
|
||||
@@ -152,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'},
|
||||
@@ -186,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'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -196,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);
|
||||
});
|
||||
@@ -208,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);
|
||||
});
|
||||
@@ -220,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);
|
||||
});
|
||||
@@ -232,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);
|
||||
});
|
||||
@@ -244,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);
|
||||
});
|
||||
@@ -259,14 +359,96 @@ 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);
|
||||
});
|
||||
});
|
||||
|
||||
it("should expose a histogram", function (done) {
|
||||
var testClient = new TestClient(overviewsMapConfig);
|
||||
testClient.getDataview('test_histogram', function (err, histogram) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.ok(histogram);
|
||||
assert.equal(histogram.type, 'histogram');
|
||||
assert.ok(Array.isArray(histogram.bins));
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filters', function() {
|
||||
|
||||
describe('histogram', function () {
|
||||
|
||||
it("should expose a filtered histogram", function (done) {
|
||||
var params = {
|
||||
filters: {
|
||||
dataviews: { test_histogram: { min: 2 } }
|
||||
}
|
||||
};
|
||||
var testClient = new TestClient(overviewsMapConfig);
|
||||
testClient.getDataview('test_histogram', params, function (err, histogram) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.ok(histogram);
|
||||
assert.equal(histogram.type, 'histogram');
|
||||
assert.ok(Array.isArray(histogram.bins));
|
||||
assert.equal(histogram.bins.length, 4);
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it("should expose a filtered histogram with no results", function (done) {
|
||||
var params = {
|
||||
filters: {
|
||||
dataviews: { test_histogram: { max: -1 } }
|
||||
}
|
||||
};
|
||||
var testClient = new TestClient(overviewsMapConfig);
|
||||
testClient.getDataview('test_histogram', params, function (err, histogram) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.ok(histogram);
|
||||
assert.equal(histogram.type, 'histogram');
|
||||
assert.ok(Array.isArray(histogram.bins));
|
||||
assert.equal(histogram.bins.length, 0);
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
|
||||
it("should expose a filtered date histogram with no results", function (done) {
|
||||
// This most likely works because the overviews will pass
|
||||
// the responsibility to the normal dataviews.
|
||||
var params = {
|
||||
filters: {
|
||||
dataviews: { test_histogram_date: { max: -1 } }
|
||||
}
|
||||
};
|
||||
var testClient = new TestClient(overviewsMapConfig);
|
||||
testClient.getDataview('test_histogram_date', params, function (err, histogram) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.ok(histogram);
|
||||
assert.equal(histogram.type, 'histogram');
|
||||
assert.ok(Array.isArray(histogram.bins));
|
||||
assert.equal(histogram.bins.length, 0);
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('category', function () {
|
||||
|
||||
var params = {
|
||||
@@ -281,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);
|
||||
});
|
||||
});
|
||||
@@ -292,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);
|
||||
});
|
||||
@@ -304,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);
|
||||
});
|
||||
@@ -316,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);
|
||||
});
|
||||
@@ -328,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);
|
||||
});
|
||||
@@ -346,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);
|
||||
});
|
||||
});
|
||||
@@ -354,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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
68
test/acceptance/errors-with-context.js
Normal file
68
test/acceptance/errors-with-context.js
Normal file
@@ -0,0 +1,68 @@
|
||||
var assert = require('../support/assert');
|
||||
var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/server');
|
||||
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options');
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
|
||||
describe('error with context', function () {
|
||||
var layerOK = {
|
||||
options: {
|
||||
sql: 'select cartodb_id, ST_Translate(the_geom_webmercator, 5e6, 0) as the_geom_webmercator ' +
|
||||
'from test_table',
|
||||
cartocss: '#layer { marker-fill:red; marker-width:32; marker-allow-overlap:true; }',
|
||||
cartocss_version: '2.0.1',
|
||||
interactivity: 'cartodb_id'
|
||||
}
|
||||
};
|
||||
var layerKO = {
|
||||
options: {
|
||||
sql: 'select cartodb_id from test_table offset 3', // it doesn't return the_geom_webmercator so it must fail
|
||||
cartocss: '#layer { marker-fill:blue; marker-allow-overlap:true; }',
|
||||
cartocss_version: '2.0.2',
|
||||
interactivity: 'cartodb_id'
|
||||
}
|
||||
};
|
||||
|
||||
var DB_ERROR_MESSAGE = 'Postgis Plugin: ERROR: column "the_geom_webmercator" does not exist';
|
||||
var scenarios = [{
|
||||
description: 'layergroup with 2 layers, second one has query error',
|
||||
layergroup: {
|
||||
version: '1.0.0',
|
||||
layers: [layerOK, layerKO]
|
||||
},
|
||||
expectedFailingLayer: { id: 'layer1', index: 1, type: 'mapnik' }
|
||||
}, {
|
||||
description: 'layergroup with 2 layers, first one has query error',
|
||||
layergroup: {
|
||||
version: '1.0.0',
|
||||
layers: [layerKO, layerOK]
|
||||
},
|
||||
expectedFailingLayer: { id: 'layer0', index: 0, type: 'mapnik' }
|
||||
}];
|
||||
|
||||
scenarios.forEach(function (scenario) {
|
||||
it(scenario.description, function (done) {
|
||||
assert.response(server, {
|
||||
url: '/api/v1/map',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(scenario.layergroup)
|
||||
}, {
|
||||
status: 400
|
||||
}, function (res) {
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
|
||||
assert.ok(Array.isArray(parsedBody.errors_with_context));
|
||||
|
||||
var err = parsedBody.errors_with_context[0];
|
||||
assert.equal(err.type, 'layer');
|
||||
assert.equal(err.subtype, 'query');
|
||||
assert.ok(err.message.indexOf(DB_ERROR_MESSAGE) >= 0);
|
||||
assert.deepEqual(err.layer, scenario.expectedFailingLayer);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -168,4 +168,176 @@ describe('use only needed columns', function() {
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
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);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -5,6 +5,9 @@ var step = require('step');
|
||||
var strftime = require('strftime');
|
||||
var redis_stats_db = 5;
|
||||
|
||||
var mapnik = require('windshaft').mapnik;
|
||||
var semver = require('semver');
|
||||
|
||||
var helper = require(__dirname + '/../support/test_helper');
|
||||
var LayergroupToken = require('../support/layergroup-token');
|
||||
|
||||
@@ -574,7 +577,7 @@ describe(suiteName, function() {
|
||||
if ( err ) {
|
||||
return done(err);
|
||||
}
|
||||
// trip epoch
|
||||
// strip epoch
|
||||
expected_token = expected_token.split(':')[0];
|
||||
keysToDelete['map_cfg|' + expected_token] = 0;
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
@@ -973,69 +976,71 @@ describe(suiteName, function() {
|
||||
});
|
||||
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/issues/93
|
||||
it("accepts unused directives", function(done) {
|
||||
var layergroup = {
|
||||
version: '1.0.0',
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: "select 'SRID=3857;POINT(0 0)'::geometry as the_geom_webmercator",
|
||||
cartocss: '#layer { point-transform:"scale(20)"; }',
|
||||
cartocss_version: '2.0.1'
|
||||
} }
|
||||
]
|
||||
};
|
||||
var expected_token; // = "e34dd7e235138a062f8ba7ad051aa3a7";
|
||||
step(
|
||||
function do_post()
|
||||
{
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: layergroup_url,
|
||||
method: 'POST',
|
||||
headers: {host: 'localhost', 'Content-Type': 'application/json' },
|
||||
data: JSON.stringify(layergroup)
|
||||
}, {}, function(res) {
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
if ( expected_token ) {
|
||||
assert.equal(parsedBody.layergroupid, expected_token + ':' + expected_last_updated_epoch);
|
||||
assert.equal(res.headers['x-layergroup-id'], parsedBody.layergroupid);
|
||||
}
|
||||
else {
|
||||
var token_components = parsedBody.layergroupid.split(':');
|
||||
expected_token = token_components[0];
|
||||
expected_last_updated_epoch = token_components[1];
|
||||
}
|
||||
next(null, res);
|
||||
});
|
||||
},
|
||||
function do_get_tile(err)
|
||||
{
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: layergroup_url + "/" + expected_token + ':cb0/0/0/0.png',
|
||||
method: 'GET',
|
||||
headers: {host: 'localhost' },
|
||||
encoding: 'binary'
|
||||
}, {}, function(res) {
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
assert.equal(res.headers['content-type'], "image/png");
|
||||
assert.imageBufferIsSimilarToFile(res.body, windshaft_fixtures + '/test_default_mapnik_point.png',
|
||||
IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err/*, similarity*/) {
|
||||
next(err);
|
||||
if (semver.satisfies(mapnik.versions.mapnik, '2.3.x')) {
|
||||
it("accepts unused directives", function(done) {
|
||||
var layergroup = {
|
||||
version: '1.0.0',
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: "select 'SRID=3857;POINT(0 0)'::geometry as the_geom_webmercator",
|
||||
cartocss: '#layer { point-transform:"scale(20)"; }',
|
||||
cartocss_version: '2.0.1'
|
||||
} }
|
||||
]
|
||||
};
|
||||
var expected_token; // = "e34dd7e235138a062f8ba7ad051aa3a7";
|
||||
step(
|
||||
function do_post()
|
||||
{
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: layergroup_url,
|
||||
method: 'POST',
|
||||
headers: {host: 'localhost', 'Content-Type': 'application/json' },
|
||||
data: JSON.stringify(layergroup)
|
||||
}, {}, function(res) {
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
if ( expected_token ) {
|
||||
assert.equal(parsedBody.layergroupid, expected_token + ':' + expected_last_updated_epoch);
|
||||
assert.equal(res.headers['x-layergroup-id'], parsedBody.layergroupid);
|
||||
}
|
||||
);
|
||||
});
|
||||
},
|
||||
function finish(err) {
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
keysToDelete['map_cfg|' + expected_token] = 0;
|
||||
else {
|
||||
var token_components = parsedBody.layergroupid.split(':');
|
||||
expected_token = token_components[0];
|
||||
expected_last_updated_epoch = token_components[1];
|
||||
}
|
||||
next(null, res);
|
||||
});
|
||||
},
|
||||
function do_get_tile(err)
|
||||
{
|
||||
assert.ifError(err);
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: layergroup_url + "/" + expected_token + ':cb0/0/0/0.png',
|
||||
method: 'GET',
|
||||
headers: {host: 'localhost' },
|
||||
encoding: 'binary'
|
||||
}, {}, function(res) {
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
assert.equal(res.headers['content-type'], "image/png");
|
||||
assert.imageBufferIsSimilarToFile(res.body, windshaft_fixtures + '/test_default_mapnik_point.png',
|
||||
IMAGE_EQUALS_TOLERANCE_PER_MIL, function(err/*, similarity*/) {
|
||||
next(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
},
|
||||
function finish(err) {
|
||||
keysToDelete['user:localhost:mapviews:global'] = 5;
|
||||
keysToDelete['map_cfg|' + expected_token] = 0;
|
||||
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/issues/91
|
||||
// and https://github.com/CartoDB/Windshaft-cartodb/issues/38
|
||||
|
||||
@@ -228,7 +228,9 @@ describe('tests from old api translated to multilayer', function() {
|
||||
},
|
||||
function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.deepEqual(parsed, { errors: [ 'Unexpected token W' ] });
|
||||
assert.ok(parsed.errors);
|
||||
assert.equal(parsed.errors.length, 1);
|
||||
assert.ok(parsed.errors[0].match(/^Unexpected token W/));
|
||||
|
||||
done();
|
||||
}
|
||||
@@ -334,9 +336,7 @@ describe('tests from old api translated to multilayer', function() {
|
||||
assert.ok(!res.headers.hasOwnProperty('x-cache-channel'));
|
||||
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.deepEqual(parsed, {
|
||||
errors: ["fake error message"]
|
||||
});
|
||||
assert.deepEqual(parsed.errors, ["fake error message"]);
|
||||
|
||||
done();
|
||||
}
|
||||
|
||||
63
test/acceptance/mvt.js
Normal file
63
test/acceptance/mvt.js
Normal file
@@ -0,0 +1,63 @@
|
||||
require('../support/test_helper');
|
||||
|
||||
const assert = require('../support/assert');
|
||||
const TestClient = require('../support/test-client');
|
||||
|
||||
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', function () {
|
||||
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 => {
|
||||
const testClient = new TestClient(test.mapConfig, 1234);
|
||||
const { z, x, y } = test.coords;
|
||||
const { format, response } = test;
|
||||
|
||||
testClient.getTile(z, x, y, { format, response }, (err, res) => {
|
||||
assert.ifError(err);
|
||||
|
||||
assert.equal(res.statusCode, test.response.status);
|
||||
testClient.drain(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -181,7 +181,7 @@ describe('named_layers', function() {
|
||||
}
|
||||
|
||||
var parsedBody = JSON.parse(response.body);
|
||||
assert.deepEqual(parsedBody, { errors: ["Template 'nonexistent' of user 'localhost' not found"] });
|
||||
assert.deepEqual(parsedBody.errors, ["Template 'nonexistent' of user 'localhost' not found"]);
|
||||
|
||||
return null;
|
||||
},
|
||||
@@ -234,10 +234,7 @@ describe('named_layers', function() {
|
||||
}
|
||||
|
||||
var parsedBody = JSON.parse(response.body);
|
||||
assert.deepEqual(
|
||||
parsedBody,
|
||||
{ errors: [ "Unauthorized 'auth_valid_template' template instantiation" ] }
|
||||
);
|
||||
assert.deepEqual(parsedBody.errors, [ "Unauthorized 'auth_valid_template' template instantiation" ]);
|
||||
|
||||
return null;
|
||||
},
|
||||
@@ -347,7 +344,7 @@ describe('named_layers', function() {
|
||||
}
|
||||
|
||||
var parsedBody = JSON.parse(response.body);
|
||||
assert.deepEqual(parsedBody, { errors: [ 'Nested named layers are not allowed' ] });
|
||||
assert.deepEqual(parsedBody.errors, ['Nested named layers are not allowed' ]);
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
210
test/acceptance/named_layers_visibility.js
Normal file
210
test/acceptance/named_layers_visibility.js
Normal file
@@ -0,0 +1,210 @@
|
||||
var step = require('step');
|
||||
var test_helper = require('../support/test_helper');
|
||||
|
||||
var assert = require('../support/assert');
|
||||
var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/server');
|
||||
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options');
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
|
||||
var RedisPool = require('redis-mpool');
|
||||
var TemplateMaps = require('../../lib/cartodb/backends/template_maps.js');
|
||||
var mapnik = require('windshaft').mapnik;
|
||||
|
||||
var IMAGE_TOLERANCE = 20;
|
||||
|
||||
describe('layers visibility for previews', function() {
|
||||
// configure redis pool instance to use in tests
|
||||
var redisPool = new RedisPool(global.environment.redis);
|
||||
var templateMaps = new TemplateMaps(redisPool, {
|
||||
max_user_templates: global.environment.maxUserTemplates
|
||||
});
|
||||
|
||||
var username = 'localhost';
|
||||
|
||||
function createLayer (color, layerId) {
|
||||
var mod;
|
||||
|
||||
if (color === 'red') {
|
||||
mod = 0;
|
||||
} else if (color === 'orange') {
|
||||
mod = 1;
|
||||
} else if (color === 'blue') {
|
||||
mod = 2;
|
||||
} else {
|
||||
mod = 0;
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'mapnik',
|
||||
id: layerId,
|
||||
options: {
|
||||
sql: 'select * from populated_places_simple_reduced where cartodb_id % 3 = ' + mod,
|
||||
cartocss: '#layer { marker-fill: ' + color + '; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createTemplate(scenario) {
|
||||
return {
|
||||
version: '0.0.1',
|
||||
name: scenario.name,
|
||||
auth: {
|
||||
method: 'open'
|
||||
},
|
||||
view: {
|
||||
bounds: {
|
||||
west: 0,
|
||||
south: 0,
|
||||
east: 45,
|
||||
north: 45
|
||||
},
|
||||
zoom: 4,
|
||||
center: {
|
||||
lng: 40,
|
||||
lat: 20
|
||||
},
|
||||
preview_layers: scenario.layerPerview
|
||||
},
|
||||
layergroup: {
|
||||
layers: scenario.layers
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
afterEach(function (done) {
|
||||
test_helper.deleteRedisKeys({
|
||||
'user:localhost:mapviews:global': 5
|
||||
}, done);
|
||||
});
|
||||
|
||||
function previewFixture(version) {
|
||||
return './test/fixtures/previews/populated_places_simple_reduced-' + version + '.png';
|
||||
}
|
||||
|
||||
var threeLayerPointDistintColor = [
|
||||
createLayer('red'),
|
||||
createLayer('orange'),
|
||||
createLayer('blue', 'layer2')
|
||||
];
|
||||
|
||||
var scenarios = [{
|
||||
name: 'preview_layers_red',
|
||||
layerPerview: {
|
||||
'0': true,
|
||||
'1': false,
|
||||
'layer2': false
|
||||
},
|
||||
layers: threeLayerPointDistintColor
|
||||
}, {
|
||||
name: 'preview_layers_orange',
|
||||
layerPerview: {
|
||||
'0': false,
|
||||
'1': true,
|
||||
'layer2': false
|
||||
},
|
||||
layers: threeLayerPointDistintColor
|
||||
}, {
|
||||
name: 'preview_layers_blue',
|
||||
layerPerview: {
|
||||
'0': false,
|
||||
'1': false,
|
||||
'layer2': true
|
||||
},
|
||||
layers: threeLayerPointDistintColor
|
||||
}, {
|
||||
name: 'preview_layers_red_orange',
|
||||
layerPerview: {
|
||||
'0': true,
|
||||
'1': true,
|
||||
'layer2': false
|
||||
},
|
||||
layers: threeLayerPointDistintColor
|
||||
}, {
|
||||
name: 'preview_layers_red_blue',
|
||||
layerPerview: {
|
||||
'0': true,
|
||||
'1': false,
|
||||
'layer2': true
|
||||
},
|
||||
layers: threeLayerPointDistintColor
|
||||
}, {
|
||||
name: 'preview_layers_orange_blue',
|
||||
layerPerview: {
|
||||
'0': false,
|
||||
'1': true,
|
||||
'layer2': true
|
||||
},
|
||||
layers: threeLayerPointDistintColor
|
||||
}, {
|
||||
name: 'preview_layers_red_orange_blue',
|
||||
layerPerview: {
|
||||
'0': true,
|
||||
'1': true,
|
||||
'layer2': true
|
||||
},
|
||||
layers: threeLayerPointDistintColor
|
||||
}, {
|
||||
name: 'preview_layers_all',
|
||||
layerPerview: {},
|
||||
layers: threeLayerPointDistintColor
|
||||
}, {
|
||||
name: 'preview_layers_undefined',
|
||||
layerPerview: undefined,
|
||||
layers: threeLayerPointDistintColor
|
||||
}];
|
||||
|
||||
scenarios.forEach(function (scenario) {
|
||||
it('should filter layers for template: ' + scenario.name, function (done) {
|
||||
step(
|
||||
function addTemplate () {
|
||||
var next = this;
|
||||
var template = createTemplate(scenario);
|
||||
|
||||
templateMaps.addTemplate(username, template, next);
|
||||
},
|
||||
function requestPreview (err) {
|
||||
assert.ifError(err);
|
||||
|
||||
var next = this;
|
||||
|
||||
assert.response(server, {
|
||||
url: '/api/v1/map/static/named/' + scenario.name + '/640/480.png',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
},
|
||||
encoding: 'binary'
|
||||
}, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'image/png'
|
||||
}
|
||||
}, function (res, err) {
|
||||
next(err, res);
|
||||
});
|
||||
},
|
||||
function checkPreview (err, res) {
|
||||
assert.ifError(err);
|
||||
|
||||
var next = this;
|
||||
var img = mapnik.Image.fromBytes(new Buffer(res.body, 'binary'));
|
||||
var previewFixturePath = previewFixture(scenario.name);
|
||||
|
||||
assert.imageIsSimilarToFile(img, previewFixturePath, IMAGE_TOLERANCE, next);
|
||||
},
|
||||
function deleteTemplate(err) {
|
||||
assert.ifError(err);
|
||||
|
||||
var next = this;
|
||||
|
||||
templateMaps.delTemplate(username, scenario.name, next);
|
||||
},
|
||||
function finish (err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -169,8 +169,8 @@ describe('named maps authentication', function() {
|
||||
getNamedTile(nonexistentName, 0, 0, 0, { status: 404 }, function(err, res) {
|
||||
assert.ok(!err);
|
||||
assert.deepEqual(
|
||||
JSON.parse(res.body),
|
||||
{ errors: ["Template '" + nonexistentName + "' of user '" + username + "' not found"] }
|
||||
JSON.parse(res.body).errors,
|
||||
["Template '" + nonexistentName + "' of user '" + username + "' not found"]
|
||||
);
|
||||
done();
|
||||
});
|
||||
@@ -179,7 +179,7 @@ describe('named maps authentication', function() {
|
||||
it('should return 403 if not properly authorized', function(done) {
|
||||
getNamedTile(tokenAuthTemplateName, 0, 0, 0, { status: 403 }, function(err, res) {
|
||||
assert.ok(!err);
|
||||
assert.deepEqual(JSON.parse(res.body), { errors: ['Unauthorized template instantiation'] });
|
||||
assert.deepEqual(JSON.parse(res.body).errors, ['Unauthorized template instantiation']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -238,8 +238,8 @@ describe('named maps authentication', function() {
|
||||
getStaticMap(nonexistentName, { status: 404 }, function(err, res) {
|
||||
assert.ok(!err);
|
||||
assert.deepEqual(
|
||||
JSON.parse(res.body),
|
||||
{ errors: ["Template '" + nonexistentName + "' of user '" + username + "' not found"] }
|
||||
JSON.parse(res.body).errors,
|
||||
["Template '" + nonexistentName + "' of user '" + username + "' not found"]
|
||||
);
|
||||
done();
|
||||
});
|
||||
@@ -248,7 +248,7 @@ describe('named maps authentication', function() {
|
||||
it('should return 403 if not properly authorized', function(done) {
|
||||
getStaticMap(tokenAuthTemplateName, { status: 403 }, function(err, res) {
|
||||
assert.ok(!err);
|
||||
assert.deepEqual(JSON.parse(res.body), { errors: ['Unauthorized template instantiation'] });
|
||||
assert.deepEqual(JSON.parse(res.body).errors, ['Unauthorized template instantiation']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -124,8 +124,8 @@ describe('named maps provider cache', function() {
|
||||
getNamedTile({ statusCode: 404 }, function(err, res) {
|
||||
assert.ok(!err);
|
||||
assert.deepEqual(
|
||||
JSON.parse(res.body),
|
||||
{ errors: ["Template 'template_with_color' of user 'localhost' not found"] }
|
||||
JSON.parse(res.body).errors,
|
||||
["Template 'template_with_color' of user 'localhost' not found"]
|
||||
);
|
||||
|
||||
// add template again so it's clean in afterEach
|
||||
|
||||
@@ -21,7 +21,7 @@ describe('named maps static view', function() {
|
||||
|
||||
var IMAGE_TOLERANCE = 20;
|
||||
|
||||
function createTemplate(view) {
|
||||
function createTemplate(view, layers) {
|
||||
return {
|
||||
version: '0.0.1',
|
||||
name: templateName,
|
||||
@@ -36,7 +36,7 @@ describe('named maps static view', function() {
|
||||
},
|
||||
view: view,
|
||||
layergroup: {
|
||||
layers: [
|
||||
layers: layers || [
|
||||
{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
@@ -198,4 +198,43 @@ describe('named maps static view', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow to select the layers to render', function (done) {
|
||||
var view = {
|
||||
bounds: {
|
||||
west: 0,
|
||||
south: 0,
|
||||
east: 45,
|
||||
north: 45
|
||||
}
|
||||
};
|
||||
|
||||
var layers = [
|
||||
{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select * from populated_places_simple_reduced',
|
||||
cartocss: '#layer { marker-fill: <%= color %>; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select ST_Transform(ST_MakeEnvelope(-45, -45, 45, 45, 4326), 3857) the_geom_webmercator',
|
||||
cartocss: '#layer { polygon-fill: <%= color %>; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
];
|
||||
templateMaps.addTemplate(username, createTemplate(view, layers), function (err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
getStaticMap({ layer: 0 }, function(err, img) {
|
||||
assert.ok(!err);
|
||||
assert.imageIsSimilarToFile(img, previewFixture('bounds'), IMAGE_TOLERANCE, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -231,7 +231,11 @@ describe('attributes', function() {
|
||||
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
|
||||
assert.equal(
|
||||
res.body,
|
||||
'/**/ typeof test === \'function\' && test({"errors":["Layer 0 has no exposed attributes"]});'
|
||||
'/**/ typeof test === \'function\' && ' +
|
||||
'test({"errors":["Layer 0 has no exposed attributes"],' +
|
||||
'"errors_with_context":[{' +
|
||||
'"type":"unknown","message":"Layer 0 has no exposed attributes"' +
|
||||
'}]});'
|
||||
);
|
||||
return null;
|
||||
},
|
||||
|
||||
@@ -138,11 +138,9 @@ describe('blend http fallback', function() {
|
||||
testClient.getTileLayer(mapConfig, tileRequest, expectedResponse, function(err, res) {
|
||||
assert.ok(!err);
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
assert.deepEqual(parsedBody, {
|
||||
errors: [
|
||||
"Unable to fetch http tile: http://127.0.0.1:8033/error404/1/0/0.png [404]"
|
||||
]
|
||||
});
|
||||
assert.deepEqual(parsedBody.errors, [
|
||||
"Unable to fetch http tile: http://127.0.0.1:8033/error404/1/0/0.png [404]"
|
||||
]);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user