Compare commits
634 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38242813be | ||
|
|
f9373dd8d0 | ||
|
|
0e4e56f333 | ||
|
|
c1d4da870f | ||
|
|
3c26f1f986 | ||
|
|
e9195967a4 | ||
|
|
30c6a390ac | ||
|
|
3a97af767f | ||
|
|
57dd36a476 | ||
|
|
6ab6fd91e4 | ||
|
|
c41c223b84 | ||
|
|
7e2be7b30f | ||
|
|
e690170689 | ||
|
|
81f1b0dcf8 | ||
|
|
146a2b2606 | ||
|
|
ff811ac1b5 | ||
|
|
11d9f5dd76 | ||
|
|
571a635fed | ||
|
|
6e70518146 | ||
|
|
fabb438cf0 | ||
|
|
0abd6a2293 | ||
|
|
272e8cd221 | ||
|
|
885accdadf | ||
|
|
f5a3b77737 | ||
|
|
56abcfd2f4 | ||
|
|
ecd570323b | ||
|
|
20eb92a3b1 | ||
|
|
8d22ed7594 | ||
|
|
3321987c33 | ||
|
|
981be0edd5 | ||
|
|
e8ab3a48c6 | ||
|
|
58a54de5a6 | ||
|
|
21b1cea5e8 | ||
|
|
64b5a64e1b | ||
|
|
f1b6be1ecb | ||
|
|
ac84fc569f | ||
|
|
4bdc43ff7c | ||
|
|
3afbbccfa2 | ||
|
|
8bc08d75b7 | ||
|
|
c14157acc2 | ||
|
|
595dac57a0 | ||
|
|
5632b19e16 | ||
|
|
007196555d | ||
|
|
62ffc05ef4 | ||
|
|
5962141114 | ||
|
|
7901a05b55 | ||
|
|
4c2a0ca048 | ||
|
|
b40c8e6624 | ||
|
|
97d3b1a03b | ||
|
|
fcea0c9b83 | ||
|
|
7ce8737e75 | ||
|
|
1d91f0fca9 | ||
|
|
23fe7fb0f7 | ||
|
|
1880b5d261 | ||
|
|
cf004322fd | ||
|
|
30d8f28221 | ||
|
|
caa05e779a | ||
|
|
f13fec13b8 | ||
|
|
a93f346948 | ||
|
|
48d44bada1 | ||
|
|
a20d08ddc8 | ||
|
|
4f18e31af5 | ||
|
|
41f6a172ee | ||
|
|
1776d31ba4 | ||
|
|
845ebcac15 | ||
|
|
45f73d4be8 | ||
|
|
ebdd71f342 | ||
|
|
597f8a7bab | ||
|
|
3f1aa9955b | ||
|
|
aad2a1e098 | ||
|
|
07fd7619bc | ||
|
|
96bcd14bb8 | ||
|
|
db9d350cae | ||
|
|
5914498027 | ||
|
|
180109e3aa | ||
|
|
929dac0df0 | ||
|
|
6cd9a53aa5 | ||
|
|
cd585dd657 | ||
|
|
2a150a6e7e | ||
|
|
902b7339d1 | ||
|
|
f84b907dc8 | ||
|
|
72cf5f8b04 | ||
|
|
e1d7852877 | ||
|
|
3f66c20616 | ||
|
|
a5f908d70e | ||
|
|
e9383a2f0c | ||
|
|
0453166326 | ||
|
|
670478e9ca | ||
|
|
1a9bc5550c | ||
|
|
25f7e58b3a | ||
|
|
839f8b062b | ||
|
|
eae1fbff8a | ||
|
|
d628b2de27 | ||
|
|
56e4cb765f | ||
|
|
21179c56d4 | ||
|
|
3da2830cfa | ||
|
|
873d6287c4 | ||
|
|
ea7eed4ad0 | ||
|
|
3b4b5ab298 | ||
|
|
2711c9b78c | ||
|
|
48d60821a7 | ||
|
|
076f4b441f | ||
|
|
e9eca83cd1 | ||
|
|
f6aa20b96d | ||
|
|
c15a384622 | ||
|
|
46c3bedd15 | ||
|
|
bc587f17de | ||
|
|
5905971178 | ||
|
|
afc7a7c956 | ||
|
|
bf970803ec | ||
|
|
3de473662f | ||
|
|
4bad92e3dd | ||
|
|
2089a299f1 | ||
|
|
10b1081960 | ||
|
|
c636a820d5 | ||
|
|
6c4bb59f06 | ||
|
|
97c55c1187 | ||
|
|
2c5db229c6 | ||
|
|
7c389a8010 | ||
|
|
c84ed0a4b4 | ||
|
|
1c638aa661 | ||
|
|
6325a23bb4 | ||
|
|
07abae30ba | ||
|
|
494e2f48d5 | ||
|
|
c7875b3f53 | ||
|
|
c88330f5f2 | ||
|
|
ff4ec19fff | ||
|
|
697f3473f6 | ||
|
|
439cd65050 | ||
|
|
79c7a559ad | ||
|
|
18d5315c2f | ||
|
|
06693aeac7 | ||
|
|
d5e6f9906c | ||
|
|
fac4de21de | ||
|
|
e3d0f0ec8f | ||
|
|
81c86019ab | ||
|
|
95c4a25bd2 | ||
|
|
d2f801c7d6 | ||
|
|
f72c4f28da | ||
|
|
a248fe5c4b | ||
|
|
e9495ccd84 | ||
|
|
cf5e34eae6 | ||
|
|
e52f583e20 | ||
|
|
98967cdf88 | ||
|
|
ceb1bb7f50 | ||
|
|
94c61cb959 | ||
|
|
20cb559714 | ||
|
|
2a49770fc0 | ||
|
|
4a71a9f7b5 | ||
|
|
b4b596ad8b | ||
|
|
a865bcfa1d | ||
|
|
3587cf5154 | ||
|
|
12b1d6e53b | ||
|
|
f4cb87f493 | ||
|
|
804088009e | ||
|
|
9f5faf7cf8 | ||
|
|
711c1a89ee | ||
|
|
30dd0604ca | ||
|
|
0b67eed92f | ||
|
|
9782cbae35 | ||
|
|
6ca4c0b23f | ||
|
|
a672ac66ae | ||
|
|
65c4ca01d0 | ||
|
|
6c3e2513d5 | ||
|
|
cc50364453 | ||
|
|
774104b34e | ||
|
|
5a3e427249 | ||
|
|
a9e2f95fdb | ||
|
|
d89b2986fd | ||
|
|
91229dd1e1 | ||
|
|
f52c65ea25 | ||
|
|
3dad6e96e3 | ||
|
|
8fd6e22dc4 | ||
|
|
474198b4e1 | ||
|
|
fbdafcb7b7 | ||
|
|
9b0232c48d | ||
|
|
723a184086 | ||
|
|
db34a4ffff | ||
|
|
0b896fb935 | ||
|
|
e0331ec022 | ||
|
|
e31ef916f0 | ||
|
|
55669f88ff | ||
|
|
c13dbc9a57 | ||
|
|
e8e03585ff | ||
|
|
b4bee864d2 | ||
|
|
b41d1e84da | ||
|
|
b5d5d7c2b0 | ||
|
|
3e571b4ce8 | ||
|
|
fb8fd5121e | ||
|
|
ac2a3243b5 | ||
|
|
1c10b8193b | ||
|
|
abf0fa1b32 | ||
|
|
4c5bc13c7f | ||
|
|
c33b81e3ad | ||
|
|
f88f0b5019 | ||
|
|
3b96f0d535 | ||
|
|
243672ead5 | ||
|
|
7009eb20f8 | ||
|
|
24cbd192aa | ||
|
|
9d36ae293c | ||
|
|
9496d83d1c | ||
|
|
18233e9ea1 | ||
|
|
6523c4bbbb | ||
|
|
5bafbdfaa0 | ||
|
|
7afa869833 | ||
|
|
0d32036523 | ||
|
|
a084ec19ff | ||
|
|
7faff8f887 | ||
|
|
f406001315 | ||
|
|
2b2020b43b | ||
|
|
4886e4b34a | ||
|
|
65e0364d37 | ||
|
|
13e16e0a26 | ||
|
|
965e1cd0c4 | ||
|
|
3750b67110 | ||
|
|
7e1f48f212 | ||
|
|
42457df2c1 | ||
|
|
56d8f1acfe | ||
|
|
56ac4acc9f | ||
|
|
d91b0f1af5 | ||
|
|
f986516379 | ||
|
|
fa59255556 | ||
|
|
ab1c11faf2 | ||
|
|
307f220de4 | ||
|
|
50c8a2dc69 | ||
|
|
3f726dc4c5 | ||
|
|
105d50f1f4 | ||
|
|
a3a5964926 | ||
|
|
6a8cff6fcd | ||
|
|
b42e9c80f7 | ||
|
|
23a7684208 | ||
|
|
c52c245b9d | ||
|
|
4555b2107a | ||
|
|
49b0120c6d | ||
|
|
f2541d8cae | ||
|
|
d1fb792709 | ||
|
|
86fb58155a | ||
|
|
d3c656893c | ||
|
|
713e394e7b | ||
|
|
40acf533ae | ||
|
|
13fdfc602e | ||
|
|
8255f3eb51 | ||
|
|
e7ab71c606 | ||
|
|
3eab0d6349 | ||
|
|
58047fac17 | ||
|
|
6e4144d015 | ||
|
|
8b0fda8d89 | ||
|
|
2ed656ca0d | ||
|
|
5cf79c82bb | ||
|
|
d1373bec66 | ||
|
|
325a0503cb | ||
|
|
fa72f52ad4 | ||
|
|
528815a564 | ||
|
|
dabcba9f5f | ||
|
|
5d9afc18f5 | ||
|
|
995dabc9b7 | ||
|
|
36145542af | ||
|
|
06eca6525a | ||
|
|
414673b347 | ||
|
|
a9767c049f | ||
|
|
507a6a8979 | ||
|
|
73d1db3bd2 | ||
|
|
9b5921e8e1 | ||
|
|
799a999148 | ||
|
|
eafe3af13e | ||
|
|
4e420c2f33 | ||
|
|
654b3ad6d3 | ||
|
|
9f8d73a1df | ||
|
|
f6e0b4ca9f | ||
|
|
1dbad1f0b8 | ||
|
|
8f9e19e3e2 | ||
|
|
b1a0b5e235 | ||
|
|
bce13944c3 | ||
|
|
c8fc3d1e7a | ||
|
|
e6f7b9c1f9 | ||
|
|
552ebaaaac | ||
|
|
6019fb2ca3 | ||
|
|
3af45e1a32 | ||
|
|
75088c89d3 | ||
|
|
2c1d46f159 | ||
|
|
15b9a1f34b | ||
|
|
5c70dd0557 | ||
|
|
dc0acdbee1 | ||
|
|
ae01047e8c | ||
|
|
1b7c2a0208 | ||
|
|
a8b01f523a | ||
|
|
23cbad8ba6 | ||
|
|
984e0f6e83 | ||
|
|
67df6a4d73 | ||
|
|
f756b9d77f | ||
|
|
0dfd51f81a | ||
|
|
bfdcee3772 | ||
|
|
470aea22d9 | ||
|
|
32e4c26c95 | ||
|
|
6a34568935 | ||
|
|
3548106a6c | ||
|
|
3806ad8843 | ||
|
|
037ce2dc12 | ||
|
|
338c0bcdbe | ||
|
|
bc3baf3094 | ||
|
|
8a91b5cfb5 | ||
|
|
4cf1ddd6fc | ||
|
|
cb781aeb00 | ||
|
|
2dd03e21e1 | ||
|
|
055bacbad7 | ||
|
|
46ae6d1fe4 | ||
|
|
5e73b12cf5 | ||
|
|
86c6f3eeac | ||
|
|
8922ae3a45 | ||
|
|
318e22e9fa | ||
|
|
4738b880a6 | ||
|
|
49829f8935 | ||
|
|
8e9d72982a | ||
|
|
d2f0180475 | ||
|
|
4da0b1e07c | ||
|
|
5a4a35b665 | ||
|
|
248cb4bd76 | ||
|
|
140001f036 | ||
|
|
3917cac800 | ||
|
|
ee37da5b35 | ||
|
|
6f8f3d2057 | ||
|
|
882ec65ba0 | ||
|
|
7e1aba3368 | ||
|
|
8aeadd1960 | ||
|
|
a5b091eec8 | ||
|
|
bbd4db6ddb | ||
|
|
312194228a | ||
|
|
5c1125900b | ||
|
|
08b8741282 | ||
|
|
e8367b765a | ||
|
|
91cd0df7b3 | ||
|
|
dff0a2aa1f | ||
|
|
1bf7bf66b3 | ||
|
|
9e495b42ee | ||
|
|
5f30b9e798 | ||
|
|
7c892de7b1 | ||
|
|
898f717254 | ||
|
|
800ef32959 | ||
|
|
609d69c4c9 | ||
|
|
9e1be39774 | ||
|
|
87ac44a1f1 | ||
|
|
9c4feac19b | ||
|
|
471edabe4d | ||
|
|
86841f80ca | ||
|
|
79348178a7 | ||
|
|
60b552027b | ||
|
|
62cbb15089 | ||
|
|
667b911023 | ||
|
|
071e86799b | ||
|
|
4164cf7adb | ||
|
|
b61aee36e7 | ||
|
|
7b16676f63 | ||
|
|
ff4f46abcc | ||
|
|
09c1bd96df | ||
|
|
40a190c29c | ||
|
|
5bfc360856 | ||
|
|
7eb26a7326 | ||
|
|
0afc9c154b | ||
|
|
97e00fb47d | ||
|
|
dbae0eeb31 | ||
|
|
bd9a21b805 | ||
|
|
033f8df500 | ||
|
|
ffda103d61 | ||
|
|
ecc9ea1226 | ||
|
|
93345a19b2 | ||
|
|
1741a20575 | ||
|
|
30eb939dc7 | ||
|
|
40a254922a | ||
|
|
7bc5bab432 | ||
|
|
6034f49f40 | ||
|
|
087eff4734 | ||
|
|
ed5b045a15 | ||
|
|
c1a3cbc28c | ||
|
|
bddc65a504 | ||
|
|
ddd2628c19 | ||
|
|
cf0c33a85d | ||
|
|
f46dc90035 | ||
|
|
73276b1003 | ||
|
|
16e67387c9 | ||
|
|
ca1b31bd9c | ||
|
|
55f333c0b7 | ||
|
|
f24e4f8a0a | ||
|
|
eec9933fb8 | ||
|
|
238e8f39f2 | ||
|
|
919bcb6888 | ||
|
|
50ebb25205 | ||
|
|
625642ca33 | ||
|
|
36632c762e | ||
|
|
f284362988 | ||
|
|
cf01f01bc9 | ||
|
|
5d0c71d292 | ||
|
|
b3d3269d3d | ||
|
|
a13c1f61af | ||
|
|
4064b8f254 | ||
|
|
5c466c51a8 | ||
|
|
36628ce78e | ||
|
|
d2d7bba357 | ||
|
|
8e68716d16 | ||
|
|
6824c09916 | ||
|
|
09ea924eb2 | ||
|
|
c8a042abdd | ||
|
|
019540e622 | ||
|
|
9a5243ade3 | ||
|
|
b4fc8ec4a5 | ||
|
|
30a2d85e92 | ||
|
|
98603594b1 | ||
|
|
7410d98d56 | ||
|
|
1f552a9e24 | ||
|
|
6c6f3d02f6 | ||
|
|
36a135f02b | ||
|
|
1c3734fde7 | ||
|
|
3c09be64ce | ||
|
|
719346a472 | ||
|
|
69693acea0 | ||
|
|
3873fdf5db | ||
|
|
c3a05e5041 | ||
|
|
4c0ab92771 | ||
|
|
c14378ca5d | ||
|
|
26b9c8123d | ||
|
|
5a504ac1dc | ||
|
|
8401dcf6d7 | ||
|
|
1f2e4edd35 | ||
|
|
212eec2ca6 | ||
|
|
935826ed1a | ||
|
|
8f3c6c3c87 | ||
|
|
cd3f8dcf89 | ||
|
|
9ff192366a | ||
|
|
63401ca3df | ||
|
|
8e323a6c07 | ||
|
|
d50c6c6dc3 | ||
|
|
def474c611 | ||
|
|
c1b2d16119 | ||
|
|
678d653ee9 | ||
|
|
4a6af108b4 | ||
|
|
e4cd37647e | ||
|
|
4254f56093 | ||
|
|
f7cef9dcd8 | ||
|
|
1c69eb1ae4 | ||
|
|
b673cb2a1f | ||
|
|
e88e49001a | ||
|
|
6a599ccb5d | ||
|
|
a90bf2e87b | ||
|
|
115b1a5267 | ||
|
|
84e346057e | ||
|
|
333de67ed5 | ||
|
|
e4dd215808 | ||
|
|
c214e269e9 | ||
|
|
466eac18a7 | ||
|
|
6e5d8b2d30 | ||
|
|
bf45bbea56 | ||
|
|
cdbcc7dc18 | ||
|
|
66e57606d2 | ||
|
|
c7f3bb5722 | ||
|
|
0e2f921b7e | ||
|
|
c421ea6bfc | ||
|
|
01feeae6f4 | ||
|
|
1ff52fcd00 | ||
|
|
6db25c3b6a | ||
|
|
88deded0fe | ||
|
|
fc0f2b5952 | ||
|
|
e211e944e5 | ||
|
|
a948038ff4 | ||
|
|
c70d192987 | ||
|
|
3fc8630634 | ||
|
|
8c013ed2d1 | ||
|
|
7a749631e8 | ||
|
|
e3a5f398e4 | ||
|
|
747f4803ba | ||
|
|
24709e8341 | ||
|
|
53861ad327 | ||
|
|
399bed34ad | ||
|
|
6b41fef96c | ||
|
|
031e2a2e0c | ||
|
|
9b4787c4b7 | ||
|
|
fe6e915c0d | ||
|
|
b5d67ec6c0 | ||
|
|
f5e0d06e2f | ||
|
|
78f69d5236 | ||
|
|
ab7d603171 | ||
|
|
b4936ffafa | ||
|
|
752e9ec655 | ||
|
|
9018e39762 | ||
|
|
a964ed5fe6 | ||
|
|
b862904506 | ||
|
|
7197cc2d62 | ||
|
|
b01570924d | ||
|
|
db478579c5 | ||
|
|
978ea9cd04 | ||
|
|
ca4f3d2025 | ||
|
|
c0020fd75a | ||
|
|
add4255bdc | ||
|
|
1f0faba71c | ||
|
|
e3f2658d53 | ||
|
|
f7cdb5f0b7 | ||
|
|
d32278b227 | ||
|
|
76acc5af99 | ||
|
|
5755e382fb | ||
|
|
95c450fe99 | ||
|
|
ad0b2ffc8e | ||
|
|
1b1b6b975e | ||
|
|
67e4e7e99b | ||
|
|
ac31c69c80 | ||
|
|
92ca447c06 | ||
|
|
bdea9f10fc | ||
|
|
dc3d36e0a5 | ||
|
|
99ef396aeb | ||
|
|
69d7fb0344 | ||
|
|
e4e08db0b4 | ||
|
|
164d952e56 | ||
|
|
c711dc328e | ||
|
|
8b80ad8ba1 | ||
|
|
5772c81590 | ||
|
|
09d4467e22 | ||
|
|
d22f399f18 | ||
|
|
f89fd98ed7 | ||
|
|
b01ce9d4cc | ||
|
|
18ccd3cbaf | ||
|
|
d6fe5339cf | ||
|
|
2690ef3f05 | ||
|
|
ae82d0ab47 | ||
|
|
90e0a5dc30 | ||
|
|
c1b6b865a7 | ||
|
|
d849ae216d | ||
|
|
4ee4492490 | ||
|
|
fcd17692ee | ||
|
|
36159a7697 | ||
|
|
7886189bce | ||
|
|
3a681b6670 | ||
|
|
3e4c141913 | ||
|
|
ef3733aebe | ||
|
|
b5f54ff534 | ||
|
|
ba494374d0 | ||
|
|
c7465479a2 | ||
|
|
b14830e4e3 | ||
|
|
288f23eea2 | ||
|
|
50a902a90b | ||
|
|
277c00c7f8 | ||
|
|
4a09ac5b8f | ||
|
|
d5e9e0559b | ||
|
|
0dffb0fe85 | ||
|
|
0f90d687c7 | ||
|
|
84b7d78ea4 | ||
|
|
241480bb23 | ||
|
|
73a065c1cc | ||
|
|
1f693c6c78 | ||
|
|
e9db535dd8 | ||
|
|
7b7408dab7 | ||
|
|
9c897a91a9 | ||
|
|
4189f8187f | ||
|
|
98565b0c6b | ||
|
|
38342a7f5f | ||
|
|
6f689745c0 | ||
|
|
63fd660eb1 | ||
|
|
fa14b6045d | ||
|
|
f2528fb462 | ||
|
|
0db0809146 | ||
|
|
276422f4be | ||
|
|
e6b55ac034 | ||
|
|
58af35fdea | ||
|
|
763989bc87 | ||
|
|
385022de80 | ||
|
|
6c104e2aca | ||
|
|
363c0d28f4 | ||
|
|
a378fc4e68 | ||
|
|
01de288c35 | ||
|
|
f1a68e4451 | ||
|
|
f429b86f48 | ||
|
|
ccfdacff5b | ||
|
|
a9d9b765e8 | ||
|
|
5298f4b517 | ||
|
|
53d03e82ab | ||
|
|
2fa288fc4d | ||
|
|
73819579f3 | ||
|
|
271ff4faeb | ||
|
|
c04ac4fc7e | ||
|
|
5a87a16311 | ||
|
|
dd48aa73e2 | ||
|
|
6dd046a1a4 | ||
|
|
baaacbed31 | ||
|
|
0b3fdb07f6 | ||
|
|
cc09a8b66f | ||
|
|
a60a3adc12 | ||
|
|
e412a0f4b6 | ||
|
|
ed23d10364 | ||
|
|
4c95af2c69 | ||
|
|
baa95a62d1 | ||
|
|
c7494c3c73 | ||
|
|
12f0826d32 | ||
|
|
428e8631e2 | ||
|
|
d3e3cfa385 | ||
|
|
3120d56e80 | ||
|
|
07cb36ebc7 | ||
|
|
d7c82e7a51 | ||
|
|
bf340e684a | ||
|
|
8d1b394df1 | ||
|
|
d305dbd468 | ||
|
|
eb51d18012 | ||
|
|
4f3f87fc13 | ||
|
|
3e6070bd9b | ||
|
|
0daba348fe | ||
|
|
f874e8844c | ||
|
|
a8fef04455 | ||
|
|
2f74a080ee | ||
|
|
198748feea | ||
|
|
9f73be0d5c | ||
|
|
8aea5041c7 | ||
|
|
1856b824cb | ||
|
|
a27cf1b41c | ||
|
|
b610b9aca2 | ||
|
|
f5c24cf252 | ||
|
|
8303068310 | ||
|
|
c17fd3b254 | ||
|
|
d82838a137 | ||
|
|
4506a9e905 | ||
|
|
b4580943e8 | ||
|
|
730f9534dc | ||
|
|
a7cc7ceeb8 | ||
|
|
7861852078 | ||
|
|
dbf6bb5fca | ||
|
|
d4d5272bf2 | ||
|
|
0c4bcca7c9 | ||
|
|
0414307679 | ||
|
|
0f3a5501d4 | ||
|
|
9d4ce3f070 | ||
|
|
7b43a0f0bd | ||
|
|
8b7cc64567 | ||
|
|
b0b40933d8 | ||
|
|
f20c98e49c | ||
|
|
627b3f084d | ||
|
|
b98a32c296 | ||
|
|
b678f82be8 | ||
|
|
9162d2cd43 | ||
|
|
4995011f1e | ||
|
|
fea30dcea4 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,6 +1,9 @@
|
||||
node_modules*
|
||||
config.status*
|
||||
config/environments/*.js
|
||||
.idea
|
||||
tools/munin/windshaft.conf
|
||||
logs/
|
||||
pids/
|
||||
redis.pid
|
||||
test.log
|
||||
|
||||
21
.travis.yml
Normal file
21
.travis.yml
Normal file
@@ -0,0 +1,21 @@
|
||||
addons:
|
||||
postgresql: "9.3"
|
||||
|
||||
before_install:
|
||||
- sudo apt-get install -y pkg-config libcairo2-dev libjpeg8-dev libgif-dev
|
||||
- createdb template_postgis
|
||||
- psql -c "CREATE EXTENSION postgis" template_postgis
|
||||
|
||||
env:
|
||||
- NPROCS=1 JOBS=1 PGUSER=postgres
|
||||
|
||||
language: node_js
|
||||
node_js:
|
||||
- "0.8"
|
||||
- "0.10"
|
||||
|
||||
notifications:
|
||||
irc:
|
||||
channels:
|
||||
- "irc.freenode.org#cartodb"
|
||||
use_notice: true
|
||||
20
HOWTO_RELEASE
Normal file
20
HOWTO_RELEASE
Normal file
@@ -0,0 +1,20 @@
|
||||
1. Test (make clean all check), fix if broken before proceeding
|
||||
2. Ensure proper version in package.json
|
||||
3. Ensure NEWS section exists for the new version, review it, add release date
|
||||
4. Drop npm-shrinkwrap.json
|
||||
5. Run npm shrinkwrap to recreate npm-shrinkwrap.json
|
||||
6. Commit package.json, npm-shrinwrap.json, NEWS
|
||||
7. git tag -a Major.Minor.Patch # use NEWS section as content
|
||||
8. Announce on cartodb@googlegroups.com
|
||||
9. Stub NEWS/package for next version
|
||||
|
||||
Versions:
|
||||
|
||||
Bugfix releases increment Patch component of version.
|
||||
Feature releases increment Minor and set Patch to zero.
|
||||
If backward compatibility is broken, increment Major and
|
||||
set to zero Minor and Patch.
|
||||
|
||||
Branches named 'b<Major>.<Minor>' are kept for any critical
|
||||
fix that might need to be shipped before next feature release
|
||||
is ready.
|
||||
27
LICENCE
27
LICENCE
@@ -1,27 +0,0 @@
|
||||
Copyright (c) 2011, Vizzuality
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
3. All advertising materials mentioning features or use of this software
|
||||
must display the following acknowledgement:
|
||||
This product includes software developed by Vizzuality.
|
||||
4. Neither the name of Vizzuality nor the
|
||||
names of its contributors may be used to endorse or promote products
|
||||
derived from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY
|
||||
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
|
||||
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
27
LICENSE
Normal file
27
LICENSE
Normal file
@@ -0,0 +1,27 @@
|
||||
Copyright (c) 2014, Vizzuality
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
15
Makefile
15
Makefile
@@ -6,16 +6,19 @@ all:
|
||||
clean:
|
||||
rm -rf node_modules/*
|
||||
|
||||
config/environments/test.js: config/environments/test.js.example Makefile
|
||||
distclean: clean
|
||||
rm config.status*
|
||||
|
||||
config.status--test:
|
||||
./configure --environment=test
|
||||
|
||||
config/environments/test.js: config.status--test
|
||||
./config.status--test
|
||||
|
||||
check-local: config/environments/test.js
|
||||
./run_tests.sh ${RUNTESTFLAGS} \
|
||||
test/unit/cartodb/redis_pool.test.js \
|
||||
test/unit/cartodb/req2params.test.js \
|
||||
test/acceptance/cache_validator.js \
|
||||
test/acceptance/server.js \
|
||||
test/acceptance/multilayer.js
|
||||
test/unit/cartodb/*.js \
|
||||
test/acceptance/*.js
|
||||
|
||||
check-submodules:
|
||||
PATH="$$PATH:$(srcdir)/node_modules/.bin/"; \
|
||||
|
||||
575
NEWS.md
575
NEWS.md
@@ -1,3 +1,578 @@
|
||||
1.26.1 -- 2015-01-28
|
||||
--------------------
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to 0.35.1, see https://github.com/CartoDB/Windshaft/pull/254
|
||||
|
||||
|
||||
1.26.0 -- 2015-01-27
|
||||
--------------------
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to 0.35.0, supports mapconfig version `1.3.0`
|
||||
|
||||
|
||||
1.25.0 -- 2015-01-26
|
||||
--------------------
|
||||
|
||||
Announcements:
|
||||
- No more signed maps (#227 and #238)
|
||||
- Splits template maps endpoint into its own controller
|
||||
- Removes TemplateMaps dependency on SignedMaps
|
||||
- Token validation is done against the template
|
||||
- Template is always extended with default values for auth and placeholders
|
||||
- MapConfig is extended, in order to validate auth_tokens, with template info:
|
||||
- template name
|
||||
- template auth
|
||||
- No more locks to create, update or delete templates
|
||||
- Trusting in redis' hash semantics
|
||||
- Some tradeoffs:
|
||||
* A client having more templates than allowed by a race condition between limit (HLEN) check and creation (HSET)
|
||||
* Updating a template could happen while deleting it, resulting in a new template
|
||||
* Templates already instantiated will be accessible through their layergroup so it is possible to continue requesting tiles/grids/etc.
|
||||
- Authorisation is now handled by template maps
|
||||
- Template instantiation returns new instances with default values if they are missing
|
||||
|
||||
|
||||
New features:
|
||||
- Basic layergroup validation on named map creation/update (#196)
|
||||
- Add named maps surrogate keys and call invalidation on template modification/deletion (#247)
|
||||
- Extends TemplateMaps backend with EventEmitter
|
||||
- Emits for create, update and delete templates
|
||||
- VarnishHttpCacheBackend will invalidate a varnish instance via HTTP PURGE method
|
||||
- In the future there could be more backends, for instance to invalidate a CDN.
|
||||
- NamedMapsEntry has the responsibility to generate a cache key for a named map
|
||||
- It probably should receive a template/named map instead of owner and template name
|
||||
- SurrogateKeysCache is responsible to tag responses with a header
|
||||
- It also is responsible for invalidations given an Invalidation Backend
|
||||
- In the future it could have several backends so it can invalidates different caches
|
||||
- SurrogateKeysCache is subscribed to TemplateMaps events to do the invalidations
|
||||
|
||||
|
||||
1.24.0 -- 2015-01-15
|
||||
--------------------
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to 0.34.0 for retina support
|
||||
|
||||
|
||||
1.23.1 -- 2015-01-14
|
||||
--------------------
|
||||
|
||||
Announcements:
|
||||
- Regenerate npm-shrinkwrap.json
|
||||
|
||||
|
||||
1.23.0 -- 2015-01-14
|
||||
--------------------
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to 0.33.0
|
||||
|
||||
New features:
|
||||
- Sets HTTP renderer configuration in server_options
|
||||
|
||||
|
||||
1.22.0 -- 2015-01-13
|
||||
--------------------
|
||||
|
||||
New features:
|
||||
- Health check endpoint
|
||||
|
||||
|
||||
1.21.2 -- 2014-12-15
|
||||
--------------------
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to 0.32.4
|
||||
|
||||
|
||||
1.21.1 -- 2014-12-11
|
||||
--------------------
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to 0.32.2
|
||||
|
||||
Bugfixes:
|
||||
- Closes fd for log files on `kill -HUP` (#230)
|
||||
|
||||
|
||||
|
||||
1.21.0 -- 2014-10-24
|
||||
--------------------
|
||||
|
||||
New features:
|
||||
- Allow a different cache-control max-age for layergroup responses
|
||||
|
||||
|
||||
1.20.2 -- 2014-10-20
|
||||
--------------------
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to 0.31.0
|
||||
|
||||
|
||||
1.20.1 -- 2014-10-17
|
||||
--------------------
|
||||
|
||||
Announcements:
|
||||
- Upgrades redis-mpool to 0.3.0
|
||||
|
||||
|
||||
1.20.0 -- 2014-10-15
|
||||
--------------------
|
||||
|
||||
New features:
|
||||
- Report to statsd the status of redis pools
|
||||
- Upgrades Windshaft to start reporting redis/renderers/mapnik pool metrics
|
||||
|
||||
Enhancements:
|
||||
- Share one redis-mpool across the application
|
||||
|
||||
|
||||
1.19.0 -- 2014-10-14
|
||||
--------------------
|
||||
|
||||
Announcements:
|
||||
- Dropping support for npm <1.2.1
|
||||
npm-shrinkwrap.json is incompatible when generated with npm >=1.2.1 and consumed by npm <1.2.1
|
||||
- Upgrades windshaft to 0.28.2
|
||||
- Generates npm-shrinkwrap.json with npm >1.2.0
|
||||
|
||||
|
||||
1.18.2 -- 2014-10-13
|
||||
--------------------
|
||||
|
||||
Bug fixes:
|
||||
- Defaults resultSet to object if undefined in QueryTablesApi
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to 0.28.1
|
||||
|
||||
|
||||
1.18.1 -- 2014-10-13
|
||||
--------------------
|
||||
|
||||
New features:
|
||||
- Allow to add more node.js' threadpool workers via process.env.UV_THREADPOOL_SIZE
|
||||
|
||||
|
||||
1.18.0 -- 2014-10-03
|
||||
--------------------
|
||||
|
||||
Announcements:
|
||||
- Comes back to use mapnik 2.3.x based on cartodb/node-mapnik@1.4.15-cdb from windshaft@0.28.0
|
||||
|
||||
|
||||
1.17.2 -- 2014-10-01
|
||||
--------------------
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to 0.27.2 which downgrades node-mapnik to 0.7.26-cdb1
|
||||
|
||||
|
||||
1.17.1 -- 2014-09-30
|
||||
--------------------
|
||||
|
||||
Announcements:
|
||||
- Upgrades windshaft to 0.27.1 which downgrades node-mapnik to 1.4.10
|
||||
|
||||
Enhancements:
|
||||
- TTL for template locks so they are not kept forever
|
||||
- Upgrades mocha
|
||||
|
||||
|
||||
1.17.0 -- 2014-09-25
|
||||
--------------------
|
||||
|
||||
New features:
|
||||
- Starts using mapnik 2.3.x
|
||||
|
||||
Enhancements:
|
||||
- Upgrades windshaft and cartodb-redis
|
||||
- Supports `!scale_denominator!` dynamic param in SQL queries
|
||||
- Metrics revamp: removes and adds some metrics
|
||||
- Adds poolSize configuration for mapnik
|
||||
|
||||
1.16.1 -- 2014-08-19
|
||||
--------------------
|
||||
|
||||
Enhancements:
|
||||
- Upgrades cartodb-redis
|
||||
|
||||
1.16.0 -- 2014-08-18
|
||||
--------------------
|
||||
|
||||
New features:
|
||||
- Configurable QueryTablesAPI to call directly postgresql using cartodb-psql
|
||||
or to keep using a request to the SQL API
|
||||
|
||||
Enhancements:
|
||||
- Removes mapnik dependency as it now relies on Windshaft to check mapnik version
|
||||
- Upgrades dependencies:
|
||||
- underscore
|
||||
- lzma
|
||||
- log4js
|
||||
- rollbar
|
||||
- windshaft
|
||||
- request
|
||||
|
||||
1.15.0 -- 2014-08-13
|
||||
--------------------
|
||||
Enhancements:
|
||||
- Upgrades dependencies:
|
||||
- redis-mpool
|
||||
- cartodb-redis
|
||||
- windshaft
|
||||
- Specifies name in the redis pool
|
||||
- Slow pool configuration in example configurations
|
||||
|
||||
|
||||
1.14.0 -- 2014-08-07
|
||||
--------------------
|
||||
|
||||
Enhancements:
|
||||
- SQL API requests moved to its own entity
|
||||
|
||||
New features:
|
||||
- Affected tables and last updated time for a query are performed in a single
|
||||
request to the SQL API
|
||||
- Allow specifying the tile format, upgrades windshaft and grainstore
|
||||
dependencies for this matter
|
||||
|
||||
|
||||
1.13.1 -- 2014-08-04
|
||||
--------------------
|
||||
|
||||
Enhancements:
|
||||
- Profiler header sent as JSON string
|
||||
|
||||
|
||||
1.13.0 -- 2014-07-30
|
||||
--------------------
|
||||
|
||||
New features:
|
||||
- Support for postgresql schemas
|
||||
- Use public user from redis
|
||||
- Support for several auth tokens
|
||||
|
||||
1.12.1 -- 2014-06-24
|
||||
--------------------
|
||||
|
||||
Enhancements:
|
||||
- Caches layergroup and sets X-Cache-Channel in GET requests also in named maps
|
||||
|
||||
1.12.0 -- 2014-06-24
|
||||
--------------------
|
||||
|
||||
New features:
|
||||
- Caches layergroup and sets X-Cache-Channel in GET requests
|
||||
|
||||
1.11.1 -- 2014-05-07
|
||||
--------------------
|
||||
|
||||
Enhancements:
|
||||
|
||||
- Upgrade Windshaft to 0.21.0, see
|
||||
http://github.com/CartoDB/Windshaft/blob/0.21.0/NEWS
|
||||
|
||||
1.11.0 -- 2014-04-28
|
||||
--------------------
|
||||
|
||||
New features:
|
||||
|
||||
- Add support for log_filename directive
|
||||
- Reopen log file on SIGHUP, for better logrotate integration
|
||||
|
||||
Enhancements:
|
||||
|
||||
- Set default PostgreSQL application name to "cartodb_tiler"
|
||||
|
||||
1.10.2 -- 2014-04-08
|
||||
--------------------
|
||||
|
||||
Bug fixes:
|
||||
|
||||
- Fix show_style tool broken since 1.8.1
|
||||
- Fix X-Cache-Channel of tiles accessed via signed token (#188)
|
||||
|
||||
1.10.1 -- 2014-03-21
|
||||
--------------------
|
||||
|
||||
Bug fixes:
|
||||
|
||||
- Do not cache non-success jsonp responses (#186)
|
||||
|
||||
1.10.0 -- 2014-03-20
|
||||
--------------------
|
||||
|
||||
New features:
|
||||
|
||||
- Add optional support for rollbar (#150)
|
||||
|
||||
Enhancements:
|
||||
|
||||
- Do not send connection details to client (#183)
|
||||
- Upgrade node-varnish to 0.3.0
|
||||
- Upgrade Windshaft to 0.20.0, see
|
||||
http://github.com/CartoDB/Windshaft/blob/0.20.0/NEWS
|
||||
- Include tiler version in startup log
|
||||
- Install an uncaught exception handler
|
||||
- Require own fork of node-mapnik, with temptative fix
|
||||
for libxml usage (glibc detected corruptions)
|
||||
|
||||
Other changes:
|
||||
|
||||
- Switch to 3-clause BSD license (#184)
|
||||
|
||||
1.9.0 -- 2014-03-10
|
||||
-------------------
|
||||
|
||||
New features:
|
||||
|
||||
- Allow to set server related configuration in serverMetadata (#182)
|
||||
|
||||
1.8.5 -- 2014-03-10
|
||||
-------------------
|
||||
|
||||
Enhancements:
|
||||
|
||||
- Set statsd prefix for all endpoints
|
||||
- Respond with a permission denied on attempt to access map tiles waiving
|
||||
signature of someone who had not left any (#170)
|
||||
- Do not log an error on GET / (#177)
|
||||
- Do not UNWATCH on every redis client release (#161)
|
||||
- Include API docs (#164)
|
||||
- Add "cacheDns" statsd setting in the example configs
|
||||
- Do not send duplicated stats on template instanciation
|
||||
- Do not die on dns resolution errors (#178, #180)
|
||||
|
||||
Bug fixes:
|
||||
|
||||
- Do not cache map creation responses (#176)
|
||||
|
||||
1.8.4 -- 2014-03-03
|
||||
-------------------
|
||||
|
||||
Enhancements:
|
||||
|
||||
- Really skip CDB_TableMetadata lookup for sql affected by no tables (#169)
|
||||
- Upgrade windshaft to 0.19.2, see node_modules/windshaft/NEWS
|
||||
- Clarify obscure "ECONNREFUSED" error message (#171)
|
||||
- Change some http status responses to be more appropriate to the case
|
||||
- Forbid using map signatures of foreign users (#172)
|
||||
- Forbid instanciating templates of foreign users (#173)
|
||||
- Allow passing environment configuration name via NODE_ENV to app.js
|
||||
- Print environment configuration name on app start
|
||||
|
||||
Bug fixes:
|
||||
|
||||
- Fix database connection settings on template instanciation (#174)
|
||||
|
||||
1.8.3 -- 2014-02-27
|
||||
-------------------
|
||||
|
||||
Enhancements:
|
||||
|
||||
- Upgrades windshaft to 0.19.1 with many performance improvements,
|
||||
See node_modules/windshaft/NEWS
|
||||
- Improve speed of instanciating a map (#147, #159, #165)
|
||||
- Give meaningful error on attempts to use map tokens
|
||||
with attribute service (#156)
|
||||
- Reduce sql-api communication timeout, and allow overriding (#167)
|
||||
[ new sqlapi.timeout directive, defaults to 100 ms ]
|
||||
- Do not query CDB_TableMetadata for queries affected by no tables (#168)
|
||||
|
||||
1.8.2 -- 2014-02-25
|
||||
-------------------
|
||||
|
||||
Enhancements:
|
||||
|
||||
* Allow using ":host" as part of statsd.prefix (#153)
|
||||
* Expand "addCacheChannel" stats
|
||||
* Allow using GET with sql-api for queries shorter than configured len (#155)
|
||||
[ new sqlapi.max_get_sql_length directive, defaults to 2048 ]
|
||||
* Do not log an error for a legit request requiring no X-Cache-Channel
|
||||
|
||||
Bug fixes:
|
||||
|
||||
* Fix munin plugin after log format changes (#154)
|
||||
|
||||
1.8.1 -- 2014-02-19
|
||||
-------------------
|
||||
|
||||
Enhancements:
|
||||
|
||||
* Use log4js logger (#138)
|
||||
|
||||
Bug fixes:
|
||||
|
||||
* Always generate X-Cache-Channel for token-based tile responses (#152)
|
||||
|
||||
1.8.0 -- 2014-02-18
|
||||
-------------------
|
||||
|
||||
Enhancements:
|
||||
|
||||
* Add script to flush caches (#140)
|
||||
* Add script to list templates
|
||||
* Add statsd support (#139)
|
||||
* Add support for specifying a varnish password
|
||||
* Avoid sending multiple varnish invalidation at once (#135)
|
||||
* Tested with node-0.10 (#141)
|
||||
* Use single redis pooler for torque and grainstore
|
||||
* Reduce cost of garbage collection for localized resources
|
||||
* Allow limiting number of templates for each user (#136)
|
||||
* Allow configuring TTL of mapConfigs via "mapConfigTTL"
|
||||
|
||||
1.7.1 -- 2014-02-11
|
||||
-------------------
|
||||
|
||||
Enhancements:
|
||||
|
||||
* Disable debug logging unless "debug" config param evaluates to true (#137)
|
||||
* Require windshaft 0.17.2 for further reducing log noise (#137)
|
||||
|
||||
1.7.0 -- 2014-02-11
|
||||
-------------------
|
||||
|
||||
New features:
|
||||
|
||||
* Add support for torque tiles (#112)
|
||||
* Add attributes service (#118)
|
||||
* Implement Unified Map API (#126)
|
||||
* Make endpoints configurable (#127)
|
||||
|
||||
Enhancements:
|
||||
|
||||
* Allow specifying fixed sqlapi host address (#117)
|
||||
* Include template hash in template instance response, to keep caches
|
||||
of different instances separated (#105)
|
||||
|
||||
Bug fixes:
|
||||
|
||||
* Allow space padding in template variables usage (#129)
|
||||
* Allow passing numbers as values for numeric template variables (#130)
|
||||
|
||||
|
||||
1.6.3 -- 2014-01-30
|
||||
-------------------
|
||||
|
||||
Bug fixes:
|
||||
|
||||
* layergroup accept both map_key and api_key (#91)
|
||||
* Fix public instanciation of signed template accessing private data (#114)
|
||||
* Fix show_style in presence of complex styles
|
||||
* Fix use of maxzoom in layergroup config (via windshaft-0.15.1)
|
||||
|
||||
Enhancements:
|
||||
|
||||
* Add support for instanciating a template map with JSONP (#116)
|
||||
* Stop processing XML on renderer creation, not needed anymore since 1.6.1
|
||||
introduced on-demand XML generation.
|
||||
|
||||
1.6.2 -- 2014-01-23
|
||||
-------------------
|
||||
|
||||
Bug fixes:
|
||||
|
||||
* Fix support for long (>64k chars) queries in layergroup creation (#111)
|
||||
|
||||
Enhancements:
|
||||
|
||||
* Enhance tools/show_style to accept an environment parameter and
|
||||
print XML style now it is not in redis anymore (#110)
|
||||
* Support CORS in template instanciation endpoint (#113)
|
||||
|
||||
1.6.1 -- 2014-01-15
|
||||
-------------------
|
||||
|
||||
Bug fixes:
|
||||
|
||||
* Drop cache headers from error responses (#107)
|
||||
* Localize external CartoCSS resources at renderer creation time (#108)
|
||||
|
||||
1.6.0 -- 2014-01-10
|
||||
-------------------
|
||||
|
||||
New features:
|
||||
|
||||
* Add 'user_from_host' directive to generalize username extraction (#100)
|
||||
* Implement signed template maps (#98)
|
||||
|
||||
|
||||
Other changes:
|
||||
|
||||
* Update cartodb-redis dependency to "~0.3.0"
|
||||
* Update redis-server dependency to "2.4.0+"
|
||||
|
||||
1.5.2 -- 2013-12-05
|
||||
-------------------
|
||||
|
||||
Bug fixes:
|
||||
|
||||
* Fix configuration-level compatibility with versions prior to 1.5 (#96)
|
||||
* Fix use of old layergroups on mapnik upgrade (#97)
|
||||
|
||||
1.5.1 -- 2013-11-28
|
||||
-------------------
|
||||
|
||||
Bug fixes:
|
||||
|
||||
* Survive presence of malformed CartoCSS in redis (#94)
|
||||
* Accept useless point-transform:scale directives (#93)
|
||||
|
||||
1.5.0 -- 2013-11-19
|
||||
-------------------
|
||||
|
||||
NOTE: new configuration directives `postgres_auth_pass` and
|
||||
`postgres.password` added; see config/environments/*.example
|
||||
for documentation.
|
||||
|
||||
Improvements:
|
||||
|
||||
* Add support for configuring database connection passwords
|
||||
* Optionally read user-specific database_host and database_password
|
||||
from redis as per CartoDB-2.5.0 model (#88, #89)
|
||||
* Do not force ending dot in SQL-API hostname, for easier testing
|
||||
|
||||
Bug fixes:
|
||||
|
||||
* Return CORS headers when creating layergroups via GET (windshaft/#92)
|
||||
* Fix http status on database authentication error (windshaft/#94)
|
||||
* Fix text-face-name error at layergroup creation (windshaft/#93)
|
||||
|
||||
Other changes:
|
||||
|
||||
* CartoDB redis interaction delegated to "cartodb-redis" module
|
||||
|
||||
|
||||
1.4.1 -- 2013-11-08
|
||||
-------------------
|
||||
|
||||
* Fix support for exponential notation in CartoCSS filter values (#87)
|
||||
|
||||
1.4.0 -- 2013-10-31
|
||||
-------------------
|
||||
|
||||
* Add Support for Mapnik-2.2.0 (#78)
|
||||
|
||||
1.3.6 -- 2013-10-11
|
||||
-------------------
|
||||
|
||||
* Restore support for node-0.8.9 accidentally dropped by 1.3.5
|
||||
NOTE: needs removing node_modules/windshaft and re-running npm install
|
||||
|
||||
1.3.5 -- 2013-10-03
|
||||
-------------------
|
||||
|
||||
* Fixing apostrophes in CartoCSS
|
||||
* Fix "sql/table must contain zoom variable" error when using
|
||||
"[ zoom > 3]" CartoCSS snippets (note the space)
|
||||
* Fix backward compatibility handling of sqlapi.host configuration (#82)
|
||||
* Fix error for invalid text-name in CartoCSS (#81)
|
||||
* Do not let anonymous requests use authorized renderer caches
|
||||
|
||||
1.3.4
|
||||
------
|
||||
|
||||
|
||||
18
README.md
18
README.md
@@ -1,7 +1,8 @@
|
||||
Windshaft-CartoDB
|
||||
==================
|
||||
|
||||
NOTE: requires node-0.8.x
|
||||
[]
|
||||
(http://travis-ci.org/CartoDB/Windshaft-cartodb)
|
||||
|
||||
This is the CartoDB map tiler. It extends Windshaft with some extra
|
||||
functionality and custom filters for authentication
|
||||
@@ -11,23 +12,28 @@ functionality and custom filters for authentication
|
||||
* gets the default geometry type from the cartodb redis store
|
||||
* allows tiles to be styled individually
|
||||
* provides a link to varnish high speed cache
|
||||
* provides a infowindow endpoint for windshaft
|
||||
* provides a ``map_metadata`` endpoint for windshaft
|
||||
* provides a ``infowindow`` endpoint for windshaft (DEPRECATED)
|
||||
* provides a ``map_metadata`` endpoint for windshaft (DEPRECATED)
|
||||
* provides signed template maps API
|
||||
(http://github.com/CartoDB/Windshaft-cartodb/wiki/Template-maps)
|
||||
|
||||
Requirements
|
||||
------------
|
||||
|
||||
[core]
|
||||
- node-0.6.x+
|
||||
- node-0.8.x+
|
||||
- PostgreSQL-8.3+
|
||||
- PostGIS-1.5.0+
|
||||
- Redis 2.2.0+ (http://www.redis.io)
|
||||
- Redis 2.4.0+ (http://www.redis.io)
|
||||
- Mapnik 2.0 or 2.1
|
||||
|
||||
[for cache control]
|
||||
- CartoDB-SQL-API 1.0.0+
|
||||
- CartoDB 0.9.5+ (for ``CDB_QueryTables``)
|
||||
- Varnish (https://www.varnish-cache.org)
|
||||
- Varnish (http://www.varnish-cache.org)
|
||||
|
||||
[for running the testsuite]
|
||||
- Imagemagick (http://www.imagemagick.org)
|
||||
|
||||
Configure
|
||||
---------
|
||||
|
||||
97
app.js
97
app.js
@@ -7,33 +7,88 @@
|
||||
* environments: [development, production]
|
||||
*/
|
||||
|
||||
var path = require('path'),
|
||||
fs = require('fs'),
|
||||
RedisPool = require('redis-mpool')
|
||||
;
|
||||
|
||||
|
||||
if ( process.argv[2] ) ENV = process.argv[2];
|
||||
else if ( process.env['NODE_ENV'] ) ENV = process.env['NODE_ENV'];
|
||||
else ENV = 'development';
|
||||
|
||||
process.env['NODE_ENV'] = ENV;
|
||||
|
||||
// sanity check
|
||||
var ENV = process.argv[2]
|
||||
if (ENV != 'development' && ENV != 'production' && ENV != 'staging' ){
|
||||
console.error("\nnode app.js [environment]");
|
||||
console.error("environments: [development, production, staging]\n");
|
||||
console.error("environments: development, production, staging\n");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
var _ = require('underscore')
|
||||
, Step = require('step')
|
||||
, CartodbWindshaft = require('./lib/cartodb/cartodb_windshaft');
|
||||
var _ = require('underscore');
|
||||
|
||||
// set environment specific variables
|
||||
global.settings = require(__dirname + '/config/settings');
|
||||
global.environment = require(__dirname + '/config/environments/' + ENV);
|
||||
_.extend(global.settings, global.environment);
|
||||
global.environment.api_hostname = require('os').hostname().split('.')[0];
|
||||
|
||||
// Include cart_data.js only _after_ the "global" variable is set
|
||||
global.log4js = require('log4js');
|
||||
log4js_config = {
|
||||
appenders: [],
|
||||
replaceConsole:true
|
||||
};
|
||||
|
||||
if (global.environment.uv_threadpool_size) {
|
||||
process.env.UV_THREADPOOL_SIZE = global.environment.uv_threadpool_size;
|
||||
}
|
||||
|
||||
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) ) {
|
||||
console.error("Log filename directory does not exist: " + logdir);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("Logs will be written to " + global.environment.log_filename);
|
||||
log4js_config.appenders.push(
|
||||
{ type: "file", filename: global.environment.log_filename }
|
||||
);
|
||||
} else {
|
||||
log4js_config.appenders.push(
|
||||
{ type: "console", layout: { type:'basic' } }
|
||||
);
|
||||
}
|
||||
|
||||
if ( global.environment.rollbar ) {
|
||||
log4js_config.appenders.push({
|
||||
type: __dirname + "/lib/cartodb/log4js_rollbar.js",
|
||||
options: global.environment.rollbar
|
||||
});
|
||||
}
|
||||
|
||||
log4js.configure(log4js_config, { cwd: __dirname });
|
||||
global.logger = log4js.getLogger();
|
||||
|
||||
var redisOpts = _.extend(global.environment.redis, { name: 'windshaft' }),
|
||||
redisPool = new RedisPool(redisOpts);
|
||||
|
||||
// Include cartodb_windshaft only _after_ the "global" variable is set
|
||||
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/28
|
||||
var cartoData = require('./lib/cartodb/carto_data');
|
||||
|
||||
var Windshaft = require('windshaft');
|
||||
var serverOptions = require('./lib/cartodb/server_options');
|
||||
var CartodbWindshaft = require('./lib/cartodb/cartodb_windshaft'),
|
||||
serverOptions = require('./lib/cartodb/server_options')(redisPool);
|
||||
|
||||
ws = CartodbWindshaft(serverOptions);
|
||||
|
||||
if (global.statsClient) {
|
||||
redisPool.on('status', function(status) {
|
||||
var keyPrefix = 'windshaft.redis-pool.' + status.name + '.db' + status.db + '.';
|
||||
global.statsClient.gauge(keyPrefix + 'count', status.count);
|
||||
global.statsClient.gauge(keyPrefix + 'unused', status.unused);
|
||||
global.statsClient.gauge(keyPrefix + 'waiting', status.waiting);
|
||||
});
|
||||
}
|
||||
|
||||
// Maximum number of connections for one process
|
||||
// 128 is a good number if you have up to 1024 filedescriptors
|
||||
// 4 is good if you have max 32 filedescriptors
|
||||
@@ -42,8 +97,12 @@ ws.maxConnections = global.environment.maxConnections || 128;
|
||||
|
||||
ws.listen(global.environment.port, global.environment.host);
|
||||
|
||||
var version = require("./package").version;
|
||||
|
||||
ws.on('listening', function() {
|
||||
console.log("Windshaft tileserver started on " + global.environment.host + ':' + global.environment.port);
|
||||
console.log("Windshaft tileserver " + version + " started on "
|
||||
+ global.environment.host + ':' + global.environment.port
|
||||
+ " (" + ENV + ")");
|
||||
});
|
||||
|
||||
// DEPRECATED, use SIGUSR2
|
||||
@@ -55,3 +114,15 @@ process.on('SIGUSR1', function() {
|
||||
process.on('SIGUSR2', function() {
|
||||
ws.dumpCacheStats();
|
||||
});
|
||||
|
||||
process.on('SIGHUP', function() {
|
||||
global.log4js.clearAndShutdownAppenders(function() {
|
||||
global.log4js.configure(log4js_config);
|
||||
global.logger = log4js.getLogger();
|
||||
console.log('Log files reloaded');
|
||||
});
|
||||
});
|
||||
|
||||
process.on('uncaughtException', function(err) {
|
||||
logger.error('Uncaught exception: ' + err.stack);
|
||||
});
|
||||
|
||||
@@ -2,20 +2,57 @@ var config = {
|
||||
environment: 'development'
|
||||
,port: 8181
|
||||
,host: '127.0.0.1'
|
||||
,uv_threadpool_size: undefined
|
||||
// Regular expression pattern to extract username
|
||||
// from hostname. Must have a single grabbing block.
|
||||
,user_from_host: '^(.*)\\.localhost'
|
||||
|
||||
// Base URLs for the APIs
|
||||
//
|
||||
// See http://github.com/CartoDB/Windshaft-cartodb/wiki/Unified-Map-API
|
||||
//
|
||||
// Base url for the Templated Maps API
|
||||
// "/api/v1/map/named" is the new API,
|
||||
// "/tiles/template" is for compatibility with versions up to 1.6.x
|
||||
,base_url_templated: '(?:/api/v1/map/named|/tiles/template)'
|
||||
// Base url for the Detached Maps API
|
||||
// "maps" is the the new API,
|
||||
// "tiles/layergroup" is for compatibility with versions up to 1.6.x
|
||||
,base_url_detached: '(?:/api/v1/map|/tiles/layergroup)'
|
||||
// Base url for the Inline Maps and Table Maps API
|
||||
,base_url_legacy: '/tiles/:table'
|
||||
|
||||
// Maximum number of connections for one process
|
||||
// 128 is a good value with a limit of 1024 open file descriptors
|
||||
,maxConnections:128
|
||||
// idle socket timeout, in miliseconds
|
||||
// Maximum number of templates per user. Unlimited by default.
|
||||
,maxUserTemplates:1024
|
||||
// Seconds since "last creation" before a detached
|
||||
// or template instance map expires. Or: how long do you want
|
||||
// to be able to navigate the map without a reload ?
|
||||
// Defaults to 7200 (2 hours)
|
||||
,mapConfigTTL: 7200
|
||||
// idle socket timeout, in milliseconds
|
||||
,socket_timeout: 600000
|
||||
,enable_cors: true
|
||||
,cache_enabled: false
|
||||
,log_format: '[:date] :req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler])'
|
||||
,log_format: ':req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler])'
|
||||
// If log_filename is given logs will be written
|
||||
// there, in append mode. Otherwise stdout is used (default).
|
||||
// Log file will be re-opened on receiving the HUP signal
|
||||
,log_filename: undefined
|
||||
// Templated database username for authorized user
|
||||
// Supported labels: 'user_id' (read from redis)
|
||||
,postgres_auth_user: 'development_cartodb_user_<%= user_id %>'
|
||||
// Templated database password for authorized user
|
||||
// Supported labels: 'user_id', 'user_password' (both read from redis)
|
||||
,postgres_auth_pass: '<%= user_password %>'
|
||||
,postgres: {
|
||||
// Parameters to pass to datasource plugin of mapnik
|
||||
// See http://github.com/mapnik/mapnik/wiki/PostGIS
|
||||
type: "postgis",
|
||||
user: "publicuser",
|
||||
password: "public",
|
||||
host: '127.0.0.1',
|
||||
port: 5432,
|
||||
extent: "-20037508.3,-20037508.3,20037508.3,20037508.3",
|
||||
@@ -26,14 +63,39 @@ var config = {
|
||||
*/
|
||||
row_limit: 65535,
|
||||
simplify_geometries: true,
|
||||
use_overviews: true, // use overviews to retrieve raster
|
||||
/*
|
||||
* Set persist_connection to false if you want
|
||||
* database connections to be closed on renderer
|
||||
* expiration (1 minute after last use).
|
||||
* Setting to true (the default) would never
|
||||
* close any connection for the server's lifetime
|
||||
*/
|
||||
persist_connection: false,
|
||||
max_size: 500
|
||||
}
|
||||
,mapnik_version: undefined
|
||||
,mapnik_tile_format: 'png8:m=h'
|
||||
,statsd: {
|
||||
host: 'localhost',
|
||||
port: 8125,
|
||||
prefix: 'dev.',
|
||||
cacheDns: true
|
||||
// support all allowed node-statsd options
|
||||
}
|
||||
,renderer: {
|
||||
// Milliseconds since last access before renderer cache item expires
|
||||
cache_ttl: 60000,
|
||||
metatile: 4,
|
||||
bufferSize: 64
|
||||
bufferSize: 64,
|
||||
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
|
||||
http: {
|
||||
timeout: 2000, // the timeout in ms for a http tile request
|
||||
proxy: undefined, // the url for a proxy server
|
||||
whitelist: [ // the whitelist of urlTemplates that can be used
|
||||
'http://{s}.example.com/{z}/{x}/{y}.png'
|
||||
]
|
||||
}
|
||||
}
|
||||
,millstone: {
|
||||
// Needs to be writable by server user
|
||||
@@ -45,29 +107,67 @@ var config = {
|
||||
// Max number of connections in each pool.
|
||||
// Users will be put on a queue when the limit is hit.
|
||||
// Set to maxConnection to have no possible queues.
|
||||
// There are currently 3 pools involved in serving
|
||||
// There are currently 2 pools involved in serving
|
||||
// windshaft-cartodb requests so multiply this number
|
||||
// by 3 to know how many possible connections will be
|
||||
// by 2 to know how many possible connections will be
|
||||
// kept open by the server. The default is 50.
|
||||
max: 50,
|
||||
returnToHead: true, // defines the behaviour of the pool: false => queue, true => stack
|
||||
idleTimeoutMillis: 1, // idle time before dropping connection
|
||||
reapIntervalMillis: 1 // time between cleanups
|
||||
reapIntervalMillis: 1, // time between cleanups
|
||||
slowQueries: {
|
||||
log: true,
|
||||
elapsedThreshold: 200
|
||||
},
|
||||
slowPool: {
|
||||
log: true, // whether a slow acquire must be logged or not
|
||||
elapsedThreshold: 25 // the threshold to determine an slow acquire must be reported or not
|
||||
},
|
||||
emitter: {
|
||||
statusInterval: 5000 // time, in ms, between each status report is emitted from the pool, status is sent to statsd
|
||||
}
|
||||
}
|
||||
,sqlapi: {
|
||||
protocol: 'http',
|
||||
domain: 'localhost.lan',
|
||||
// If "host" is given, it will be used
|
||||
// to connect to the SQL-API without a
|
||||
// DNS lookup
|
||||
host: '127.0.0.1',
|
||||
port: 8080,
|
||||
version: 'v1'
|
||||
// The "domain" part will be appended to
|
||||
// the cartodb username and passed to
|
||||
// SQL-API requests in the Host HTTP header
|
||||
domain: 'localhost.lan',
|
||||
version: 'v1',
|
||||
// Maximum lenght of SQL query for GET
|
||||
// requests. Longer queries will be sent
|
||||
// using POST. Defaults to 2048
|
||||
max_get_sql_length: 2048,
|
||||
// Maximum time to wait for a response,
|
||||
// in milliseconds. Defaults to 100.
|
||||
timeout: 100
|
||||
}
|
||||
,varnish: {
|
||||
host: 'localhost',
|
||||
port: 6082,
|
||||
ttl: 86400
|
||||
port: 6082, // the por for the telnet interface where varnish is listening to
|
||||
http_port: 6081, // the port for the HTTP interface where varnish is listening to
|
||||
purge_enabled: false, // whether the purge/invalidation mechanism is enabled in varnish or not
|
||||
secret: 'xxx',
|
||||
ttl: 86400,
|
||||
layergroupTtl: 86400 // the max-age for cache-control header in layergroup responses
|
||||
}
|
||||
// If useProfiler is true every response will be served with an
|
||||
// X-Tiler-Profile header containing elapsed timing for various
|
||||
// steps taken for producing the response.
|
||||
,useProfiler:true
|
||||
// Settings for the health check available at /health
|
||||
,health: {
|
||||
enabled: false,
|
||||
username: 'localhost',
|
||||
z: 0,
|
||||
x: 0,
|
||||
y: 0
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
|
||||
@@ -2,32 +2,94 @@ var config = {
|
||||
environment: 'production'
|
||||
,port: 8181
|
||||
,host: '127.0.0.1'
|
||||
,uv_threadpool_size: undefined
|
||||
// Regular expression pattern to extract username
|
||||
// from hostname. Must have a single grabbing block.
|
||||
,user_from_host: '^(.*)\\.cartodb\\.com$'
|
||||
|
||||
// Base URLs for the APIs
|
||||
//
|
||||
// See http://github.com/CartoDB/Windshaft-cartodb/wiki/Unified-Map-API
|
||||
//
|
||||
// Base url for the Templated Maps API
|
||||
// "/api/v1/map/named" is the new API,
|
||||
// "/tiles/template" is for compatibility with versions up to 1.6.x
|
||||
,base_url_templated: '(?:/api/v1/map/named|/tiles/template)'
|
||||
// Base url for the Detached Maps API
|
||||
// "maps" is the the new API,
|
||||
// "tiles/layergroup" is for compatibility with versions up to 1.6.x
|
||||
,base_url_detached: '(?:/api/v1/map|/tiles/layergroup)'
|
||||
// Base url for the Inline Maps and Table Maps API
|
||||
,base_url_legacy: '/tiles/:table'
|
||||
|
||||
// Maximum number of connections for one process
|
||||
// 128 is a good value with a limit of 1024 open file descriptors
|
||||
,maxConnections:128
|
||||
// idle socket timeout, in miliseconds
|
||||
// Maximum number of templates per user. Unlimited by default.
|
||||
,maxUserTemplates:1024
|
||||
// Seconds since "last creation" before a detached
|
||||
// or template instance map expires. Or: how long do you want
|
||||
// to be able to navigate the map without a reload ?
|
||||
// Defaults to 7200 (2 hours)
|
||||
,mapConfigTTL: 7200
|
||||
// idle socket timeout, in milliseconds
|
||||
,socket_timeout: 600000
|
||||
,enable_cors: true
|
||||
,cache_enabled: true
|
||||
,log_format: '[:date] :req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler])'
|
||||
,log_format: ':req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler])'
|
||||
// If log_filename is given logs will be written
|
||||
// there, in append mode. Otherwise stdout is used (default).
|
||||
// Log file will be re-opened on receiving the HUP signal
|
||||
,log_filename: 'logs/node-windshaft.log'
|
||||
// Templated database username for authorized user
|
||||
// Supported labels: 'user_id' (read from redis)
|
||||
,postgres_auth_user: 'cartodb_user_<%= user_id %>'
|
||||
// Templated database password for authorized user
|
||||
// Supported labels: 'user_id', 'user_password' (both read from redis)
|
||||
,postgres_auth_pass: '<%= user_password %>'
|
||||
,postgres: {
|
||||
// Parameters to pass to datasource plugin of mapnik
|
||||
// See http://github.com/mapnik/mapnik/wiki/PostGIS
|
||||
user: "publicuser",
|
||||
password: "public",
|
||||
host: '127.0.0.1',
|
||||
port: 6432,
|
||||
extent: "-20037508.3,-20037508.3,20037508.3,20037508.3",
|
||||
row_limit: 65535,
|
||||
/*
|
||||
* Set persist_connection to false if you want
|
||||
* database connections to be closed on renderer
|
||||
* expiration (1 minute after last use).
|
||||
* Setting to true (the default) would never
|
||||
* close any connection for the server's lifetime
|
||||
*/
|
||||
persist_connection: false,
|
||||
simplify_geometries: true,
|
||||
use_overviews: true, // use overviews to retrieve raster
|
||||
max_size: 500
|
||||
}
|
||||
,mapnik_version: undefined
|
||||
,mapnik_tile_format: 'png8:m=h'
|
||||
,statsd: {
|
||||
host: 'localhost',
|
||||
port: 8125,
|
||||
prefix: ':host.', // could be hostname, better not containing dots
|
||||
cacheDns: true
|
||||
// support all allowed node-statsd options
|
||||
}
|
||||
,renderer: {
|
||||
// Milliseconds since last access before renderer cache item expires
|
||||
cache_ttl: 60000,
|
||||
metatile: 4,
|
||||
bufferSize: 64
|
||||
bufferSize: 64,
|
||||
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
|
||||
http: {
|
||||
timeout: 2000, // the timeout in ms for a http tile request
|
||||
proxy: undefined, // the url for a proxy server
|
||||
whitelist: [ // the whitelist of urlTemplates that can be used
|
||||
'http://{s}.example.com/{z}/{x}/{y}.png'
|
||||
]
|
||||
}
|
||||
}
|
||||
,millstone: {
|
||||
// Needs to be writable by server user
|
||||
@@ -39,29 +101,82 @@ var config = {
|
||||
// Max number of connections in each pool.
|
||||
// Users will be put on a queue when the limit is hit.
|
||||
// Set to maxConnection to have no possible queues.
|
||||
// There are currently 3 pools involved in serving
|
||||
// There are currently 2 pools involved in serving
|
||||
// windshaft-cartodb requests so multiply this number
|
||||
// by 3 to know how many possible connections will be
|
||||
// by 2 to know how many possible connections will be
|
||||
// kept open by the server. The default is 50.
|
||||
max: 50,
|
||||
returnToHead: true, // defines the behaviour of the pool: false => queue, true => stack
|
||||
idleTimeoutMillis: 30000, // idle time before dropping connection
|
||||
reapIntervalMillis: 1000 // time between cleanups
|
||||
reapIntervalMillis: 1000, // time between cleanups
|
||||
slowQueries: {
|
||||
log: true,
|
||||
elapsedThreshold: 200
|
||||
},
|
||||
slowPool: {
|
||||
log: true, // whether a slow acquire must be logged or not
|
||||
elapsedThreshold: 25 // the threshold to determine an slow acquire must be reported or not
|
||||
},
|
||||
emitter: {
|
||||
statusInterval: 5000 // time, in ms, between each status report is emitted from the pool, status is sent to statsd
|
||||
}
|
||||
}
|
||||
,sqlapi: {
|
||||
protocol: 'https',
|
||||
domain: 'cartodb.com',
|
||||
// If "host" is given, it will be used
|
||||
// to connect to the SQL-API without a
|
||||
// DNS lookup
|
||||
//host: '127.0.0.1',
|
||||
port: 8080,
|
||||
version: 'v2'
|
||||
// The "domain" part will be appended to
|
||||
// the cartodb username and passed to
|
||||
// SQL-API requests in the Host HTTP header
|
||||
domain: 'cartodb.com',
|
||||
version: 'v2',
|
||||
// Maximum lenght of SQL query for GET
|
||||
// requests. Longer queries will be sent
|
||||
// using POST. Defaults to 2048
|
||||
max_get_sql_length: 2048,
|
||||
// Maximum time to wait for a response,
|
||||
// in milliseconds. Defaults to 100.
|
||||
timeout: 100
|
||||
}
|
||||
,varnish: {
|
||||
host: 'localhost',
|
||||
port: 6082,
|
||||
ttl: 86400
|
||||
port: 6082, // the por for the telnet interface where varnish is listening to
|
||||
http_port: 6081, // the port for the HTTP interface where varnish is listening to
|
||||
purge_enabled: false, // whether the purge/invalidation mechanism is enabled in varnish or not
|
||||
secret: 'xxx',
|
||||
ttl: 86400,
|
||||
layergroupTtl: 86400 // the max-age for cache-control header in layergroup responses
|
||||
}
|
||||
// If useProfiler is true every response will be served with an
|
||||
// X-Tiler-Profile header containing elapsed timing for various
|
||||
// steps taken for producing the response.
|
||||
,useProfiler:false
|
||||
,serverMetadata: {
|
||||
cdn_url: {
|
||||
http: 'api.cartocdn.com',
|
||||
https: 'cartocdn.global.ssl.fastly.net'
|
||||
}
|
||||
}
|
||||
// Optional rollbar support
|
||||
,rollbar: {
|
||||
token: 'secret',
|
||||
// See http://github.com/rollbar/node_rollbar#configuration-reference
|
||||
options: {
|
||||
endpoint: 'https://api.rollbar.com/api/1/',
|
||||
handler: 'inline'
|
||||
}
|
||||
}
|
||||
// Settings for the health check available at /health
|
||||
,health: {
|
||||
enabled: true,
|
||||
username: 'localhost',
|
||||
z: 0,
|
||||
x: 0,
|
||||
y: 0
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
|
||||
@@ -2,32 +2,94 @@ var config = {
|
||||
environment: 'production'
|
||||
,port: 8181
|
||||
,host: '127.0.0.1'
|
||||
,uv_threadpool_size: undefined
|
||||
// Regular expression pattern to extract username
|
||||
// from hostname. Must have a single grabbing block.
|
||||
,user_from_host: '^(.*)\\.cartodb\\.com$'
|
||||
|
||||
// Base URLs for the APIs
|
||||
//
|
||||
// See http://github.com/CartoDB/Windshaft-cartodb/wiki/Unified-Map-API
|
||||
//
|
||||
// Base url for the Templated Maps API
|
||||
// "/api/v1/maps/named" is the new API,
|
||||
// "/tiles/template" is for compatibility with versions up to 1.6.x
|
||||
,base_url_templated: '(?:/api/v1/maps/named|/tiles/template)'
|
||||
// Base url for the Detached Maps API
|
||||
// "/api/v1/maps" is the the new API,
|
||||
// "/tiles/layergroup" is for compatibility with versions up to 1.6.x
|
||||
,base_url_detached: '(?:/api/v1/maps|/tiles/layergroup)'
|
||||
// Base url for the Inline Maps and Table Maps API
|
||||
,base_url_legacy: '/tiles/:table'
|
||||
|
||||
// Maximum number of connections for one process
|
||||
// 128 is a good value with a limit of 1024 open file descriptors
|
||||
,maxConnections:128
|
||||
// idle socket timeout, in miliseconds
|
||||
// Maximum number of templates per user. Unlimited by default.
|
||||
,maxUserTemplates:1024
|
||||
// Seconds since "last creation" before a detached
|
||||
// or template instance map expires. Or: how long do you want
|
||||
// to be able to navigate the map without a reload ?
|
||||
// Defaults to 7200 (2 hours)
|
||||
,mapConfigTTL: 7200
|
||||
// idle socket timeout, in milliseconds
|
||||
,socket_timeout: 600000
|
||||
,enable_cors: true
|
||||
,cache_enabled: true
|
||||
,log_format: '[:date] :req[X-Real-IP] :method :req[Host]:url :status :response-time ms (:res[X-Tiler-Profiler]) -> :res[Content-Type]'
|
||||
,log_format: ':req[X-Real-IP] :method :req[Host]:url :status :response-time ms (:res[X-Tiler-Profiler]) -> :res[Content-Type]'
|
||||
// If log_filename is given logs will be written
|
||||
// there, in append mode. Otherwise stdout is used (default).
|
||||
// Log file will be re-opened on receiving the HUP signal
|
||||
,log_filename: 'logs/node-windshaft.log'
|
||||
// Templated database username for authorized user
|
||||
// Supported labels: 'user_id' (read from redis)
|
||||
,postgres_auth_user: 'cartodb_staging_user_<%= user_id %>'
|
||||
// Templated database password for authorized user
|
||||
// Supported labels: 'user_id', 'user_password' (both read from redis)
|
||||
,postgres_auth_pass: '<%= user_password %>'
|
||||
,postgres: {
|
||||
// Parameters to pass to datasource plugin of mapnik
|
||||
// See http://github.com/mapnik/mapnik/wiki/PostGIS
|
||||
user: "publicuser",
|
||||
password: "public",
|
||||
host: '127.0.0.1',
|
||||
port: 6432,
|
||||
extent: "-20037508.3,-20037508.3,20037508.3,20037508.3",
|
||||
row_limit: 65535,
|
||||
simplify_geometries: true,
|
||||
use_overviews: true, // use overviews to retrieve raster
|
||||
/*
|
||||
* Set persist_connection to false if you want
|
||||
* database connections to be closed on renderer
|
||||
* expiration (1 minute after last use).
|
||||
* Setting to true (the default) would never
|
||||
* close any connection for the server's lifetime
|
||||
*/
|
||||
persist_connection: false,
|
||||
max_size: 500
|
||||
}
|
||||
,mapnik_version: undefined
|
||||
,mapnik_tile_format: 'png8:m=h'
|
||||
,statsd: {
|
||||
host: 'localhost',
|
||||
port: 8125,
|
||||
prefix: 'stage.:host.',
|
||||
cacheDns: true
|
||||
// support all allowed node-statsd options
|
||||
}
|
||||
,renderer: {
|
||||
// Milliseconds since last access before renderer cache item expires
|
||||
cache_ttl: 60000,
|
||||
metatile: 4,
|
||||
bufferSize: 64
|
||||
bufferSize: 64,
|
||||
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
|
||||
http: {
|
||||
timeout: 2000, // the timeout in ms for a http tile request
|
||||
proxy: undefined, // the url for a proxy server
|
||||
whitelist: [ // the whitelist of urlTemplates that can be used
|
||||
'http://{s}.example.com/{z}/{x}/{y}.png'
|
||||
]
|
||||
}
|
||||
}
|
||||
,millstone: {
|
||||
// Needs to be writable by server user
|
||||
@@ -39,29 +101,82 @@ var config = {
|
||||
// Max number of connections in each pool.
|
||||
// Users will be put on a queue when the limit is hit.
|
||||
// Set to maxConnection to have no possible queues.
|
||||
// There are currently 3 pools involved in serving
|
||||
// There are currently 2 pools involved in serving
|
||||
// windshaft-cartodb requests so multiply this number
|
||||
// by 3 to know how many possible connections will be
|
||||
// by 2 to know how many possible connections will be
|
||||
// kept open by the server. The default is 50.
|
||||
max: 50,
|
||||
returnToHead: true, // defines the behaviour of the pool: false => queue, true => stack
|
||||
idleTimeoutMillis: 30000, // idle time before dropping connection
|
||||
reapIntervalMillis: 1000 // time between cleanups
|
||||
reapIntervalMillis: 1000, // time between cleanups
|
||||
slowQueries: {
|
||||
log: true,
|
||||
elapsedThreshold: 200
|
||||
},
|
||||
slowPool: {
|
||||
log: true, // whether a slow acquire must be logged or not
|
||||
elapsedThreshold: 25 // the threshold to determine an slow acquire must be reported or not
|
||||
},
|
||||
emitter: {
|
||||
statusInterval: 5000 // time, in ms, between each status report is emitted from the pool, status is sent to statsd
|
||||
}
|
||||
}
|
||||
,sqlapi: {
|
||||
protocol: 'https',
|
||||
domain: 'cartodb.com',
|
||||
// If "host" is given, it will be used
|
||||
// to connect to the SQL-API without a
|
||||
// DNS lookup
|
||||
//host: '127.0.0.1',
|
||||
port: 8080,
|
||||
version: 'v2'
|
||||
// The "domain" part will be appended to
|
||||
// the cartodb username and passed to
|
||||
// SQL-API requests in the Host HTTP header
|
||||
domain: 'cartodb.com',
|
||||
version: 'v2',
|
||||
// Maximum lenght of SQL query for GET
|
||||
// requests. Longer queries will be sent
|
||||
// using POST. Defaults to 2048
|
||||
max_get_sql_length: 2048,
|
||||
// Maximum time to wait for a response,
|
||||
// in milliseconds. Defaults to 100.
|
||||
timeout: 100
|
||||
}
|
||||
,varnish: {
|
||||
host: 'localhost',
|
||||
port: 6082,
|
||||
ttl: 86400
|
||||
port: 6082, // the por for the telnet interface where varnish is listening to
|
||||
http_port: 6081, // the port for the HTTP interface where varnish is listening to
|
||||
purge_enabled: false, // whether the purge/invalidation mechanism is enabled in varnish or not
|
||||
secret: 'xxx',
|
||||
ttl: 86400,
|
||||
layergroupTtl: 86400 // the max-age for cache-control header in layergroup responses
|
||||
}
|
||||
// If useProfiler is true every response will be served with an
|
||||
// X-Tiler-Profile header containing elapsed timing for various
|
||||
// steps taken for producing the response.
|
||||
,useProfiler:true
|
||||
,serverMetadata: {
|
||||
cdn_url: {
|
||||
http: 'api.cartocdn.com',
|
||||
https: 'cartocdn.global.ssl.fastly.net'
|
||||
}
|
||||
}
|
||||
// Optional rollbar support
|
||||
,rollbar: {
|
||||
token: 'secret',
|
||||
// See http://github.com/rollbar/node_rollbar#configuration-reference
|
||||
options: {
|
||||
endpoint: 'https://api.rollbar.com/api/1/',
|
||||
handler: 'inline'
|
||||
}
|
||||
}
|
||||
// Settings for the health check available at /health
|
||||
,health: {
|
||||
enabled: false,
|
||||
username: 'localhost',
|
||||
z: 0,
|
||||
x: 0,
|
||||
y: 0
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
|
||||
@@ -2,32 +2,94 @@ var config = {
|
||||
environment: 'test'
|
||||
,port: 8888
|
||||
,host: '127.0.0.1'
|
||||
,uv_threadpool_size: undefined
|
||||
// Regular expression pattern to extract username
|
||||
// from hostname. Must have a single grabbing block.
|
||||
,user_from_host: '(.*)'
|
||||
|
||||
// Base URLs for the APIs
|
||||
//
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/wiki/Unified-Map-API
|
||||
//
|
||||
// Base url for the Templated Maps API
|
||||
// "/api/v1/map/named" is the new API,
|
||||
// "/tiles/template" is for compatibility with versions up to 1.6.x
|
||||
,base_url_templated: '(?:/api/v1/map/named|/tiles/template)'
|
||||
// Base url for the Detached Maps API
|
||||
// "maps" is the the new API,
|
||||
// "tiles/layergroup" is for compatibility with versions up to 1.6.x
|
||||
,base_url_detached: '(?:/api/v1/map|/tiles/layergroup)'
|
||||
// Base url for the Inline Maps and Table Maps API
|
||||
,base_url_legacy: '/tiles/:table'
|
||||
|
||||
// Maximum number of connections for one process
|
||||
// 128 is a good value with a limit of 1024 open file descriptors
|
||||
,maxConnections:128
|
||||
// idle socket timeout, in miliseconds
|
||||
// Maximum number of templates per user. Unlimited by default.
|
||||
,maxUserTemplates:1024
|
||||
// Seconds since "last creation" before a detached
|
||||
// or template instance map expires. Or: how long do you want
|
||||
// to be able to navigate the map without a reload ?
|
||||
// Defaults to 7200 (2 hours)
|
||||
,mapConfigTTL: 7200
|
||||
// idle socket timeout, in milliseconds
|
||||
,socket_timeout: 600000
|
||||
,enable_cors: true
|
||||
,cache_enabled: false
|
||||
,log_format: '[:date] :req[X-Real-IP] :method :req[Host]:url :status :response-time ms -> :res[Content-Type] (:res[X-Tiler-Profiler])'
|
||||
,postgres_auth_user: 'test_cartodb_user_<%= user_id %>'
|
||||
// If log_filename is given logs will be written
|
||||
// there, in append mode. Otherwise stdout is used (default).
|
||||
// Log file will be re-opened on receiving the HUP signal
|
||||
//,log_filename: 'logs/node-windshaft.log'
|
||||
// Templated database username for authorized user
|
||||
// Supported labels: 'user_id' (read from redis)
|
||||
,postgres_auth_user: 'test_windshaft_cartodb_user_<%= user_id %>'
|
||||
// Templated database password for authorized user
|
||||
// Supported labels: 'user_id', 'user_password' (both read from redis)
|
||||
,postgres_auth_pass: 'test_windshaft_cartodb_user_<%= user_id %>_pass'
|
||||
,postgres: {
|
||||
// Parameters to pass to datasource plugin of mapnik
|
||||
// See http://github.com/mapnik/mapnik/wiki/PostGIS
|
||||
user: "publicuser",
|
||||
user: "testpublicuser",
|
||||
password: "public",
|
||||
host: '127.0.0.1',
|
||||
port: 5432,
|
||||
extent: "-20037508.3,-20037508.3,20037508.3,20037508.3",
|
||||
row_limit: 65535,
|
||||
simplify_geometries: true,
|
||||
use_overviews: true, // use overviews to retrieve raster
|
||||
/*
|
||||
* Set persist_connection to false if you want
|
||||
* database connections to be closed on renderer
|
||||
* expiration (1 minute after last use).
|
||||
* Setting to true (the default) would never
|
||||
* close any connection for the server's lifetime
|
||||
*/
|
||||
persist_connection: false,
|
||||
max_size: 500
|
||||
}
|
||||
,mapnik_version: '2.0.2'
|
||||
,mapnik_version: ''
|
||||
,mapnik_tile_format: 'png8:m=h'
|
||||
,statsd: {
|
||||
host: 'localhost',
|
||||
port: 8125,
|
||||
prefix: 'test.:host.',
|
||||
cacheDns: true
|
||||
// support all allowed node-statsd options
|
||||
}
|
||||
,renderer: {
|
||||
// Milliseconds since last access before renderer cache item expires
|
||||
cache_ttl: 60000,
|
||||
metatile: 4,
|
||||
bufferSize: 64
|
||||
bufferSize: 64,
|
||||
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
|
||||
http: {
|
||||
timeout: 2000, // the timeout in ms for a http tile request
|
||||
proxy: undefined, // the url for a proxy server
|
||||
whitelist: [ // the whitelist of urlTemplates that can be used
|
||||
'http://{s}.example.com/{z}/{x}/{y}.png'
|
||||
]
|
||||
}
|
||||
}
|
||||
,millstone: {
|
||||
// Needs to be writable by server user
|
||||
@@ -35,35 +97,73 @@ var config = {
|
||||
}
|
||||
,redis: {
|
||||
host: '127.0.0.1',
|
||||
port: 6333,
|
||||
port: 6335,
|
||||
// Max number of connections in each pool.
|
||||
// Users will be put on a queue when the limit is hit.
|
||||
// Set to maxConnection to have no possible queues.
|
||||
// There are currently 3 pools involved in serving
|
||||
// There are currently 2 pools involved in serving
|
||||
// windshaft-cartodb requests so multiply this number
|
||||
// by 3 to know how many possible connections will be
|
||||
// by 2 to know how many possible connections will be
|
||||
// kept open by the server. The default is 50.
|
||||
max: 50,
|
||||
returnToHead: true, // defines the behaviour of the pool: false => queue, true => stack
|
||||
idleTimeoutMillis: 1, // idle time before dropping connection
|
||||
reapIntervalMillis: 1 // time between cleanups
|
||||
reapIntervalMillis: 1, // time between cleanups
|
||||
slowQueries: {
|
||||
log: true,
|
||||
elapsedThreshold: 200
|
||||
},
|
||||
slowPool: {
|
||||
log: true, // whether a slow acquire must be logged or not
|
||||
elapsedThreshold: 25 // the threshold to determine an slow acquire must be reported or not
|
||||
},
|
||||
emitter: {
|
||||
statusInterval: 5000 // time, in ms, between each status report is emitted from the pool, status is sent to statsd
|
||||
}
|
||||
}
|
||||
,sqlapi: {
|
||||
protocol: 'http',
|
||||
domain: '',
|
||||
// If "host" is given, it will be used
|
||||
// to connect to the SQL-API without a
|
||||
// DNS lookup
|
||||
host: '127.0.0.1',
|
||||
port: 1080,
|
||||
// The "domain" part will be appended to
|
||||
// the cartodb username and passed to
|
||||
// SQL-API requests in the Host HTTP header
|
||||
domain: 'donot_look_this_up',
|
||||
// This port will be used by "make check" for testing purposes
|
||||
// It must be available
|
||||
port: 1080,
|
||||
version: 'v1'
|
||||
version: 'v1',
|
||||
// Maximum lenght of SQL query for GET
|
||||
// requests. Longer queries will be sent
|
||||
// using POST. Defaults to 2048
|
||||
max_get_sql_length: 2048,
|
||||
// Maximum time to wait for a response,
|
||||
// in milliseconds. Defaults to 100.
|
||||
timeout: 100
|
||||
}
|
||||
,varnish: {
|
||||
host: '',
|
||||
port: null,
|
||||
ttl: 86400
|
||||
port: null, // the por for the telnet interface where varnish is listening to
|
||||
http_port: 6081, // the port for the HTTP interface where varnish is listening to
|
||||
purge_enabled: false, // whether the purge/invalidation mechanism is enabled in varnish or not
|
||||
secret: 'xxx',
|
||||
ttl: 86400,
|
||||
layergroupTtl: 86400 // the max-age for cache-control header in layergroup responses
|
||||
}
|
||||
// If useProfiler is true every response will be served with an
|
||||
// X-Tiler-Profile header containing elapsed timing for various
|
||||
// steps taken for producing the response.
|
||||
,useProfiler:false
|
||||
,useProfiler:true
|
||||
// Settings for the health check available at /health
|
||||
,health: {
|
||||
enabled: false,
|
||||
username: 'localhost',
|
||||
z: 0,
|
||||
x: 0,
|
||||
y: 0
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
module.exports.oneDay = 86400000;
|
||||
32
configure
vendored
32
configure
vendored
@@ -17,11 +17,15 @@
|
||||
# --strk(2012-07-23)
|
||||
#
|
||||
|
||||
PGPORT=5432
|
||||
SQLAPI_PORT=8080
|
||||
ENVDIR=config/environments
|
||||
|
||||
PGPORT=
|
||||
SQLAPI_PORT=
|
||||
MAPNIK_VERSION=
|
||||
ENVIRONMENT=development
|
||||
|
||||
STATUS="$0 $*"
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 [OPTION]"
|
||||
echo
|
||||
@@ -59,20 +63,30 @@ while test -n "$1"; do
|
||||
shift
|
||||
done
|
||||
|
||||
ENVEX=./${ENVDIR}/${ENVIRONMENT}.js.example
|
||||
|
||||
if [ -z "$PGPORT" ]; then
|
||||
PGPORT=`node -e "console.log(require('${ENVEX}').postgres.port)"`
|
||||
fi
|
||||
if [ -z "$SQLAPI_PORT" ]; then
|
||||
SQLAPI_PORT=`node -e "console.log(require('${ENVEX}').sqlapi.port)"`
|
||||
fi
|
||||
|
||||
echo "PGPORT: $PGPORT"
|
||||
echo "SQLAPI_PORT: $SQLAPI_PORT"
|
||||
echo "MAPNIK_VERSION: $MAPNIK_VERSION"
|
||||
echo "ENVIRONMENT: $ENVIRONMENT"
|
||||
|
||||
# TODO: allow specifying configuration settings !
|
||||
for f in config/environments/${ENVIRONMENT}.js.example; do
|
||||
o=`dirname "$f"`/`basename "$f" .example`
|
||||
echo "Writing $o"
|
||||
o=`dirname "${ENVEX}"`/`basename "${ENVEX}" .example`
|
||||
echo "Writing $o"
|
||||
|
||||
# See http://austinmatzko.com/2008/04/26/sed-multi-line-search-and-replace/
|
||||
sed -n "1h;1!H;\${;g;s/\(,postgres: {[^}]*port: *'\?\)[^',]*\('\?,\)/\1$PGPORT\2/;p;}" < "$f" \
|
||||
# See http://austinmatzko.com/2008/04/26/sed-multi-line-search-and-replace/
|
||||
sed -n "1h;1!H;\${;g;s/\(,postgres: {[^}]*port: *'\?\)[^',]*\('\?,\)/\1$PGPORT\2/;p;}" < "${ENVEX}" \
|
||||
| sed "s/mapnik_version:.*/mapnik_version: '$MAPNIK_VERSION'/" \
|
||||
| sed -n "1h;1!H;\${;g;s/\(,sqlapi: {[^}]*port: *'\?\)[^',]*\('\?,\)/\1$SQLAPI_PORT\2/;p;}" \
|
||||
> "$o"
|
||||
|
||||
done
|
||||
STATUSFILE=config.status--${ENVIRONMENT}
|
||||
echo "Writing ${STATUSFILE}"
|
||||
echo ${STATUS} > ${STATUSFILE} && chmod +x ${STATUSFILE}
|
||||
|
||||
|
||||
111
docs/Map-API-internal.md
Normal file
111
docs/Map-API-internal.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# Kind of maps
|
||||
|
||||
Windshaft-CartoDB supports the following types of maps:
|
||||
|
||||
- [Temporary maps](#temporary-maps) (created by anyone)
|
||||
- [Detached maps](#detached-maps)
|
||||
- [Inline maps](#inline-maps) (legacy)
|
||||
- [Persistent maps](#peristent-maps) (created by CartDB user)
|
||||
- [Template maps](#template-maps)
|
||||
- [Table maps](#table-maps) (legacy, deprecated)
|
||||
|
||||
## Temporary maps
|
||||
|
||||
Temporary maps have no owners and are anonymous in nature.
|
||||
There are two kinds of temporary maps:
|
||||
|
||||
- Detached maps (aka MultiLayer-API)
|
||||
- Inline maps
|
||||
|
||||
### Detached maps
|
||||
|
||||
Detached maps are maps that are configured with a request
|
||||
obtaining a temporary token and then used by referencing
|
||||
the obtained token. The token expires automatically when unused.
|
||||
|
||||
Anyone can create detached maps, but users will need read access
|
||||
to the data source of the map layers.
|
||||
|
||||
The configuration format is a [MapConfig]
|
||||
(http://github.com/CartoDB/Windshaft/wiki/MapConfig-specification) document.
|
||||
|
||||
The HTTP endpoints for creating the map and using it are described [here]
|
||||
(http://github.com/CartoDB/Windshaft-cartodb/wiki/MultiLayer-API)
|
||||
|
||||
*TODO* cleanup the referenced document
|
||||
|
||||
### Inline maps
|
||||
|
||||
Inline maps are maps that only exist for a single request,
|
||||
being the request for a specific map resource (tile).
|
||||
|
||||
Inline maps are always bound to a table, and can only be
|
||||
obtained by those having read access to the that table.
|
||||
Additionally, users need to have access to any datasource
|
||||
specified as part of the configuration.
|
||||
|
||||
Inline maps only support PNG and UTF8GRID tiles.
|
||||
|
||||
The configuration consist in a set of parameters, to be
|
||||
specified in the query string of the tile request:
|
||||
|
||||
* sql - the query to run as datasource, can be an array
|
||||
* style - the CartoCSS style for the datasource, can be an array
|
||||
* style_version - version of the CartoCSS style, can be an array
|
||||
* interactivity - only for fetching UTF8GRID,
|
||||
|
||||
If the style is not provided, style of the associated table is
|
||||
used; if the sql is not provided, all records of the associated
|
||||
table are used as the datasource; the two possibilities result
|
||||
in a mix between _inline_ maps and [Table maps][].
|
||||
|
||||
*TODO* specify (or link) api endpoints
|
||||
|
||||
## Persistent maps
|
||||
|
||||
Persistent maps can only be created by a CartoDB user who has full
|
||||
responsibility over editing and deleting them. There are two
|
||||
kind of persistent maps:
|
||||
|
||||
- Template maps
|
||||
- Table maps (legacy, deprecated)
|
||||
|
||||
### Templated maps
|
||||
|
||||
Templated maps are templated [MapConfig]
|
||||
(http://github.com/CartoDB/Windshaft/wiki/MapConfig-specification) documents
|
||||
associated with an authorization certificate.
|
||||
|
||||
The authorization certificate determines who can instanciate the
|
||||
template and use the resulting map. Authorized users of the instanciated
|
||||
maps will have the same database access privilege of the template owner.
|
||||
|
||||
The HTTP endpoints for creating and using templated maps are described [here]
|
||||
(http://github.com/CartoDB/Windshaft-cartodb/wiki/Template-maps).
|
||||
|
||||
*TODO* cleanup the referenced document
|
||||
|
||||
### Table maps
|
||||
|
||||
Table maps are maps associated with a table.
|
||||
Configuration of such maps is limited to the CartoCSS style.
|
||||
|
||||
* style - the CartoCSS style for the datasource, can be an array
|
||||
* style_version - version of the CartoCSS style, can be an array
|
||||
|
||||
You can only fetch PNG or UTF8GRID tiles from these maps.
|
||||
|
||||
Access method is the same as the one for [Inline maps](#inline-maps)
|
||||
|
||||
# Endpoints description
|
||||
|
||||
- **/api/maps/** (same interface than https://github.com/CartoDB/Windshaft/wiki/Multilayer-API)
|
||||
- **/api/maps/named** (same interface than https://github.com/CartoDB/Windshaft-cartodb/wiki/Template-maps)
|
||||
|
||||
|
||||
NOTE: in case Multilayer-API does not contain this info yet, the
|
||||
endpoint for fetching attributes is this:
|
||||
|
||||
- **/api/maps/:map_id/:layer_index/attributes/:feature_id**
|
||||
- would return { c: 1, d: 2 }
|
||||
|
||||
674
docs/Map-API.md
Normal file
674
docs/Map-API.md
Normal file
@@ -0,0 +1,674 @@
|
||||
## Maps API
|
||||
|
||||
The CartoDB Maps API allows you to generate maps based on data hosted in your CartoDB account, and you can 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**
|
||||
Maps that can be created 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.html' | prepend: site.baseurl }}).
|
||||
|
||||
- **Named maps**
|
||||
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.
|
||||
|
||||
## Quickstart
|
||||
|
||||
### Anonymous maps
|
||||
|
||||
Here is an example of how to create an anonymous map with JavaScript:
|
||||
|
||||
```javascript
|
||||
var mapconfig = {
|
||||
"version": "1.0.1",
|
||||
"layers": [{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"cartocss_version": "2.1.1",
|
||||
"cartocss": "#layer { polygon-fill: #FFF; }",
|
||||
"sql": "select * from european_countries_e"
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
crossOrigin: true,
|
||||
type: 'POST',
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
url: 'http://documentation.cartodb.com/api/v1/map',
|
||||
data: JSON.stringify(mapconfig),
|
||||
success: function(data) {
|
||||
var templateUrl = 'http://documentation.cartodb.com/api/v1/map/' + data.layergroupid + '/{z}/{x}/{y}.png'
|
||||
console.log(templateUrl);
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Named maps
|
||||
|
||||
Let's create a named map using some private tables in a CartoDB account.
|
||||
The following map config sets up a map of European countries that have a white fill color:
|
||||
|
||||
```javascript
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"name": "test",
|
||||
"auth": {
|
||||
"method": "open"
|
||||
},
|
||||
"layergroup": {
|
||||
"layers": [{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"cartocss_version": "2.1.1",
|
||||
"cartocss": "#layer { polygon-fill: #FFF; }",
|
||||
"sql": "select * from european_countries_e"
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The map config 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:
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```bash
|
||||
curl 'https://{account}.cartodb.com/api/v1/map/named?api_key=APIKEY' -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.
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```bash
|
||||
curl -X POST 'http://{account}.cartodb.com/api/v1/map/named/:template_id' -H 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
The response will return JSON with properties for the `layergroupid`, `cdn_url`, and the timestamp (`last_updated`) of the last data modification.
|
||||
|
||||
Here is an example response:
|
||||
|
||||
```javascript
|
||||
{
|
||||
"cdn_url": {
|
||||
"http": "ashbu.cartocdn.com",
|
||||
"https": "cartocdn-ashbu.global.ssl.fastly.net"
|
||||
},
|
||||
"layergroupid": "c01a54877c62831bb51720263f91fb33:0",
|
||||
"last_updated": "1970-01-01T00:00:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
http://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/{z}/{x}/{y}.png
|
||||
```
|
||||
|
||||
## General Concepts
|
||||
|
||||
The following concepts are the same for every endpoint in the API except when it's noted explicitly.
|
||||
|
||||
### 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).
|
||||
|
||||
To execute an authorized request, api_key=YOURAPIKEY should be added to the request URL. The param can be also passed as POST param. We **strongly advise** using HTTPS when you are performing requests that include your `api_key`.
|
||||
|
||||
### Errors
|
||||
|
||||
Errors are reported using standard HTTP codes and extended information encoded in JSON with this format:
|
||||
|
||||
```javascript
|
||||
{
|
||||
"errors": [
|
||||
"access forbidden to table TABLE"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
If you use JSONP, the 200 HTTP code is always returned so the JavaScript client can receive errors from the JSON object.
|
||||
|
||||
### CORS support
|
||||
|
||||
All the endpoints which might be accessed using a web browser add CORS headers and allow OPTIONS method.
|
||||
|
||||
## Anonymous Maps
|
||||
|
||||
Anonymous maps allows you to instantiate a map given SQL and CartoCSS. It also allows you to add interaction capabilities using [UTF Grid.](https://github.com/mapbox/utfgrid-spec)
|
||||
|
||||
### Instantiate
|
||||
|
||||
#### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```html
|
||||
POST /api/v1/map
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
```javascript
|
||||
{
|
||||
"version": "1.0.1",
|
||||
"layers": [{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"cartocss_version": "2.1.1",
|
||||
"cartocss": "#layer { polygon-fill: #FFF; }",
|
||||
"sql": "select * from european_countries_e",
|
||||
"interactivity": ["cartodb_id", "iso3"]
|
||||
}
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
Should be a [Mapconfig](https://github.com/CartoDB/Windshaft/blob/0.19.1/doc/MapConfig-1.1.0.md).
|
||||
|
||||
#### Response
|
||||
|
||||
The response includes:
|
||||
|
||||
- **layergroupid**
|
||||
The ID for that map, used to compose the URL for the tiles. The final URL is:
|
||||
|
||||
```html
|
||||
http://{account}.cartodb.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** *(optional)*
|
||||
Includes information about the layers. Some layers may not have metadata.
|
||||
|
||||
- **cdn_url**
|
||||
URLs to fetch the data using the best CDN for your zone.
|
||||
|
||||
#### Example
|
||||
|
||||
<div class="code-title code-request with-result">REQUEST</div>
|
||||
```bash
|
||||
curl 'http://documentation.cartodb.com/api/v1/map' -H 'Content-Type: application/json' -d @mapconfig.json
|
||||
```
|
||||
|
||||
<div class="code-title">RESPONSE</div>
|
||||
```javascript
|
||||
{
|
||||
"layergroupid":"c01a54877c62831bb51720263f91fb33:0",
|
||||
"last_updated":"1970-01-01T00:00:00.000Z"
|
||||
"cdn_url": {
|
||||
"http": "http://cdb.com",
|
||||
"https": "https://cdb.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The tiles can be accessed using:
|
||||
|
||||
```bash
|
||||
http://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/{z}/{x}/{y}.png
|
||||
```
|
||||
|
||||
For UTF grid tiles:
|
||||
|
||||
```bash
|
||||
http://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/:layer/{z}/{x}/{y}.grid.json
|
||||
```
|
||||
|
||||
For attributes defined in `attributes` section:
|
||||
|
||||
```bash
|
||||
http://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/:layer/attributes/:feature_id
|
||||
```
|
||||
|
||||
Which returns JSON with the attributes defined, like:
|
||||
|
||||
```javascript
|
||||
{ c: 1, d: 2 }
|
||||
```
|
||||
|
||||
Notice UTF Grid and attributes endpoints need an integer parameter, ``layer``. That number is the 0-based index of the layer inside the mapconfig. So in this case 0 returns the UTF grid tiles/attributes for layer 0, the only layer in the example mapconfig. If a second layer was available it could be returned with 1, a third layer with 2, etc.
|
||||
|
||||
### Create JSONP
|
||||
|
||||
The JSONP endpoint is provided in order to allow web browsers access which don't support CORS.
|
||||
|
||||
#### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```bash
|
||||
GET /api/v1/map?callback=method
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
- **config**
|
||||
Encoded JSON with the params for creating named maps (the variables defined in the template).
|
||||
|
||||
- **lmza**
|
||||
This attribute contains the same as config but LZMA compressed. It cannot be used at the same time as `config`.
|
||||
|
||||
- **callback**
|
||||
JSON callback name.
|
||||
|
||||
#### Example
|
||||
|
||||
<div class="code-title code-request with-result">REQUEST</div>
|
||||
```bash
|
||||
curl "https://documentation.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"
|
||||
```
|
||||
|
||||
<div class="code-title">RESPONSE</div>
|
||||
```javascript
|
||||
callback({
|
||||
layergroupid: "d9034c133262dfb90285cea26c5c7ad7:0",
|
||||
cdn_url: {
|
||||
"http": "http://cdb.com",
|
||||
"https": "https://cdb.com"
|
||||
},
|
||||
last_updated: "1970-01-01T00:00:00.000Z"
|
||||
})
|
||||
```
|
||||
|
||||
### Remove
|
||||
|
||||
Anonymous maps cannot be removed by an API call. They will expire after about five minutes but sometimes longer. If an anonymous map expires and tiles are requested from it, an error will be raised. This could happen if a user leaves a map open and after time returns to the map an attempts to interact with it in a way that requires new tiles (e.g. zoom). The client will need to go through the steps of creating the map again to fix the problem.
|
||||
|
||||
|
||||
## 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. Two other big differences are that you can create named maps from private data, and that users without an API Key can see them even though they are from that private data.
|
||||
|
||||
The main two differences compared to anonymous maps are:
|
||||
|
||||
- **auth layer**
|
||||
This allows you to control who is able to see the map based on a token auth
|
||||
|
||||
- **templates**
|
||||
Since the mapconfig is static it can contain some variables so the client can modify the map's appearance using those variables
|
||||
|
||||
Template 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 section](http://docs.cartodb.com/cartodb-platform/maps-api.html#auth)).
|
||||
|
||||
### Create
|
||||
|
||||
#### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```html
|
||||
POST /api/v1/map/named
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
- **api_key** is required
|
||||
|
||||
<div class="code-title">template.json</div>
|
||||
```javascript
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"name": "template_name",
|
||||
"auth": {
|
||||
"method": "token",
|
||||
"valid_tokens": [
|
||||
"auth_token1",
|
||||
"auth_token2"
|
||||
]
|
||||
},
|
||||
"placeholders": {
|
||||
"color": {
|
||||
"type": "css_color",
|
||||
"default": "red"
|
||||
},
|
||||
"cartodb_id": {
|
||||
"type": "number",
|
||||
"default": 1
|
||||
}
|
||||
},
|
||||
"layergroup": {
|
||||
"version": "1.0.1",
|
||||
"layers": [
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"cartocss_version": "2.1.1",
|
||||
"cartocss": "#layer { polygon-fill: <%= color %>; }",
|
||||
"sql": "select * from european_countries_e WHERE cartodb_id = <%= cartodb_id %>"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### Arguments
|
||||
|
||||
- **name**: There can be at most _one_ template with the same name for any user. Valid names start with a letter, and only contain letters, numbers, or underscores (_).
|
||||
- **auth**:
|
||||
- **method** `"token"` or `"open"` (the default if no `"method"` is given).
|
||||
- **valid_tokens** when `"method"` is set to `"token"`, the values listed here allow you to instantiate the named map.
|
||||
- **placeholders**: Variables not listed here are not substituted. Variable not provided at instantiation time trigger an error. A default is required for optional variables. Type specification is used for quoting, to avoid injections see template format section below.
|
||||
- **layergroup**: the layer list definition. This is the MapConfig explained in anonymous maps. See [MapConfig documentation](https://github.com/CartoDB/Windshaft/blob/master/doc/MapConfig-1.1.0.md) for more info.
|
||||
|
||||
#### Template Format
|
||||
|
||||
A templated `layergroup` allows the use of placeholders in the "cartocss" and "sql" elements of the "option" object in any "layer" of a `layergroup` configuration
|
||||
|
||||
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.
|
||||
|
||||
##### Example
|
||||
|
||||
```javascript
|
||||
<%= my_color %>
|
||||
```
|
||||
|
||||
The set of supported placeholders for a template will need to be explicitly defined with a specific type and default value for each.
|
||||
|
||||
#### Placeholder Types
|
||||
|
||||
The placeholder type will determine the kind of escaping for the associated value. Supported types are:
|
||||
|
||||
- **sql_literal** internal single-quotes will be sql-escaped
|
||||
- **sql_ident** internal double-quotes will be sql-escaped
|
||||
- **number** can only contain numerical representation
|
||||
- **css_color** can only contain color names or hex-values
|
||||
|
||||
Placeholder default values will be used whenever new values are not provided as options at the time of creation on the client. They can also be used to test the template by creating a default version with now options provided.
|
||||
|
||||
When using templates, be very careful about your selections as they can give broad access to your data if they are defined losely.
|
||||
|
||||
<div class="code-title code-request with-result">REQUEST</div>
|
||||
```html
|
||||
curl -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d @template.json \
|
||||
'https://documentation.cartodb.com/api/v1/map/named?api_key=APIKEY'
|
||||
```
|
||||
|
||||
<div class="code-title">RESPONSE</div>
|
||||
```javascript
|
||||
{
|
||||
"template_id":"name",
|
||||
}
|
||||
```
|
||||
|
||||
### Instantiate
|
||||
|
||||
Instantiating a map allows you to get the information needed to fetch tiles. That temporal map is an anonymous map.
|
||||
|
||||
#### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```html
|
||||
POST /api/v1/map/named/:template_name
|
||||
```
|
||||
|
||||
#### Param
|
||||
|
||||
- **auth_token** optional, but required when `"method"` is set to `"token"`
|
||||
|
||||
```javascript
|
||||
// params.json
|
||||
{
|
||||
"color": "#ff0000",
|
||||
"cartodb_id": 3
|
||||
}
|
||||
```
|
||||
|
||||
The fields you pass as `params.json` depend on the variables allowed by the named map. If there are variables missing it will raise an error (HTTP 400).
|
||||
|
||||
- **auth_token** *optional* if the named map needs auth
|
||||
|
||||
#### Example
|
||||
|
||||
You can initialize a template map by passing all of the required parameters in a POST to `/api/v1/map/named/:template_name`.
|
||||
|
||||
Valid credentials will be needed if required by the template.
|
||||
|
||||
<div class="code-title code-request with-result">REQUEST</div>
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d @params.json \
|
||||
'https://documentation.cartodb.com/api/v1/map/named/@template_name?auth_token=AUTH_TOKEN'
|
||||
```
|
||||
|
||||
<div class="code-title">Response</div>
|
||||
```javascript
|
||||
{
|
||||
"layergroupid": "docs@fd2861af@c01a54877c62831bb51720263f91fb33:123456788",
|
||||
"last_updated": "2013-11-14T11:20:15.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
<div class="code-title">Error</div>
|
||||
```javascript
|
||||
{
|
||||
"error": "Some error string here"
|
||||
}
|
||||
```
|
||||
|
||||
You can then use the `layergroupid` for fetching tiles and grids as you would normally (see anonymous map section). However, you'll need to show the `auth_token`, if required by the template.
|
||||
|
||||
### Using JSONP
|
||||
|
||||
There is also a special endpoint to be able to initialize a map using JSONP (for old browsers).
|
||||
|
||||
#### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```bash
|
||||
GET /api/v1/map/named/:template_name/jsonp
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
- **auth_token** optional, but required when `"method"` is set to `"token"`
|
||||
- **config** Encoded JSON with the params for creating named maps (the variables defined in the template)
|
||||
- **lmza** This attribute contains the same as config but LZMA compressed. It cannot be used at the same time than `config`.
|
||||
- **callback:** JSON callback name
|
||||
|
||||
<div class="code-title code-request with-result">REQUEST</div>
|
||||
```bash
|
||||
curl 'https://documentation.cartodb.com/api/v1/map/named/:template_name/jsonp?auth_token=AUTH_TOKEN&callback=callback&config=template_params_json'
|
||||
```
|
||||
|
||||
<div class="code-title">RESPONSE</div>
|
||||
```javascript
|
||||
callback({
|
||||
"layergroupid":"c01a54877c62831bb51720263f91fb33:0",
|
||||
"last_updated":"1970-01-01T00:00:00.000Z"
|
||||
"cdn_url": {
|
||||
"http": "http://cdb.com",
|
||||
"https": "https://cdb.com"
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
This takes the `callback` function (required), `auth_token` if the template needs auth, and `config` which is the variable for the template (in cases where it has variables).
|
||||
|
||||
```javascript
|
||||
url += "config=" + encodeURIComponent(
|
||||
JSON.stringify({ color: 'red' });
|
||||
```
|
||||
|
||||
The response is in this format:
|
||||
|
||||
```javascript
|
||||
callback({
|
||||
layergroupid: "dev@744bd0ed9b047f953fae673d56a47b4d:1390844463021.1401",
|
||||
last_updated: "2014-01-27T17:41:03.021Z"
|
||||
})
|
||||
```
|
||||
|
||||
### Update
|
||||
|
||||
#### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```bash
|
||||
PUT /api/v1/map/named/:template_name
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
- **api_key** is required
|
||||
|
||||
#### Response
|
||||
|
||||
Same as updating a map.
|
||||
|
||||
#### Other Info
|
||||
|
||||
Updating a named map removes all the named map instances so they need to be initialized again.
|
||||
|
||||
#### Example
|
||||
|
||||
<div class="code-title code-request with-result">REQUEST</div>
|
||||
```bash
|
||||
curl -X PUT \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d @template.json \
|
||||
'https://documentation.cartodb.com/api/v1/map/named/:template_name?api_key=APIKEY'
|
||||
```
|
||||
|
||||
<div class="code-title">RESPONSE</div>
|
||||
```javascript
|
||||
{
|
||||
"template_id": "@template_name"
|
||||
}
|
||||
```
|
||||
|
||||
If any template has the same name, it will be updated.
|
||||
|
||||
If a template with the same name does NOT exist, a 400 HTTP response is generated with an error in this format:
|
||||
|
||||
```javascript
|
||||
{
|
||||
"error": "error string here"
|
||||
}
|
||||
```
|
||||
|
||||
### Delete
|
||||
|
||||
Delete the specified template map from the server and disables any previously initialized versions of the map.
|
||||
|
||||
#### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```bash
|
||||
DELETE /api/v1/map/named/:template_name
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
- **api_key** is required
|
||||
|
||||
#### Example
|
||||
|
||||
<div class="code-title code-request">REQUEST</div>
|
||||
```bash
|
||||
curl -X DELETE 'https://documentation.cartodb.com/api/v1/map/named/:template_name?api_key=APIKEY'
|
||||
```
|
||||
|
||||
<div class="code-title">RESPONSE</div>
|
||||
```javascript
|
||||
{
|
||||
"error": "Some error string here"
|
||||
}
|
||||
```
|
||||
|
||||
On success, a 204 (No Content) response would be issued. Otherwise a 4xx response with with an error will be returned.
|
||||
|
||||
### Listing Available Templates
|
||||
|
||||
This allows you to get a list of all available templates.
|
||||
|
||||
#### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```bash
|
||||
GET /api/v1/map/named/
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
- **api_key** is required
|
||||
|
||||
#### Example
|
||||
|
||||
<div class="code-title code-request with-result">REQUEST</div>
|
||||
```bash
|
||||
curl -X GET 'https://documentation.cartodb.com/api/v1/map/named?api_key=APIKEY'
|
||||
```
|
||||
|
||||
<div class="code-title with-result">RESPONSE</div>
|
||||
```javascript
|
||||
{
|
||||
"template_ids": ["@template_name1","@template_name2"]
|
||||
}
|
||||
```
|
||||
|
||||
<div class="code-title">ERROR</div>
|
||||
```javascript
|
||||
{
|
||||
"error": "Some error string here"
|
||||
}
|
||||
```
|
||||
|
||||
### Getting a Specific Template
|
||||
|
||||
This gets the definition of a template.
|
||||
|
||||
#### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```bash
|
||||
GET /api/v1/map/named/:template_name
|
||||
```
|
||||
|
||||
#### Params
|
||||
|
||||
- **api_key** is required
|
||||
|
||||
#### Example
|
||||
|
||||
<div class="code-title code-request with-result">REQUEST</div>
|
||||
```bash
|
||||
curl -X GET 'https://documentation.cartodb.com/api/v1/map/named/:template_name?api_key=APIKEY'
|
||||
```
|
||||
|
||||
<div class="code-title with-result">RESPONSE</div>
|
||||
```javascript
|
||||
{
|
||||
"template": {...} // see template.json above
|
||||
}
|
||||
```
|
||||
|
||||
<div class="code-title">ERROR</div>
|
||||
```javascript
|
||||
{
|
||||
"error": "Some error string here"
|
||||
}
|
||||
```
|
||||
|
||||
### Use with CartoDB.js
|
||||
Named maps can be used with CartoDB.js by specifying a named map in a layer source as follows. Named maps are treated almost the same as other layer source types in most other ways.
|
||||
|
||||
```js
|
||||
var layerSource = {
|
||||
user_name: '{your_user_name}',
|
||||
type: 'namedmap',
|
||||
named_map: {
|
||||
name: '{template_name}',
|
||||
layers: [{
|
||||
layer_name: "layer1",
|
||||
interactivity: "column1, column2, ..."
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
cartodb.createLayer('map_dom_id',layerSource)
|
||||
.addTo(map_object);
|
||||
|
||||
```
|
||||
|
||||
See [CartoDB.js](http://docs.cartodb.com/cartodb-platform/cartodb-js.html) methods [layer.setParams()](http://docs.cartodb.com/cartodb-platform/cartodb-js.html#layersetparamskey-value) and [layer.setAuthToken()](http://docs.cartodb.com/cartodb-platform/cartodb-js.html#layersetauthtokenauthtoken).
|
||||
28
docs/MultiLayer-API.md
Normal file
28
docs/MultiLayer-API.md
Normal file
@@ -0,0 +1,28 @@
|
||||
The Windshaft-CartoDB MultiLayer API extends the [Windshaft MultiLayer API](https://github.com/Vizzuality/Windshaft/wiki/Multilayer-API) in a few ways.
|
||||
|
||||
## Last modification timestamp embedded in the token
|
||||
|
||||
It encodes a timestamp of 'last modification time' into the map token (token:EPOCH) returned to the client.
|
||||
It accepts tokens with encoded timestamp from the client considering the token suffix as a cache_buster value.
|
||||
|
||||
Clients don't need to be aware of the extension but rather use the API as they would use the base one.
|
||||
The only difference will be that the _same_ layergroup configuration may result in different tokens if source data was modified between the mapview requests.
|
||||
|
||||
## Additional attributes in the response object
|
||||
|
||||
Windshaft-CartoDB adds the following attributes in the response object
|
||||
|
||||
- ``last_update`` field with ISO format (2013-11-30T12:23:10).
|
||||
- ``cdn_url`` object containing CDN url client should use (not mandatory) to access the tiles. It's in the form:
|
||||
|
||||
```json
|
||||
{
|
||||
http: 'http://cdn_url.com/'
|
||||
https: 'https://secure.cdn_url.com/'
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Stats tag
|
||||
|
||||
Windshaft-CartoDB adds support for a ``stat_tag`` element in the multilayer configuration to help [stats](https://github.com/CartoDB/Windshaft-cartodb/wiki/Redis-stats-format) gathering.
|
||||
272
docs/Template-maps.md
Normal file
272
docs/Template-maps.md
Normal file
@@ -0,0 +1,272 @@
|
||||
Template maps are layergroup configurations that rather than being
|
||||
fully defined contain variables that can be set to produce a different
|
||||
layergroup configurations (instantiation).
|
||||
|
||||
Template maps are persistent, can only be created and deleted by the
|
||||
CartoDB user showing a valid API_KEY.
|
||||
|
||||
|
||||
# Template format
|
||||
|
||||
A templated layergroup would allow using placeholders
|
||||
in the "cartocss" and "sql" elements in the "option"
|
||||
field of any "layer" of a layergroup configuration
|
||||
(see https://github.com/CartoDB/Windshaft/wiki/MapConfig-specification).
|
||||
|
||||
Valid placeholder names start with a letter and can only
|
||||
contain letters, numbers or underscores. They have to be
|
||||
written between ``<%= `` and `` %>`` strings in order to be
|
||||
replaced. Example: ``<%= my_color %>``.
|
||||
|
||||
The set of supported placeholders for a template will need to be
|
||||
explicitly defined specifying type and default value for each.
|
||||
|
||||
**placeholder types**
|
||||
|
||||
Placeholder type will determine the kind of escaping for the
|
||||
associated value. Supported types are:
|
||||
|
||||
* sql_literal (internal single-quotes will be sql-escaped)
|
||||
* sql_ident (internal double-quotes will be sql-escaped)
|
||||
* number (can only contain numerical representation)
|
||||
* css_color (can only contain color names or hex-values)
|
||||
* ... (add more as need arises)
|
||||
|
||||
Placeholder default value will be used when not provided at
|
||||
instantiation time and could be used to test validity of the
|
||||
template by creating a default instance.
|
||||
|
||||
```js
|
||||
// template.json
|
||||
{
|
||||
version: "0.0.1",
|
||||
// there can be at most 1 template with the same name for any user
|
||||
// valid names start with a letter and only contains letter, numbers
|
||||
// or underscores
|
||||
name: "template_name",
|
||||
// embedded authorization certificate
|
||||
auth: {
|
||||
method: "token", // or "open" (the default if no "method" is given)
|
||||
// only (required and non empty) for "token" method
|
||||
valid_tokens: ["auth_token1","auth_token2"]
|
||||
},
|
||||
// Variables not listed here are not substituted
|
||||
// Variable not provided at instantiation time trigger an error
|
||||
// A default is required for optional variables
|
||||
// Type specification is used for quoting, to avoid injections
|
||||
placeholders: {
|
||||
color: {
|
||||
type:"css_color",
|
||||
default:"red"
|
||||
},
|
||||
cartodb_id: {
|
||||
type:"number",
|
||||
default: 1
|
||||
}
|
||||
},
|
||||
layergroup: {
|
||||
// see https://github.com/CartoDB/Windshaft/wiki/MapConfig-specification
|
||||
"version": "1.0.1",
|
||||
"layers": [{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"cartocss_version": "2.1.1",
|
||||
"cartocss": "#layer { polygon-fill: <%= color %>; }",
|
||||
"sql": "select * from european_countries_e WHERE cartodb_id = <%= cartodb_id %>"
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# Creating a templated map
|
||||
|
||||
You can create a template map with a single call (for simplicity).
|
||||
|
||||
You'd use a POST sending JSON data:
|
||||
|
||||
```sh
|
||||
curl -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d @template.json \
|
||||
'https://docs.cartodb.com/tiles/template?api_key=APIKEY'
|
||||
```
|
||||
|
||||
The response would be like this:
|
||||
```js
|
||||
{
|
||||
"template_id":"@template_name"
|
||||
}
|
||||
```
|
||||
|
||||
If a template with the same name exists in the user storage,
|
||||
a 400 response is generated.
|
||||
|
||||
Errors are in this form:
|
||||
```js
|
||||
{
|
||||
"error":"Some error string here"
|
||||
}
|
||||
```
|
||||
|
||||
# Updating an existing template
|
||||
|
||||
You can update a template map with a PUT:
|
||||
|
||||
```sh
|
||||
curl -X PUT \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d @template.json \
|
||||
'https://docs.cartodb.com/tiles/template/:template_name?api_key=APIKEY'
|
||||
```
|
||||
A template with the same name will be updated, if any.
|
||||
|
||||
The response would be like this:
|
||||
```js
|
||||
{
|
||||
"template_id":"@template_name"
|
||||
}
|
||||
```
|
||||
|
||||
If a template with the same name does NOT exist,
|
||||
a 400 HTTP response is generated with an error in this format:
|
||||
|
||||
```js
|
||||
{
|
||||
"error":"Some error string here"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
# Listing available templates
|
||||
|
||||
You can get a list of available templates with a GET to ``/template``.
|
||||
A valid api_key is required.
|
||||
|
||||
```sh
|
||||
curl -X GET 'https://docs.cartodb.com/tiles/template?api_key=APIKEY'
|
||||
```
|
||||
|
||||
The response would be like this:
|
||||
```js
|
||||
{
|
||||
"template_ids": ["@template_name1","@template_name2"]
|
||||
}
|
||||
```
|
||||
|
||||
Or, on error:
|
||||
|
||||
```js
|
||||
{
|
||||
"error":"Some error string here"
|
||||
}
|
||||
```
|
||||
|
||||
# Getting a specific template
|
||||
|
||||
You can get the definition of a template with a
|
||||
GET to ``/template/:template_name``.
|
||||
A valid api_key is required.
|
||||
|
||||
Example:
|
||||
|
||||
```sh
|
||||
curl -X GET 'https://docs.cartodb.com/tiles/template/@template_name?auth_token=AUTH_TOKEN'
|
||||
```
|
||||
|
||||
The response would be like this:
|
||||
```js
|
||||
{
|
||||
"template": {...} // see template.json above
|
||||
}
|
||||
```
|
||||
|
||||
Or, on error:
|
||||
|
||||
```js
|
||||
{
|
||||
"error":"Some error string here"
|
||||
}
|
||||
```
|
||||
|
||||
# Instantiating a template map
|
||||
|
||||
You can instantiate a template map passing all required parameters with
|
||||
a POST to ``/template/:template_name``.
|
||||
|
||||
Valid credentials will be needed, if required by the template.
|
||||
|
||||
```js
|
||||
// params.js
|
||||
{
|
||||
color: '#ff0000',
|
||||
cartodb_id: 3
|
||||
}
|
||||
```
|
||||
|
||||
```sh
|
||||
curl -X POST \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d @params.js \
|
||||
'https://docs.cartodb.com/tiles/template/@template_name?auth_token=AUTH_TOKEN'
|
||||
|
||||
```
|
||||
|
||||
The response would be like this:
|
||||
```js
|
||||
{
|
||||
"layergroupid":"docs@fd2861af@c01a54877c62831bb51720263f91fb33:123456788",
|
||||
"last_updated":"2013-11-14T11:20:15.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
or, on error:
|
||||
|
||||
```js
|
||||
{
|
||||
"error":"Some error string here"
|
||||
}
|
||||
```
|
||||
|
||||
You can then use the ``layergroupid`` for fetching tiles and grids as you do
|
||||
normally ( see https://github.com/CartoDB/Windshaft/wiki/Multilayer-API).
|
||||
But you'll still have to show the ``auth_token``, if required by the template.
|
||||
|
||||
### using JSONP
|
||||
There is also a special endpoint to be able to instanciate using JSONP (for old browsers)
|
||||
|
||||
```
|
||||
curl 'https://docs.cartodb.com/tiles/template/@template_name/jsonp?auth_token=AUTH_TOKEN&callback=function_name&config=template_params_json'
|
||||
```
|
||||
|
||||
it takes the ``callback`` function (required), ``auth_token`` in case the template needs auth and ``config`` which is the variabñes for the template (in case it has variables). For example config may be created (using javascript)
|
||||
```
|
||||
url += "config=" + encodeURIComponent(
|
||||
JSON.stringify({ color: 'red' });
|
||||
```
|
||||
|
||||
the response it's in this format:
|
||||
```
|
||||
jQuery17205720721024554223_1390996319118(
|
||||
{
|
||||
layergroupid: "dev@744bd0ed9b047f953fae673d56a47b4d:1390844463021.1401",
|
||||
last_updated: "2014-01-27T17:41:03.021Z"
|
||||
}
|
||||
)
|
||||
```
|
||||
# Deleting a template map
|
||||
|
||||
You can delete a templated map with a DELETE to ``/template/:template_name``:
|
||||
|
||||
```sh
|
||||
curl -X DELETE 'https://docs.cartodb.com/tiles/template/@template_name?auth_token=AUTH_TOKEN'
|
||||
```
|
||||
|
||||
On success, a 204 (No Content) response would be issued.
|
||||
Otherwise a 4xx response with this format:
|
||||
|
||||
```js
|
||||
{
|
||||
"error":"Some error string here"
|
||||
}
|
||||
```
|
||||
44
docs/metrics.md
Normal file
44
docs/metrics.md
Normal file
@@ -0,0 +1,44 @@
|
||||
Windshaft-cartodb metrics
|
||||
=========================
|
||||
See [Windshaft metrics documentation](https://github.com/CartoDB/Windshaft/blob/master/doc/metrics.md) to understand the full picture.
|
||||
|
||||
The next list includes the API endpoints, each endpoint may have several inner timers, some of them are displayed within this list as subitems. Find the description for them in the Inner timers section.
|
||||
## Timers
|
||||
- **windshaft-cartodb.flush_cache**: time to flush the tile and sql cache
|
||||
- **windshaft-cartodb.get_template**: time to retrieve an specific template
|
||||
- **windshaft-cartodb.delete_template**: time to delete an specific template
|
||||
- **windshaft-cartodb.get_template_list**: time to retrieve the list of owned templates
|
||||
- **windshaft-cartodb.instance_template_post**: time to create a template via HTTP POST
|
||||
- **windshaft-cartodb.instance_template_get**: time to create a template via HTTP GET
|
||||
+ TemplateMaps_instance
|
||||
+ createLayergroup
|
||||
|
||||
There are some endpoints that are not being tracked:
|
||||
- Adding a template
|
||||
- Updating a template
|
||||
|
||||
### Inner timers
|
||||
Again, each inner timer may have several inner timers.
|
||||
|
||||
- **addCacheChannel**: time to add X-Cache-Channel header based on table last modifications
|
||||
- **LZMA decompress**: time to decompress request params with LZMA
|
||||
- **TemplateMaps_instance**: time to retrieve a map template instance, see *getTemplate* and *authorizedByCert*
|
||||
- **affectedTables**: time to check what are the affected tables for adding the cache channel, see *addCacheChannel*
|
||||
- **authorize**: time to authorize a request, see *authorizedByAPIKey*, *authorizedByCert*, *authorizedBySigner*
|
||||
- **authorizedByAPIKey**: time to authorize using an API KEY
|
||||
- **authorizedByCert**: time to authorize a template instantiation
|
||||
- **authorizedBySigner**: time to authorize a request with auth_token
|
||||
- **findLastUpdated**: time to retrieve the last update time for a list of tables, see *affectedTables*
|
||||
- **generateCacheChannel**: time to generate the headers for the cache channel based on the request, see *addCacheChannel*
|
||||
- **getSignerMapKey**: time to retrieve from redis the authorized user for a template map
|
||||
- **getTablePrivacy**: time to retrieve from redis the privacy of a table
|
||||
- **getTemplate**: time to retrieve from redis the template for a map
|
||||
- **getUserMapKey**: time to retrieve from redis the user key for a map
|
||||
- **incMapviewCount**: time to incremenent in redis the map views
|
||||
- **mapStore_load**: time to retrieve from redis a map configuration
|
||||
- **req2params.setup**: time to prepare the params from a request, see *req2params* in Windshaft documentation
|
||||
- **setDBAuth**: time to retrieve from redis and set db user and db password from a user
|
||||
- **setDBConn**: time to retrieve from redis and set db host and db name from a user
|
||||
- **setDBParams**: time to prepare all db params to be able to connect/query a database, see *setDBAuth* and *setDBConn*
|
||||
- **tablePrivacy_getUserDBName**: time to retrieve from redis the database for a user
|
||||
|
||||
122
lib/cartodb/api/query_tables_api.js
Normal file
122
lib/cartodb/api/query_tables_api.js
Normal file
@@ -0,0 +1,122 @@
|
||||
var sqlApi = require('../sql/sql_api'),
|
||||
PSQL = require('cartodb-psql');
|
||||
|
||||
function QueryTablesApi() {
|
||||
}
|
||||
|
||||
var affectedTableRegexCache = {
|
||||
bbox: /!bbox!/g,
|
||||
scale_denominator: /!scale_denominator!/g,
|
||||
pixel_width: /!pixel_width!/g,
|
||||
pixel_height: /!pixel_height!/g
|
||||
};
|
||||
|
||||
module.exports = QueryTablesApi;
|
||||
|
||||
QueryTablesApi.prototype.getLastUpdatedTime = function (username, api_key, tableNames, callback) {
|
||||
var sql = 'SELECT EXTRACT(EPOCH FROM max(updated_at)) as max FROM CDB_TableMetadata m WHERE m.tabname = any (ARRAY['+
|
||||
tableNames.map(function(t) { return "'" + t + "'::regclass"; }).join(',') +
|
||||
'])';
|
||||
|
||||
// call sql api
|
||||
sqlApi.query(username, api_key, sql, function(err, rows){
|
||||
if (err){
|
||||
var msg = err.message ? err.message : err;
|
||||
callback(new Error('could not find last updated timestamp: ' + msg));
|
||||
return;
|
||||
}
|
||||
// when the table has not updated_at means it hasn't been changed so a default last_updated is set
|
||||
var last_updated = 0;
|
||||
if(rows.length !== 0) {
|
||||
last_updated = rows[0].max || 0;
|
||||
}
|
||||
|
||||
callback(null, last_updated*1000);
|
||||
});
|
||||
};
|
||||
|
||||
QueryTablesApi.prototype.getAffectedTablesInQuery = function (username, options, sql, callback) {
|
||||
|
||||
var query = 'SELECT CDB_QueryTables($windshaft$' + prepareSql(sql) + '$windshaft$)';
|
||||
|
||||
runQuery(username, options, query, handleAffectedTablesInQueryRows, callback);
|
||||
};
|
||||
|
||||
function handleAffectedTablesInQueryRows(err, rows, callback) {
|
||||
if (err){
|
||||
var msg = err.message ? err.message : err;
|
||||
callback(new Error('could not fetch source tables: ' + msg));
|
||||
return;
|
||||
}
|
||||
var qtables = rows[0].cdb_querytables;
|
||||
var tableNames = qtables.split(/^\{(.*)\}$/)[1];
|
||||
tableNames = tableNames ? tableNames.split(',') : [];
|
||||
callback(null, tableNames);
|
||||
}
|
||||
|
||||
QueryTablesApi.prototype.getAffectedTablesAndLastUpdatedTime = function (username, options, sql, callback) {
|
||||
|
||||
var query = [
|
||||
'WITH querytables AS (',
|
||||
'SELECT * FROM CDB_QueryTables($windshaft$' + prepareSql(sql) + '$windshaft$) as tablenames',
|
||||
')',
|
||||
'SELECT (SELECT tablenames FROM querytables), EXTRACT(EPOCH FROM max(updated_at)) as max',
|
||||
'FROM CDB_TableMetadata m',
|
||||
'WHERE m.tabname = any ((SELECT tablenames from querytables)::regclass[])'
|
||||
].join(' ');
|
||||
|
||||
runQuery(username, options, query, handleAffectedTablesAndLastUpdatedTimeRows, callback);
|
||||
};
|
||||
|
||||
function handleAffectedTablesAndLastUpdatedTimeRows(err, rows, callback) {
|
||||
if (err || rows.length === 0) {
|
||||
var msg = err.message ? err.message : err;
|
||||
callback(new Error('could not fetch affected tables and last updated time: ' + msg));
|
||||
return;
|
||||
}
|
||||
|
||||
var result = rows[0];
|
||||
|
||||
var tableNames = result.tablenames.split(/^\{(.*)\}$/)[1];
|
||||
tableNames = tableNames ? tableNames.split(',') : [];
|
||||
|
||||
var lastUpdatedTime = result.max || 0;
|
||||
|
||||
callback(null, {
|
||||
affectedTables: tableNames,
|
||||
lastUpdatedTime: lastUpdatedTime * 1000
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function runQuery(username, options, query, queryHandler, callback) {
|
||||
if (shouldQueryPostgresDirectly()) {
|
||||
var psql = new PSQL(options);
|
||||
psql.query(query, function(err, resultSet) {
|
||||
resultSet = resultSet || {};
|
||||
var rows = resultSet.rows || [];
|
||||
queryHandler(err, rows, callback);
|
||||
});
|
||||
} else {
|
||||
sqlApi.query(username, options.api_key, query, function(err, rows) {
|
||||
queryHandler(err, rows, callback);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function prepareSql(sql) {
|
||||
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')
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
function shouldQueryPostgresDirectly() {
|
||||
return global.environment
|
||||
&& global.environment.enabledFeatures
|
||||
&& global.environment.enabledFeatures.cdbQueryTablesFromPostgres;
|
||||
}
|
||||
32
lib/cartodb/cache/backend/varnish_http.js
vendored
Normal file
32
lib/cartodb/cache/backend/varnish_http.js
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
var request = require('request');
|
||||
|
||||
function VarnishHttpCacheBackend(host, port) {
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
module.exports = VarnishHttpCacheBackend;
|
||||
|
||||
/**
|
||||
* @param cacheObject should respond to `key() -> String` method
|
||||
* @param {Function} callback
|
||||
*/
|
||||
VarnishHttpCacheBackend.prototype.invalidate = function(cacheObject, callback) {
|
||||
request(
|
||||
{
|
||||
method: 'PURGE',
|
||||
url: 'http://' + this.host + ':' + this.port + '/key',
|
||||
headers: {
|
||||
'Invalidation-Match': '\\b' + cacheObject.key() + '\\b'
|
||||
}
|
||||
},
|
||||
function(err, response) {
|
||||
if (err || response.statusCode !== 204) {
|
||||
return callback(new Error('Unable to invalidate Varnish object'));
|
||||
}
|
||||
return callback(null);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = VarnishHttpCacheBackend;
|
||||
18
lib/cartodb/cache/model/named_maps_entry.js
vendored
Normal file
18
lib/cartodb/cache/model/named_maps_entry.js
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
var crypto = require('crypto');
|
||||
|
||||
function NamedMaps(owner, name) {
|
||||
this.namespace = 'n';
|
||||
this.owner = owner;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
module.exports = NamedMaps;
|
||||
|
||||
|
||||
NamedMaps.prototype.key = function() {
|
||||
return this.namespace + ':' + shortHashKey(this.owner + ':' + this.name);
|
||||
};
|
||||
|
||||
function shortHashKey(target) {
|
||||
return crypto.createHash('sha256').update(target).digest('base64').substring(0,6);
|
||||
}
|
||||
26
lib/cartodb/cache/surrogate_keys_cache.js
vendored
Normal file
26
lib/cartodb/cache/surrogate_keys_cache.js
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* @param cacheBackend should respond to `invalidate(cacheObject, callback)` method
|
||||
* @constructor
|
||||
*/
|
||||
function SurrogateKeysCache(cacheBackend) {
|
||||
this.cacheBackend = cacheBackend;
|
||||
}
|
||||
|
||||
module.exports = SurrogateKeysCache;
|
||||
|
||||
|
||||
/**
|
||||
* @param response should respond to `header(key, value)` method
|
||||
* @param cacheObject should respond to `key() -> String` method
|
||||
*/
|
||||
SurrogateKeysCache.prototype.tag = function(response, cacheObject) {
|
||||
response.header('Surrogate-Key', cacheObject.key());
|
||||
};
|
||||
|
||||
/**
|
||||
* @param cacheObject should respond to `key() -> String` method
|
||||
* @param {Function} callback
|
||||
*/
|
||||
SurrogateKeysCache.prototype.invalidate = function(cacheObject, callback) {
|
||||
this.cacheBackend.invalidate(cacheObject, callback);
|
||||
};
|
||||
@@ -2,16 +2,21 @@ var _ = require('underscore'),
|
||||
Varnish = require('node-varnish'),
|
||||
varnish_queue = null;
|
||||
|
||||
function init(host, port) {
|
||||
varnish_queue = new Varnish.VarnishQueue(host, port);
|
||||
function init(host, port, secret) {
|
||||
varnish_queue = new Varnish.VarnishQueue(host, port, secret);
|
||||
varnish_queue.on('error', function(e) {
|
||||
console.log("[CACHE VALIDATOR ERROR] " + e);
|
||||
});
|
||||
}
|
||||
|
||||
function invalidate_db(dbname, table) {
|
||||
var cmd = 'purge obj.http.X-Cache-Channel ~ "^' + dbname +
|
||||
':(.*'+ table +'.*)|(table)$"';
|
||||
try{
|
||||
varnish_queue.run_cmd('purge obj.http.X-Cache-Channel ~ "^' + dbname + ':(.*'+ table +'.*)|(table)$"');
|
||||
console.log('[SUCCESS FLUSHING CACHE]');
|
||||
varnish_queue.run_cmd(cmd, false);
|
||||
} catch (e) {
|
||||
console.log("[ERROR FLUSHING CACHE] Is enable_cache set to true? Failed for: " + 'purge obj.http.X-Cache-Channel ~ "^' + dbname + ':(.*'+ table +'.*)|(table)$"');
|
||||
console.log("[CACHE VALIDATOR ERROR] could not queue command " +
|
||||
cmd + " -- " + e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,279 +0,0 @@
|
||||
/**
|
||||
* User: simon
|
||||
* Date: 30/08/2011
|
||||
* Time: 21:10
|
||||
* Desc: CartoDB helper.
|
||||
* Retrieves dbname (based on subdomain/username)
|
||||
* and geometry type from the redis stores of cartodb
|
||||
*/
|
||||
|
||||
var strftime = require('strftime');
|
||||
|
||||
var RedisPool = require("./redis_pool")
|
||||
, _ = require('underscore')
|
||||
, Step = require('step');
|
||||
|
||||
module.exports = function() {
|
||||
var redis_pool = new RedisPool(global.environment.redis);
|
||||
|
||||
|
||||
var me = {
|
||||
user_metadata_db: 5,
|
||||
table_metadata_db: 0,
|
||||
user_key: "rails:users:<%= username %>",
|
||||
table_key: "rails:<%= database_name %>:<%= table_name %>",
|
||||
global_mapview_key: "user:<%= username %>:mapviews:global",
|
||||
tagged_mapview_key: "user:<%= username %>:mapviews:stat_tag:<%= stat_tag %>"
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get the database name for this particular subdomain/username
|
||||
*
|
||||
* @param req - standard express req object. importantly contains host information
|
||||
* @param callback - gets called with args(err, dbname)
|
||||
*/
|
||||
me.getDatabase = function(req, callback) {
|
||||
// strip subdomain from header host
|
||||
var username = req.headers.host.split('.')[0]
|
||||
var redisKey = _.template(this.user_key, {username: username});
|
||||
|
||||
this.retrieve(this.user_metadata_db, redisKey, 'database_name', function(err, dbname) {
|
||||
if ( err ) callback(err, null);
|
||||
else if ( dbname === null ) {
|
||||
callback(new Error("missing " + username + "'s dbname in redis (try CARTODB/script/restore_redis)"), null);
|
||||
}
|
||||
else callback(err, dbname);
|
||||
});
|
||||
};
|
||||
|
||||
me.userFromHostname = function(hostname) {
|
||||
return hostname.split('.')[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment mapview count for a user
|
||||
*
|
||||
* @param username
|
||||
* @param stat_tag
|
||||
* @param callback will be called with the new value
|
||||
*/
|
||||
me.incMapviewCount = function(username, stat_tag, callback) {
|
||||
var that = this;
|
||||
var now = strftime("%Y%m%d", new Date());
|
||||
var redisKey;
|
||||
Step (
|
||||
function incrementGlobal() {
|
||||
redisKey = _.template(that.global_mapview_key, {username: username});
|
||||
that.redisCmd(me.user_metadata_db, 'ZINCRBY', [redisKey, 1, now], this);
|
||||
},
|
||||
function incrementTag(err, val) {
|
||||
if ( err ) throw err;
|
||||
if ( _.isUndefined(stat_tag) ) return 1;
|
||||
redisKey = _.template(that.tagged_mapview_key, {username: username, stat_tag: stat_tag});
|
||||
that.redisCmd(me.user_metadata_db, 'ZINCRBY', [redisKey, 1, now], this);
|
||||
},
|
||||
function finish(err, val) {
|
||||
if ( callback ) callback(err);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get the user id for this particular subdomain/username
|
||||
*
|
||||
* @param req - standard express req object. importantly contains host information
|
||||
* @param callback
|
||||
*/
|
||||
me.getId= function(req, callback) {
|
||||
// strip subdomain from header host
|
||||
var username = req.headers.host.split('.')[0];
|
||||
var redisKey = _.template(this.user_key, {username: username});
|
||||
|
||||
this.retrieve(this.user_metadata_db, redisKey, 'id', function(err, dbname) {
|
||||
if ( err ) callback(err, null);
|
||||
else if ( dbname === null ) {
|
||||
callback(new Error("missing " + username + "'s dbuser in redis (try CARTODB/script/restore_redis)"), null);
|
||||
}
|
||||
else callback(err, dbname);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Check the user map key for this particular subdomain/username
|
||||
*
|
||||
* @param req - standard express req object. importantly contains host information
|
||||
* @param callback
|
||||
*/
|
||||
me.checkMapKey = function(req, callback) {
|
||||
// strip subdomain from header host
|
||||
var username = req.headers.host.split('.')[0];
|
||||
var redisKey = "rails:users:" + username;
|
||||
this.retrieve(this.user_metadata_db, redisKey, "map_key", function(err, val) {
|
||||
var valid = 0;
|
||||
if ( val ) {
|
||||
if ( val == req.query.map_key ) valid = 1;
|
||||
else if ( val == req.query.api_key ) valid = 1;
|
||||
// check also in request body
|
||||
else if ( req.body && req.body.map_key && val == req.body.map_key ) valid = 1;
|
||||
else if ( req.body && req.body.api_key && val == req.body.api_key ) valid = 1;
|
||||
}
|
||||
callback(err, valid);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get privacy for cartodb table
|
||||
*
|
||||
* @param req - standard req object. Importantly contains table and host information
|
||||
* @param callback - is the table private or not?
|
||||
*/
|
||||
me.authorize= function(req, callback) {
|
||||
var that = this;
|
||||
|
||||
Step(
|
||||
function(){
|
||||
that.checkMapKey(req, this);
|
||||
},
|
||||
function checkIfInternal(err, check_result){
|
||||
if (err) throw err;
|
||||
if (check_result === 1) {
|
||||
// authorized by key, login as db owner
|
||||
that.getId(req, function(err, user_id) {
|
||||
if (err) throw err;
|
||||
var dbuser = _.template(global.settings.postgres_auth_user, {user_id: user_id});
|
||||
_.extend(req, {dbuser:dbuser});
|
||||
callback(err, true);
|
||||
});
|
||||
} else {
|
||||
return true; // continue to check if the table is public/private
|
||||
}
|
||||
}
|
||||
,function (err, data){
|
||||
if (err) throw err;
|
||||
that.getDatabase(req, this);
|
||||
},
|
||||
function(err, data){
|
||||
if (err) throw err;
|
||||
var redisKey = _.template(that.table_key, {database_name: data, table_name: req.params.table});
|
||||
|
||||
that.retrieve(that.table_metadata_db, redisKey, 'privacy', this);
|
||||
},
|
||||
function(err, data){
|
||||
callback(err, data);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get the geometry type for this particular table;
|
||||
* @param req - standard req object. Importantly contains table and host information
|
||||
* @param callback
|
||||
*/
|
||||
me.getGeometryType = function(req, callback){
|
||||
var that = this;
|
||||
|
||||
Step(
|
||||
function(){
|
||||
that.getDatabase(req, this)
|
||||
},
|
||||
function(err, data){
|
||||
if (err) throw err;
|
||||
var redisKey = _.template(that.table_key, {database_name: data, table_name: req.params.table});
|
||||
|
||||
that.retrieve(that.table_metadata_db, redisKey, 'the_geom_type', this);
|
||||
},
|
||||
function(err, data){
|
||||
callback(err, data);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
me.getInfowindow = function(req, callback){
|
||||
var that = this;
|
||||
|
||||
Step(
|
||||
function(){
|
||||
that.getDatabase(req, this);
|
||||
},
|
||||
function(err, data) {
|
||||
if (err) throw err;
|
||||
var redisKey = _.template(that.table_key, {database_name: data, table_name: req.params.table});
|
||||
that.retrieve(that.table_metadata_db, redisKey, 'infowindow', this);
|
||||
},
|
||||
function(err, data){
|
||||
callback(err, data);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
me.getMapMetadata = function(req, callback){
|
||||
var that = this;
|
||||
|
||||
Step(
|
||||
function(){
|
||||
that.getDatabase(req, this);
|
||||
},
|
||||
function(err, data) {
|
||||
if (err) throw err;
|
||||
var redisKey = _.template(that.table_key, {database_name: data, table_name: req.params.table});
|
||||
|
||||
that.retrieve(that.table_metadata_db, redisKey, 'map_metadata', this);
|
||||
},
|
||||
function(err, data){
|
||||
callback(err, data);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Redis Hash lookup
|
||||
// @param callback will be invoked with args (err, reply)
|
||||
// note that reply is null when the key is missing
|
||||
me.retrieve = function(db, redisKey, hashKey, callback) {
|
||||
this.redisCmd(db,'HGET',[redisKey, hashKey], callback);
|
||||
};
|
||||
|
||||
// Redis Set member check
|
||||
me.inSet = function(db, setKey, member, callback) {
|
||||
this.redisCmd(db,'SISMEMBER',[setKey, member], callback);
|
||||
};
|
||||
|
||||
// Redis INCREMENT
|
||||
me.increment = function(db, key, callback) {
|
||||
this.redisCmd(db,'INCR', key, callback);
|
||||
};
|
||||
|
||||
/**
|
||||
* Use Redis
|
||||
*
|
||||
* @param db - redis database number
|
||||
* @param redisFunc - the redis function to execute
|
||||
* @param redisArgs - the arguments for the redis function in an array
|
||||
* @param callback - function to pass results too.
|
||||
*/
|
||||
me.redisCmd = function(db, redisFunc, redisArgs, callback) {
|
||||
var redisClient;
|
||||
|
||||
Step(
|
||||
function getRedisClient() {
|
||||
redis_pool.acquire(db, this);
|
||||
},
|
||||
function executeQuery(err, data) {
|
||||
if ( err ) throw err;
|
||||
redisClient = data;
|
||||
redisArgs.push(this);
|
||||
redisClient[redisFunc.toUpperCase()].apply(redisClient, redisArgs);
|
||||
},
|
||||
function releaseRedisClient(err, data) {
|
||||
if ( ! _.isUndefined(redisClient) ) redis_pool.release(db, redisClient);
|
||||
callback(err, data);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return me;
|
||||
}();
|
||||
@@ -1,14 +1,33 @@
|
||||
|
||||
var _ = require('underscore')
|
||||
, Step = require('step')
|
||||
, Windshaft = require('windshaft')
|
||||
, Cache = require('./cache_validator');
|
||||
, TemplateMaps = require('./template_maps.js')
|
||||
, Cache = require('./cache_validator')
|
||||
, os = require('os')
|
||||
, HealthCheck = require('./monitoring/health_check')
|
||||
;
|
||||
|
||||
if ( ! process.env['PGAPPNAME'] )
|
||||
process.env['PGAPPNAME']='cartodb_tiler';
|
||||
|
||||
var CartodbWindshaft = function(serverOptions) {
|
||||
// Perform keyword substitution in statsd
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/issues/153
|
||||
if ( global.environment.statsd ) {
|
||||
if ( global.environment.statsd.prefix ) {
|
||||
var host_token = os.hostname().split('.').reverse().join('.');
|
||||
global.environment.statsd.prefix = global.environment.statsd.prefix.replace(/:host/, host_token);
|
||||
}
|
||||
}
|
||||
|
||||
var redisPool = serverOptions.redis.pool
|
||||
|| require('redis-mpool')(_.extend(global.environment.redis, {name: 'windshaft:cartodb'}));
|
||||
|
||||
var cartoData = require('cartodb-redis')({pool: redisPool});
|
||||
|
||||
if(serverOptions.cache_enabled) {
|
||||
console.log("cache invalidation enabled, varnish on ", serverOptions.varnish_host, ' ', serverOptions.varnish_port);
|
||||
Cache.init(serverOptions.varnish_host, serverOptions.varnish_port);
|
||||
Cache.init(serverOptions.varnish_host, serverOptions.varnish_port, serverOptions.varnish_secret);
|
||||
serverOptions.afterStateChange = function(req, data, callback) {
|
||||
Cache.invalidate_db(req.params.dbname, req.params.table);
|
||||
callback(null, data);
|
||||
@@ -17,10 +36,42 @@ var CartodbWindshaft = function(serverOptions) {
|
||||
|
||||
serverOptions.beforeStateChange = function(req, callback) {
|
||||
var err = null;
|
||||
if ( ! req.hasOwnProperty('dbuser') ) {
|
||||
if ( ! req.params.hasOwnProperty('_authorizedByApiKey') ) {
|
||||
err = new Error("map state cannot be changed by unauthenticated request!");
|
||||
}
|
||||
callback(err, req);
|
||||
};
|
||||
|
||||
// This is for Templated maps
|
||||
//
|
||||
// "named" is the official, "template" is for backward compatibility up to 1.6.x
|
||||
//
|
||||
var template_baseurl = global.environment.base_url_templated || '(?:/maps/named|/tiles/template)';
|
||||
|
||||
var templateMapsOpts = {
|
||||
max_user_templates: global.environment.maxUserTemplates
|
||||
};
|
||||
var templateMaps = new TemplateMaps(redisPool, templateMapsOpts);
|
||||
serverOptions.templateMaps = templateMaps;
|
||||
|
||||
var SurrogateKeysCache = require('./cache/surrogate_keys_cache'),
|
||||
NamedMapsCacheEntry = require('./cache/model/named_maps_entry'),
|
||||
VarnishHttpCacheBackend = require('./cache/backend/varnish_http'),
|
||||
varnishHttpCacheBackend = new VarnishHttpCacheBackend(serverOptions.varnish_host, serverOptions.varnish_http_port),
|
||||
surrogateKeysCache = new SurrogateKeysCache(varnishHttpCacheBackend);
|
||||
|
||||
if (serverOptions.varnish_purge_enabled) {
|
||||
function invalidateNamedMap(owner, templateName) {
|
||||
surrogateKeysCache.invalidate(new NamedMapsCacheEntry(owner, templateName), function(err) {
|
||||
if (err) {
|
||||
console.warn('Cache: surrogate key invalidation failed');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
['update', 'delete'].forEach(function(eventType) {
|
||||
templateMaps.on(eventType, invalidateNamedMap);
|
||||
});
|
||||
}
|
||||
|
||||
// boot
|
||||
@@ -32,7 +83,88 @@ var CartodbWindshaft = function(serverOptions) {
|
||||
var version = wsversion();
|
||||
version.windshaft_cartodb = require('../../package.json').version;
|
||||
return version;
|
||||
}
|
||||
};
|
||||
|
||||
var ws_sendResponse = ws.sendResponse;
|
||||
// GET routes for which we don't want to request any caching.
|
||||
// POST/PUT/DELETE requests are never cached anyway.
|
||||
var noCacheGETRoutes = [
|
||||
'/',
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/issues/176
|
||||
serverOptions.base_url_mapconfig,
|
||||
template_baseurl + '/:template_id/jsonp'
|
||||
];
|
||||
ws.sendResponse = function(res, args) {
|
||||
var that = this;
|
||||
var thatArgs = arguments;
|
||||
var statusCode;
|
||||
if ( res._windshaftStatusCode ) {
|
||||
// Added by our override of sendError
|
||||
statusCode = res._windshaftStatusCode;
|
||||
} else {
|
||||
if ( args.length > 2 ) statusCode = args[2];
|
||||
else {
|
||||
statusCode = args[1] || 200;
|
||||
}
|
||||
}
|
||||
var req = res.req;
|
||||
Step (
|
||||
function addCacheChannel() {
|
||||
if ( ! req ) {
|
||||
// having no associated request can happen when
|
||||
// using fake response objects for testing layergroup
|
||||
// creation
|
||||
return false;
|
||||
}
|
||||
if ( ! req.params ) {
|
||||
// service requests (/version, /)
|
||||
// have no need for an X-Cache-Channel
|
||||
return false;
|
||||
}
|
||||
if ( statusCode != 200 ) {
|
||||
// We do not want to cache
|
||||
// unsuccessful responses
|
||||
return false;
|
||||
}
|
||||
if ( _.contains(noCacheGETRoutes, req.route.path) ) {
|
||||
//console.log("Skipping cache channel in route:\n" + req.route.path);
|
||||
return false;
|
||||
}
|
||||
//console.log("Adding cache channel to route\n" + req.route.path + " not matching any in:\n" + mapCreateRoutes.join("\n"));
|
||||
serverOptions.addCacheChannel(that, req, this);
|
||||
},
|
||||
function sendResponse(err, added) {
|
||||
if ( err ) console.log(err + err.stack);
|
||||
ws_sendResponse.apply(that, thatArgs);
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
if ( err ) console.log(err + err.stack);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
var ws_sendError = ws.sendError;
|
||||
ws.sendError = function() {
|
||||
var res = arguments[0];
|
||||
var statusCode = arguments[2];
|
||||
res._windshaftStatusCode = statusCode;
|
||||
ws_sendError.apply(this, arguments);
|
||||
};
|
||||
|
||||
/*******************************************************************************************************************
|
||||
* Routing
|
||||
******************************************************************************************************************/
|
||||
|
||||
var TemplateMapsController = require('./controllers/template_maps'),
|
||||
templateMapsController = new TemplateMapsController(
|
||||
ws, serverOptions, templateMaps, cartoData, template_baseurl, surrogateKeysCache, NamedMapsCacheEntry
|
||||
);
|
||||
templateMapsController.register(ws);
|
||||
|
||||
/*******************************************************************************************************************
|
||||
* END Routing
|
||||
******************************************************************************************************************/
|
||||
|
||||
/**
|
||||
* Helper to allow access to the layer to be used in the maps infowindow popup.
|
||||
@@ -45,10 +177,10 @@ var CartodbWindshaft = function(serverOptions) {
|
||||
},
|
||||
function(err, data){
|
||||
if (err){
|
||||
ws.sendError(res, {error: err.message}, 500, 'GET INFOWINDOW');
|
||||
//res.send({error: err.message}, 500);
|
||||
ws.sendError(res, {error: err.message}, 500, 'GET INFOWINDOW', err);
|
||||
//ws.sendResponse(res, [{error: err.message}, 500]);
|
||||
} else {
|
||||
res.send({infowindow: data}, 200);
|
||||
ws.sendResponse(res, [{infowindow: data}, 200]);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -66,10 +198,10 @@ var CartodbWindshaft = function(serverOptions) {
|
||||
},
|
||||
function(err, data){
|
||||
if (err){
|
||||
ws.sendError(res, {error: err.message}, 500, 'GET MAP_METADATA');
|
||||
//res.send(err.message, 500);
|
||||
ws.sendError(res, {error: err.message}, 500, 'GET MAP_METADATA', err);
|
||||
//ws.sendResponse(res, [err.message, 500]);
|
||||
} else {
|
||||
res.send({map_metadata: data}, 200);
|
||||
ws.sendResponse(res, [{map_metadata: data}, 200]);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -80,6 +212,9 @@ var CartodbWindshaft = function(serverOptions) {
|
||||
* TODO: Move?
|
||||
*/
|
||||
ws.del(serverOptions.base_url + '/flush_cache', function(req, res){
|
||||
if ( req.profiler && req.profiler.statsd_client ) {
|
||||
req.profiler.start('windshaft-cartodb.flush_cache');
|
||||
}
|
||||
ws.doCORS(res);
|
||||
Step(
|
||||
function flushCache(){
|
||||
@@ -87,15 +222,41 @@ var CartodbWindshaft = function(serverOptions) {
|
||||
},
|
||||
function sendResponse(err, data){
|
||||
if (err){
|
||||
ws.sendError(res, {error: err.message}, 500, 'DELETE CACHE');
|
||||
//res.send(500);
|
||||
ws.sendError(res, {error: err.message}, 500, 'DELETE CACHE', err);
|
||||
//ws.sendResponse(res, [500]);
|
||||
} else {
|
||||
res.send({status: 'ok'}, 200);
|
||||
ws.sendResponse(res, [{status: 'ok'}, 200]);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
var healthCheck = new HealthCheck(cartoData, Windshaft.tilelive);
|
||||
ws.get('/health', function(req, res) {
|
||||
var healthConfig = global.environment.health || {};
|
||||
|
||||
if (!!healthConfig.enabled) {
|
||||
var startTime = Date.now();
|
||||
healthCheck.check(healthConfig, function(err, result) {
|
||||
var ok = !err;
|
||||
var response = {
|
||||
enabled: true,
|
||||
ok: ok,
|
||||
elapsed: Date.now() - startTime,
|
||||
result: result
|
||||
};
|
||||
if (err) {
|
||||
response.err = err.message;
|
||||
}
|
||||
res.send(response, ok ? 200 : 503);
|
||||
|
||||
});
|
||||
} else {
|
||||
res.send({enabled: false, ok: true}, 200);
|
||||
}
|
||||
});
|
||||
|
||||
return ws;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = CartodbWindshaft;
|
||||
|
||||
478
lib/cartodb/controllers/template_maps.js
Normal file
478
lib/cartodb/controllers/template_maps.js
Normal file
@@ -0,0 +1,478 @@
|
||||
var Step = require('step');
|
||||
var _ = require('underscore');
|
||||
|
||||
function TemplateMapsController(app, serverOptions, templateMaps, metadataBackend, templateBaseUrl, surrogateKeysCache,
|
||||
NamedMapsCacheEntry) {
|
||||
this.app = app;
|
||||
this.serverOptions = serverOptions;
|
||||
this.templateMaps = templateMaps;
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.templateBaseUrl = templateBaseUrl;
|
||||
this.surrogateKeysCache = surrogateKeysCache;
|
||||
this.NamedMapsCacheEntry = NamedMapsCacheEntry;
|
||||
}
|
||||
|
||||
module.exports = TemplateMapsController;
|
||||
|
||||
|
||||
TemplateMapsController.prototype.register = function(app) {
|
||||
app.get(this.templateBaseUrl + '/:template_id/jsonp', this.jsonp.bind(this));
|
||||
app.post(this.templateBaseUrl, this.create.bind(this));
|
||||
app.put(this.templateBaseUrl + '/:template_id', this.update.bind(this));
|
||||
app.get(this.templateBaseUrl + '/:template_id', this.retrieve.bind(this));
|
||||
app.del(this.templateBaseUrl + '/:template_id', this.destroy.bind(this));
|
||||
app.get(this.templateBaseUrl, this.list.bind(this));
|
||||
app.options(this.templateBaseUrl + '/:template_id', this.options.bind(this));
|
||||
app.post(this.templateBaseUrl + '/:template_id', this.instantiate.bind(this));
|
||||
};
|
||||
|
||||
// Add a template
|
||||
TemplateMapsController.prototype.create = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
this.app.doCORS(res);
|
||||
|
||||
var cdbuser = self.serverOptions.userByReq(req);
|
||||
|
||||
Step(
|
||||
function checkPerms(){
|
||||
self.serverOptions.authorizedByAPIKey(req, this);
|
||||
},
|
||||
function addTemplate(err, authenticated) {
|
||||
if ( err ) throw err;
|
||||
if (authenticated !== 1) {
|
||||
err = new Error("Only authenticated user can create templated maps");
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
if ( ! req.headers['content-type'] || req.headers['content-type'].split(';')[0] != 'application/json' )
|
||||
throw new Error('template POST data must be of type application/json');
|
||||
var cfg = req.body;
|
||||
self.templateMaps.addTemplate(cdbuser, cfg, this);
|
||||
},
|
||||
function prepareResponse(err, tpl_id){
|
||||
if ( err ) throw err;
|
||||
// NOTE: might omit "cdbuser" if == dbowner ...
|
||||
return { template_id: cdbuser + '@' + tpl_id };
|
||||
},
|
||||
function finish(err, response){
|
||||
if ( req.profiler ) {
|
||||
res.header('X-Tiler-Profiler', req.profiler.toJSONString());
|
||||
}
|
||||
if (err){
|
||||
response = { error: ''+err };
|
||||
var statusCode = 400;
|
||||
if ( ! _.isUndefined(err.http_status) ) {
|
||||
statusCode = err.http_status;
|
||||
}
|
||||
self.app.sendError(res, response, statusCode, 'POST TEMPLATE', err);
|
||||
} else {
|
||||
self.app.sendResponse(res, [response, 200]);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Update a template
|
||||
TemplateMapsController.prototype.update = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
this.app.doCORS(res);
|
||||
|
||||
var cdbuser = this.serverOptions.userByReq(req);
|
||||
var template;
|
||||
var tpl_id;
|
||||
Step(
|
||||
function checkPerms(){
|
||||
self.serverOptions.authorizedByAPIKey(req, this);
|
||||
},
|
||||
function updateTemplate(err, authenticated) {
|
||||
if ( err ) throw err;
|
||||
if (authenticated !== 1) {
|
||||
err = new Error("Only authenticated user can list templated maps");
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
if ( ! req.headers['content-type'] || req.headers['content-type'].split(';')[0] != 'application/json' )
|
||||
throw new Error('template PUT data must be of type application/json');
|
||||
template = req.body;
|
||||
tpl_id = req.params.template_id.split('@');
|
||||
if ( tpl_id.length > 1 ) {
|
||||
if ( tpl_id[0] != cdbuser ) {
|
||||
err = new Error("Invalid template id '"
|
||||
+ req.params.template_id + "' for user '" + cdbuser + "'");
|
||||
err.http_status = 404;
|
||||
throw err;
|
||||
}
|
||||
tpl_id = tpl_id[1];
|
||||
}
|
||||
self.templateMaps.updTemplate(cdbuser, tpl_id, template, this);
|
||||
},
|
||||
function prepareResponse(err){
|
||||
if ( err ) throw err;
|
||||
return { template_id: cdbuser + '@' + tpl_id };
|
||||
},
|
||||
function finish(err, response){
|
||||
if ( req.profiler ) {
|
||||
res.header('X-Tiler-Profiler', req.profiler.toJSONString());
|
||||
}
|
||||
if (err){
|
||||
var statusCode = 400;
|
||||
response = { error: ''+err };
|
||||
if ( ! _.isUndefined(err.http_status) ) {
|
||||
statusCode = err.http_status;
|
||||
}
|
||||
self.app.sendError(res, response, statusCode, 'PUT TEMPLATE', err);
|
||||
} else {
|
||||
self.app.sendResponse(res, [response, 200]);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Get a specific template
|
||||
TemplateMapsController.prototype.retrieve = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
if ( req.profiler && req.profiler.statsd_client ) {
|
||||
req.profiler.start('windshaft-cartodb.get_template');
|
||||
}
|
||||
|
||||
this.app.doCORS(res);
|
||||
|
||||
var cdbuser = this.serverOptions.userByReq(req);
|
||||
var template;
|
||||
var tpl_id;
|
||||
Step(
|
||||
function checkPerms(){
|
||||
self.serverOptions.authorizedByAPIKey(req, this);
|
||||
},
|
||||
function updateTemplate(err, authenticated) {
|
||||
if ( err ) throw err;
|
||||
if (authenticated !== 1) {
|
||||
err = new Error("Only authenticated users can get template maps");
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
tpl_id = req.params.template_id.split('@');
|
||||
if ( tpl_id.length > 1 ) {
|
||||
if ( tpl_id[0] != cdbuser ) {
|
||||
var err = new Error("Cannot get template id '"
|
||||
+ req.params.template_id + "' for user '" + cdbuser + "'");
|
||||
err.http_status = 404;
|
||||
throw err;
|
||||
}
|
||||
tpl_id = tpl_id[1];
|
||||
}
|
||||
self.templateMaps.getTemplate(cdbuser, tpl_id, this);
|
||||
},
|
||||
function prepareResponse(err, tpl_val){
|
||||
if ( err ) throw err;
|
||||
if ( ! tpl_val ) {
|
||||
err = new Error("Cannot find template '" + tpl_id + "' of user '" + cdbuser + "'");
|
||||
err.http_status = 404;
|
||||
throw err;
|
||||
}
|
||||
// auth_id was added by ourselves,
|
||||
// so we remove it before returning to the user
|
||||
delete tpl_val.auth_id;
|
||||
return { template: tpl_val };
|
||||
},
|
||||
function finish(err, response){
|
||||
if (err){
|
||||
var statusCode = 400;
|
||||
response = { error: ''+err };
|
||||
if ( ! _.isUndefined(err.http_status) ) {
|
||||
statusCode = err.http_status;
|
||||
}
|
||||
self.app.sendError(res, response, statusCode, 'GET TEMPLATE', err);
|
||||
} else {
|
||||
self.app.sendResponse(res, [response, 200]);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Delete a specific template
|
||||
TemplateMapsController.prototype.destroy = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
if ( req.profiler && req.profiler.statsd_client ) {
|
||||
req.profiler.start('windshaft-cartodb.delete_template');
|
||||
}
|
||||
this.app.doCORS(res);
|
||||
|
||||
var cdbuser = this.serverOptions.userByReq(req);
|
||||
var template;
|
||||
var tpl_id;
|
||||
Step(
|
||||
function checkPerms(){
|
||||
self.serverOptions.authorizedByAPIKey(req, this);
|
||||
},
|
||||
function updateTemplate(err, authenticated) {
|
||||
if ( err ) throw err;
|
||||
if (authenticated !== 1) {
|
||||
err = new Error("Only authenticated users can delete template maps");
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
tpl_id = req.params.template_id.split('@');
|
||||
if ( tpl_id.length > 1 ) {
|
||||
if ( tpl_id[0] != cdbuser ) {
|
||||
var err = new Error("Cannot find template id '"
|
||||
+ req.params.template_id + "' for user '" + cdbuser + "'");
|
||||
err.http_status = 404;
|
||||
throw err;
|
||||
}
|
||||
tpl_id = tpl_id[1];
|
||||
}
|
||||
self.templateMaps.delTemplate(cdbuser, tpl_id, this);
|
||||
},
|
||||
function prepareResponse(err, tpl_val){
|
||||
if ( err ) throw err;
|
||||
return { status: 'ok' };
|
||||
},
|
||||
function finish(err, response){
|
||||
if (err){
|
||||
var statusCode = 400;
|
||||
response = { error: ''+err };
|
||||
if ( ! _.isUndefined(err.http_status) ) {
|
||||
statusCode = err.http_status;
|
||||
}
|
||||
self.app.sendError(res, response, statusCode, 'DELETE TEMPLATE', err);
|
||||
} else {
|
||||
self.app.sendResponse(res, ['', 204]);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Get a list of owned templates
|
||||
TemplateMapsController.prototype.list = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
if ( req.profiler && req.profiler.statsd_client ) {
|
||||
req.profiler.start('windshaft-cartodb.get_template_list');
|
||||
}
|
||||
this.app.doCORS(res);
|
||||
|
||||
var cdbuser = this.serverOptions.userByReq(req);
|
||||
|
||||
Step(
|
||||
function checkPerms(){
|
||||
self.serverOptions.authorizedByAPIKey(req, this);
|
||||
},
|
||||
function listTemplates(err, authenticated) {
|
||||
if ( err ) throw err;
|
||||
if (authenticated !== 1) {
|
||||
err = new Error("Only authenticated user can list templated maps");
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
self.templateMaps.listTemplates(cdbuser, this);
|
||||
},
|
||||
function prepareResponse(err, tpl_ids){
|
||||
if ( err ) throw err;
|
||||
// NOTE: might omit "cbduser" if == dbowner ...
|
||||
var ids = _.map(tpl_ids, function(id) { return cdbuser + '@' + id; });
|
||||
return { template_ids: ids };
|
||||
},
|
||||
function finish(err, response){
|
||||
var statusCode = 200;
|
||||
if (err){
|
||||
response = { error: ''+err };
|
||||
if ( ! _.isUndefined(err.http_status) ) {
|
||||
statusCode = err.http_status;
|
||||
}
|
||||
self.app.sendError(res, response, statusCode, 'GET TEMPLATE LIST', err);
|
||||
} else {
|
||||
self.app.sendResponse(res, [response, statusCode]);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
TemplateMapsController.prototype.instantiate = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
if ( req.profiler && req.profiler.statsd_client) {
|
||||
req.profiler.start('windshaft-cartodb.instance_template_post');
|
||||
}
|
||||
Step(
|
||||
function() {
|
||||
if ( ! req.headers['content-type'] || req.headers['content-type'].split(';')[0] != 'application/json') {
|
||||
throw new Error('template POST data must be of type application/json, it is instead ');
|
||||
}
|
||||
self.instantiateTemplate(req, res, req.body, this);
|
||||
}, function(err, response) {
|
||||
self.finish_instantiation(err, response, res, req);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
TemplateMapsController.prototype.options = function(req, res) {
|
||||
this.app.doCORS(res, "Content-Type");
|
||||
return next();
|
||||
};
|
||||
|
||||
/**
|
||||
* jsonp endpoint, allows to instantiate a template with a json call.
|
||||
* callback query argument is mandatory
|
||||
*/
|
||||
TemplateMapsController.prototype.jsonp = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
if ( req.profiler && req.profiler.statsd_client) {
|
||||
req.profiler.start('windshaft-cartodb.instance_template_get');
|
||||
}
|
||||
Step(
|
||||
function() {
|
||||
if ( req.query.callback === undefined || req.query.callback.length === 0) {
|
||||
throw new Error('callback parameter should be present and be a function name');
|
||||
}
|
||||
var config = {};
|
||||
if(req.query.config) {
|
||||
try {
|
||||
config = JSON.parse(req.query.config);
|
||||
} catch(e) {
|
||||
throw new Error('badformed config parameter, should be a valid JSON');
|
||||
}
|
||||
}
|
||||
self.instantiateTemplate(req, res, config, this);
|
||||
}, function(err, response) {
|
||||
self.finish_instantiation(err, response, res, req);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// Instantiate a template
|
||||
TemplateMapsController.prototype.instantiateTemplate = function(req, res, template_params, callback) {
|
||||
var self = this;
|
||||
|
||||
this.app.doCORS(res);
|
||||
|
||||
var template;
|
||||
var layergroup;
|
||||
var fakereq; // used for call to createLayergroup
|
||||
var cdbuser = self.serverOptions.userByReq(req);
|
||||
// Format of template_id: [<template_owner>]@<template_id>
|
||||
var tpl_id = req.params.template_id.split('@');
|
||||
if ( tpl_id.length > 1 ) {
|
||||
if ( tpl_id[0] && tpl_id[0] != cdbuser ) {
|
||||
var err = new Error('Cannot instanciate map of user "'
|
||||
+ tpl_id[0] + '" on database of user "'
|
||||
+ cdbuser + '"');
|
||||
err.http_status = 403;
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
tpl_id = tpl_id[1];
|
||||
}
|
||||
var auth_token = req.query.auth_token;
|
||||
Step(
|
||||
function getTemplate(){
|
||||
self.templateMaps.getTemplate(cdbuser, tpl_id, this);
|
||||
},
|
||||
function checkAuthorized(err, templateValue) {
|
||||
if ( req.profiler ) req.profiler.done('getTemplate');
|
||||
if ( err ) throw err;
|
||||
if ( ! templateValue ) {
|
||||
err = new Error("Template '" + tpl_id + "' of user '" + cdbuser + "' not found");
|
||||
err.http_status = 404;
|
||||
throw err;
|
||||
}
|
||||
|
||||
template = templateValue;
|
||||
|
||||
var authorized = false;
|
||||
try {
|
||||
authorized = self.templateMaps.isAuthorized(template, auth_token);
|
||||
} catch (err) {
|
||||
// we catch to add http_status
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
if ( ! authorized ) {
|
||||
err = new Error('Unauthorized template instanciation');
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (req.profiler) {
|
||||
req.profiler.done('authorizedByCert');
|
||||
}
|
||||
|
||||
return self.templateMaps.instance(template, template_params);
|
||||
},
|
||||
function prepareParams(err, instance){
|
||||
if ( req.profiler ) req.profiler.done('TemplateMaps_instance');
|
||||
if ( err ) throw err;
|
||||
layergroup = instance;
|
||||
fakereq = { query: {}, params: {}, headers: _.clone(req.headers),
|
||||
method: req.method,
|
||||
res: res,
|
||||
profiler: req.profiler
|
||||
};
|
||||
self.setDBParams(cdbuser, fakereq.params, this);
|
||||
},
|
||||
function setApiKey(err){
|
||||
if ( req.profiler ) req.profiler.done('setDBParams');
|
||||
if ( err ) throw err;
|
||||
self.metadataBackend.getUserMapKey(cdbuser, this);
|
||||
},
|
||||
function createLayergroup(err, val) {
|
||||
if ( req.profiler ) req.profiler.done('getUserMapKey');
|
||||
if ( err ) throw err;
|
||||
fakereq.params.api_key = val;
|
||||
self.app.createLayergroup(layergroup, fakereq, this);
|
||||
},
|
||||
function prepareResponse(err, layergroup) {
|
||||
if ( err ) {
|
||||
throw err;
|
||||
}
|
||||
var tplhash = self.templateMaps.fingerPrint(template).substring(0,8);
|
||||
layergroup.layergroupid = cdbuser + '@' + tplhash + '@' + layergroup.layergroupid;
|
||||
|
||||
self.surrogateKeysCache.tag(res, new self.NamedMapsCacheEntry(cdbuser, template.name));
|
||||
|
||||
return layergroup;
|
||||
},
|
||||
callback
|
||||
);
|
||||
};
|
||||
|
||||
TemplateMapsController.prototype.finish_instantiation = function(err, response, res, req) {
|
||||
if ( req.profiler ) {
|
||||
res.header('X-Tiler-Profiler', req.profiler.toJSONString());
|
||||
}
|
||||
if (err) {
|
||||
var statusCode = 400;
|
||||
response = { error: ''+err };
|
||||
if ( ! _.isUndefined(err.http_status) ) {
|
||||
statusCode = err.http_status;
|
||||
}
|
||||
if(global.environment.debug) {
|
||||
response.stack = err.stack;
|
||||
}
|
||||
this.app.sendError(res, response, statusCode, 'POST INSTANCE TEMPLATE', err);
|
||||
} else {
|
||||
this.app.sendResponse(res, [response, 200]);
|
||||
}
|
||||
};
|
||||
|
||||
TemplateMapsController.prototype.setDBParams = function(cdbuser, params, callback) {
|
||||
var self = this;
|
||||
Step(
|
||||
function setAuth() {
|
||||
self.serverOptions.setDBAuth(cdbuser, params, this);
|
||||
},
|
||||
function setConn(err) {
|
||||
if ( err ) throw err;
|
||||
self.serverOptions.setDBConn(cdbuser, params, this);
|
||||
},
|
||||
function finish(err) {
|
||||
callback(err);
|
||||
}
|
||||
);
|
||||
};
|
||||
49
lib/cartodb/log4js_rollbar.js
Normal file
49
lib/cartodb/log4js_rollbar.js
Normal file
@@ -0,0 +1,49 @@
|
||||
var rollbar = require("rollbar");
|
||||
|
||||
/**
|
||||
* Rollbar Appender. Sends logging events to Rollbar using node-rollbar
|
||||
*
|
||||
* @param config object with rollbar configuration data
|
||||
* {
|
||||
* token: 'your-secret-token',
|
||||
* options: node-rollbar options
|
||||
* }
|
||||
*/
|
||||
function rollbarAppender(config) {
|
||||
|
||||
var opt = config.options;
|
||||
rollbar.init(opt.token, opt.options);
|
||||
|
||||
return function(loggingEvent) {
|
||||
/*
|
||||
For logger.trace('one','two','three'):
|
||||
{ startTime: Wed Mar 12 2014 16:27:40 GMT+0100 (CET),
|
||||
categoryName: '[default]',
|
||||
data: [ 'one', 'two', 'three' ],
|
||||
level: { level: 5000, levelStr: 'TRACE' },
|
||||
logger: { category: '[default]', _events: { log: [Object] } } }
|
||||
*/
|
||||
|
||||
// Levels:
|
||||
// TRACE 5000
|
||||
// DEBUG 10000
|
||||
// INFO 20000
|
||||
// WARN 30000
|
||||
// ERROR 40000
|
||||
// FATAL 50000
|
||||
//
|
||||
// We only log error and higher errors
|
||||
//
|
||||
if ( loggingEvent.level.level < 40000 ) return;
|
||||
|
||||
rollbar.reportMessage(loggingEvent.data);
|
||||
};
|
||||
}
|
||||
|
||||
function configure(config) {
|
||||
return rollbarAppender(config);
|
||||
}
|
||||
|
||||
exports.name = "rollbar";
|
||||
exports.appender = rollbarAppender;
|
||||
exports.configure = configure;
|
||||
90
lib/cartodb/monitoring/health_check.js
Normal file
90
lib/cartodb/monitoring/health_check.js
Normal file
@@ -0,0 +1,90 @@
|
||||
var _ = require('underscore'),
|
||||
dot = require('dot'),
|
||||
fs = require('fs'),
|
||||
path = require('path'),
|
||||
Step = require('step');
|
||||
|
||||
function HealthCheck(metadataBackend, tilelive) {
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.tilelive = tilelive;
|
||||
}
|
||||
|
||||
module.exports = HealthCheck;
|
||||
|
||||
|
||||
var mapnikOptions = {
|
||||
query: {
|
||||
metatile: 1,
|
||||
poolSize: 4,
|
||||
bufferSize: 64
|
||||
},
|
||||
protocol: 'mapnik:',
|
||||
slashes: true,
|
||||
xml: null
|
||||
};
|
||||
|
||||
var xmlTemplate = dot.template(fs.readFileSync(path.resolve(__dirname, 'map-config.xml'), 'utf-8'));
|
||||
|
||||
HealthCheck.prototype.check = function(config, callback) {
|
||||
|
||||
var self = this,
|
||||
startTime,
|
||||
result = {
|
||||
redis: {
|
||||
ok: false
|
||||
},
|
||||
mapnik: {
|
||||
ok: false
|
||||
},
|
||||
tile: {
|
||||
ok: false
|
||||
}
|
||||
};
|
||||
mapnikXmlParams = config;
|
||||
|
||||
Step(
|
||||
function getDBParams() {
|
||||
startTime = Date.now();
|
||||
self.metadataBackend.getAllUserDBParams(config.username, this);
|
||||
},
|
||||
function loadMapnik(err, dbParams) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
result.redis = {
|
||||
ok: !err,
|
||||
elapsed: Date.now() - startTime,
|
||||
size: Object.keys(dbParams).length
|
||||
};
|
||||
mapnikOptions.xml = xmlTemplate(mapnikXmlParams);
|
||||
|
||||
startTime = Date.now();
|
||||
self.tilelive.load(mapnikOptions, this);
|
||||
},
|
||||
function getTile(err, source) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
result.mapnik = {
|
||||
ok: !err,
|
||||
elapsed: Date.now() - startTime
|
||||
};
|
||||
|
||||
startTime = Date.now();
|
||||
source.getTile(config.z, config.x, config.y, this);
|
||||
},
|
||||
function handleTile(err, tile) {
|
||||
result.tile = {
|
||||
ok: !err
|
||||
};
|
||||
|
||||
if (tile) {
|
||||
result.tile.elapsed = Date.now() - startTime;
|
||||
result.tile.size = tile.length;
|
||||
}
|
||||
|
||||
callback(err, result);
|
||||
}
|
||||
);
|
||||
};
|
||||
4
lib/cartodb/monitoring/map-config.xml
Normal file
4
lib/cartodb/monitoring/map-config.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<Map
|
||||
background-color="#c33"
|
||||
srs="+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs">
|
||||
</Map>
|
||||
@@ -1,80 +0,0 @@
|
||||
/**
|
||||
* RedisPool. A database specific redis pooling lib
|
||||
*
|
||||
*/
|
||||
|
||||
var redis = require('redis')
|
||||
, _ = require('underscore')
|
||||
, Pool = require('generic-pool').Pool;
|
||||
|
||||
// constructor.
|
||||
//
|
||||
// - `opts` {Object} optional config for redis and pooling
|
||||
var RedisPool = function(opts){
|
||||
var opts = opts || {};
|
||||
var defaults = {
|
||||
host: '127.0.0.1',
|
||||
port: '6379',
|
||||
max: 50,
|
||||
idleTimeoutMillis: 10000,
|
||||
reapIntervalMillis: 1000,
|
||||
log: false
|
||||
};
|
||||
var options = _.defaults(opts, defaults)
|
||||
|
||||
var me = {
|
||||
pools: {} // cached pools by DB name
|
||||
};
|
||||
|
||||
// Acquire resource.
|
||||
//
|
||||
// - `database` {String} redis database name
|
||||
// - `callback` {Function} callback to call once acquired. Takes the form
|
||||
// `callback(err, resource)`
|
||||
me.acquire = function(database, callback) {
|
||||
if (!this.pools[database]) {
|
||||
this.pools[database] = this.makePool(database);
|
||||
}
|
||||
this.pools[database].acquire(function(err,resource) {
|
||||
callback(err, resource);
|
||||
});
|
||||
};
|
||||
|
||||
// Release resource.
|
||||
//
|
||||
// - `database` {String} redis database name
|
||||
// - `resource` {Object} resource object to release
|
||||
me.release = function(database, resource) {
|
||||
this.pools[database] && this.pools[database].release(resource);
|
||||
};
|
||||
|
||||
// Factory for pool objects.
|
||||
me.makePool = function(database) {
|
||||
return Pool({
|
||||
name: database,
|
||||
create: function(callback){
|
||||
var client = redis.createClient(options.port, options.host);
|
||||
client.on('connect', function () {
|
||||
client.send_anyway = true;
|
||||
client.select(database);
|
||||
client.send_anyway = false;
|
||||
callback(null, client);
|
||||
});
|
||||
client.on('error', function (err) {
|
||||
callback(err, null);
|
||||
});
|
||||
},
|
||||
destroy: function(client) {
|
||||
return client.quit();
|
||||
},
|
||||
max: options.max,
|
||||
idleTimeoutMillis: options.idleTimeoutMillis,
|
||||
reapIntervalMillis: options.reapIntervalMillis,
|
||||
log: options.log
|
||||
});
|
||||
};
|
||||
|
||||
return me;
|
||||
};
|
||||
|
||||
module.exports = RedisPool;
|
||||
@@ -1,56 +1,108 @@
|
||||
var _ = require('underscore')
|
||||
, Step = require('step')
|
||||
, cartoData = require('./carto_data')
|
||||
, Cache = require('./cache_validator')
|
||||
, mapnik = require('mapnik')
|
||||
, Cache = require('./cache_validator')
|
||||
, QueryTablesApi = require('./api/query_tables_api')
|
||||
, crypto = require('crypto')
|
||||
, request = require('request')
|
||||
, LZMA = require('lzma/lzma_worker.js').LZMA
|
||||
, LZMA = require('lzma').LZMA
|
||||
;
|
||||
|
||||
// This is for backward compatibility with 1.3.3
|
||||
if ( ! global.environment.sqlapi.domain ) global.environment.sqlapi.host;
|
||||
if ( _.isUndefined(global.environment.sqlapi.domain) ) {
|
||||
// Only use "host" as "domain" if it contains alphanumeric characters
|
||||
var host = global.environment.sqlapi.host;
|
||||
if ( host && host.match(/[a-zA-Z]/) ) {
|
||||
global.environment.sqlapi.domain = host;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = function(){
|
||||
// Whitelist query parameters and attach format
|
||||
var REQUEST_QUERY_PARAMS_WHITELIST = [
|
||||
'sql',
|
||||
'geom_type',
|
||||
'cache_buster',
|
||||
'cache_policy',
|
||||
'callback',
|
||||
'interactivity',
|
||||
'map_key',
|
||||
'api_key',
|
||||
'auth_token',
|
||||
'style',
|
||||
'style_version',
|
||||
'style_convert',
|
||||
'config',
|
||||
'scale_factor'
|
||||
];
|
||||
|
||||
module.exports = function(redisPool) {
|
||||
var redisOpts = redisPool ? {pool: redisPool} : global.environment.redis;
|
||||
var cartoData = require('cartodb-redis')(redisOpts),
|
||||
lzmaWorker = new LZMA(),
|
||||
queryTablesApi = new QueryTablesApi();
|
||||
|
||||
var rendererConfig = _.defaults(global.environment.renderer || {}, {
|
||||
cache_ttl: 60000, // milliseconds
|
||||
metatile: 4,
|
||||
bufferSize: 64
|
||||
cache_ttl: 60000, // milliseconds
|
||||
metatile: 4,
|
||||
bufferSize: 64,
|
||||
statsInterval: 60000
|
||||
});
|
||||
|
||||
var me = {
|
||||
base_url: '/tiles/:table',
|
||||
base_url_notable: '/tiles',
|
||||
// This is for inline maps and table maps
|
||||
base_url: global.environment.base_url_legacy || '/tiles/:table',
|
||||
|
||||
/// @deprecated with Windshaft-0.17.0
|
||||
///base_url_notable: '/tiles',
|
||||
|
||||
// This is for Detached maps
|
||||
//
|
||||
// "maps" is the official, while
|
||||
// "tiles/layergroup" is for backward compatibility up to 1.6.x
|
||||
//
|
||||
base_url_mapconfig: global.environment.base_url_detached || '(?:/maps|/tiles/layergroup)',
|
||||
|
||||
grainstore: {
|
||||
map: {
|
||||
// TODO: allow to specify in configuration
|
||||
srid: 3857
|
||||
},
|
||||
datasource: global.environment.postgres,
|
||||
cachedir: global.environment.millstone.cache_basedir,
|
||||
mapnik_version: global.environment.mapnik_version || mapnik.versions.mapnik,
|
||||
default_layergroup_ttl: 7200 // seconds (defaultis 300)
|
||||
mapnik_version: global.environment.mapnik_version,
|
||||
mapnik_tile_format: global.environment.mapnik_tile_format || 'png',
|
||||
default_layergroup_ttl: global.environment.mapConfigTTL || 7200,
|
||||
gc_prob: 0.01 // @deprecated since Windshaft-1.8.0
|
||||
},
|
||||
mapnik: {
|
||||
poolSize: rendererConfig.poolSize,
|
||||
metatile: rendererConfig.metatile,
|
||||
bufferSize: rendererConfig.bufferSize
|
||||
},
|
||||
statsd: global.environment.statsd,
|
||||
renderCache: {
|
||||
ttl: rendererConfig.cache_ttl
|
||||
ttl: rendererConfig.cache_ttl,
|
||||
statsInterval: rendererConfig.statsInterval
|
||||
},
|
||||
renderer: {
|
||||
http: rendererConfig.http
|
||||
},
|
||||
redis: global.environment.redis,
|
||||
enable_cors: global.environment.enable_cors,
|
||||
varnish_host: global.environment.varnish.host,
|
||||
varnish_port: global.environment.varnish.port,
|
||||
varnish_http_port: global.environment.varnish.http_port,
|
||||
varnish_secret: global.environment.varnish.secret,
|
||||
varnish_purge_enabled: global.environment.varnish.purge_enabled,
|
||||
cache_enabled: global.environment.cache_enabled,
|
||||
log_format: global.environment.log_format,
|
||||
useProfiler: global.environment.useProfiler
|
||||
};
|
||||
|
||||
// Do not send unwatch on release
|
||||
// See http://github.com/CartoDB/Windshaft-cartodb/issues/161
|
||||
me.redis.unwatchOnRelease = false;
|
||||
|
||||
// Be nice and warn if configured mapnik version
|
||||
// is != instaled mapnik version
|
||||
if ( mapnik.versions.mapnik != me.grainstore.mapnik_version ) {
|
||||
console.warn("WARNING: detected mapnik version ("
|
||||
+ mapnik.versions.mapnik + ") != configured mapnik version ("
|
||||
+ me.grainstore.mapnik_version + ")");
|
||||
}
|
||||
// Re-use redisPool
|
||||
me.redis.pool = redisPool;
|
||||
|
||||
/* This whole block is about generating X-Cache-Channel { */
|
||||
|
||||
@@ -60,84 +112,6 @@ module.exports = function(){
|
||||
// we have no SQL after layer creation.
|
||||
me.channelCache = {};
|
||||
|
||||
// Run a query through the SQL api
|
||||
me.sqlQuery = function (username, api_key, sql, callback) {
|
||||
var api = global.environment.sqlapi;
|
||||
|
||||
// build up api string
|
||||
var sqlapi = api.protocol + '://' + username + '.' + api.domain + ':' + api.port + '/api/' + api.version + '/sql'
|
||||
|
||||
var qs = { q: sql }
|
||||
|
||||
// add api_key if given
|
||||
if (_.isString(api_key) && api_key != '') { qs.api_key = api_key; }
|
||||
|
||||
// call sql api
|
||||
request.get({url:sqlapi, qs:qs, json:true}, function(err, res, body){
|
||||
if (err){
|
||||
console.log('ERROR running connecting to SQL API on ' + sqlapi + ': ' + err);
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
if (res.statusCode != 200) {
|
||||
var msg = res.body.error ? res.body.error : res.body;
|
||||
callback(new Error(msg));
|
||||
console.log('unexpected response status (' + res.statusCode + ') for sql query: ' + sql + ': ' + msg);
|
||||
return;
|
||||
}
|
||||
callback(null, body.rows);
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Invoke callback with number of milliseconds since
|
||||
// last update in any of the given tables
|
||||
//
|
||||
me.findLastUpdated = function (username, api_key, tableNames, callback) {
|
||||
var sql = 'SELECT EXTRACT(EPOCH FROM max(updated_at)) as max FROM CDB_TableMetadata m WHERE m.tabname::name = any (\'{'
|
||||
+ tableNames.join(',') + '}\')';
|
||||
|
||||
// call sql api
|
||||
me.sqlQuery(username, api_key, sql, function(err, rows){
|
||||
if (err){
|
||||
var msg = err.message ? err.message : err;
|
||||
callback(new Error('could not find last updated timestamp: ' + msg));
|
||||
return;
|
||||
}
|
||||
// when the table has not updated_at means it hasn't been changed so a default last_updated is set
|
||||
var last_updated = 0;
|
||||
if(rows.length !== 0) {
|
||||
last_updated = rows[0].max || 0;
|
||||
}
|
||||
callback(null, last_updated*1000);
|
||||
});
|
||||
};
|
||||
|
||||
me.affectedTables = function (username, api_key, sql, callback) {
|
||||
|
||||
// Replace mapnik tokens
|
||||
sql = sql.replace(RegExp('!bbox!', 'g'), 'ST_MakeEnvelope(0,0,0,0)')
|
||||
.replace(RegExp('!pixel_width!', 'g'), '1')
|
||||
.replace(RegExp('!pixel_height!', 'g'), '1')
|
||||
;
|
||||
|
||||
// Pass to CDB_QueryTables
|
||||
sql = 'SELECT CDB_QueryTables($windshaft$' + sql + '$windshaft$)';
|
||||
|
||||
// call sql api
|
||||
me.sqlQuery(username, api_key, sql, function(err, rows){
|
||||
if (err){
|
||||
var msg = err.message ? err.message : err;
|
||||
callback(new Error('could not fetch source tables: ' + msg));
|
||||
return;
|
||||
}
|
||||
var qtables = rows[0].cdb_querytables;
|
||||
var tableNames = qtables.split(/^\{(.*)\}$/)[1];
|
||||
tableNames = tableNames.split(',');
|
||||
callback(null, tableNames);
|
||||
});
|
||||
};
|
||||
|
||||
me.buildCacheChannel = function (dbName, tableNames){
|
||||
return dbName + ':' + tableNames.join(',');
|
||||
};
|
||||
@@ -146,55 +120,132 @@ module.exports = function(){
|
||||
var hash = crypto.createHash('md5');
|
||||
hash.update(data);
|
||||
return hash.digest('hex');
|
||||
}
|
||||
};
|
||||
|
||||
me.generateCacheChannel = function(req, callback){
|
||||
|
||||
// use key to call sql api with sql request if present, else
|
||||
// just return dbname and table name base key
|
||||
var dbName = req.params.dbname;
|
||||
me.generateCacheChannel = function(app, req, callback){
|
||||
|
||||
// Build channelCache key
|
||||
var dbName = req.params.dbname;
|
||||
var cacheKey = [ dbName ];
|
||||
if ( req.params.token ) cacheKey.push(req.params.token);
|
||||
else if ( req.params.sql ) cacheKey.push( me.generateMD5(req.params.sql) );
|
||||
cacheKey = cacheKey.join(':');
|
||||
|
||||
if ( me.channelCache.hasOwnProperty(cacheKey) ) {
|
||||
callback(null, me.channelCache[cacheKey]);
|
||||
return;
|
||||
}
|
||||
else if ( req.params.token ) {
|
||||
// cached cache channel for token-based access should be constructed
|
||||
// at cache creation time
|
||||
callback(new Error('missing channel cache for token ' + req.params.token));
|
||||
return;
|
||||
}
|
||||
var that = this;
|
||||
|
||||
if ( ! req.params.sql && ! req.params.token ) {
|
||||
var cacheChannel = me.buildCacheChannel(dbName, [req.params.table]);
|
||||
// not worth caching this
|
||||
callback(null, cacheChannel);
|
||||
return;
|
||||
}
|
||||
Step (
|
||||
function checkCached() {
|
||||
if ( me.channelCache.hasOwnProperty(cacheKey) ) {
|
||||
callback(null, me.channelCache[cacheKey]);
|
||||
return;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
function extractSQL(err) {
|
||||
if ( err ) throw err;
|
||||
|
||||
if ( ! req.params.sql ) {
|
||||
callback(new Error("this request doesn't need an X-Cache-Channel generated"));
|
||||
return;
|
||||
}
|
||||
if ( req.params.token ) {
|
||||
// TODO: cached cache channel for token-based access should
|
||||
// be constructed at renderer cache creation time
|
||||
// See http://github.com/CartoDB/Windshaft-cartodb/issues/152
|
||||
if ( ! app.mapStore ) {
|
||||
throw new Error('missing channel cache for token ' + req.params.token);
|
||||
}
|
||||
var next = this;
|
||||
var mapStore = app.mapStore;
|
||||
Step(
|
||||
function loadFromStore() {
|
||||
mapStore.load(req.params.token, this);
|
||||
},
|
||||
function getSQL(err, mapConfig) {
|
||||
if (req.profiler) req.profiler.done('mapStore_load');
|
||||
if ( err ) throw err;
|
||||
var sql = [];
|
||||
_.each(mapConfig.obj().layers, function(lyr) {
|
||||
sql.push(lyr.options.sql);
|
||||
});
|
||||
sql = sql.join(';');
|
||||
return sql;
|
||||
},
|
||||
function finish(err, sql) {
|
||||
next(err, sql);
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
var dbName = req.params.dbname;
|
||||
var username = req.headers.host.split('.')[0];
|
||||
if ( ! req.params.sql ) {
|
||||
return null; // no sql
|
||||
}
|
||||
|
||||
// strip out windshaft/mapnik inserted sql if present
|
||||
var sql = req.params.sql.match(/^\((.*)\)\sas\scdbq$/);
|
||||
sql = (sql != null) ? sql[1] : req.params.sql;
|
||||
// We have sql, and no token...
|
||||
|
||||
me.affectedTables(username, req.params.map_key, sql, function(err, tableNames) {
|
||||
if ( err ) { callback(err); return; }
|
||||
// strip out windshaft/mapnik inserted sql if present
|
||||
var sql = req.params.sql.match(/^\((.*)\)\sas\scdbq$/);
|
||||
sql = (sql != null) ? sql[1] : req.params.sql;
|
||||
|
||||
return sql;
|
||||
},
|
||||
function findAffectedTables(err, sql) {
|
||||
if ( err ) throw err;
|
||||
if ( ! sql ) {
|
||||
if ( ! req.params.table ) {
|
||||
throw new Error("this request doesn't need an X-Cache-Channel generated");
|
||||
}
|
||||
return [req.params.table];
|
||||
}
|
||||
var user, key;
|
||||
var next = this;
|
||||
Step (
|
||||
function findUserKey() {
|
||||
if ( req.params.hasOwnProperty('_authorizedBySigner') ) {
|
||||
user = req.params._authorizedBySigner;
|
||||
cartoData.getUserMapKey(user, this);
|
||||
} else {
|
||||
user = that.userByReq(req);
|
||||
key = req.params.map_key || req.params.api_key;
|
||||
return null;
|
||||
}
|
||||
},
|
||||
function getAffected(err, data) {
|
||||
if ( err ) throw err;
|
||||
if ( data ) {
|
||||
if ( req.profiler ) req.profiler.done('getSignerMapKey');
|
||||
key = data;
|
||||
}
|
||||
queryTablesApi.getAffectedTablesInQuery(user, {
|
||||
user: req.params.dbuser,
|
||||
pass: req.params.dbpass,
|
||||
host: req.params.dbhost,
|
||||
port: req.params.dbport,
|
||||
dbname: req.params.dbname,
|
||||
api_key: key
|
||||
}, sql, this); // in addCacheChannel
|
||||
},
|
||||
function finish(err, data) {
|
||||
next(err,data);
|
||||
}
|
||||
);
|
||||
},
|
||||
function buildCacheChannel(err, tableNames) {
|
||||
if ( err ) throw err;
|
||||
if (req.profiler && ! req.params.table ) {
|
||||
req.profiler.done('affectedTables');
|
||||
}
|
||||
|
||||
var dbName = req.params.dbname;
|
||||
var cacheChannel = me.buildCacheChannel(dbName,tableNames);
|
||||
me.channelCache[cacheKey] = cacheChannel; // store for caching
|
||||
callback(null, cacheChannel);
|
||||
});
|
||||
// store for caching from me.generateCacheChannel
|
||||
// (not worth when table was specified in params)
|
||||
if ( ! req.params.table ) {
|
||||
me.channelCache[cacheKey] = cacheChannel;
|
||||
}
|
||||
return cacheChannel;
|
||||
},
|
||||
function finish(err, cacheChannel) {
|
||||
callback(err, cacheChannel);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Set the cache chanel info to invalidate the cache on the frontend server
|
||||
@@ -206,9 +257,10 @@ module.exports = function(){
|
||||
// @param cb function(err, channel) will be called when ready.
|
||||
// the channel parameter will be null if nothing was added
|
||||
//
|
||||
me.addCacheChannel = function(req, cb) {
|
||||
me.addCacheChannel = function(app, req, cb) {
|
||||
// skip non-GET requests, or requests for which there's no response
|
||||
if ( req.method != 'GET' || ! req.res ) { cb(null, null); return; }
|
||||
if (req.profiler) req.profiler.start('addCacheChannel');
|
||||
var res = req.res;
|
||||
var cache_policy = req.query.cache_policy;
|
||||
if ( req.params.token ) cache_policy = 'persist';
|
||||
@@ -230,10 +282,12 @@ module.exports = function(){
|
||||
}
|
||||
res.header('Last-Modified', lastUpdated.toUTCString());
|
||||
|
||||
me.generateCacheChannel(req, function(err, channel){
|
||||
me.generateCacheChannel(app, req, function(err, channel){
|
||||
if (req.profiler) req.profiler.done('generateCacheChannel');
|
||||
if (req.profiler) req.profiler.end();
|
||||
if ( ! err ) {
|
||||
res.header('X-Cache-Channel', channel);
|
||||
cb(null, channel);
|
||||
cb(null, channel);
|
||||
} else {
|
||||
console.log('ERROR generating cache channel: ' + ( err.message ? err.message : err ));
|
||||
// TODO: evaluate if we should bubble up the error instead
|
||||
@@ -245,7 +299,7 @@ module.exports = function(){
|
||||
me.afterLayergroupCreate = function(req, mapconfig, response, callback) {
|
||||
var token = response.layergroupid;
|
||||
|
||||
var username = cartoData.userFromHostname(req.headers.host);
|
||||
var username = this.userByReq(req);
|
||||
|
||||
var tasksleft = 2; // redis key and affectedTables
|
||||
var errors = [];
|
||||
@@ -258,12 +312,21 @@ module.exports = function(){
|
||||
err = errors.length ? new Error(errors.join('\n')) : null;
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
// include in layergroup response the variables in serverMedata
|
||||
// those variables are useful to send to the client information
|
||||
// about how to reach this server or information about it
|
||||
var serverMetadata = global.environment.serverMetadata;
|
||||
if (serverMetadata) {
|
||||
_.extend(response, serverMetadata);
|
||||
}
|
||||
|
||||
// Don't wait for the mapview count increment to
|
||||
// take place before proceeding. Error will be logged
|
||||
// asyncronously
|
||||
cartoData.incMapviewCount(username, mapconfig.stat_tag, function(err) {
|
||||
if (req.profiler) req.profiler.done('incMapviewCount');
|
||||
if ( err ) console.log("ERROR: failed to increment mapview count for user '" + username + "': " + err);
|
||||
done();
|
||||
});
|
||||
@@ -275,28 +338,316 @@ module.exports = function(){
|
||||
sql = sql.join(';');
|
||||
|
||||
var dbName = req.params.dbname;
|
||||
var usr = req.headers.host.split('.')[0];
|
||||
var key = req.params.map_key;
|
||||
var usr = this.userByReq(req);
|
||||
var key = req.params.map_key || req.params.api_key;
|
||||
|
||||
var cacheKey = dbName + ':' + token;
|
||||
|
||||
me.affectedTables(usr, key, sql, function(err, tableNames) {
|
||||
Step(
|
||||
function getAffectedTablesAndLastUpdatedTime() {
|
||||
queryTablesApi.getAffectedTablesAndLastUpdatedTime(usr, {
|
||||
user: req.params.dbuser,
|
||||
pass: req.params.dbpass,
|
||||
host: req.params.dbhost,
|
||||
port: req.params.dbport,
|
||||
dbname: req.params.dbname,
|
||||
api_key: key
|
||||
}, sql, this);
|
||||
},
|
||||
function handleAffectedTablesAndLastUpdatedTime(err, result) {
|
||||
if (req.profiler) req.profiler.done('queryTablesAndLastUpdated');
|
||||
if ( err ) throw err;
|
||||
var cacheChannel = me.buildCacheChannel(dbName, result.affectedTables);
|
||||
me.channelCache[cacheKey] = cacheChannel;
|
||||
|
||||
if ( err ) { done(err); return; }
|
||||
var cacheChannel = me.buildCacheChannel(dbName,tableNames);
|
||||
me.channelCache[cacheKey] = cacheChannel; // store for caching
|
||||
// find last updated
|
||||
me.findLastUpdated(usr, key, tableNames, function(err, lastUpdated) {
|
||||
if ( err ) { done(err); return; }
|
||||
response.layergroupid = response.layergroupid + ':' + lastUpdated; // use epoch
|
||||
response.last_updated = new Date(lastUpdated).toISOString(); // TODO: use ISO format
|
||||
done(null);
|
||||
});
|
||||
});
|
||||
if (req.res && req.method == 'GET') {
|
||||
var res = req.res;
|
||||
if ( req.query && req.query.cache_policy == 'persist' ) {
|
||||
res.header('Cache-Control', 'public,max-age=31536000'); // 1 year
|
||||
} else {
|
||||
var ttl = global.environment.varnish.layergroupTtl || 86400;
|
||||
res.header('Cache-Control', 'public,max-age='+ttl+',must-revalidate');
|
||||
}
|
||||
res.header('Last-Modified', (new Date()).toUTCString());
|
||||
res.header('X-Cache-Channel', cacheChannel);
|
||||
}
|
||||
|
||||
// last update for layergroup cache buster
|
||||
response.layergroupid = response.layergroupid + ':' + result.lastUpdatedTime;
|
||||
response.last_updated = new Date(result.lastUpdatedTime).toISOString();
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/* X-Cache-Channel generation } */
|
||||
|
||||
me.re_userFromHost = new RegExp(
|
||||
global.environment.user_from_host ||
|
||||
'^([^\\.]+)\\.' // would extract "strk" from "strk.cartodb.com"
|
||||
);
|
||||
|
||||
me.userByReq = function(req) {
|
||||
var host = req.headers.host;
|
||||
var mat = host.match(this.re_userFromHost);
|
||||
if ( ! mat ) {
|
||||
console.error("ERROR: user pattern '" + this.re_userFromHost
|
||||
+ "' does not match hostname '" + host + "'");
|
||||
return;
|
||||
}
|
||||
// console.log("Matches: "); console.dir(mat);
|
||||
if ( ! mat.length === 2 ) {
|
||||
console.error("ERROR: pattern '" + this.re_userFromHost
|
||||
+ "' gave unexpected matches against '" + host + "': " + mat);
|
||||
return;
|
||||
}
|
||||
return mat[1];
|
||||
};
|
||||
|
||||
// Set db authentication parameters to those of the given username
|
||||
//
|
||||
// @param username the cartodb username, mapped to a database username
|
||||
// via CartodbRedis metadata records
|
||||
//
|
||||
// @param params the parameters to set auth options into
|
||||
// added params are: "dbuser" and "dbpassword"
|
||||
//
|
||||
// @param callback function(err)
|
||||
//
|
||||
me.setDBAuth = function(username, params, callback) {
|
||||
|
||||
var user_params = {};
|
||||
var auth_user = global.environment.postgres_auth_user;
|
||||
var auth_pass = global.environment.postgres_auth_pass;
|
||||
Step(
|
||||
function getId() {
|
||||
cartoData.getUserId(username, this);
|
||||
},
|
||||
function(err, user_id) {
|
||||
if (err) throw err;
|
||||
user_params['user_id'] = user_id;
|
||||
var dbuser = _.template(auth_user, user_params);
|
||||
_.extend(params, {dbuser:dbuser});
|
||||
|
||||
// skip looking up user_password if postgres_auth_pass
|
||||
// doesn't contain the "user_password" label
|
||||
if (!auth_pass || ! auth_pass.match(/\buser_password\b/) ) return null;
|
||||
|
||||
cartoData.getUserDBPass(username, this);
|
||||
},
|
||||
function(err, user_password) {
|
||||
if (err) throw err;
|
||||
user_params['user_password'] = user_password;
|
||||
if ( auth_pass ) {
|
||||
var dbpass = _.template(auth_pass, user_params);
|
||||
_.extend(params, {dbpassword:dbpass});
|
||||
}
|
||||
return true;
|
||||
},
|
||||
function finish(err) {
|
||||
callback(err);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Set db connection parameters to those for the given username
|
||||
//
|
||||
// @param dbowner cartodb username of database owner,
|
||||
// mapped to a database username
|
||||
// via CartodbRedis metadata records
|
||||
//
|
||||
// @param params the parameters to set connection options into
|
||||
// added params are: "dbname", "dbhost"
|
||||
//
|
||||
// @param callback function(err)
|
||||
//
|
||||
me.setDBConn = function(dbowner, params, callback) {
|
||||
// Add default database connection parameters
|
||||
// if none given
|
||||
_.defaults(params, {
|
||||
dbuser: global.environment.postgres.user,
|
||||
dbpassword: global.environment.postgres.password,
|
||||
dbhost: global.environment.postgres.host,
|
||||
dbport: global.environment.postgres.port
|
||||
});
|
||||
Step(
|
||||
function getConnectionParams() {
|
||||
cartoData.getUserDBConnectionParams(dbowner, this);
|
||||
},
|
||||
function extendParams(err, dbParams){
|
||||
if (err) throw err;
|
||||
// we don't want null values or overwrite a non public user
|
||||
if (params.dbuser != 'publicuser' || !dbParams.dbuser) {
|
||||
delete dbParams.dbuser;
|
||||
}
|
||||
if ( dbParams ) _.extend(params, dbParams);
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
callback(err);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// Check if a request is authorized by a signer
|
||||
//
|
||||
// @param req express request object
|
||||
// @param callback function(err, signed_by) signed_by will be
|
||||
// null if the request is not signed by anyone
|
||||
// or will be a string cartodb username otherwise.
|
||||
//
|
||||
me.authorizedBySigner = function(req, callback)
|
||||
{
|
||||
if ( ! req.params.token || ! req.params.signer ) {
|
||||
//console.log("No signature provided"); // debugging
|
||||
callback(null, null); // no signer requested
|
||||
return;
|
||||
}
|
||||
|
||||
var signer = req.params.signer;
|
||||
var layergroup_id = req.params.token;
|
||||
var auth_token = req.params.auth_token;
|
||||
|
||||
//console.log("Checking authorization from signer " + signer + " for resource " + layergroup_id + " with auth_token " + auth_token);
|
||||
var mapStore = req.app.mapStore;
|
||||
if (!mapStore) {
|
||||
throw new Error('Unable to retrieve map configuration token');
|
||||
}
|
||||
|
||||
mapStore.load(layergroup_id, function(err, mapConfig) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
var authorized = me.templateMaps.isAuthorized(mapConfig.obj().template, auth_token);
|
||||
callback(null, authorized ? signer : null);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
// Check if a request is authorized by api_key
|
||||
//
|
||||
// @param req express request object
|
||||
// @param callback function(err, authorized)
|
||||
// NOTE: authorized is expected to be 0 or 1 (integer)
|
||||
//
|
||||
me.authorizedByAPIKey = function(req, callback)
|
||||
{
|
||||
var givenKey = req.query.api_key || req.query.map_key;
|
||||
if ( ! givenKey && req.body ) {
|
||||
// check also in request body
|
||||
givenKey = req.body.api_key || req.body.map_key;
|
||||
}
|
||||
if ( ! givenKey ) {
|
||||
callback(null, 0); // no api key, no authorization...
|
||||
return;
|
||||
}
|
||||
//console.log("given ApiKey: " + givenKey);
|
||||
var user = me.userByReq(req);
|
||||
Step(
|
||||
function (){
|
||||
cartoData.getUserMapKey(user, this);
|
||||
},
|
||||
function checkApiKey(err, val){
|
||||
if (err) throw err;
|
||||
return ( val && givenKey == val ) ? 1 : 0;
|
||||
},
|
||||
function finish(err, authorized) {
|
||||
callback(err, authorized);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check access authorization
|
||||
*
|
||||
* @param req - standard req object. Importantly contains table and host information
|
||||
* @param callback function(err, allowed) is access allowed not?
|
||||
*/
|
||||
me.authorize = function(req, callback) {
|
||||
var that = this;
|
||||
var user = me.userByReq(req);
|
||||
|
||||
Step(
|
||||
function (){
|
||||
that.authorizedByAPIKey(req, this);
|
||||
},
|
||||
function checkApiKey(err, authorized){
|
||||
if (req.profiler) req.profiler.done('authorizedByAPIKey');
|
||||
if (err) throw err;
|
||||
|
||||
// if not authorized by api_key, continue
|
||||
if (authorized !== 1) {
|
||||
// not authorized by api_key,
|
||||
// check if authorized by signer
|
||||
that.authorizedBySigner(req, this);
|
||||
return;
|
||||
}
|
||||
|
||||
_.extend(req.params, { _authorizedByApiKey: true });
|
||||
|
||||
// authorized by api key, login as the given username and stop
|
||||
that.setDBAuth(user, req.params, function(err) {
|
||||
callback(err, true); // authorized (or error)
|
||||
});
|
||||
},
|
||||
function checkSignAuthorized(err, signed_by){
|
||||
if (err) throw err;
|
||||
if (req.profiler) {
|
||||
if ( req.params._authorizedByApiKey ) {
|
||||
req.profiler.done('setDBAuth');
|
||||
} else {
|
||||
req.profiler.done('authorizedBySigner');
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! signed_by ) {
|
||||
// request not authorized by signer.
|
||||
|
||||
// if table was given, continue to check table privacy
|
||||
if ( req.params.table ) return null;
|
||||
|
||||
// if no signer name was given, let dbparams and
|
||||
// PostgreSQL do the rest.
|
||||
//
|
||||
if ( ! req.params.signer ) {
|
||||
callback(null, true); // authorized so far
|
||||
return;
|
||||
}
|
||||
|
||||
// if signer name was given, return no authorization
|
||||
callback(null, false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Authorized by "signed_by" !
|
||||
_.extend(req.params, { _authorizedBySigner: signed_by });
|
||||
that.setDBAuth(signed_by, req.params, function(err) {
|
||||
if (req.profiler) req.profiler.done('setDBAuth');
|
||||
callback(err, true); // authorized (or error)
|
||||
});
|
||||
},
|
||||
function getDatabase(err){
|
||||
if (err) throw err;
|
||||
// NOTE: only used to get to table privacy
|
||||
cartoData.getUserDBName(user, this);
|
||||
},
|
||||
function getPrivacy(err, dbname){
|
||||
if (err) throw err;
|
||||
if (req.profiler) req.profiler.done('tablePrivacy_getUserDBName');
|
||||
cartoData.getTablePrivacy(dbname, req.params.table, this);
|
||||
},
|
||||
function(err, privacy){
|
||||
if (req.profiler) req.profiler.done('getTablePrivacy');
|
||||
callback(err, privacy !== "0");
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Whitelist input and get database name & default geometry type from
|
||||
* subdomain/user metadata held in CartoDB Redis
|
||||
@@ -311,18 +662,18 @@ module.exports = function(){
|
||||
//console.log("type of req.query.lzma is " + typeof(req.query.lzma));
|
||||
|
||||
// Decode (from base64)
|
||||
var lzma = (new Buffer(req.query.lzma, 'base64').toString('binary')).split('').map(function(c) { return c.charCodeAt(0) - 128 })
|
||||
var lzma = (new Buffer(req.query.lzma, 'base64').toString('binary')).split('').map(function(c) { return c.charCodeAt(0) - 128 });
|
||||
|
||||
// Decompress
|
||||
LZMA.decompress(
|
||||
lzmaWorker.decompress(
|
||||
lzma,
|
||||
function(result) {
|
||||
if (req.profiler) req.profiler.done('LZMA decompress');
|
||||
try {
|
||||
delete req.query.lzma
|
||||
_.extend(req.query, JSON.parse(result))
|
||||
delete req.query.lzma;
|
||||
_.extend(req.query, JSON.parse(result));
|
||||
me.req2params(req, callback);
|
||||
} catch (err) {
|
||||
} catch (err) {
|
||||
callback(new Error('Error parsing lzma as JSON: ' + err));
|
||||
}
|
||||
},
|
||||
@@ -333,18 +684,34 @@ module.exports = function(){
|
||||
return;
|
||||
}
|
||||
|
||||
// Whitelist query parameters and attach format
|
||||
var good_query = ['sql', 'geom_type', 'cache_buster', 'cache_policy', 'callback', 'interactivity', 'map_key', 'api_key', 'style', 'style_version', 'style_convert', 'config' ];
|
||||
var bad_query = _.difference(_.keys(req.query), good_query);
|
||||
var bad_query = _.difference(_.keys(req.query), REQUEST_QUERY_PARAMS_WHITELIST);
|
||||
|
||||
_.each(bad_query, function(key){ delete req.query[key]; });
|
||||
req.params = _.extend({}, req.params); // shuffle things as request is a strange array/object
|
||||
|
||||
var user = me.userByReq(req);
|
||||
|
||||
if ( req.params.token ) {
|
||||
//console.log("Request parameters include token " + req.params.token);
|
||||
var tksplit = req.params.token.split(':');
|
||||
req.params.token = tksplit[0];
|
||||
if ( tksplit.length > 1 ) req.params.cache_buster= tksplit[1];
|
||||
tksplit = req.params.token.split('@');
|
||||
if ( tksplit.length > 1 ) {
|
||||
req.params.signer = tksplit.shift();
|
||||
if ( ! req.params.signer ) req.params.signer = user;
|
||||
else if ( req.params.signer != user ) {
|
||||
var err = new Error('Cannot use map signature of user "' + req.params.signer + '" on database of user "' + user + '"');
|
||||
err.http_status = 403;
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
if ( tksplit.length > 1 ) {
|
||||
var template_hash = tksplit.shift(); // unused
|
||||
}
|
||||
req.params.token = tksplit.shift();
|
||||
//console.log("Request for token " + req.params.token + " with signature from " + req.params.signer);
|
||||
}
|
||||
}
|
||||
|
||||
// bring all query values onto req.params object
|
||||
@@ -353,50 +720,50 @@ module.exports = function(){
|
||||
// for cartodb, ensure interactivity is cartodb_id or user specified
|
||||
req.params.interactivity = req.params.interactivity || 'cartodb_id';
|
||||
|
||||
req.params.processXML = function(req, xml, callback) {
|
||||
var dbuser = req.dbuser ? req.dbuser : global.settings.postgres.user;
|
||||
if ( ! me.rx_dbuser ) me.rx_dbuser = /(<Parameter name="user"><!\[CDATA\[)[^\]]*(]]><\/Parameter>)/g;
|
||||
xml = xml.replace(me.rx_dbuser, "$1" + dbuser + "$2");
|
||||
callback(null, xml);
|
||||
}
|
||||
|
||||
var that = this;
|
||||
|
||||
if (req.profiler) req.profiler.done('req2params.setup');
|
||||
|
||||
Step(
|
||||
function getPrivacy(){
|
||||
cartoData.authorize(req, this);
|
||||
me.authorize(req, this);
|
||||
},
|
||||
function gatekeep(err, data){
|
||||
if (req.profiler) req.profiler.done('cartoData.authorize');
|
||||
function gatekeep(err, authorized){
|
||||
if (req.profiler) req.profiler.done('authorize');
|
||||
if(err) throw err;
|
||||
if(data === "0") throw new Error("Sorry, you are unauthorized (permission denied)");
|
||||
return data;
|
||||
if(!authorized) {
|
||||
err = new Error("Sorry, you are unauthorized (permission denied)");
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
function getDatabase(err, data){
|
||||
function getDatabase(err){
|
||||
if(err) throw err;
|
||||
|
||||
cartoData.getDatabase(req, this);
|
||||
that.setDBConn(user, req.params, this);
|
||||
},
|
||||
function getGeometryType(err, data){
|
||||
if (req.profiler) req.profiler.done('cartoData.getDatabase');
|
||||
function getGeometryType(err){
|
||||
if (req.profiler) req.profiler.done('setDBConn');
|
||||
if (err) throw err;
|
||||
_.extend(req.params, {dbname:data});
|
||||
|
||||
cartoData.getGeometryType(req, this);
|
||||
if ( ! req.params.table ) return null;
|
||||
cartoData.getTableGeometryType(req.params.dbname, req.params.table, this);
|
||||
},
|
||||
function finishSetup(err, data){
|
||||
if (req.profiler) req.profiler.done('cartoData.getGeometryType');
|
||||
if ( err ) { callback(err, req); return; }
|
||||
|
||||
if (!_.isNull(data))
|
||||
_.extend(req.params, {geom_type: data});
|
||||
|
||||
that.addCacheChannel(req, function(err) {
|
||||
if (req.profiler) req.profiler.done('addCacheChannel');
|
||||
callback(err, req);
|
||||
// Add default database connection parameters
|
||||
// if none given
|
||||
_.defaults(req.params, {
|
||||
dbuser: global.environment.postgres.user,
|
||||
dbpassword: global.environment.postgres.password,
|
||||
dbhost: global.environment.postgres.host,
|
||||
dbport: global.environment.postgres.port
|
||||
});
|
||||
|
||||
callback(null, req);
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -408,14 +775,23 @@ module.exports = function(){
|
||||
*/
|
||||
me.getInfowindow = function(req, callback){
|
||||
var that = this;
|
||||
var user = me.userByReq(req);
|
||||
|
||||
Step(
|
||||
function(){
|
||||
// TODO: if this step really needed ?
|
||||
that.req2params(req, this);
|
||||
},
|
||||
function getDatabase(err){
|
||||
if (err) throw err;
|
||||
cartoData.getUserDBName(user, this);
|
||||
},
|
||||
function getInfowindow(err, dbname){
|
||||
if (err) throw err;
|
||||
cartoData.getTableInfowindow(dbname, req.params.table, this);
|
||||
},
|
||||
function(err, data){
|
||||
if (err) callback(err, null);
|
||||
else cartoData.getInfowindow(data, callback);
|
||||
callback(err, data);
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -427,14 +803,23 @@ module.exports = function(){
|
||||
*/
|
||||
me.getMapMetadata = function(req, callback){
|
||||
var that = this;
|
||||
var user = me.userByReq(req);
|
||||
|
||||
Step(
|
||||
function(){
|
||||
// TODO: if this step really needed ?
|
||||
that.req2params(req, this);
|
||||
},
|
||||
function getDatabase(err){
|
||||
if (err) throw err;
|
||||
cartoData.getUserDBName(user, this);
|
||||
},
|
||||
function getMapMetadata(err, dbname){
|
||||
if (err) throw err;
|
||||
cartoData.getTableMapMetadata(dbname, req.params.table, this);
|
||||
},
|
||||
function(err, data){
|
||||
if (err) callback(err, null);
|
||||
else cartoData.getMapMetadata(data, callback);
|
||||
callback(err, data);
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -468,4 +853,4 @@ module.exports = function(){
|
||||
};
|
||||
|
||||
return me;
|
||||
}();
|
||||
};
|
||||
|
||||
66
lib/cartodb/sql/sql_api.js
Normal file
66
lib/cartodb/sql/sql_api.js
Normal file
@@ -0,0 +1,66 @@
|
||||
var _ = require('underscore'),
|
||||
request = require('request');
|
||||
|
||||
module.exports.query = function (username, api_key, sql, callback) {
|
||||
var api = global.environment.sqlapi;
|
||||
|
||||
// build up api string
|
||||
var sqlapihostname = username;
|
||||
if ( api.domain ) sqlapihostname += '.' + api.domain;
|
||||
|
||||
var sqlapi = api.protocol + '://';
|
||||
if ( api.host && api.host != api.domain ) sqlapi += api.host;
|
||||
else sqlapi += sqlapihostname;
|
||||
sqlapi += ':' + api.port + '/api/' + api.version + '/sql';
|
||||
|
||||
var qs = { q: sql };
|
||||
|
||||
// add api_key if given
|
||||
if (_.isString(api_key) && api_key != '') { qs.api_key = api_key; }
|
||||
|
||||
// call sql api
|
||||
//
|
||||
// NOTE: using POST to avoid size limits:
|
||||
// See http://github.com/CartoDB/Windshaft-cartodb/issues/111
|
||||
//
|
||||
// NOTE: uses "host" header to allow IP based specification
|
||||
// of sqlapi address (and avoid a DNS lookup)
|
||||
//
|
||||
// NOTE: allows for keeping up to "maxConnections" concurrent
|
||||
// sockets opened per SQL-API host.
|
||||
// See http://nodejs.org/api/http.html#http_agent_maxsockets
|
||||
//
|
||||
var maxSockets = global.environment.maxConnections || 128;
|
||||
var maxGetLen = api.max_get_sql_length || 2048;
|
||||
var maxSQLTime = api.timeout || 100; // 1/10 of a second by default
|
||||
var reqSpec = {
|
||||
url:sqlapi,
|
||||
json:true,
|
||||
headers:{host: sqlapihostname}
|
||||
// http://nodejs.org/api/http.html#http_agent_maxsockets
|
||||
,pool:{maxSockets:maxSockets}
|
||||
// timeout in milliseconds
|
||||
,timeout:maxSQLTime
|
||||
};
|
||||
if ( sql.length > maxGetLen ) {
|
||||
reqSpec.method = 'POST';
|
||||
reqSpec.body = qs;
|
||||
} else {
|
||||
reqSpec.method = 'GET';
|
||||
reqSpec.qs = qs;
|
||||
}
|
||||
request(reqSpec, function(err, res, body) {
|
||||
if (err){
|
||||
console.log('ERROR connecting to SQL API on ' + sqlapi + ': ' + err);
|
||||
callback(err);
|
||||
return;
|
||||
}
|
||||
if (res.statusCode != 200) {
|
||||
var msg = res.body.error ? res.body.error : res.body;
|
||||
callback(new Error(msg));
|
||||
console.log('unexpected response status (' + res.statusCode + ') for sql query: ' + sql + ': ' + msg);
|
||||
return;
|
||||
}
|
||||
callback(null, body.rows);
|
||||
});
|
||||
};
|
||||
497
lib/cartodb/template_maps.js
Normal file
497
lib/cartodb/template_maps.js
Normal file
@@ -0,0 +1,497 @@
|
||||
var crypto = require('crypto'),
|
||||
Step = require('step'),
|
||||
_ = require('underscore'),
|
||||
dot = require('dot');
|
||||
|
||||
|
||||
var EventEmitter = require('events').EventEmitter;
|
||||
var util = require('util');
|
||||
|
||||
|
||||
// Class handling map templates
|
||||
//
|
||||
// See http://github.com/CartoDB/Windshaft-cartodb/wiki/Template-maps
|
||||
//
|
||||
// @param redis_pool an instance of a "redis-mpool"
|
||||
// See https://github.com/CartoDB/node-redis-mpool
|
||||
// Needs version 0.x.x of the API.
|
||||
//
|
||||
// @param opts TemplateMap options. Supported elements:
|
||||
// 'max_user_templates' limit on the number of per-user
|
||||
//
|
||||
//
|
||||
function TemplateMaps(redis_pool, opts) {
|
||||
if (!(this instanceof TemplateMaps)) return new TemplateMaps();
|
||||
|
||||
EventEmitter.call(this);
|
||||
|
||||
this.redis_pool = redis_pool;
|
||||
this.opts = opts || {};
|
||||
|
||||
// Database containing templates
|
||||
// TODO: allow configuring ?
|
||||
// NOTE: currently it is the same as
|
||||
// the one containing layergroups
|
||||
this.db_signatures = 0;
|
||||
|
||||
//
|
||||
// Map templates are owned by a user that specifies access permissions
|
||||
// for their instances.
|
||||
//
|
||||
// We have the following datastores:
|
||||
//
|
||||
// 1. User templates: set of per-user map templates
|
||||
|
||||
// User templates (HASH:tpl_id->tpl_val)
|
||||
this.key_usr_tpl = dot.template("map_tpl|{{=it.owner}}");
|
||||
|
||||
// User template locks (HASH:tpl_id->ctime)
|
||||
this.key_usr_tpl_lck = dot.template("map_tpl|{{=it.owner}}|locks");
|
||||
|
||||
this.lock_ttl = this.opts['lock_ttl'] || 5000;
|
||||
}
|
||||
|
||||
util.inherits(TemplateMaps, EventEmitter);
|
||||
|
||||
module.exports = TemplateMaps;
|
||||
|
||||
|
||||
var o = TemplateMaps.prototype;
|
||||
|
||||
//--------------- PRIVATE METHODS --------------------------------
|
||||
|
||||
o._userTemplateLimit = function() {
|
||||
return this.opts['max_user_templates'] || 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Internal function to communicate with redis
|
||||
*
|
||||
* @param redisFunc - the redis function to execute
|
||||
* @param redisArgs - the arguments for the redis function in an array
|
||||
* @param callback - function to pass results too.
|
||||
*/
|
||||
o._redisCmd = function(redisFunc, redisArgs, callback) {
|
||||
var redisClient;
|
||||
var that = this;
|
||||
var db = that.db_signatures;
|
||||
|
||||
Step(
|
||||
function getRedisClient() {
|
||||
that.redis_pool.acquire(db, this);
|
||||
},
|
||||
function executeQuery(err, data) {
|
||||
if ( err ) throw err;
|
||||
redisClient = data;
|
||||
redisArgs.push(this);
|
||||
redisClient[redisFunc.toUpperCase()].apply(redisClient, redisArgs);
|
||||
},
|
||||
function releaseRedisClient(err, data) {
|
||||
if ( ! _.isUndefined(redisClient) ) that.redis_pool.release(db, redisClient);
|
||||
callback(err, data);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
var _reValidIdentifier = /^[a-zA-Z][0-9a-zA-Z_]*$/;
|
||||
o._checkInvalidTemplate = function(template) {
|
||||
if ( template.version != '0.0.1' ) {
|
||||
return new Error("Unsupported template version " + template.version);
|
||||
}
|
||||
var tplname = template.name;
|
||||
if ( ! tplname ) {
|
||||
return new Error("Missing template name");
|
||||
}
|
||||
if ( ! tplname.match(_reValidIdentifier) ) {
|
||||
return new Error("Invalid characters in template name '" + tplname + "'");
|
||||
}
|
||||
|
||||
var invalidError = isInvalidLayergroup(template.layergroup);
|
||||
if (invalidError) {
|
||||
return invalidError;
|
||||
}
|
||||
|
||||
var placeholders = template.placeholders || {};
|
||||
|
||||
var placeholderKeys = Object.keys(placeholders);
|
||||
for (var i = 0, len = placeholderKeys.length; i < len; i++) {
|
||||
var placeholderKey = placeholderKeys[i];
|
||||
|
||||
if (!placeholderKey.match(_reValidIdentifier)) {
|
||||
return new Error("Invalid characters in placeholder name '" + placeholderKey + "'");
|
||||
}
|
||||
if ( ! placeholders[placeholderKey].hasOwnProperty('default') ) {
|
||||
return new Error("Missing default for placeholder '" + placeholderKey + "'");
|
||||
}
|
||||
if ( ! placeholders[placeholderKey].hasOwnProperty('type') ) {
|
||||
return new Error("Missing type for placeholder '" + placeholderKey + "'");
|
||||
}
|
||||
}
|
||||
|
||||
var auth = template.auth || {};
|
||||
|
||||
switch ( auth.method ) {
|
||||
case 'open':
|
||||
break;
|
||||
case 'token':
|
||||
if ( ! _.isArray(auth.valid_tokens) )
|
||||
return new Error("Invalid 'token' authentication: missing valid_tokens");
|
||||
if ( ! auth.valid_tokens.length )
|
||||
return new Error("Invalid 'token' authentication: no valid_tokens");
|
||||
break;
|
||||
default:
|
||||
return new Error("Unsupported authentication method: " + auth.method);
|
||||
break;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
function isInvalidLayergroup(layergroup) {
|
||||
if (!layergroup) {
|
||||
return new Error('Missing layergroup');
|
||||
}
|
||||
|
||||
var layers = layergroup.layers;
|
||||
|
||||
if (!_.isArray(layers) || layers.length === 0) {
|
||||
return new Error('Missing or empty layers array from layergroup config');
|
||||
}
|
||||
|
||||
var invalidLayers = layers
|
||||
.map(function(layer, layerIndex) {
|
||||
return layer.options ? null : layerIndex;
|
||||
})
|
||||
.filter(function(layerIndex) {
|
||||
return layerIndex !== null;
|
||||
});
|
||||
|
||||
if (invalidLayers.length) {
|
||||
return new Error('Missing `options` in layergroup config for layers: ' + invalidLayers.join(', '));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function templateDefaults(template) {
|
||||
var templateAuth = _.defaults({}, template.auth || {}, {
|
||||
method: 'open'
|
||||
});
|
||||
return _.defaults({ auth: templateAuth }, template, {
|
||||
placeholders: {}
|
||||
});
|
||||
}
|
||||
|
||||
//--------------- PUBLIC API -------------------------------------
|
||||
|
||||
// Add a template
|
||||
//
|
||||
// NOTE: locks user+template_name or fails
|
||||
//
|
||||
// @param owner cartodb username of the template owner
|
||||
//
|
||||
// @param template layergroup template, see
|
||||
// http://github.com/CartoDB/Windshaft-cartodb/wiki/Template-maps#template-format
|
||||
//
|
||||
// @param callback function(err, tpl_id)
|
||||
// Return template identifier (only valid for given user)
|
||||
//
|
||||
o.addTemplate = function(owner, template, callback) {
|
||||
var self = this;
|
||||
|
||||
template = templateDefaults(template);
|
||||
|
||||
var invalidError = this._checkInvalidTemplate(template);
|
||||
if ( invalidError ) {
|
||||
return callback(invalidError);
|
||||
}
|
||||
|
||||
var templateName = template.name;
|
||||
var userTemplatesKey = this.key_usr_tpl({ owner:owner });
|
||||
var limit = this._userTemplateLimit();
|
||||
|
||||
Step(
|
||||
function checkLimit() {
|
||||
if ( ! limit ) {
|
||||
return 0;
|
||||
}
|
||||
self._redisCmd('HLEN', [ userTemplatesKey ], this);
|
||||
},
|
||||
function installTemplateIfDoesNotExist(err, numberOfTemplates) {
|
||||
if ( err ) {
|
||||
throw err;
|
||||
}
|
||||
if ( limit && numberOfTemplates >= limit ) {
|
||||
throw new Error("User '" + owner + "' reached limit on number of templates " +
|
||||
"("+ numberOfTemplates + "/" + limit + ")");
|
||||
}
|
||||
self._redisCmd('HSETNX', [ userTemplatesKey, templateName, JSON.stringify(template) ], this);
|
||||
},
|
||||
function validateInstallation(err, wasSet) {
|
||||
if ( err ) {
|
||||
throw err;
|
||||
}
|
||||
if ( ! wasSet ) {
|
||||
throw new Error("Template '" + templateName + "' of user '" + owner + "' already exists");
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
function finish(err) {
|
||||
if (!err) {
|
||||
self.emit('add', owner, templateName, template);
|
||||
}
|
||||
|
||||
callback(err, templateName, template);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Delete a template
|
||||
//
|
||||
// @param owner cartodb username of the template owner
|
||||
//
|
||||
// @param tpl_id template identifier as returned
|
||||
// by addTemplate or listTemplates
|
||||
//
|
||||
// @param callback function(err)
|
||||
//
|
||||
o.delTemplate = function(owner, tpl_id, callback) {
|
||||
var self = this;
|
||||
Step(
|
||||
function deleteTemplate() {
|
||||
self._redisCmd('HDEL', [ self.key_usr_tpl({ owner:owner }), tpl_id ], this);
|
||||
},
|
||||
function handleDeletion(err, deleted) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
if (!deleted) {
|
||||
throw new Error("Template '" + tpl_id + "' of user '" + owner + "' does not exist");
|
||||
}
|
||||
return true;
|
||||
},
|
||||
function finish(err) {
|
||||
if (!err) {
|
||||
self.emit('delete', owner, tpl_id);
|
||||
}
|
||||
|
||||
callback(err);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Update a template
|
||||
//
|
||||
// NOTE: locks user+template_name or fails
|
||||
//
|
||||
// Also deletes and re-creates associated authentication certificate,
|
||||
// which in turn deletes all instance signatures
|
||||
//
|
||||
// @param owner cartodb username of the template owner
|
||||
//
|
||||
// @param tpl_id template identifier as returned by addTemplate
|
||||
//
|
||||
// @param template layergroup template, see
|
||||
// http://github.com/CartoDB/Windshaft-cartodb/wiki/Template-maps#template-format
|
||||
//
|
||||
// @param callback function(err)
|
||||
//
|
||||
o.updTemplate = function(owner, tpl_id, template, callback) {
|
||||
var self = this;
|
||||
|
||||
template = templateDefaults(template);
|
||||
|
||||
var invalidError = this._checkInvalidTemplate(template);
|
||||
|
||||
if ( invalidError ) {
|
||||
return callback(invalidError);
|
||||
}
|
||||
|
||||
var templateName = template.name;
|
||||
|
||||
if ( tpl_id != templateName ) {
|
||||
return callback(new Error("Cannot update name of a map template ('" + tpl_id + "' != '" + templateName + "')"));
|
||||
}
|
||||
|
||||
var userTemplatesKey = this.key_usr_tpl({ owner:owner });
|
||||
|
||||
Step(
|
||||
function getExistingTemplate() {
|
||||
self._redisCmd('HGET', [ userTemplatesKey, tpl_id ], this);
|
||||
},
|
||||
function updateTemplate(err, currentTemplate) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
if (!currentTemplate) {
|
||||
throw new Error("Template '" + tpl_id + "' of user '" + owner + "' does not exist");
|
||||
}
|
||||
self._redisCmd('HSET', [ userTemplatesKey, templateName, JSON.stringify(template) ], this);
|
||||
},
|
||||
function handleTemplateUpdate(err, didSetNewField) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
if (didSetNewField) {
|
||||
console.warn('New template created on update operation');
|
||||
}
|
||||
return true;
|
||||
},
|
||||
function finish(err) {
|
||||
if (!err) {
|
||||
self.emit('update', owner, templateName, template);
|
||||
}
|
||||
|
||||
callback(err, template);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// List user templates
|
||||
//
|
||||
// @param owner cartodb username of the templates owner
|
||||
//
|
||||
// @param callback function(err, tpl_id_list)
|
||||
// Returns a list of template identifiers
|
||||
//
|
||||
o.listTemplates = function(owner, callback) {
|
||||
this._redisCmd('HKEYS', [ this.key_usr_tpl({owner:owner}) ], callback);
|
||||
};
|
||||
|
||||
// Get a templates
|
||||
//
|
||||
// @param owner cartodb username of the template owner
|
||||
//
|
||||
// @param tpl_id template identifier as returned
|
||||
// by addTemplate or listTemplates
|
||||
//
|
||||
// @param callback function(err, template)
|
||||
// Return full template definition
|
||||
//
|
||||
o.getTemplate = function(owner, tpl_id, callback) {
|
||||
var self = this;
|
||||
Step(
|
||||
function getTemplate() {
|
||||
self._redisCmd('HGET', [ self.key_usr_tpl({owner:owner}), tpl_id ], this);
|
||||
},
|
||||
function parseTemplate(err, tpl_val) {
|
||||
if ( err ) throw err;
|
||||
return JSON.parse(tpl_val);
|
||||
},
|
||||
function finish(err, tpl) {
|
||||
callback(err, tpl);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
o.isAuthorized = function(template, authTokens) {
|
||||
if (!template) {
|
||||
return false;
|
||||
}
|
||||
|
||||
authTokens = _.isArray(authTokens) ? authTokens : [authTokens];
|
||||
|
||||
var templateAuth = template.auth;
|
||||
|
||||
if (!templateAuth) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (templateAuth.method === 'open') {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (templateAuth.method === 'token') {
|
||||
return _.intersection(templateAuth.valid_tokens, authTokens).length > 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
// Perform placeholder substitutions on a template
|
||||
//
|
||||
// @param template a template object (will not be modified)
|
||||
//
|
||||
// @param params an object containing named subsitution parameters
|
||||
// Only the ones found in the template's placeholders object
|
||||
// will be used, with missing ones taking default values.
|
||||
//
|
||||
// @returns a layergroup configuration
|
||||
//
|
||||
// @throws Error on malformed template or parameter
|
||||
//
|
||||
var _reNumber = /^([-+]?[\d\.]?\d+([eE][+-]?\d+)?)$/,
|
||||
_reCSSColorName = /^[a-zA-Z]+$/,
|
||||
_reCSSColorVal = /^#[0-9a-fA-F]{3,6}$/;
|
||||
|
||||
_replaceVars = function(str, params) {
|
||||
//return _.template(str, params); // lazy way, possibly dangerous
|
||||
// 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) {
|
||||
var all_params = {};
|
||||
var phold = template.placeholders || {};
|
||||
Object.keys(phold).forEach(function(k) {
|
||||
var val = params.hasOwnProperty(k) ? params[k] : phold[k].default;
|
||||
var type = phold[k].type;
|
||||
// properly escape
|
||||
if ( type === 'sql_literal' ) {
|
||||
// duplicate any single-quote
|
||||
val = val.replace(/'/g, "''");
|
||||
}
|
||||
else if ( type === 'sql_ident' ) {
|
||||
// duplicate any double-quote
|
||||
val = val.replace(/"/g, '""');
|
||||
}
|
||||
else if ( type === 'number' ) {
|
||||
// check it's a number
|
||||
if ( typeof(val) !== 'number' && ! val.match(_reNumber) ) {
|
||||
throw new Error("Invalid number value for template parameter '"
|
||||
+ k + "': " + val);
|
||||
}
|
||||
}
|
||||
else if ( type === 'css_color' ) {
|
||||
// check it only contains letters or
|
||||
// starts with # and only contains hexdigits
|
||||
if ( ! val.match(_reCSSColorName) && ! val.match(_reCSSColorVal) ) {
|
||||
throw new Error("Invalid css_color value for template parameter '"
|
||||
+ k + "': " + val);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// NOTE: should be checked at template create/update time
|
||||
throw new Error("Invalid placeholder type '" + type + "'");
|
||||
}
|
||||
all_params[k] = val;
|
||||
});
|
||||
|
||||
// NOTE: we're deep-cloning the layergroup here
|
||||
var layergroup = JSON.parse(JSON.stringify(template.layergroup));
|
||||
for (var i=0; i<layergroup.layers.length; ++i) {
|
||||
var lyropt = layergroup.layers[i].options;
|
||||
if ( lyropt.cartocss ) lyropt.cartocss = _replaceVars(lyropt.cartocss, all_params);
|
||||
if ( lyropt.sql) lyropt.sql = _replaceVars(lyropt.sql, all_params);
|
||||
// Anything else ?
|
||||
}
|
||||
|
||||
// extra information about the template
|
||||
layergroup.template = {
|
||||
name: template.name,
|
||||
auth: template.auth
|
||||
};
|
||||
|
||||
return layergroup;
|
||||
};
|
||||
|
||||
// Return a fingerPrint of the object
|
||||
o.fingerPrint = function(template) {
|
||||
return crypto.createHash('md5')
|
||||
.update(JSON.stringify(template))
|
||||
.digest('hex')
|
||||
;
|
||||
};
|
||||
2419
npm-shrinkwrap.json
generated
2419
npm-shrinkwrap.json
generated
File diff suppressed because it is too large
Load Diff
38
package.json
38
package.json
@@ -1,8 +1,11 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "windshaft-cartodb",
|
||||
"version": "1.3.4",
|
||||
"version": "1.26.1",
|
||||
"description": "A map tile server for CartoDB",
|
||||
"keywords": [
|
||||
"cartodb"
|
||||
],
|
||||
"url": "https://github.com/CartoDB/Windshaft-cartodb",
|
||||
"licenses": [{
|
||||
"type": "BSD",
|
||||
@@ -19,23 +22,30 @@
|
||||
"Sandro Santilli <strk@vizzuality.com>"
|
||||
],
|
||||
"dependencies": {
|
||||
"node-varnish": "0.1.1",
|
||||
"underscore" : "~1.3.3",
|
||||
"windshaft" : "~0.13.4",
|
||||
"step": "0.0.x",
|
||||
"generic-pool": "~2.0.3",
|
||||
"redis": "~0.8.3",
|
||||
"hiredis": "~0.1.14",
|
||||
"request": "2.9.202",
|
||||
"mapnik": "~0.7.14",
|
||||
"strftime": "~0.6.0",
|
||||
"lzma": "~1.2.3"
|
||||
"node-varnish": "https://github.com/Vizzuality/node-varnish/tarball/0.3.0",
|
||||
"underscore" : "~1.6.0",
|
||||
"dot": "~1.0.2",
|
||||
"windshaft": "https://github.com/CartoDB/Windshaft/tarball/0.35.1",
|
||||
"step": "~0.0.5",
|
||||
"request": "~2.9.203",
|
||||
"cartodb-redis": "https://github.com/CartoDB/node-cartodb-redis/tarball/0.11.0",
|
||||
"cartodb-psql": "https://github.com/CartoDB/node-cartodb-psql/tarball/0.4.0",
|
||||
"redis-mpool": "https://github.com/CartoDB/node-redis-mpool/tarball/0.3.0",
|
||||
"lzma": "~1.3.7",
|
||||
"log4js": "https://github.com/CartoDB/log4js-node/tarball/cdb",
|
||||
"rollbar": "~0.3.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mocha": "1.2.1",
|
||||
"semver": "~1.1.0"
|
||||
"mocha": "~1.21.4",
|
||||
"redis": "~0.8.6",
|
||||
"strftime": "~0.8.2",
|
||||
"semver": "~1.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "make check"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8 <0.11",
|
||||
"npm": ">=1.2.1"
|
||||
}
|
||||
}
|
||||
|
||||
63
run_tests.sh
63
run_tests.sh
@@ -1,18 +1,21 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Must match redis_port in config/environments/test.js
|
||||
# TODO: read from there
|
||||
REDIS_PORT=6333
|
||||
OPT_CREATE_REDIS=yes # create the redis test environment
|
||||
OPT_CREATE_PGSQL=yes # create the PostgreSQL test environment
|
||||
OPT_DROP_REDIS=yes # drop the redis test environment
|
||||
OPT_DROP_PGSQL=yes # drop the PostgreSQL test environment
|
||||
|
||||
OPT_CREATE=yes # create the test environment
|
||||
OPT_DROP=yes # drop the test environment
|
||||
export PGAPPNAME=cartodb_tiler_tester
|
||||
|
||||
cd $(dirname $0)
|
||||
BASEDIR=$(pwd)
|
||||
cd -
|
||||
|
||||
REDIS_PORT=`node -e "console.log(require('${BASEDIR}/config/environments/test.js').redis.port)"`
|
||||
export REDIS_PORT
|
||||
|
||||
cleanup() {
|
||||
if test x"$OPT_DROP" = xyes; then
|
||||
if test x"$OPT_DROP_REDIS" = xyes; then
|
||||
if test x"$PID_REDIS" = x; then
|
||||
PID_REDIS=$(cat ${BASEDIR}/redis.pid)
|
||||
if test x"$PID_REDIS" = x; then
|
||||
@@ -20,9 +23,13 @@ cleanup() {
|
||||
return;
|
||||
fi
|
||||
fi
|
||||
echo "Cleaning up"
|
||||
echo "Killing test redis pid ${PID_REDIS}"
|
||||
kill ${PID_REDIS}
|
||||
fi
|
||||
if test x"$OPT_DROP_PGSQL" = xyes; then
|
||||
# TODO: drop postgresql ?
|
||||
echo "Dropping PostgreSQL test database isn't implemented yet"
|
||||
fi
|
||||
}
|
||||
|
||||
cleanup_and_exit() {
|
||||
@@ -40,12 +47,32 @@ die() {
|
||||
trap 'cleanup_and_exit' 1 2 3 5 9 13
|
||||
|
||||
while [ -n "$1" ]; do
|
||||
# This is kept for backward compatibility
|
||||
if test "$1" = "--nodrop"; then
|
||||
OPT_DROP=no
|
||||
OPT_DROP_REDIS=no
|
||||
OPT_DROP_PGSQL=no
|
||||
shift
|
||||
continue
|
||||
elif test "$1" = "--nodrop-pg"; then
|
||||
OPT_DROP_PGSQL=no
|
||||
shift
|
||||
continue
|
||||
elif test "$1" = "--nodrop-redis"; then
|
||||
OPT_DROP_REDIS=no
|
||||
shift
|
||||
continue
|
||||
elif test "$1" = "--nocreate-pg"; then
|
||||
OPT_CREATE_PGSQL=no
|
||||
shift
|
||||
continue
|
||||
elif test "$1" = "--nocreate-redis"; then
|
||||
OPT_CREATE_REDIS=no
|
||||
shift
|
||||
continue
|
||||
# This is kept for backward compatibility
|
||||
elif test "$1" = "--nocreate"; then
|
||||
OPT_CREATE=no
|
||||
OPT_CREATE_REDIS=no
|
||||
OPT_CREATE_PGSQL=no
|
||||
shift
|
||||
continue
|
||||
else
|
||||
@@ -63,16 +90,26 @@ fi
|
||||
|
||||
TESTS=$@
|
||||
|
||||
if test x"$OPT_CREATE" = xyes; then
|
||||
if test x"$OPT_CREATE_REDIS" = xyes; then
|
||||
echo "Starting redis on port ${REDIS_PORT}"
|
||||
echo "port ${REDIS_PORT}" | redis-server - > ${BASEDIR}/test.log &
|
||||
PID_REDIS=$!
|
||||
echo ${PID_REDIS} > ${BASEDIR}/redis.pid
|
||||
|
||||
echo "Preparing the environment"
|
||||
cd ${BASEDIR}/test/support; sh prepare_db.sh || die "database preparation failure"; cd -
|
||||
fi
|
||||
|
||||
PREPARE_DB_OPTS=
|
||||
if test x"$OPT_CREATE_PGSQL" != xyes; then
|
||||
PREPARE_DB_OPTS="$PREPARE_DB_OPTS --skip-pg"
|
||||
fi
|
||||
if test x"$OPT_CREATE_REDIS" != xyes; then
|
||||
PREPARE_DB_OPTS="$PREPARE_DB_OPTS --skip-redis"
|
||||
fi
|
||||
|
||||
echo "Preparing the environment"
|
||||
cd ${BASEDIR}/test/support
|
||||
sh prepare_db.sh ${PREPARE_DB_OPTS} || die "database preparation failure"
|
||||
cd -
|
||||
|
||||
PATH=node_modules/.bin/:$PATH
|
||||
|
||||
echo "Running tests"
|
||||
|
||||
211
test/acceptance/cache/surrogate_keys_invalidation.js
vendored
Normal file
211
test/acceptance/cache/surrogate_keys_invalidation.js
vendored
Normal file
@@ -0,0 +1,211 @@
|
||||
var assert = require('../../support/assert');
|
||||
var redis = require('redis');
|
||||
var Step = require('step');
|
||||
|
||||
var helper = require(__dirname + '/../../support/test_helper');
|
||||
|
||||
var SqlApiEmulator = require(__dirname + '/../../support/SQLAPIEmu.js');
|
||||
|
||||
var NamedMapsCacheEntry = require(__dirname + '/../../../lib/cartodb/cache/model/named_maps_entry');
|
||||
var SurrogateKeysCache = require(__dirname + '/../../../lib/cartodb/cache/surrogate_keys_cache');
|
||||
|
||||
var CartodbWindshaft = require(__dirname + '/../../../lib/cartodb/cartodb_windshaft');
|
||||
var ServerOptions = require(__dirname + '/../../../lib/cartodb/server_options');
|
||||
var serverOptions = ServerOptions();
|
||||
|
||||
|
||||
suite('templates surrogate keys', function() {
|
||||
|
||||
var redisClient,
|
||||
sqlApiServer,
|
||||
server;
|
||||
|
||||
var templateOwner = 'localhost',
|
||||
templateName = 'acceptance',
|
||||
expectedTemplateId = templateOwner + '@' + templateName,
|
||||
template = {
|
||||
version: '0.0.1',
|
||||
name: templateName,
|
||||
auth: {
|
||||
method: 'open'
|
||||
},
|
||||
layergroup: {
|
||||
version: '1.2.0',
|
||||
layers: [
|
||||
{
|
||||
options: {
|
||||
sql: 'select 1 cartodb_id, null::geometry as the_geom_webmercator',
|
||||
cartocss: '#layer { marker-fill:blue; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
expectedBody = { template_id: expectedTemplateId };
|
||||
|
||||
suiteSetup(function(done) {
|
||||
// Enable Varnish purge for tests
|
||||
serverOptions.varnish_purge_enabled = true;
|
||||
|
||||
server = new CartodbWindshaft(serverOptions);
|
||||
|
||||
sqlApiServer = new SqlApiEmulator(global.environment.sqlapi.port, done);
|
||||
|
||||
redisClient = redis.createClient(global.environment.redis.port);
|
||||
});
|
||||
|
||||
var surrogateKeysCacheInvalidateFn = SurrogateKeysCache.prototype.invalidate;
|
||||
|
||||
beforeEach(function(done) {
|
||||
var postTemplateRequest = {
|
||||
url: '/tiles/template?api_key=1234',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: templateOwner,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(template)
|
||||
};
|
||||
|
||||
Step(
|
||||
function postTemplate() {
|
||||
var next = this;
|
||||
assert.response(server,
|
||||
postTemplateRequest,
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
next(null, res);
|
||||
}
|
||||
);
|
||||
},
|
||||
function rePostTemplate(err, res) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
assert.deepEqual(parsedBody, expectedBody);
|
||||
return true;
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("update template calls surrogate keys invalidation", function(done) {
|
||||
var cacheEntryKey;
|
||||
var surrogateKeysCacheInvalidateMethodInvoked = false;
|
||||
SurrogateKeysCache.prototype.invalidate = function(cacheEntry) {
|
||||
cacheEntryKey = cacheEntry.key();
|
||||
surrogateKeysCacheInvalidateMethodInvoked = true;
|
||||
};
|
||||
|
||||
Step(
|
||||
function putValidTemplate() {
|
||||
var updateTemplateRequest = {
|
||||
url: '/tiles/template/' + expectedTemplateId + '/?api_key=1234',
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
host: templateOwner,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(template)
|
||||
};
|
||||
var next = this;
|
||||
assert.response(server,
|
||||
updateTemplateRequest,
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
next(null, res);
|
||||
}
|
||||
);
|
||||
},
|
||||
function checkValidUpdate(err, res) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
var parsedBody = JSON.parse(res.body);
|
||||
assert.deepEqual(parsedBody, expectedBody);
|
||||
|
||||
assert.ok(surrogateKeysCacheInvalidateMethodInvoked);
|
||||
assert.equal(cacheEntryKey, new NamedMapsCacheEntry(templateOwner, templateName).key());
|
||||
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
if ( err ) {
|
||||
return done(err);
|
||||
}
|
||||
redisClient.keys("map_*|localhost", function(err, keys) {
|
||||
if ( err ) {
|
||||
return done(err);
|
||||
}
|
||||
redisClient.del(keys, function(err) {
|
||||
return done(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("delete template calls surrogate keys invalidation", function(done) {
|
||||
|
||||
var cacheEntryKey;
|
||||
var surrogateKeysCacheInvalidateMethodInvoked = false;
|
||||
SurrogateKeysCache.prototype.invalidate = function(cacheEntry) {
|
||||
cacheEntryKey = cacheEntry.key();
|
||||
surrogateKeysCacheInvalidateMethodInvoked = true;
|
||||
};
|
||||
|
||||
Step(
|
||||
function putValidTemplate() {
|
||||
var deleteTemplateRequest = {
|
||||
url: '/tiles/template/' + expectedTemplateId + '/?api_key=1234',
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
host: templateOwner,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
};
|
||||
var next = this;
|
||||
assert.response(server,
|
||||
deleteTemplateRequest,
|
||||
{
|
||||
status: 204
|
||||
},
|
||||
function(res) {
|
||||
next(null, res);
|
||||
}
|
||||
);
|
||||
},
|
||||
function checkValidUpdate(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
assert.ok(surrogateKeysCacheInvalidateMethodInvoked);
|
||||
assert.equal(cacheEntryKey, new NamedMapsCacheEntry(templateOwner, templateName).key());
|
||||
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(function(done) {
|
||||
SurrogateKeysCache.prototype.invalidate = surrogateKeysCacheInvalidateFn;
|
||||
done();
|
||||
});
|
||||
|
||||
suiteTeardown(function(done) {
|
||||
sqlApiServer.close(done);
|
||||
});
|
||||
|
||||
});
|
||||
72
test/acceptance/health_check.js
Normal file
72
test/acceptance/health_check.js
Normal file
@@ -0,0 +1,72 @@
|
||||
var helper = require(__dirname + '/../support/test_helper');
|
||||
|
||||
var assert = require('../support/assert');
|
||||
var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/cartodb_windshaft');
|
||||
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options')();
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
|
||||
suite('health checks', function () {
|
||||
|
||||
beforeEach(function (done) {
|
||||
global.environment.health = {
|
||||
enabled: true,
|
||||
username: 'localhost',
|
||||
z: 0,
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
done();
|
||||
});
|
||||
|
||||
var healthCheckRequest = {
|
||||
url: '/health',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
}
|
||||
};
|
||||
|
||||
test('returns 200 and ok=true with enabled configuration', function (done) {
|
||||
assert.response(server,
|
||||
healthCheckRequest,
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function (res, err) {
|
||||
console.log(res.body);
|
||||
assert.ok(!err);
|
||||
|
||||
var parsed = JSON.parse(res.body);
|
||||
|
||||
assert.ok(parsed.enabled);
|
||||
assert.ok(parsed.ok);
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('fails for invalid user because it is not in redis', function (done) {
|
||||
global.environment.health.username = 'invalid';
|
||||
|
||||
assert.response(server,
|
||||
healthCheckRequest,
|
||||
{
|
||||
status: 503
|
||||
},
|
||||
function (res, err) {
|
||||
assert.ok(!err);
|
||||
|
||||
var parsed = JSON.parse(res.body);
|
||||
|
||||
assert.equal(parsed.enabled, true);
|
||||
assert.equal(parsed.ok, false);
|
||||
|
||||
assert.equal(parsed.result.redis.ok, false);
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,30 +4,45 @@ var _ = require('underscore');
|
||||
var redis = require('redis');
|
||||
var querystring = require('querystring');
|
||||
var semver = require('semver');
|
||||
var mapnik = require('mapnik');
|
||||
var Step = require('step');
|
||||
var http = require('http');
|
||||
var SQLAPIEmu = require(__dirname + '/../support/SQLAPIEmu.js');
|
||||
|
||||
var helper = require(__dirname + '/../support/test_helper');
|
||||
|
||||
var IMAGE_EQUALS_TOLERANCE_PER_MIL = 20,
|
||||
IMAGE_EQUALS_ZERO_TOLERANCE_PER_MIL = 0;
|
||||
|
||||
var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/cartodb_windshaft');
|
||||
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options');
|
||||
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options')();
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
server.setMaxListeners(0);
|
||||
|
||||
suite('server', function() {
|
||||
[true, false].forEach(function(cdbQueryTablesFromPostgresEnabledValue) {
|
||||
|
||||
global.environment.enabledFeatures = {cdbQueryTablesFromPostgres: cdbQueryTablesFromPostgresEnabledValue};
|
||||
|
||||
suite('multilayer:postgres=' + cdbQueryTablesFromPostgresEnabledValue, function() {
|
||||
|
||||
|
||||
var redis_client = redis.createClient(global.environment.redis.port);
|
||||
var sqlapi_server;
|
||||
|
||||
var default_style = semver.satisfies(mapnik.versions.mapnik, '<2.1.0')
|
||||
?
|
||||
var mapnik_version = global.environment.mapnik_version || server.getVersion().mapnik;
|
||||
var test_database = _.template(global.environment.postgres_auth_user, {user_id:1}) + '_db';
|
||||
var default_style;
|
||||
if ( semver.satisfies(mapnik_version, '<2.1.0') ) {
|
||||
// 2.0.0 default
|
||||
'#<%= table %>{marker-fill: #FF6600;marker-opacity: 1;marker-width: 8;marker-line-color: white;marker-line-width: 3;marker-line-opacity: 0.9;marker-placement: point;marker-type: ellipse;marker-allow-overlap: true;}'
|
||||
:
|
||||
default_style = '#<%= table %>{marker-fill: #FF6600;marker-opacity: 1;marker-width: 8;marker-line-color: white;marker-line-width: 3;marker-line-opacity: 0.9;marker-placement: point;marker-type: ellipse;marker-allow-overlap: true;}';
|
||||
}
|
||||
else if ( semver.satisfies(mapnik_version, '<2.2.0') ) {
|
||||
// 2.1.0 default
|
||||
'#<%= table %>[mapnik-geometry-type=1] {marker-fill: #FF6600;marker-opacity: 1;marker-width: 16;marker-line-color: white;marker-line-width: 3;marker-line-opacity: 0.9;marker-placement: point;marker-type: ellipse;marker-allow-overlap: true;}#<%= table %>[mapnik-geometry-type=2] {line-color:#FF6600; line-width:1; line-opacity: 0.7;}#<%= table %>[mapnik-geometry-type=3] {polygon-fill:#FF6600; polygon-opacity: 0.7; line-opacity:1; line-color: #FFFFFF;}';
|
||||
default_style = '#<%= table %>[mapnik-geometry-type=1] {marker-fill: #FF6600;marker-opacity: 1;marker-width: 16;marker-line-color: white;marker-line-width: 3;marker-line-opacity: 0.9;marker-placement: point;marker-type: ellipse;marker-allow-overlap: true;}#<%= table %>[mapnik-geometry-type=2] {line-color:#FF6600; line-width:1; line-opacity: 0.7;}#<%= table %>[mapnik-geometry-type=3] {polygon-fill:#FF6600; polygon-opacity: 0.7; line-opacity:1; line-color: #FFFFFF;}';
|
||||
}
|
||||
else {
|
||||
// 2.2.0+ default
|
||||
default_style = '#<%= table %>["mapnik::geometry_type"=1] {marker-fill: #FF6600;marker-opacity: 1;marker-width: 16;marker-line-color: white;marker-line-width: 3;marker-line-opacity: 0.9;marker-placement: point;marker-type: ellipse;marker-allow-overlap: true;}#<%= table %>["mapnik::geometry_type"=2] {line-color:#FF6600; line-width:1; line-opacity: 0.7;}#<%= table %>["mapnik::geometry_type"=3] {polygon-fill:#FF6600; polygon-opacity: 0.7; line-opacity:1; line-color: #FFFFFF;}';
|
||||
}
|
||||
|
||||
// A couple of styles to use during testing
|
||||
var test_style_black_200 = "#test_table{marker-fill:black;marker-line-color:red;marker-width:10}";
|
||||
@@ -45,12 +60,25 @@ suite('server', function() {
|
||||
|
||||
// TODO: I guess this should be a 404 instead...
|
||||
test("get call to server returns 200", function(done){
|
||||
assert.response(server, {
|
||||
url: '/',
|
||||
method: 'GET'
|
||||
},{
|
||||
status: 200
|
||||
}, function() { done(); });
|
||||
Step(
|
||||
function doGet() {
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
url: '/',
|
||||
method: 'GET'
|
||||
},{}, function(res, err) { next(err,res); });
|
||||
},
|
||||
function doCheck(err, res) {
|
||||
if ( err ) throw err;
|
||||
assert.ok(res.statusCode, 200);
|
||||
var cc = res.headers['x-cache-channel'];
|
||||
assert.ok(!cc);
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -95,11 +123,11 @@ suite('server', function() {
|
||||
method: 'GET'
|
||||
},{
|
||||
status: 200,
|
||||
headers: { 'X-Cache-Channel': 'cartodb_test_user_1_db:my_table' },
|
||||
headers: { 'X-Cache-Channel': test_database+':my_table' },
|
||||
}, function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.equal(parsed.style, _.template(default_style, {table: 'my_table'}));
|
||||
assert.equal(parsed.style_version, mapnik.versions.mapnik);
|
||||
assert.equal(parsed.style_version, mapnik_version);
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -113,10 +141,10 @@ suite('server', function() {
|
||||
method: 'GET'
|
||||
},{
|
||||
}, function(res) {
|
||||
// FIXME: should be 401 Unauthorized
|
||||
assert.equal(res.statusCode, 400, res.body);
|
||||
assert.equal(res.statusCode, 403, res.statusCode + ':' + res.body);
|
||||
assert.deepEqual(JSON.parse(res.body),
|
||||
{error: 'Sorry, you are unauthorized (permission denied)'});
|
||||
assert.ok(!res.headers.hasOwnProperty('cache-control'));
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -130,10 +158,11 @@ suite('server', function() {
|
||||
method: 'GET'
|
||||
},{
|
||||
}, function(res) {
|
||||
// FIXME: should be 401 Unauthorized
|
||||
// FIXME: should be 403 Forbidden or 404 User Not Found
|
||||
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
|
||||
assert.deepEqual(JSON.parse(res.body),
|
||||
{error:"missing unknown_user's dbname in redis (try CARTODB/script/restore_redis)"});
|
||||
{error:"missing unknown_user's database_name in redis (try CARTODB/script/restore_redis)"});
|
||||
assert.ok(!res.headers.hasOwnProperty('cache-control'));
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -150,11 +179,49 @@ suite('server', function() {
|
||||
var parsed = JSON.parse(res.body);
|
||||
var style = _.template(default_style, {table: 'test_table_private_1'});
|
||||
assert.equal(parsed.style, style);
|
||||
assert.equal(parsed.style_version, mapnik.versions.mapnik);
|
||||
assert.equal(parsed.style_version, mapnik_version);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/issues/94
|
||||
test("get'ing unrenderable style", function(done) {
|
||||
var base_key = 'map_style|'+test_database+'|issue94';
|
||||
var style = '#s{bogus}';
|
||||
Step(
|
||||
function checkRedis() {
|
||||
redis_client.keys(base_key+'*', this);
|
||||
},
|
||||
function setupRedisBase(err, matches) {
|
||||
if ( err ) throw err;
|
||||
assert.equal(matches.length, 0,
|
||||
'Unexpected redis keys at test start: ' + matches.join("\n"));
|
||||
redis_client.set(base_key,
|
||||
JSON.stringify({ style: style }),
|
||||
this);
|
||||
},
|
||||
function getStyle(err) {
|
||||
if ( err ) throw err;
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
headers: {host: 'localhost'},
|
||||
url: '/tiles/issue94/style',
|
||||
method: 'GET'
|
||||
}, {}, function(res) { next(null, res); });
|
||||
},
|
||||
function checkStyle(err, res) {
|
||||
if ( err ) throw err;
|
||||
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.equal(parsed.style, style);
|
||||
return null
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// POST STYLE
|
||||
@@ -167,9 +234,12 @@ suite('server', function() {
|
||||
url: '/tiles/my_table/style',
|
||||
method: 'POST'
|
||||
},{
|
||||
status: 400,
|
||||
body: '{"error":"must send style information"}'
|
||||
}, function() { done(); });
|
||||
}, function(res) {
|
||||
assert.equal(res.statusCode, 400);
|
||||
assert.ok(!res.headers.hasOwnProperty('cache-control'));
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test("post'ing bad style returns 400 with error", function(done){
|
||||
@@ -190,10 +260,11 @@ suite('server', function() {
|
||||
method: 'POST',
|
||||
headers: {host: 'localhost', 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
data: querystring.stringify({style: '#my_table3{'})
|
||||
},{
|
||||
status: 400,
|
||||
body: /Missing closing/
|
||||
}, function() { done(); });
|
||||
},{}, function(res) {
|
||||
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
|
||||
assert.ok( RegExp(/missing closing/i).test(res.body) );
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test("post'ing multiple bad styles returns 400 with error array", function(done){
|
||||
@@ -255,7 +326,7 @@ suite('server', function() {
|
||||
headers: {host: 'localhost', 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
data: querystring.stringify({style: 'Map { background-color:#aaa; }'})
|
||||
},{}, function(res) {
|
||||
// FIXME: should be 401 Unauthorized
|
||||
// FIXME: should be 403 Forbidden
|
||||
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
|
||||
assert.ok(res.body.indexOf('map state cannot be changed by unauthenticated request') != -1, res.body);
|
||||
|
||||
@@ -305,7 +376,7 @@ suite('server', function() {
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.equal(parsed.style, style);
|
||||
assert.equal(parsed.style_version, mapnik.versions.mapnik);
|
||||
assert.equal(parsed.style_version, mapnik_version);
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -333,7 +404,7 @@ suite('server', function() {
|
||||
var parsed = JSON.parse(res.body);
|
||||
// NOTE: no transform expected for the specific style
|
||||
assert.equal(parsed.style, style);
|
||||
assert.equal(parsed.style_version, mapnik.versions.mapnik);
|
||||
assert.equal(parsed.style_version, mapnik_version);
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -353,8 +424,8 @@ suite('server', function() {
|
||||
method: 'DELETE',
|
||||
headers: {host: 'localhost'},
|
||||
},{}, function(res) {
|
||||
// FIXME: should be 401 Unauthorized
|
||||
assert.equal(res.statusCode, 500, res.body);
|
||||
// FIXME: should be 403 Forbidden
|
||||
assert.equal(res.statusCode, 400, res.body);
|
||||
assert.ok(res.body.indexOf('map state cannot be changed by unauthenticated request') != -1, res.body);
|
||||
// check that the style wasn't really deleted !
|
||||
assert.response(server, {
|
||||
@@ -432,7 +503,7 @@ suite('server', function() {
|
||||
method: 'GET'
|
||||
},{
|
||||
status: 200,
|
||||
headers: { 'X-Cache-Channel': 'cartodb_test_user_1_db:my_tablez' },
|
||||
headers: { 'X-Cache-Channel': test_database+':my_tablez' },
|
||||
body: '{"infowindow":null}'
|
||||
}, function() { done(); });
|
||||
});
|
||||
@@ -467,7 +538,7 @@ suite('server', function() {
|
||||
url: '/tiles/test_table_private_1/infowindow',
|
||||
method: 'GET'
|
||||
},{}, function(res) {
|
||||
// FIXME: should be 401 Unauthorized
|
||||
// FIXME: should be 403 Forbidden
|
||||
assert.equal(res.statusCode, 500, res.statusCode + ': ' + res.body);
|
||||
done();
|
||||
});
|
||||
@@ -482,10 +553,10 @@ suite('server', function() {
|
||||
method: 'GET'
|
||||
},{
|
||||
}, function(res) {
|
||||
// FIXME: should be 401 Unauthorized
|
||||
// FIXME: should be 403 Forbidden
|
||||
assert.equal(res.statusCode, 500, res.statusCode + ': ' + res.body);
|
||||
assert.deepEqual(JSON.parse(res.body),
|
||||
{error:"missing unknown_user's dbname in redis (try CARTODB/script/restore_redis)"});
|
||||
{error:"missing unknown_user's database_name in redis (try CARTODB/script/restore_redis)"});
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -515,8 +586,8 @@ suite('server', function() {
|
||||
method: 'GET'
|
||||
},{
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'text/javascript; charset=utf-8; charset=utf-8',
|
||||
'X-Cache-Channel': 'cartodb_test_user_1_db:gadm4' }
|
||||
headers: { 'Content-Type': 'application/json; charset=utf-8',
|
||||
'X-Cache-Channel': test_database+':gadm4' }
|
||||
}, function() { done(); });
|
||||
});
|
||||
|
||||
@@ -527,7 +598,7 @@ suite('server', function() {
|
||||
method: 'GET'
|
||||
},{
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'text/javascript; charset=utf-8; charset=utf-8' }
|
||||
headers: { 'Content-Type': 'application/json; charset=utf-8' }
|
||||
}, function() { done(); });
|
||||
});
|
||||
|
||||
@@ -539,7 +610,7 @@ suite('server', function() {
|
||||
method: 'GET'
|
||||
},{
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'text/javascript; charset=utf-8; charset=utf-8' }
|
||||
headers: { 'Content-Type': 'application/json; charset=utf-8' }
|
||||
}, function() { done(); });
|
||||
});
|
||||
|
||||
@@ -550,8 +621,26 @@ suite('server', function() {
|
||||
url: '/tiles/test_table_private_1/6/31/24.grid.json',
|
||||
method: 'GET'
|
||||
},{}, function(res) {
|
||||
// 401 Unauthorized
|
||||
assert.equal(res.statusCode, 401, res.statusCode + ': ' + res.body);
|
||||
// 403 Forbidden
|
||||
assert.equal(res.statusCode, 403, res.statusCode + ': ' + res.body);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
// See http://github.com/CartoDB/Windshaft-cartodb/issues/186
|
||||
test("get'ing the grid of a private table should fail when unauthenticated (jsonp)",
|
||||
function(done) {
|
||||
assert.response(server, {
|
||||
headers: {host: 'localhost'},
|
||||
url: '/tiles/test_table_private_1/6/31/24.grid.json?callback=x',
|
||||
method: 'GET'
|
||||
},{}, function(res) {
|
||||
// It's forbidden, but jsonp calls for status = 200
|
||||
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
|
||||
// Still, we do NOT want to add caching headers here
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/issues/186
|
||||
assert.ok(!res.headers.hasOwnProperty('cache-control'),
|
||||
"Unexpected Cache-Control: " + res.headers['cache-control']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -565,10 +654,10 @@ suite('server', function() {
|
||||
method: 'GET'
|
||||
},{
|
||||
}, function(res) {
|
||||
// FIXME: should be 401 Unauthorized
|
||||
// FIXME: should be 403 Forbidden
|
||||
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
|
||||
assert.deepEqual(JSON.parse(res.body),
|
||||
{error:"missing unknown_user's dbname in redis (try CARTODB/script/restore_redis)"});
|
||||
{error:"missing unknown_user's database_name in redis (try CARTODB/script/restore_redis)"});
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -631,7 +720,7 @@ suite('server', function() {
|
||||
method: 'GET'
|
||||
},{
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'image/png', 'X-Cache-Channel': 'cartodb_test_user_1_db:gadm4' }
|
||||
headers: { 'Content-Type': 'image/png', 'X-Cache-Channel': test_database+':gadm4' }
|
||||
}, function() { done(); });
|
||||
});
|
||||
|
||||
@@ -699,8 +788,8 @@ suite('server', function() {
|
||||
method: 'GET'
|
||||
},{
|
||||
}, function(res) {
|
||||
// 401 Unauthorized
|
||||
assert.equal(res.statusCode, 401, res.statusCode + ': ' + res.body);
|
||||
// 403 Forbidden
|
||||
assert.equal(res.statusCode, 403, res.statusCode + ': ' + res.body);
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -716,10 +805,12 @@ suite('server', function() {
|
||||
method: 'GET'
|
||||
},{
|
||||
}, function(res) {
|
||||
// FIXME: should be 401 Unauthorized
|
||||
// FIXME: should be 403 Forbidden
|
||||
assert.equal(res.statusCode, 400, res.statusCode + ': ' + res.body);
|
||||
assert.deepEqual(JSON.parse(res.body),
|
||||
{error:"missing unknown_user's dbname in redis (try CARTODB/script/restore_redis)"});
|
||||
{error:"missing unknown_user's database_name in redis (try CARTODB/script/restore_redis)"});
|
||||
assert.ok(!res.headers.hasOwnProperty('cache-control'),
|
||||
"Unexpected Cache-Control: " + res.headers['cache-control']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -738,8 +829,11 @@ suite('server', function() {
|
||||
method: 'GET'
|
||||
},{
|
||||
}, function(res) {
|
||||
// 401 Unauthorized
|
||||
assert.equal(res.statusCode, 401, res.statusCode + ': ' + res.body);
|
||||
// 403 Forbidden
|
||||
assert.equal(res.statusCode, 403, res.statusCode + ': ' + res.body);
|
||||
// Failed in 1.6.0 of https://github.com/CartoDB/Windshaft-cartodb/issues/107
|
||||
assert.ok(!res.headers.hasOwnProperty('cache-control'),
|
||||
"Unexpected Cache-Control: " + res.headers['cache-control']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -755,7 +849,7 @@ suite('server', function() {
|
||||
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
|
||||
var ct = res.headers['content-type'];
|
||||
assert.equal(ct, 'image/png');
|
||||
assert.imageEqualsFile(res.body, './test/fixtures/test_table_15_16046_12354_styled_black.png', 2,
|
||||
assert.imageEqualsFile(res.body, './test/fixtures/test_table_15_16046_12354_styled_black.png', IMAGE_EQUALS_TOLERANCE_PER_MIL,
|
||||
function(err, similarity) {
|
||||
if (err) throw err;
|
||||
done();
|
||||
@@ -763,6 +857,79 @@ suite('server', function() {
|
||||
});
|
||||
});
|
||||
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/issues/88
|
||||
test("get'ing a tile from a user-specific database should return an expected tile", function(done){
|
||||
var style = querystring.stringify({style: test_style_black_200, style_version: '2.0.0'});
|
||||
var backupDBHost = global.environment.postgres.host;
|
||||
global.environment.postgres.host = '6.6.6.6';
|
||||
Step (
|
||||
function() {
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
headers: {host: 'cartodb250user'},
|
||||
url: '/tiles/test_table/15/16046/12354.png?cache_buster=4.10&' + style,
|
||||
method: 'GET',
|
||||
encoding: 'binary'
|
||||
},{}, function(res){
|
||||
next(null, res);
|
||||
});
|
||||
},
|
||||
function checkRes(err, res) {
|
||||
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
|
||||
var ct = res.headers['content-type'];
|
||||
assert.equal(ct, 'image/png');
|
||||
assert.imageEqualsFile(res.body,
|
||||
'./test/fixtures/test_table_15_16046_12354_styled_black.png',
|
||||
IMAGE_EQUALS_TOLERANCE_PER_MIL, this);
|
||||
},
|
||||
function checkImage(err, similarity) {
|
||||
if (err) throw err;
|
||||
return null
|
||||
},
|
||||
function finish(err) {
|
||||
global.environment.postgres.host = backupDBHost;
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/issues/89
|
||||
test("get'ing a tile with a user-specific database password", function(done){
|
||||
var style = querystring.stringify({style: test_style_black_200, style_version: '2.0.0'});
|
||||
var backupDBPass = global.environment.postgres_auth_pass;
|
||||
global.environment.postgres_auth_pass = '<%= user_password %>';
|
||||
Step (
|
||||
function() {
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
headers: {host: 'cartodb250user'},
|
||||
url: '/tiles/test_table/15/16046/12354.png?'
|
||||
+ 'cache_buster=4.20&api_key=4321&' + style,
|
||||
method: 'GET',
|
||||
encoding: 'binary'
|
||||
},{}, function(res){
|
||||
next(null, res);
|
||||
});
|
||||
},
|
||||
function checkRes(err, res) {
|
||||
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
|
||||
var ct = res.headers['content-type'];
|
||||
assert.equal(ct, 'image/png');
|
||||
assert.imageEqualsFile(res.body,
|
||||
'./test/fixtures/test_table_15_16046_12354_styled_black.png',
|
||||
IMAGE_EQUALS_TOLERANCE_PER_MIL, this);
|
||||
},
|
||||
function checkImage(err, similarity) {
|
||||
if (err) throw err;
|
||||
return null
|
||||
},
|
||||
function finish(err) {
|
||||
global.environment.postgres_auth_pass = backupDBPass;
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("get'ing a tile with url specified 2.1.0 style should return an expected tile", function(done){
|
||||
var style = querystring.stringify({style: test_style_black_210, style_version: '2.1.0'});
|
||||
assert.response(server, {
|
||||
@@ -774,7 +941,7 @@ suite('server', function() {
|
||||
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
|
||||
var ct = res.headers['content-type'];
|
||||
assert.equal(ct, 'image/png');
|
||||
assert.imageEqualsFile(res.body, './test/fixtures/test_table_15_16046_12354_styled_black.png', 2,
|
||||
assert.imageEqualsFile(res.body, './test/fixtures/test_table_15_16046_12354_styled_black.png', IMAGE_EQUALS_TOLERANCE_PER_MIL,
|
||||
function(err, similarity) {
|
||||
if (err) throw err;
|
||||
done();
|
||||
@@ -811,7 +978,7 @@ suite('server', function() {
|
||||
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
|
||||
var ct = res.headers['content-type'];
|
||||
assert.equal(ct, 'image/png');
|
||||
assert.imageEqualsFile(res.body, './test/fixtures/test_table_15_16046_12354_styled_black.png', 2,
|
||||
assert.imageEqualsFile(res.body, './test/fixtures/test_table_15_16046_12354_styled_black.png', IMAGE_EQUALS_TOLERANCE_PER_MIL,
|
||||
function(err, similarity) {
|
||||
next(err);
|
||||
});
|
||||
@@ -851,7 +1018,7 @@ suite('server', function() {
|
||||
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
|
||||
var ct = res.headers['content-type'];
|
||||
assert.equal(ct, 'image/png');
|
||||
assert.imageEqualsFile(res.body, './test/fixtures/blank.png', 0,
|
||||
assert.imageEqualsFile(res.body, './test/fixtures/blank.png', IMAGE_EQUALS_ZERO_TOLERANCE_PER_MIL,
|
||||
function(err, similarity) {
|
||||
if (err) next(err);
|
||||
else next();
|
||||
@@ -871,7 +1038,7 @@ suite('server', function() {
|
||||
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
|
||||
var ct = res.headers['content-type'];
|
||||
assert.equal(ct, 'image/png');
|
||||
assert.imageEqualsFile(res.body, './test/fixtures/blank.png', 0,
|
||||
assert.imageEqualsFile(res.body, './test/fixtures/blank.png', IMAGE_EQUALS_ZERO_TOLERANCE_PER_MIL,
|
||||
function(err, similarity) {
|
||||
if (err) next(err);
|
||||
else next();
|
||||
@@ -908,7 +1075,7 @@ suite('server', function() {
|
||||
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
|
||||
var ct = res.headers['content-type'];
|
||||
assert.equal(ct, 'image/png');
|
||||
assert.imageEqualsFile(res.body, './test/fixtures/test_table_15_16046_12354_styled_black.png', 2,
|
||||
assert.imageEqualsFile(res.body, './test/fixtures/test_table_15_16046_12354_styled_black.png', IMAGE_EQUALS_TOLERANCE_PER_MIL,
|
||||
function(err, similarity) {
|
||||
// NOTE: we expect them to be EQUAL here
|
||||
if (err) { next(err); return; }
|
||||
@@ -945,7 +1112,7 @@ suite('server', function() {
|
||||
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
|
||||
var ct = res.headers['content-type'];
|
||||
assert.equal(ct, 'image/png');
|
||||
assert.imageEqualsFile(res.body, './test/fixtures/test_table_15_16046_12354_styled_black.png', 2,
|
||||
assert.imageEqualsFile(res.body, './test/fixtures/test_table_15_16046_12354_styled_black.png', IMAGE_EQUALS_TOLERANCE_PER_MIL,
|
||||
function(err, similarity) {
|
||||
// NOTE: we expect them to be different here
|
||||
if (err) next();
|
||||
@@ -954,8 +1121,7 @@ suite('server', function() {
|
||||
});
|
||||
},
|
||||
function finish(err) {
|
||||
if ( err ) done(err);
|
||||
else done();
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -970,24 +1136,62 @@ suite('server', function() {
|
||||
var sqlapi;
|
||||
Step(
|
||||
function sendRequest(err) {
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
headers: {host: 'localhost'},
|
||||
url: '/tiles/gadm4/6/31/24.png?' + querystring.stringify(qo),
|
||||
method: 'GET'
|
||||
},{}, this);
|
||||
},{}, function(res) { next(null, res); });
|
||||
},
|
||||
function checkResponse(res) {
|
||||
function checkResponse(err, res) {
|
||||
if ( err ) throw err;
|
||||
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
|
||||
var ct = res.headers['content-type'];
|
||||
assert.equal(ct, 'image/png');
|
||||
var cc = res.headers['x-cache-channel'];
|
||||
var dbname = 'cartodb_test_user_1_db'
|
||||
assert(cc, 'Missing X-Cache-Channel');
|
||||
var dbname = test_database;
|
||||
assert.equal(cc.substring(0, dbname.length), dbname);
|
||||
var jsonquery = cc.substring(dbname.length+1);
|
||||
var sentquery = JSON.parse(jsonquery);
|
||||
assert.equal(sentquery.api_key, qo.map_key);
|
||||
assert.equal(sentquery.q, 'SELECT CDB_QueryTables($windshaft$' + qo.sql + '$windshaft$)');
|
||||
done();
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("passes hostname header to sqlapi", function(done){
|
||||
var qo = {
|
||||
sql: "SELECT * from gadm4",
|
||||
map_key: 1234
|
||||
};
|
||||
var sqlapi;
|
||||
Step(
|
||||
function sendRequest(err) {
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
headers: {host: 'localhost'},
|
||||
url: '/tiles/gadm4/6/31/24.png?' + querystring.stringify(qo),
|
||||
method: 'GET'
|
||||
},{}, function(res) { next(null, res); });
|
||||
},
|
||||
function checkResponse(err, res) {
|
||||
if ( err ) throw err;
|
||||
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
|
||||
var last_request = sqlapi_server.getLastRequest();
|
||||
assert.ok(last_request);
|
||||
var host = last_request.headers['host'];
|
||||
assert.ok(host);
|
||||
assert.equal(last_request.method, 'GET');
|
||||
assert.equal(host, 'localhost.donot_look_this_up');
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -1003,23 +1207,59 @@ suite('server', function() {
|
||||
var sqlapi;
|
||||
Step(
|
||||
function sendRequest(err) {
|
||||
var next = this;
|
||||
assert.response(server, {
|
||||
headers: {host: 'localhost'},
|
||||
url: '/tiles/gadm4/6/31/24.png?' + querystring.stringify(qo),
|
||||
method: 'GET'
|
||||
},{}, this);
|
||||
},{}, function(res) { next(null, res); });
|
||||
},
|
||||
function checkResponse(res) {
|
||||
function checkResponse(err, res) {
|
||||
if ( err ) throw err;
|
||||
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
|
||||
var ct = res.headers['content-type'];
|
||||
assert.equal(ct, 'image/png');
|
||||
// does NOT send an x-cache-channel
|
||||
assert.ok(!res.headers.hasOwnProperty('x-cache-channel'));
|
||||
done();
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// Zoom is a special variable
|
||||
test("Specifying zoom level in CartoCSS does not need a 'zoom' variable in SQL output", function(done){
|
||||
// NOTE: may fail if grainstore < 0.3.0 is used by Windshaft
|
||||
var query = querystring.stringify({
|
||||
sql: "SELECT 'SRID=3857;POINT(0 0)'::geometry as the_geom_webmercator, 1::int as cartodb_id",
|
||||
style: '#gadm4 [ zoom>=3] { marker-fill:red; }'
|
||||
});
|
||||
assert.response(server, {
|
||||
headers: {host: 'localhost'},
|
||||
url: '/tiles/gadm4/0/0/0.png?' + query,
|
||||
method: 'GET'
|
||||
},{}, function(res) {
|
||||
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/issues/115
|
||||
test.skip("get'ing tile with not-strictly-valid style", function(done) {
|
||||
var style = querystring.stringify({style: '#test_table{line-color:black}}', style_version: '2.0.0'});
|
||||
assert.response(server, {
|
||||
headers: {host: 'localhost'},
|
||||
url: '/tiles/test_table/0/0/0.png?' + style, // madrid
|
||||
method: 'GET',
|
||||
encoding: 'binary'
|
||||
},{}, function(res){
|
||||
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// DELETE CACHE
|
||||
@@ -1033,6 +1273,7 @@ suite('server', function() {
|
||||
method: 'DELETE'
|
||||
},{}, function(res) {
|
||||
assert.equal(res.statusCode, 404, res.statusCode + ': ' + res.body);
|
||||
assert.ok(!res.headers.hasOwnProperty('cache-control'));
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -1062,8 +1303,9 @@ suite('server', function() {
|
||||
url: '/tiles/test_table_private_1/map_metadata',
|
||||
method: 'GET'
|
||||
},{}, function(res) {
|
||||
// FIXME: should be 401 instead
|
||||
// FIXME: should be 403 instead
|
||||
assert.equal(res.statusCode, 500, res.statusCode + ': ' + res.body);
|
||||
assert.ok(!res.headers.hasOwnProperty('cache-control'));
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -1116,6 +1358,7 @@ suite('server', function() {
|
||||
method: 'DELETE'
|
||||
},{}, function(res) {
|
||||
assert.equal(res.statusCode, 404, res.statusCode + ': ' + res.body);
|
||||
assert.ok(!res.headers.hasOwnProperty('cache-control'));
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -1150,3 +1393,4 @@ suite('server', function() {
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
1949
test/acceptance/templates.js
Normal file
1949
test/acceptance/templates.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,38 +1,81 @@
|
||||
var http = require('http');
|
||||
var url = require('url');
|
||||
var _ = require('underscore');
|
||||
|
||||
var o = function(port, cb) {
|
||||
var SQLAPIEmulator = function(port, cb) {
|
||||
|
||||
this.queries = [];
|
||||
var that = this;
|
||||
this.requests = [];
|
||||
|
||||
this.sqlapi_server = http.createServer(function(req,res) {
|
||||
var query = url.parse(req.url, true).query;
|
||||
that.queries.push(query);
|
||||
if ( query.q.match('SQLAPIERROR') ) {
|
||||
res.statusCode = 400;
|
||||
res.write(JSON.stringify({'error':'Some error occurred'}));
|
||||
} else if ( query.q.match('EPOCH.* as max') ) {
|
||||
// This is the structure of the known query sent by tiler
|
||||
var row = {
|
||||
'max': 1234567890.123
|
||||
};
|
||||
res.write(JSON.stringify({rows: [ row ]}));
|
||||
} else {
|
||||
var qs = JSON.stringify(query);
|
||||
var row = {
|
||||
// This is the structure of the known query sent by tiler
|
||||
'cdb_querytables': '{' + qs + '}',
|
||||
'max': qs
|
||||
};
|
||||
res.write(JSON.stringify({rows: [ row ]}));
|
||||
}
|
||||
res.end();
|
||||
//console.log("server got request with method " + req.method);
|
||||
var query;
|
||||
|
||||
that.requests.push(req);
|
||||
|
||||
if ( req.method == 'GET' ) {
|
||||
query = url.parse(req.url, true).query;
|
||||
that.handleQuery(query, res);
|
||||
}
|
||||
else if ( req.method == 'POST') {
|
||||
var data = '';
|
||||
req.on('data', function(chunk) {
|
||||
//console.log("GOT Chunk " + chunk);
|
||||
data += chunk;
|
||||
});
|
||||
req.on('end', function() {
|
||||
//console.log("Data is: "); console.dir(data);
|
||||
query = JSON.parse(data);
|
||||
//console.log("handleQuery is " + that.handleQuery);
|
||||
that.handleQuery(query, res);
|
||||
});
|
||||
}
|
||||
else {
|
||||
that.handleQuery('SQLAPIEmu does not support method' + req.method, res);
|
||||
}
|
||||
}).listen(port, cb);
|
||||
};
|
||||
|
||||
o.prototype.close = function(cb) {
|
||||
SQLAPIEmulator.prototype.handleQuery = function(query, res) {
|
||||
this.queries.push(query);
|
||||
if ( query.q.match('SQLAPIERROR') ) {
|
||||
res.statusCode = 400;
|
||||
res.write(JSON.stringify({'error':'Some error occurred'}));
|
||||
} else if ( query.q.match('SQLAPINOANSWER') ) {
|
||||
console.log("SQLAPIEmulator will never respond, on request");
|
||||
return;
|
||||
} else if (query.q.match('tablenames')) {
|
||||
var tableNames = JSON.stringify(query);
|
||||
res.write(queryResult({tablenames: '{' + tableNames + '}', max: 1234567890.123}));
|
||||
} else if ( query.q.match('EPOCH.* as max') ) {
|
||||
// This is the structure of the known query sent by tiler
|
||||
res.write(queryResult({max: 1234567890.123}));
|
||||
} else {
|
||||
if ( query.q.match('_private_') && query.api_key === undefined) {
|
||||
res.statusCode = 403;
|
||||
res.write(JSON.stringify({'error':'forbidden: ' + JSON.stringify(query)}));
|
||||
} else {
|
||||
var qs = JSON.stringify(query);
|
||||
res.write(queryResult({cdb_querytables: '{' + qs + '}', max: 1234567890.123}));
|
||||
}
|
||||
}
|
||||
res.end();
|
||||
};
|
||||
|
||||
SQLAPIEmulator.prototype.close = function(cb) {
|
||||
this.sqlapi_server.close(cb);
|
||||
};
|
||||
|
||||
module.exports = o;
|
||||
SQLAPIEmulator.prototype.getLastRequest = function() {
|
||||
return this.requests.pop();
|
||||
};
|
||||
|
||||
function queryResult(row) {
|
||||
return JSON.stringify({
|
||||
rows: [row]
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = SQLAPIEmulator;
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
// Cribbed from the ever prolific Konstantin Kaefer
|
||||
// https://github.com/mapbox/tilelive-mapnik/blob/master/test/support/assert.js
|
||||
|
||||
var fs = require('fs');
|
||||
var http = require('http');
|
||||
var path = require('path');
|
||||
var exec = require('child_process').exec;
|
||||
var exec = require('child_process').exec,
|
||||
fs = require('fs'),
|
||||
http = require('http'),
|
||||
path = require('path'),
|
||||
util = require('util');
|
||||
|
||||
var assert = module.exports = exports = require('assert');
|
||||
|
||||
@@ -66,35 +67,51 @@ assert.utfgridEqualsFile = function(buffer, file_b, tolerance, callback) {
|
||||
callback(err);
|
||||
};
|
||||
|
||||
//
|
||||
// @param tol tolerated color distance as a percent over max channel value
|
||||
// by default this is zero. For meaningful values, see
|
||||
// http://www.imagemagick.org/script/command-line-options.php#metric
|
||||
//
|
||||
assert.imageEqualsFile = function(buffer, file_b, tol, callback) {
|
||||
/**
|
||||
* Takes an image data as an input and an image path and compare them using ImageMagick fuzz algorithm, if case the
|
||||
* similarity is not within the tolerance limit it will callback with an error.
|
||||
*
|
||||
* @param buffer The image data to compare from
|
||||
* @param {string} referenceImageRelativeFilePath The relative file to compare against
|
||||
* @param {number} tolerance tolerated mean color distance, as a per mil (‰)
|
||||
* @param {function} callback Will call to home with null in case there is no error, otherwise with the error itself
|
||||
* @see FUZZY in http://www.imagemagick.org/script/command-line-options.php#metric
|
||||
*/
|
||||
assert.imageEqualsFile = function(buffer, referenceImageRelativeFilePath, tolerance, callback) {
|
||||
if (!callback) callback = function(err) { if (err) throw err; };
|
||||
file_b = path.resolve(file_b);
|
||||
var file_a = '/tmp/windshaft-test-image-test.png'; // + (Math.random() * 1e16); // TODO: make predictable
|
||||
var err = fs.writeFileSync(file_a, buffer, 'binary');
|
||||
var referenceImageFilePath = path.resolve(referenceImageRelativeFilePath),
|
||||
testImageFilePath = '/tmp/windshaft-test-image-' + (Math.random() * 1e16); // TODO: make predictable
|
||||
var err = fs.writeFileSync(testImageFilePath, buffer, 'binary');
|
||||
if (err) throw err;
|
||||
|
||||
var fuzz = tol + '%';
|
||||
exec('compare -fuzz ' + fuzz + ' -metric AE "' + file_a + '" "' +
|
||||
file_b + '" /dev/null', function(err, stdout, stderr) {
|
||||
var imageMagickCmd = util.format(
|
||||
'compare -metric fuzz "%s" "%s" /dev/null',
|
||||
testImageFilePath, referenceImageFilePath
|
||||
);
|
||||
|
||||
exec(imageMagickCmd, function(err, stdout, stderr) {
|
||||
if (err) {
|
||||
fs.unlinkSync(file_a);
|
||||
fs.unlinkSync(testImageFilePath);
|
||||
callback(err);
|
||||
} else {
|
||||
stderr = stderr.trim();
|
||||
var similarity = parseFloat(stderr);
|
||||
if ( similarity > 0 ) {
|
||||
var err = new Error('Images not equal(' + similarity + '): ' +
|
||||
file_a + ' ' + file_b);
|
||||
err.similarity = similarity;
|
||||
callback(err);
|
||||
var metrics = stderr.match(/([0-9]*) \((.*)\)/);
|
||||
if ( ! metrics ) {
|
||||
callback(new Error("No match for " + stderr));
|
||||
return;
|
||||
}
|
||||
var similarity = parseFloat(metrics[2]),
|
||||
tolerancePerMil = (tolerance / 1000);
|
||||
if (similarity > tolerancePerMil) {
|
||||
err = new Error(util.format(
|
||||
'Images %s and %s are not equal (got %d similarity, expected %d)',
|
||||
testImageFilePath, referenceImageFilePath, similarity, tolerancePerMil)
|
||||
);
|
||||
err.similarity = similarity;
|
||||
callback(err);
|
||||
} else {
|
||||
fs.unlinkSync(file_a);
|
||||
callback(null);
|
||||
fs.unlinkSync(testImageFilePath);
|
||||
callback(null);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -209,6 +226,8 @@ assert.response = function(server, req, res, msg){
|
||||
response.on('end', function(){
|
||||
if (timer) clearTimeout(timer);
|
||||
|
||||
check();
|
||||
|
||||
// Assert response body
|
||||
if (res.body !== undefined) {
|
||||
var eql = res.body instanceof RegExp
|
||||
@@ -254,7 +273,6 @@ assert.response = function(server, req, res, msg){
|
||||
|
||||
// Callback
|
||||
callback(response);
|
||||
check();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
var _ = require('underscore');
|
||||
|
||||
|
||||
require(__dirname + '/test_helper');
|
||||
|
||||
module.exports = function(opts) {
|
||||
|
||||
var config = {
|
||||
@@ -7,7 +10,7 @@ module.exports = function(opts) {
|
||||
max: 10,
|
||||
idleTimeoutMillis: 1,
|
||||
reapIntervalMillis: 1,
|
||||
port: 6333 // TODO: read from test env ?
|
||||
port: global.environment.redis.port
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,28 +10,103 @@
|
||||
# TODO: fix that
|
||||
#
|
||||
|
||||
PREPARE_REDIS=yes
|
||||
PREPARE_PGSQL=yes
|
||||
|
||||
while [ -n "$1" ]; do
|
||||
if test "$1" = "--skip-pg"; then
|
||||
PREPARE_PGSQL=no
|
||||
shift; continue
|
||||
elif test "$1" = "--skip-redis"; then
|
||||
PREPARE_REDIS=no
|
||||
shift; continue
|
||||
fi
|
||||
done
|
||||
|
||||
die() {
|
||||
msg=$1
|
||||
echo "${msg}" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
TEST_DB="cartodb_test_user_1_db"
|
||||
REDIS_PORT=6333
|
||||
# This is where postgresql connection parameters are read from
|
||||
TESTENV=../../config/environments/test.js
|
||||
if [ \! -r ${TESTENV} ]; then
|
||||
echo "Cannot read ${TESTENV}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "preparing postgres..."
|
||||
dropdb "${TEST_DB}"
|
||||
createdb -Ttemplate_postgis -EUTF8 "${TEST_DB}" || die "Could not create test database"
|
||||
psql "${TEST_DB}" < ./sql/windshaft.test.sql
|
||||
psql "${TEST_DB}" < ./sql/gadm4.sql
|
||||
TESTUSERID=1
|
||||
|
||||
echo "preparing redis..."
|
||||
echo "HSET rails:users:localhost id 1" | redis-cli -p ${REDIS_PORT} -n 5
|
||||
echo 'HSET rails:users:localhost database_name "'"${TEST_DB}"'"' | redis-cli -p ${REDIS_PORT} -n 5
|
||||
echo "HSET rails:users:localhost map_key 1234" | redis-cli -p ${REDIS_PORT} -n 5
|
||||
echo "SADD rails:users:localhost:map_key 1235" | redis-cli -p ${REDIS_PORT} -n 5
|
||||
echo 'HSET rails:'"${TEST_DB}"':my_table infowindow "this, that, the other"' | redis-cli -p ${REDIS_PORT} -n 0
|
||||
echo 'HSET rails:'"${TEST_DB}"':test_table_private_1 privacy "0"' | redis-cli -p ${REDIS_PORT} -n 0
|
||||
TESTUSER=`node -e "console.log(require('${TESTENV}').postgres_auth_user || '')"`
|
||||
if test -z "$TESTUSER"; then
|
||||
echo "Missing postgres_auth_user from ${TESTENV}" >&2
|
||||
exit 1
|
||||
fi
|
||||
TESTUSER=`echo ${TESTUSER} | sed "s/<%= user_id %>/${TESTUSERID}/"`
|
||||
|
||||
TESTPASS=`node -e "console.log(require('${TESTENV}').postgres_auth_pass || 'test')"`
|
||||
# TODO: should postgres_auth_pass be optional ?
|
||||
if test -z "$TESTPASS"; then
|
||||
echo "Missing postgres_auth_pass from ${TESTENV}" >&2
|
||||
exit 1
|
||||
fi
|
||||
TESTPASS=`echo ${TESTPASS} | sed "s/<%= user_id %>/${TESTUSERID}/"`
|
||||
|
||||
TEST_DB="${TESTUSER}_db"
|
||||
|
||||
# NOTE: will be set by caller trough environment
|
||||
if test -z "$REDIS_PORT"; then REDIS_PORT=6333; fi
|
||||
|
||||
PUBLICUSER=`node -e "console.log(require('${TESTENV}').postgres.user || 'xxx')"`
|
||||
PUBLICPASS=`node -e "console.log(require('${TESTENV}').postgres.password || 'xxx')"`
|
||||
echo "PUBLICUSER: ${PUBLICUSER}"
|
||||
echo "PUBLICPASS: ${PUBLICPASS}"
|
||||
echo "TESTUSER: ${TESTUSER}"
|
||||
echo "TESTPASS: ${TESTPASS}"
|
||||
|
||||
if test x"$PREPARE_PGSQL" = xyes; then
|
||||
|
||||
echo "preparing postgres..."
|
||||
dropdb "${TEST_DB}"
|
||||
createdb -Ttemplate_postgis -EUTF8 "${TEST_DB}" || die "Could not create test database"
|
||||
|
||||
cat sql/windshaft.test.sql sql/gadm4.sql |
|
||||
sed "s/:PUBLICUSER/${PUBLICUSER}/" |
|
||||
sed "s/:PUBLICPASS/${PUBLICPASS}/" |
|
||||
sed "s/:TESTUSER/${TESTUSER}/" |
|
||||
sed "s/:TESTPASS/${TESTPASS}/" |
|
||||
psql -v ON_ERROR_STOP=1 ${TEST_DB} || exit 1
|
||||
|
||||
fi
|
||||
|
||||
if test x"$PREPARE_REDIS" = xyes; then
|
||||
|
||||
echo "preparing redis..."
|
||||
|
||||
cat <<EOF | redis-cli -p ${REDIS_PORT} -n 5
|
||||
HMSET rails:users:localhost id ${TESTUSERID} \
|
||||
database_name "${TEST_DB}" \
|
||||
database_host localhost \
|
||||
map_key 1234
|
||||
SADD rails:users:localhost:map_key 1235
|
||||
EOF
|
||||
|
||||
# A user configured as with cartodb-2.5.0+
|
||||
cat <<EOF | redis-cli -p ${REDIS_PORT} -n 5
|
||||
HMSET rails:users:cartodb250user id ${TESTUSERID} \
|
||||
database_name "${TEST_DB}" \
|
||||
database_host "localhost" \
|
||||
database_password "${TESTPASS}" \
|
||||
map_key 4321
|
||||
EOF
|
||||
|
||||
cat <<EOF | redis-cli -p ${REDIS_PORT} -n 0
|
||||
HSET rails:${TEST_DB}:my_table infowindow "this, that, the other"
|
||||
HSET rails:${TEST_DB}:test_table_private_1 privacy "0"
|
||||
EOF
|
||||
|
||||
fi
|
||||
|
||||
echo "Finished preparing data. Ready to run tests"
|
||||
|
||||
|
||||
@@ -22,15 +22,15 @@ CREATE TABLE gadm4 (
|
||||
the_geom_webmercator geometry(MultiPolygon,3857)
|
||||
);
|
||||
|
||||
GRANT ALL ON TABLE gadm4 TO postgres;
|
||||
GRANT ALL ON TABLE gadm4 TO publicuser;
|
||||
--GRANT ALL ON TABLE gadm4 TO postgres;
|
||||
GRANT ALL ON TABLE gadm4 TO :PUBLICUSER;
|
||||
CREATE SEQUENCE gadm4_seq
|
||||
START WITH 1
|
||||
INCREMENT BY 1
|
||||
NO MINVALUE
|
||||
NO MAXVALUE
|
||||
CACHE 1;
|
||||
ALTER TABLE public.gadm4_seq OWNER TO postgres;
|
||||
--ALTER TABLE public.gadm4_seq OWNER TO postgres;
|
||||
SELECT pg_catalog.setval('gadm4_seq', 58, false);
|
||||
ALTER TABLE gadm4 ALTER COLUMN cartodb_id SET DEFAULT nextval('gadm4_seq'::regclass);
|
||||
|
||||
@@ -98,6 +98,6 @@ ALTER TABLE ONLY gadm4
|
||||
|
||||
CREATE INDEX bdll25_provincias_4326_2_the_geom_webmercator_idx ON gadm4 USING gist (the_geom_webmercator);
|
||||
|
||||
GRANT ALL ON TABLE gadm4 TO test_cartodb_user_1;
|
||||
GRANT SELECT ON TABLE gadm4 TO publicuser;
|
||||
GRANT ALL ON TABLE gadm4 TO :TESTUSER;
|
||||
GRANT SELECT ON TABLE gadm4 TO :PUBLICUSER;
|
||||
|
||||
|
||||
@@ -15,11 +15,13 @@ SET search_path = public, pg_catalog;
|
||||
SET default_tablespace = '';
|
||||
SET default_with_oids = false;
|
||||
|
||||
-- publicuser role
|
||||
CREATE USER publicuser;
|
||||
-- public user role
|
||||
DROP USER IF EXISTS :PUBLICUSER;
|
||||
CREATE USER :PUBLICUSER WITH PASSWORD ':PUBLICPASS';
|
||||
|
||||
-- db owner role
|
||||
CREATE USER test_cartodb_user_1;
|
||||
DROP USER IF EXISTS :TESTUSER;
|
||||
CREATE USER :TESTUSER WITH PASSWORD ':TESTPASS';
|
||||
|
||||
-- first table
|
||||
CREATE TABLE test_table (
|
||||
@@ -63,8 +65,8 @@ ALTER TABLE ONLY test_table ADD CONSTRAINT test_table_pkey PRIMARY KEY (cartodb_
|
||||
CREATE INDEX test_table_the_geom_idx ON test_table USING gist (the_geom);
|
||||
CREATE INDEX test_table_the_geom_webmercator_idx ON test_table USING gist (the_geom_webmercator);
|
||||
|
||||
GRANT ALL ON TABLE test_table TO test_cartodb_user_1;
|
||||
GRANT SELECT ON TABLE test_table TO publicuser;
|
||||
GRANT ALL ON TABLE test_table TO :TESTUSER;
|
||||
GRANT SELECT ON TABLE test_table TO :PUBLICUSER;
|
||||
|
||||
-- second table
|
||||
CREATE TABLE test_table_2 (
|
||||
@@ -108,8 +110,8 @@ ALTER TABLE ONLY test_table_2 ADD CONSTRAINT test_table_2_pkey PRIMARY KEY (cart
|
||||
CREATE INDEX test_table_2_the_geom_idx ON test_table_2 USING gist (the_geom);
|
||||
CREATE INDEX test_table_2_the_geom_webmercator_idx ON test_table_2 USING gist (the_geom_webmercator);
|
||||
|
||||
GRANT ALL ON TABLE test_table_2 TO test_cartodb_user_1;
|
||||
GRANT SELECT ON TABLE test_table_2 TO publicuser;
|
||||
GRANT ALL ON TABLE test_table_2 TO :TESTUSER;
|
||||
GRANT SELECT ON TABLE test_table_2 TO :PUBLICUSER;
|
||||
|
||||
-- third table
|
||||
CREATE TABLE test_table_3 (
|
||||
@@ -153,8 +155,8 @@ ALTER TABLE ONLY test_table_3 ADD CONSTRAINT test_table_3_pkey PRIMARY KEY (cart
|
||||
CREATE INDEX test_table_3_the_geom_idx ON test_table_3 USING gist (the_geom);
|
||||
CREATE INDEX test_table_3_the_geom_webmercator_idx ON test_table_3 USING gist (the_geom_webmercator);
|
||||
|
||||
GRANT ALL ON TABLE test_table_3 TO test_cartodb_user_1;
|
||||
GRANT SELECT ON TABLE test_table_3 TO publicuser;
|
||||
GRANT ALL ON TABLE test_table_3 TO :TESTUSER;
|
||||
GRANT SELECT ON TABLE test_table_3 TO :PUBLICUSER;
|
||||
|
||||
-- private table
|
||||
CREATE TABLE test_table_private_1 (
|
||||
@@ -172,5 +174,6 @@ CREATE TABLE test_table_private_1 (
|
||||
CONSTRAINT enforce_srid_the_geom CHECK ((st_srid(the_geom) = 4326)),
|
||||
CONSTRAINT enforce_srid_the_geom_webmercator CHECK ((st_srid(the_geom_webmercator) = 3857))
|
||||
);
|
||||
INSERT INTO test_table_private_1 SELECT * from test_table;
|
||||
|
||||
GRANT ALL ON TABLE test_table_private_1 TO test_cartodb_user_1;
|
||||
GRANT ALL ON TABLE test_table_private_1 TO :TESTUSER;
|
||||
|
||||
@@ -6,17 +6,19 @@
|
||||
*/
|
||||
|
||||
var _ = require('underscore');
|
||||
var LZMA = require('lzma/lzma_worker.js').LZMA;
|
||||
var assert = require('assert');
|
||||
var LZMA = require('lzma').LZMA;
|
||||
|
||||
var lzmaWorker = new LZMA();
|
||||
|
||||
// set environment specific variables
|
||||
global.settings = require(__dirname + '/../../config/settings');
|
||||
global.environment = require(__dirname + '/../../config/environments/test');
|
||||
_.extend(global.settings, global.environment);
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
|
||||
// Utility function to compress & encode LZMA
|
||||
function lzma_compress_to_base64(payload, mode, callback) {
|
||||
LZMA.compress(payload, mode,
|
||||
lzmaWorker.compress(payload, mode,
|
||||
function(ints) {
|
||||
ints = ints.map(function(c) { return String.fromCharCode(c + 128) }).join('')
|
||||
var base64 = new Buffer(ints, 'binary').toString('base64');
|
||||
@@ -28,7 +30,36 @@ function lzma_compress_to_base64(payload, mode, callback) {
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
lzma_compress_to_base64: lzma_compress_to_base64
|
||||
// Check that the response headers do not request caching
|
||||
// Throws on failure
|
||||
function checkNoCache(res) {
|
||||
assert.ok(!res.headers.hasOwnProperty('x-cache-channel'));
|
||||
assert.ok(!res.headers.hasOwnProperty('cache-control')); // is this correct ?
|
||||
assert.ok(!res.headers.hasOwnProperty('last-modified')); // is this correct ?
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check that the response headers do not request caching
|
||||
* @see checkNoCache
|
||||
* @param res
|
||||
*/
|
||||
function checkCache(res) {
|
||||
assert.ok(res.headers.hasOwnProperty('x-cache-channel'));
|
||||
assert.ok(res.headers.hasOwnProperty('cache-control'));
|
||||
assert.ok(res.headers.hasOwnProperty('last-modified'));
|
||||
}
|
||||
|
||||
function checkSurrogateKey(res, expectedKey) {
|
||||
assert.ok(res.headers.hasOwnProperty('surrogate-key'));
|
||||
assert.equal(res.headers['surrogate-key'], expectedKey);
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
lzma_compress_to_base64: lzma_compress_to_base64,
|
||||
checkNoCache: checkNoCache,
|
||||
checkSurrogateKey: checkSurrogateKey,
|
||||
checkCache: checkCache
|
||||
};
|
||||
|
||||
|
||||
29
test/unit/cartodb/cache/model/named_maps_entry.test.js
vendored
Normal file
29
test/unit/cartodb/cache/model/named_maps_entry.test.js
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
var assert = require('assert');
|
||||
var _ = require('underscore');
|
||||
var NamedMapsCacheEntry = require('../../../../../lib/cartodb/cache/model/named_maps_entry');
|
||||
|
||||
suite('cache named_maps_entry', function() {
|
||||
|
||||
var namedMapOwner = 'foo',
|
||||
namedMapName = 'wadus_name',
|
||||
namedMapsCacheEntry = new NamedMapsCacheEntry(namedMapOwner, namedMapName),
|
||||
entryKey = namedMapsCacheEntry.key();
|
||||
|
||||
test('key is a string', function() {
|
||||
assert.ok(_.isString(entryKey));
|
||||
});
|
||||
|
||||
test('key is 8 chars length', function() {
|
||||
assert.equal(entryKey.length, 8);
|
||||
var entryKeyParts = entryKey.split(':');
|
||||
assert.equal(entryKeyParts.length, 2);
|
||||
assert.equal(entryKeyParts[0], 'n');
|
||||
});
|
||||
|
||||
test('key is name spaced for named maps', function() {
|
||||
var entryKeyParts = entryKey.split(':');
|
||||
assert.equal(entryKeyParts.length, 2);
|
||||
assert.equal(entryKeyParts[0], 'n');
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,61 +0,0 @@
|
||||
var assert = require('../../support/assert')
|
||||
, _ = require('underscore')
|
||||
, RedisPool = require('../../../lib/cartodb/redis_pool')
|
||||
, tests = module.exports = {};
|
||||
|
||||
suite('redis_pool', function() {
|
||||
|
||||
// configure redis pool instance to use in tests
|
||||
var test_opts = require('../../support/config').redis_pool;
|
||||
|
||||
var redis_pool = new RedisPool(test_opts);
|
||||
|
||||
test('RedisPool object exists', function(done){
|
||||
assert.ok(RedisPool);
|
||||
done();
|
||||
});
|
||||
|
||||
test('RedisPool can create new redis_pool objects with default settings', function(done){
|
||||
var redis_pool = new RedisPool();
|
||||
done();
|
||||
});
|
||||
|
||||
test('RedisPool can create new redis_pool objects with specific settings', function(done){
|
||||
var redis_pool = new RedisPool(_.extend({host:'127.0.0.1', port: '6379'}, test_opts));
|
||||
done();
|
||||
});
|
||||
|
||||
|
||||
test('pool object has an acquire function', function(done){
|
||||
var found=false;
|
||||
var functions = _.functions(redis_pool);
|
||||
for (var i=0; i<functions.length; ++i) {
|
||||
if ( functions[i] == 'acquire' ) { found=true; break; }
|
||||
}
|
||||
assert.ok(found);
|
||||
done();
|
||||
});
|
||||
|
||||
test('calling aquire returns a redis client object that can get/set', function(done){
|
||||
redis_pool.acquire(0, function(err, client){
|
||||
client.set("key","value");
|
||||
client.get("key", function(err,data){
|
||||
assert.equal(data, "value");
|
||||
redis_pool.release(0, client); // needed to exit tests
|
||||
done();
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
test('calling aquire on another DB returns a redis client object that can get/set', function(done){
|
||||
redis_pool.acquire(2, function(err, client){
|
||||
client.set("key","value");
|
||||
client.get("key", function(err,data){
|
||||
assert.equal(data, "value");
|
||||
redis_pool.release(2, client); // needed to exit tests
|
||||
done();
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -7,7 +7,12 @@ var assert = require('assert')
|
||||
suite('req2params', function() {
|
||||
|
||||
// configure redis pool instance to use in tests
|
||||
var opts = require('../../../lib/cartodb/server_options');
|
||||
var opts = require('../../../lib/cartodb/server_options')();
|
||||
|
||||
var test_user = _.template(global.environment.postgres_auth_user, {user_id:1});
|
||||
var test_pubuser = global.environment.postgres.user;
|
||||
var test_database = test_user + '_db';
|
||||
|
||||
|
||||
test('can be found in server_options', function(){
|
||||
assert.ok(_.isFunction(opts.req2params));
|
||||
@@ -15,49 +20,45 @@ suite('req2params', function() {
|
||||
|
||||
test('cleans up request', function(done){
|
||||
opts.req2params({headers: { host:'localhost' }, query: {dbuser:'hacker',dbname:'secret'}}, function(err, req) {
|
||||
if ( err ) { console.log(err); throw new Error(err); }
|
||||
if ( err ) { done(err); return; }
|
||||
assert.ok(_.isObject(req.query), 'request has query');
|
||||
assert.ok(!req.query.hasOwnProperty('dbuser'), 'dbuser was removed from query');
|
||||
assert.ok(req.hasOwnProperty('params'), 'request has params');
|
||||
assert.ok(req.params.hasOwnProperty('interactivity'), 'request params have interactivity');
|
||||
assert.equal(req.params.dbname, 'cartodb_test_user_1_db', 'could forge dbname: '+ req.params.dbname);
|
||||
assert.ok(!req.hasOwnProperty('dbuser'), 'could inject dbuser ('+req.params.dbuser+')');
|
||||
assert.equal(req.params.dbname, test_database, 'could forge dbname: '+ req.params.dbname);
|
||||
assert.ok(req.params.dbuser === test_pubuser, 'could inject dbuser ('+req.params.dbuser+')');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('sets dbname from redis metadata', function(done){
|
||||
opts.req2params({headers: { host:'localhost' }, query: {} }, function(err, req) {
|
||||
if ( err ) { console.log(err); throw new Error(err); }
|
||||
if ( err ) { done(err); return; }
|
||||
//console.dir(req);
|
||||
assert.ok(_.isObject(req.query), 'request has query');
|
||||
assert.ok(!req.query.hasOwnProperty('dbuser'), 'dbuser was removed from query');
|
||||
assert.ok(req.hasOwnProperty('params'), 'request has params');
|
||||
assert.ok(req.params.hasOwnProperty('interactivity'), 'request params have interactivity');
|
||||
// database_name for user "localhost" (see test/support/prepare_db.sh)
|
||||
assert.equal(req.params.dbname, 'cartodb_test_user_1_db');
|
||||
// unauthenticated request gets no dbuser
|
||||
assert.ok(!req.params.hasOwnProperty('dbuser'), 'could inject dbuser ('+req.params.dbuser+')');
|
||||
assert.equal(req.params.dbname, test_database);
|
||||
assert.ok(req.params.dbuser === test_pubuser, 'could inject dbuser ('+req.params.dbuser+')');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test('sets also dbuser for authenticated requests', function(done){
|
||||
opts.req2params({headers: { host:'localhost' }, query: {map_key: '1234'} }, function(err, req) {
|
||||
if ( err ) { console.log(err); throw new Error(err); }
|
||||
if ( err ) { done(err); return; }
|
||||
//console.dir(req);
|
||||
assert.ok(_.isObject(req.query), 'request has query');
|
||||
assert.ok(!req.query.hasOwnProperty('dbuser'), 'dbuser was removed from query');
|
||||
assert.ok(req.hasOwnProperty('params'), 'request has params');
|
||||
assert.ok(req.params.hasOwnProperty('interactivity'), 'request params have interactivity');
|
||||
// database_name for user "localhost" (see test/support/prepare_db.sh)
|
||||
assert.equal(req.params.dbname, 'cartodb_test_user_1_db');
|
||||
// id for user "localhost" (see test/support/prepare_db.sh)
|
||||
assert.equal(req.dbuser, 'test_cartodb_user_1');
|
||||
assert.equal(req.params.dbname, test_database);
|
||||
assert.equal(req.params.dbuser, test_user);
|
||||
|
||||
opts.req2params({headers: { host:'localhost' }, query: {map_key: '1235'} }, function(err, req) {
|
||||
// wrong key resets params to no user
|
||||
assert.ok(!req.hasOwnProperty('dbuser'), 'could inject dbuser ('+req.params.dbuser+')');
|
||||
assert.ok(req.params.dbuser === test_pubuser, 'could inject dbuser ('+req.params.dbuser+')');
|
||||
done();
|
||||
});
|
||||
});
|
||||
@@ -70,7 +71,8 @@ suite('req2params', function() {
|
||||
cache_buster: 5
|
||||
};
|
||||
test_helper.lzma_compress_to_base64(JSON.stringify(qo), 1, function(err, data) {
|
||||
opts.req2params({ query: { non_included: 'toberemoved', api_key: 'test', style: 'override', lzma: data }}, function(err, req) {
|
||||
opts.req2params({ headers: { host:'localhost' }, query: { non_included: 'toberemoved', api_key: 'test', style: 'override', lzma: data }}, function(err, req) {
|
||||
if ( err ) { done(err); return; }
|
||||
var query = req.params
|
||||
assert.equal(qo.style, query.style)
|
||||
assert.equal(qo.style_version, query.style_version)
|
||||
|
||||
525
test/unit/cartodb/template_maps.test.js
Normal file
525
test/unit/cartodb/template_maps.test.js
Normal file
@@ -0,0 +1,525 @@
|
||||
var assert = require('assert')
|
||||
//, _ = require('underscore')
|
||||
, RedisPool = require('redis-mpool')
|
||||
, TemplateMaps = require('../../../lib/cartodb/template_maps.js')
|
||||
, test_helper = require('../../support/test_helper')
|
||||
, Step = require('step')
|
||||
, _ = require('underscore')
|
||||
, tests = module.exports = {};
|
||||
|
||||
suite('template_maps', function() {
|
||||
|
||||
// configure redis pool instance to use in tests
|
||||
var redis_pool = RedisPool(global.environment.redis);
|
||||
|
||||
var wadusLayer = {
|
||||
options: {
|
||||
sql: 'select 1 cartodb_id, null::geometry the_geom_webmercator',
|
||||
cartocss: '#layer { marker-fill:blue; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
};
|
||||
|
||||
var validTemplate = {
|
||||
version:'0.0.1',
|
||||
name: 'first',
|
||||
auth: {},
|
||||
layergroup: {
|
||||
layers: [
|
||||
wadusLayer
|
||||
]
|
||||
}
|
||||
};
|
||||
var owner = 'me';
|
||||
|
||||
test('does not accept template with unsupported version', function(done) {
|
||||
var tmap = new TemplateMaps(redis_pool);
|
||||
assert.ok(tmap);
|
||||
var tpl = { version:'6.6.6',
|
||||
name:'k', auth: {}, layergroup: {layers:[wadusLayer]} };
|
||||
Step(
|
||||
function() {
|
||||
tmap.addTemplate('me', tpl, this);
|
||||
},
|
||||
function checkFailed(err) {
|
||||
assert.ok(err);
|
||||
assert.ok(err.message.match(/unsupported.*version/i), err);
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('does not accept template with missing name', function(done) {
|
||||
var tmap = new TemplateMaps(redis_pool);
|
||||
assert.ok(tmap);
|
||||
var tpl = { version:'0.0.1',
|
||||
auth: {}, layergroup: {layers:[wadusLayer]} };
|
||||
Step(
|
||||
function() {
|
||||
tmap.addTemplate('me', tpl, this);
|
||||
},
|
||||
function checkFailed(err) {
|
||||
assert.ok(err);
|
||||
assert.ok(err.message.match(/missing.*name/i), err);
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('does not accept template with invalid name', function(done) {
|
||||
var tmap = new TemplateMaps(redis_pool);
|
||||
assert.ok(tmap);
|
||||
var tpl = { version:'0.0.1',
|
||||
auth: {}, layergroup: {layers:[wadusLayer]} };
|
||||
var invalidnames = [ "ab|", "a b", "a@b", "1ab", "_x", "", " x", "x " ];
|
||||
var testNext = function() {
|
||||
if ( ! invalidnames.length ) { done(); return; }
|
||||
var n = invalidnames.pop();
|
||||
tpl.name = n;
|
||||
tmap.addTemplate('me', tpl, function(err) {
|
||||
if ( ! err ) {
|
||||
done(new Error("Unexpected success with invalid name '" + n + "'"));
|
||||
}
|
||||
else if ( ! err.message.match(/template.*name/i) ) {
|
||||
done(new Error("Unexpected error message with invalid name '" + n
|
||||
+ "': " + err));
|
||||
}
|
||||
else {
|
||||
testNext();
|
||||
}
|
||||
});
|
||||
};
|
||||
testNext();
|
||||
});
|
||||
|
||||
test('does not accept template with invalid placeholder name', function(done) {
|
||||
var tmap = new TemplateMaps(redis_pool);
|
||||
assert.ok(tmap);
|
||||
var tpl = { version:'0.0.1',
|
||||
name: "valid", placeholders: {},
|
||||
auth: {}, layergroup: {layers:[wadusLayer]} };
|
||||
var invalidnames = [ "ab|", "a b", "a@b", "1ab", "_x", "", " x", "x " ];
|
||||
var testNext = function() {
|
||||
if ( ! invalidnames.length ) { done(); return; }
|
||||
var n = invalidnames.pop();
|
||||
tpl.placeholders = {};
|
||||
tpl.placeholders[n] = { type:'number', default:1 };
|
||||
tmap.addTemplate('me', tpl, function(err) {
|
||||
if ( ! err ) {
|
||||
done(new Error("Unexpected success with invalid name '" + n + "'"));
|
||||
}
|
||||
else if ( ! err.message.match(/invalid.*name/i) ) {
|
||||
done(new Error("Unexpected error message with invalid name '" + n
|
||||
+ "': " + err));
|
||||
}
|
||||
else {
|
||||
testNext();
|
||||
}
|
||||
});
|
||||
};
|
||||
testNext();
|
||||
});
|
||||
|
||||
test('does not accept template with missing placeholder default', function(done) {
|
||||
var tmap = new TemplateMaps(redis_pool);
|
||||
assert.ok(tmap);
|
||||
var tpl = { version:'0.0.1',
|
||||
name: "valid", placeholders: { v: {} },
|
||||
auth: {}, layergroup: {layers:[wadusLayer]} };
|
||||
tmap.addTemplate('me', tpl, function(err) {
|
||||
if ( ! err ) {
|
||||
done(new Error("Unexpected success with missing placeholder default"));
|
||||
}
|
||||
else if ( ! err.message.match(/missing default/i) ) {
|
||||
done(new Error("Unexpected error message with missing placeholder default: "
|
||||
+ err));
|
||||
}
|
||||
else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('does not accept template with missing placeholder type', function(done) {
|
||||
var tmap = new TemplateMaps(redis_pool);
|
||||
assert.ok(tmap);
|
||||
var tpl = { version:'0.0.1',
|
||||
name: "valid", placeholders: { v: { default:1 } },
|
||||
auth: {}, layergroup: {layers:[wadusLayer]} };
|
||||
tmap.addTemplate('me', tpl, function(err) {
|
||||
if ( ! err ) {
|
||||
done(new Error("Unexpected success with missing placeholder type"));
|
||||
}
|
||||
else if ( ! err.message.match(/missing type/i) ) {
|
||||
done(new Error("Unexpected error message with missing placeholder default: "
|
||||
+ err));
|
||||
}
|
||||
else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// See http://github.com/CartoDB/Windshaft-cartodb/issues/128
|
||||
test('does not accept template with invalid token auth (undefined tokens)',
|
||||
function(done) {
|
||||
var tmap = new TemplateMaps(redis_pool);
|
||||
assert.ok(tmap);
|
||||
var tpl = { version:'0.0.1',
|
||||
name: "invalid_auth1", placeholders: { },
|
||||
auth: { method: 'token' }, layergroup: {layers:[wadusLayer]} };
|
||||
tmap.addTemplate('me', tpl, function(err) {
|
||||
if ( ! err ) {
|
||||
done(new Error("Unexpected success with invalid token auth (undefined tokens)"));
|
||||
}
|
||||
else if ( ! err.message.match(/invalid 'token' authentication/i) ) {
|
||||
done(new Error("Unexpected error message with invalid token auth (undefined tokens): "
|
||||
+ err));
|
||||
}
|
||||
else {
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('add, get and delete a valid template', function(done) {
|
||||
var tmap = new TemplateMaps(redis_pool);
|
||||
assert.ok(tmap);
|
||||
var expected_failure = false;
|
||||
var tpl_id;
|
||||
var tpl = { version:'0.0.1',
|
||||
name: 'first', auth: {}, layergroup: {layers:[wadusLayer]} };
|
||||
Step(
|
||||
function() {
|
||||
tmap.addTemplate('me', tpl, this);
|
||||
},
|
||||
function addOmonimousTemplate(err, id) {
|
||||
if ( err ) throw err;
|
||||
tpl_id = id;
|
||||
assert.equal(tpl_id, 'first');
|
||||
expected_failure = true;
|
||||
// should fail, as it already exists
|
||||
tmap.addTemplate('me', tpl, this);
|
||||
},
|
||||
function getTemplate(err) {
|
||||
if ( ! expected_failure && err ) throw err;
|
||||
assert.ok(err);
|
||||
assert.ok(err.message.match(/already exists/i), err);
|
||||
tmap.getTemplate('me', tpl_id, this);
|
||||
},
|
||||
function delTemplate(err, got_tpl) {
|
||||
if ( err ) throw err;
|
||||
assert.deepEqual(got_tpl, _.extend({}, tpl, {auth: {method: 'open'}, placeholders: {}}));
|
||||
tmap.delTemplate('me', tpl_id, this);
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('add multiple templates, list them', function(done) {
|
||||
var tmap = new TemplateMaps(redis_pool);
|
||||
assert.ok(tmap);
|
||||
var expected_failure = false;
|
||||
var tpl1 = { version:'0.0.1', name: 'first', auth: {}, layergroup: {layers:[wadusLayer]} };
|
||||
var tpl1_id;
|
||||
var tpl2 = { version:'0.0.1', name: 'second', auth: {}, layergroup: {layers:[wadusLayer]} };
|
||||
var tpl2_id;
|
||||
Step(
|
||||
function addTemplate1() {
|
||||
tmap.addTemplate('me', tpl1, this);
|
||||
},
|
||||
function addTemplate2(err, id) {
|
||||
if ( err ) throw err;
|
||||
tpl1_id = id;
|
||||
tmap.addTemplate('me', tpl2, this);
|
||||
},
|
||||
function listTemplates(err, id) {
|
||||
if ( err ) throw err;
|
||||
tpl2_id = id;
|
||||
tmap.listTemplates('me', this);
|
||||
},
|
||||
function checkTemplates(err, ids) {
|
||||
if ( err ) throw err;
|
||||
assert.equal(ids.length, 2);
|
||||
assert.ok(ids.indexOf(tpl1_id) != -1, ids.join(','));
|
||||
assert.ok(ids.indexOf(tpl2_id) != -1, ids.join(','));
|
||||
return null;
|
||||
},
|
||||
function delTemplate1(err) {
|
||||
if ( tpl1_id ) {
|
||||
var next = this;
|
||||
tmap.delTemplate('me', tpl1_id, function(e) {
|
||||
if ( err || e ) next(new Error(err + '; ' + e));
|
||||
else next();
|
||||
});
|
||||
} else {
|
||||
if ( err ) throw err;
|
||||
return null;
|
||||
}
|
||||
},
|
||||
function delTemplate2(err) {
|
||||
if ( tpl2_id ) {
|
||||
var next = this;
|
||||
tmap.delTemplate('me', tpl2_id, function(e) {
|
||||
if ( err || e ) next(new Error(err + '; ' + e));
|
||||
else next();
|
||||
});
|
||||
} else {
|
||||
if ( err ) throw err;
|
||||
return null;
|
||||
}
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('update templates', function(done) {
|
||||
var tmap = new TemplateMaps(redis_pool);
|
||||
assert.ok(tmap);
|
||||
var expected_failure = false;
|
||||
var owner = 'me';
|
||||
var tpl = { version:'0.0.1',
|
||||
name: 'first',
|
||||
auth: { method: 'open' },
|
||||
layergroup: {layers:[wadusLayer]}
|
||||
};
|
||||
var tpl_id;
|
||||
Step(
|
||||
function addTemplate() {
|
||||
tmap.addTemplate(owner, tpl, this);
|
||||
},
|
||||
// Updating template name should fail
|
||||
function updateTemplateName(err, id) {
|
||||
if ( err ) throw err;
|
||||
tpl_id = id;
|
||||
expected_failure = true;
|
||||
tpl.name = 'second';
|
||||
tmap.updTemplate(owner, tpl_id, tpl, this);
|
||||
},
|
||||
function updateTemplateAuth(err) {
|
||||
if ( err && ! expected_failure) throw err;
|
||||
expected_failure = false;
|
||||
assert.ok(err);
|
||||
tpl.name = 'first';
|
||||
tpl.auth.method = 'token';
|
||||
tpl.auth.valid_tokens = [ 'tok1' ];
|
||||
tmap.updTemplate(owner, tpl_id, tpl, this);
|
||||
},
|
||||
function updateTemplateWithInvalid(err) {
|
||||
if ( err ) throw err;
|
||||
tpl.version = '999.999.999';
|
||||
expected_failure = true;
|
||||
tmap.updTemplate(owner, tpl_id, tpl, this);
|
||||
},
|
||||
function updateUnexistentTemplate(err) {
|
||||
if ( err && ! expected_failure) throw err;
|
||||
expected_failure = false;
|
||||
assert.ok(err);
|
||||
assert.ok(err.message.match(/unsupported.*version/i), err);
|
||||
tpl.version = '0.0.1';
|
||||
expected_failure = true;
|
||||
tmap.updTemplate(owner, 'unexistent', tpl, this);
|
||||
},
|
||||
function delTemplate(err) {
|
||||
if ( err && ! expected_failure) throw err;
|
||||
expected_failure = false;
|
||||
assert.ok(err);
|
||||
assert.ok(err.message.match(/cannot update name/i), err);
|
||||
tmap.delTemplate(owner, tpl_id, this);
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('instanciate templates', function() {
|
||||
var tmap = new TemplateMaps(redis_pool);
|
||||
assert.ok(tmap);
|
||||
|
||||
var tpl1 = {
|
||||
version: '0.0.1',
|
||||
name: 'acceptance1',
|
||||
auth: { method: 'open' },
|
||||
placeholders: {
|
||||
fill: { type: "css_color", default: "red" },
|
||||
color: { type: "css_color", default: "#a0fF9A" },
|
||||
name: { type: "sql_literal", default: "test" },
|
||||
zoom: { type: "number", default: "0" },
|
||||
test_number: { type: "number", default: 23 },
|
||||
},
|
||||
layergroup: {
|
||||
version: '1.0.0',
|
||||
global_cartocss_version: '2.0.2',
|
||||
layers: [
|
||||
{ options: {
|
||||
sql: "select '<%=name %>' || id, g from t",
|
||||
cartocss: '#layer { marker-fill:<%= fill %>; marker-width: <%=test_number %>; }'
|
||||
} },
|
||||
{ options: {
|
||||
sql: "select fun('<%= name%>') g from x",
|
||||
cartocss: '#layer { line-color:<%= color %>; marker-fill:<%= color %>; }'
|
||||
} },
|
||||
{ options: {
|
||||
sql: "select g from x",
|
||||
cartocss: '#layer[zoom=<%=zoom%>] { }'
|
||||
} }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
var inst = tmap.instance(tpl1, {});
|
||||
|
||||
var lyr = inst.layers[0].options;
|
||||
assert.equal(lyr.sql, "select 'test' || id, g from t");
|
||||
assert.equal(lyr.cartocss, '#layer { marker-fill:red; marker-width: 23; }');
|
||||
|
||||
lyr = inst.layers[1].options;
|
||||
assert.equal(lyr.sql, "select fun('test') g from x");
|
||||
assert.equal(lyr.cartocss, '#layer { line-color:#a0fF9A; marker-fill:#a0fF9A; }');
|
||||
|
||||
inst = tmap.instance(tpl1, {color:'yellow', name:"it's dangerous"});
|
||||
|
||||
lyr = inst.layers[0].options;
|
||||
assert.equal(lyr.sql, "select 'it''s dangerous' || id, g from t");
|
||||
assert.equal(lyr.cartocss, '#layer { marker-fill:red; marker-width: 23; }');
|
||||
|
||||
lyr = inst.layers[1].options;
|
||||
assert.equal(lyr.sql, "select fun('it''s dangerous') g from x");
|
||||
assert.equal(lyr.cartocss, '#layer { line-color:yellow; marker-fill:yellow; }');
|
||||
|
||||
// Invalid css_color
|
||||
var err = null;
|
||||
try { inst = tmap.instance(tpl1, {color:'##ff00ff'}); }
|
||||
catch (e) { err = e; }
|
||||
assert.ok(err);
|
||||
assert.ok(err.message.match(/invalid css_color/i), err);
|
||||
|
||||
// Invalid css_color 2 (too few digits)
|
||||
var err = null;
|
||||
try { inst = tmap.instance(tpl1, {color:'#ff'}); }
|
||||
catch (e) { err = e; }
|
||||
assert.ok(err);
|
||||
assert.ok(err.message.match(/invalid css_color/i), err);
|
||||
|
||||
// Invalid css_color 3 (too many digits)
|
||||
var err = null;
|
||||
try { inst = tmap.instance(tpl1, {color:'#1234567'}); }
|
||||
catch (e) { err = e; }
|
||||
assert.ok(err);
|
||||
assert.ok(err.message.match(/invalid css_color/i), err);
|
||||
|
||||
// Invalid number
|
||||
var err = null;
|
||||
try { inst = tmap.instance(tpl1, {zoom:'#'}); }
|
||||
catch (e) { err = e; }
|
||||
assert.ok(err);
|
||||
assert.ok(err.message.match(/invalid number/i), err);
|
||||
|
||||
// Invalid number 2
|
||||
var err = null;
|
||||
try { inst = tmap.instance(tpl1, {zoom:'23e'}); }
|
||||
catch (e) { err = e; }
|
||||
assert.ok(err);
|
||||
assert.ok(err.message.match(/invalid number/i), err);
|
||||
|
||||
// Valid number
|
||||
var err = null;
|
||||
try { inst = tmap.instance(tpl1, {zoom:'-.23e10'}); }
|
||||
catch (e) { err = e; }
|
||||
assert.ok(!err);
|
||||
});
|
||||
|
||||
// Can set a limit on the number of user templates
|
||||
test('can limit number of user templates', function(done) {
|
||||
var tmap = new TemplateMaps(redis_pool, {
|
||||
max_user_templates: 2
|
||||
});
|
||||
assert.ok(tmap);
|
||||
var tpl = { version:'0.0.1', auth: {}, layergroup: {layers:[wadusLayer]} };
|
||||
var expectErr = false;
|
||||
var idMe = [];
|
||||
var idYou = [];
|
||||
Step(
|
||||
function oneForMe() {
|
||||
tpl.name = 'oneForMe';
|
||||
tmap.addTemplate('me', tpl, this);
|
||||
},
|
||||
function twoForMe(err, id) {
|
||||
if ( err ) throw err;
|
||||
assert.ok(id);
|
||||
idMe.push(id);
|
||||
tpl.name = 'twoForMe';
|
||||
tmap.addTemplate('me', tpl, this);
|
||||
},
|
||||
function threeForMe(err, id) {
|
||||
if ( err ) throw err;
|
||||
assert.ok(id);
|
||||
idMe.push(id);
|
||||
tpl.name = 'threeForMe';
|
||||
expectErr = true;
|
||||
tmap.addTemplate('me', tpl, this);
|
||||
},
|
||||
function errForMe(err, id) {
|
||||
if ( err && ! expectErr ) throw err;
|
||||
expectErr = false;
|
||||
assert.ok(err);
|
||||
assert.ok(err.message.match(/limit.*template/), err);
|
||||
return null;
|
||||
},
|
||||
function delOneMe(err) {
|
||||
if ( err ) throw err;
|
||||
tmap.delTemplate('me', idMe.shift(), this);
|
||||
},
|
||||
function threeForMeRetry(err) {
|
||||
if ( err ) throw err;
|
||||
tpl.name = 'threeForMe';
|
||||
tmap.addTemplate('me', tpl, this);
|
||||
},
|
||||
function oneForYou(err, id) {
|
||||
if ( err ) throw err;
|
||||
assert.ok(id);
|
||||
idMe.push(id);
|
||||
tpl.name = 'oneForYou';
|
||||
tmap.addTemplate('you', tpl, this);
|
||||
},
|
||||
function twoForYou(err, id) {
|
||||
if ( err ) throw err;
|
||||
assert.ok(id);
|
||||
idYou.push(id);
|
||||
tpl.name = 'twoForYou';
|
||||
tmap.addTemplate('you', tpl, this);
|
||||
},
|
||||
function threeForYou(err, id) {
|
||||
if ( err ) throw err;
|
||||
assert.ok(id);
|
||||
idYou.push(id);
|
||||
tpl.name = 'threeForYou';
|
||||
expectErr = true;
|
||||
tmap.addTemplate('you', tpl, this);
|
||||
},
|
||||
function errForYou(err, id) {
|
||||
if ( err && ! expectErr ) throw err;
|
||||
expectErr = false;
|
||||
assert.ok(err);
|
||||
assert.ok(err.message.match(/limit.*template/), err);
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
// TODO: delete all templates
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
95
test/unit/cartodb/template_maps_auth.test.js
Normal file
95
test/unit/cartodb/template_maps_auth.test.js
Normal file
@@ -0,0 +1,95 @@
|
||||
var assert = require('assert');
|
||||
var RedisPool = require('redis-mpool');
|
||||
var TemplateMaps = require('../../../lib/cartodb/template_maps');
|
||||
var test_helper = require('../../support/test_helper');
|
||||
var Step = require('step');
|
||||
var tests = module.exports = {};
|
||||
|
||||
suite('template_maps_auth', function() {
|
||||
|
||||
// configure redis pool instance to use in tests
|
||||
var redisPool = new RedisPool(global.environment.redis),
|
||||
templateMaps = new TemplateMaps(redisPool, {max_user_templates: 1000});
|
||||
|
||||
function makeTemplate(method, validTokens) {
|
||||
var template = {
|
||||
name: 'wadus_template',
|
||||
auth: {
|
||||
method: method
|
||||
}
|
||||
};
|
||||
|
||||
if (method === 'token') {
|
||||
template.auth.valid_tokens = validTokens || [];
|
||||
}
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
var methodToken = 'token',
|
||||
methodOpen = 'open';
|
||||
|
||||
var tokenFoo = 'foo',
|
||||
tokenBar = 'bar';
|
||||
|
||||
var authorizationTestScenarios = [
|
||||
{
|
||||
desc: 'open method is always authorized',
|
||||
template: makeTemplate(methodOpen),
|
||||
token: undefined,
|
||||
expected: true
|
||||
},
|
||||
{
|
||||
desc: 'token method is authorized for valid token',
|
||||
template: makeTemplate(methodToken, [tokenFoo]),
|
||||
token: tokenFoo,
|
||||
expected: true
|
||||
},
|
||||
{
|
||||
desc: 'token method not authorized for invalid token',
|
||||
template: makeTemplate(methodToken, [tokenFoo]),
|
||||
token: tokenBar,
|
||||
expected: false
|
||||
},
|
||||
{
|
||||
desc: 'token method is authorized for valid token array',
|
||||
template: makeTemplate(methodToken, [tokenFoo]),
|
||||
token: [tokenFoo],
|
||||
expected: true
|
||||
},
|
||||
{
|
||||
desc: 'token method not authorized for invalid token array',
|
||||
template: makeTemplate(methodToken, [tokenFoo]),
|
||||
token: [tokenBar],
|
||||
expected: false
|
||||
},
|
||||
{
|
||||
desc: 'wadus method not authorized',
|
||||
template: makeTemplate('wadus', [tokenFoo]),
|
||||
token: tokenFoo,
|
||||
expected: false
|
||||
},
|
||||
{
|
||||
desc: 'undefined template result in not authorized',
|
||||
template: undefined,
|
||||
token: tokenFoo,
|
||||
expected: false
|
||||
},
|
||||
{
|
||||
desc: 'undefined template auth result in not authorized',
|
||||
template: {},
|
||||
token: tokenFoo,
|
||||
expected: false
|
||||
}
|
||||
];
|
||||
|
||||
authorizationTestScenarios.forEach(function(testScenario) {
|
||||
test(testScenario.desc, function(done) {
|
||||
var debugMessage = testScenario.expected ? 'should be authorized' : 'unexpectedly authorized';
|
||||
var result = templateMaps.isAuthorized(testScenario.template, testScenario.token);
|
||||
assert.equal(result, testScenario.expected, debugMessage);
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
});
|
||||
106
test/unit/cartodb/template_maps_defaults.test.js
Normal file
106
test/unit/cartodb/template_maps_defaults.test.js
Normal file
@@ -0,0 +1,106 @@
|
||||
var assert = require('assert');
|
||||
var RedisPool = require('redis-mpool');
|
||||
var TemplateMaps = require('../../../lib/cartodb/template_maps.js');
|
||||
var test_helper = require('../../support/test_helper');
|
||||
var _ = require('underscore');
|
||||
|
||||
suite('template_maps', function() {
|
||||
|
||||
var redisPool = new RedisPool(global.environment.redis),
|
||||
templateMaps = new TemplateMaps(redisPool);
|
||||
|
||||
var owner = 'me';
|
||||
var templateName = 'wadus';
|
||||
|
||||
|
||||
var defaultTemplate = {
|
||||
version:'0.0.1',
|
||||
name: templateName,
|
||||
layergroup: {
|
||||
layers: [
|
||||
{
|
||||
options: {
|
||||
sql: 'select 1 cartodb_id, null::geometry the_geom_webmercator',
|
||||
cartocss: '#layer { marker-fill:blue; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
function makeTemplate(auth, placeholders) {
|
||||
return _.extend({}, defaultTemplate, {
|
||||
auth: auth,
|
||||
placeholders: placeholders
|
||||
});
|
||||
}
|
||||
|
||||
var defaultAuth = {
|
||||
method: 'open'
|
||||
};
|
||||
|
||||
var authTokenSample = {
|
||||
method: 'token',
|
||||
valid_tokens: ['wadus_token']
|
||||
};
|
||||
|
||||
var placeholdersSample = {
|
||||
wadus: {
|
||||
type: 'number',
|
||||
default: 1
|
||||
}
|
||||
};
|
||||
|
||||
var testScenarios = [
|
||||
{
|
||||
desc: 'default auth and placeholders values',
|
||||
template: defaultTemplate,
|
||||
expected: {
|
||||
auth: defaultAuth,
|
||||
placeholders: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'default placeholders but specified auth',
|
||||
template: makeTemplate(authTokenSample),
|
||||
expected: {
|
||||
auth: authTokenSample,
|
||||
placeholders: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'default auth but specified placeholders',
|
||||
template: makeTemplate(undefined, placeholdersSample),
|
||||
expected: {
|
||||
auth: defaultAuth,
|
||||
placeholders: placeholdersSample
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'specified auth and placeholders',
|
||||
template: makeTemplate(authTokenSample, placeholdersSample),
|
||||
expected: {
|
||||
auth: authTokenSample,
|
||||
placeholders: placeholdersSample
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
testScenarios.forEach(function(testScenario) {
|
||||
test('adding template returns a new instance with ' + testScenario.desc, function(done) {
|
||||
|
||||
templateMaps.addTemplate(owner, testScenario.template, function(err, templateId, template) {
|
||||
assert.ok(!err, 'Unexpected error adding template: ' + (err && err.message));
|
||||
assert.ok(testScenario.template !== template, 'template instances should be different');
|
||||
assert.equal(template.name, templateName);
|
||||
assert.deepEqual(template.auth, testScenario.expected.auth);
|
||||
assert.deepEqual(template.placeholders, testScenario.expected.placeholders);
|
||||
|
||||
templateMaps.delTemplate(owner, templateName, done);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
112
test/unit/cartodb/valid_template_maps.test.js
Normal file
112
test/unit/cartodb/valid_template_maps.test.js
Normal file
@@ -0,0 +1,112 @@
|
||||
var assert = require('assert');
|
||||
var RedisPool = require('redis-mpool');
|
||||
var TemplateMaps = require('../../../lib/cartodb/template_maps.js');
|
||||
var test_helper = require('../../support/test_helper');
|
||||
var _ = require('underscore');
|
||||
|
||||
suite('template_maps', function() {
|
||||
|
||||
var redisPool = new RedisPool(global.environment.redis),
|
||||
templateMaps = new TemplateMaps(redisPool);
|
||||
|
||||
var owner = 'me';
|
||||
var templateName = 'wadus';
|
||||
|
||||
|
||||
var defaultTemplate = {
|
||||
version:'0.0.1',
|
||||
name: templateName
|
||||
};
|
||||
|
||||
function makeTemplate(layers) {
|
||||
var layergroup = {
|
||||
layers: layers
|
||||
};
|
||||
return _.extend({}, defaultTemplate, {
|
||||
layergroup: layergroup
|
||||
});
|
||||
}
|
||||
|
||||
var layerWithMissingOptions = {},
|
||||
minimumValidLayer = {
|
||||
options: {
|
||||
sql: 'select 1 cartodb_id, null::geometry the_geom_webmercator',
|
||||
cartocss: '#layer { marker-fill:blue; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
};
|
||||
|
||||
var testScenarios = [
|
||||
{
|
||||
desc: 'Missing layers array does not validate',
|
||||
template: makeTemplate(),
|
||||
expected: {
|
||||
isValid: false,
|
||||
message: 'Missing or empty layers array from layergroup config'
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'Empty layers array does not validate',
|
||||
template: makeTemplate([]),
|
||||
expected: {
|
||||
isValid: false,
|
||||
message: 'Missing or empty layers array from layergroup config'
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'Layer with missing options does not validate',
|
||||
template: makeTemplate([
|
||||
layerWithMissingOptions
|
||||
]),
|
||||
expected: {
|
||||
isValid: false,
|
||||
message: 'Missing `options` in layergroup config for layers: 0'
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'Multiple layers report invalid layer',
|
||||
template: makeTemplate([
|
||||
minimumValidLayer,
|
||||
layerWithMissingOptions
|
||||
]),
|
||||
expected: {
|
||||
isValid: false,
|
||||
message: 'Missing `options` in layergroup config for layers: 1'
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: 'default auth but specified placeholders',
|
||||
template: makeTemplate([
|
||||
minimumValidLayer
|
||||
]),
|
||||
expected: {
|
||||
isValid: true,
|
||||
message: ''
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
testScenarios.forEach(function(testScenario) {
|
||||
test(testScenario.desc, function(done) {
|
||||
|
||||
templateMaps.addTemplate(owner, testScenario.template, function(err) {
|
||||
|
||||
if (testScenario.expected.isValid) {
|
||||
|
||||
assert.ok(!err);
|
||||
templateMaps.delTemplate(owner, templateName, done);
|
||||
|
||||
} else {
|
||||
|
||||
assert.ok(err);
|
||||
assert.equal(err.message, testScenario.expected.message);
|
||||
done();
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -23,11 +23,11 @@ if test -z "$cfg"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cmd="curl -skH Content-Type:application/json --data-binary @${cfg} ${tiler_url}"
|
||||
cmd="curl -skH Content-Type:application/json --data-binary @- ${tiler_url}"
|
||||
if test x${verbose} = xyes; then
|
||||
cmd="${cmd} -v"
|
||||
fi
|
||||
res=`${cmd}`
|
||||
res=`cat ${cfg} | tr '\n' ' ' | ${cmd}`
|
||||
if test $? -gt 0; then
|
||||
echo "curl command failed: ${cmd}"
|
||||
fi
|
||||
49
tools/create_template
Executable file
49
tools/create_template
Executable file
@@ -0,0 +1,49 @@
|
||||
#!/bin/sh
|
||||
|
||||
verbose=no
|
||||
tiler_url=http://dev.localhost.lan:8181/tiles/template
|
||||
apikey=${CDB_APIKEY}
|
||||
|
||||
while test -n "$1"; do
|
||||
if test "$1" = "-v"; then
|
||||
verbose=yes
|
||||
elif test "$1" = "-k"; then
|
||||
shift
|
||||
apikey="$1"
|
||||
elif test "$1" = "-u"; then
|
||||
shift
|
||||
tiler_url="$1"
|
||||
elif test -z "$cfg"; then
|
||||
cfg="$1"
|
||||
else
|
||||
echo "Unused parameter $1" >&2
|
||||
fi
|
||||
shift
|
||||
done
|
||||
|
||||
if test -z "$cfg"; then
|
||||
echo "Usage: $0 [-v] [-k <api_key>] [-u <tiler_url>] <template_config>" >&2
|
||||
echo "Default <tiler_url> is ${tiler_url}" >&2
|
||||
echo "Default <api_key> is read from CDB_APIKEY env variable" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cmd="curl -skH Content-Type:application/json --data-binary @- ${tiler_url}?api_key=${apikey}"
|
||||
if test x${verbose} = xyes; then
|
||||
cmd="${cmd} -v"
|
||||
fi
|
||||
res=`cat ${cfg} | tr '\n' ' ' | ${cmd}`
|
||||
if test $? -gt 0; then
|
||||
echo "curl command failed: ${cmd}"
|
||||
fi
|
||||
|
||||
if test x${verbose} = xyes; then
|
||||
echo "${res}"
|
||||
fi
|
||||
|
||||
# Successful response contains no space
|
||||
echo "$res" | grep " " && { echo $res && exit 1; }
|
||||
|
||||
tok=`echo "$res" | sed 's/.*"template_id":"\([^"]*\)".*/\1/'`
|
||||
echo $tok
|
||||
|
||||
45
tools/delete_template
Executable file
45
tools/delete_template
Executable file
@@ -0,0 +1,45 @@
|
||||
#!/bin/sh
|
||||
|
||||
verbose=no
|
||||
tiler_url=http://dev.localhost.lan:8181/tiles/template
|
||||
apikey=${CDB_APIKEY}
|
||||
|
||||
while test -n "$1"; do
|
||||
if test "$1" = "-v"; then
|
||||
verbose=yes
|
||||
elif test "$1" = "-k"; then
|
||||
shift
|
||||
apikey="$1"
|
||||
elif test "$1" = "-u"; then
|
||||
shift
|
||||
tiler_url="$1"
|
||||
elif test -z "$tpl"; then
|
||||
tpl="$1"
|
||||
else
|
||||
echo "Unused parameter $1" >&2
|
||||
fi
|
||||
shift
|
||||
done
|
||||
|
||||
if test -z "$tpl"; then
|
||||
echo "Usage: $0 [-v] [-k <api_key>] [-u <tiler_url>] <template_id>" >&2
|
||||
echo "Default <tiler_url> is ${tiler_url}" >&2
|
||||
echo "Default <api_key> is read from CDB_APIKEY env variable" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cmd="curl -X DELETE -skH Content-Type:application/json ${tiler_url}/${tpl}?api_key=${apikey}"
|
||||
if test x${verbose} = xyes; then
|
||||
cmd="${cmd} -v"
|
||||
fi
|
||||
res=`${cmd}`
|
||||
if test $? -gt 0; then
|
||||
echo "curl command failed: ${cmd}"
|
||||
fi
|
||||
|
||||
if test x${verbose} = xyes; then
|
||||
echo "${res}"
|
||||
fi
|
||||
|
||||
tok=`echo "$res" | sed 's/.*"template_id":"\([^"]*\)".*/\1/'`
|
||||
echo $tok
|
||||
11
tools/examples/mapconfig_simple.js
Normal file
11
tools/examples/mapconfig_simple.js
Normal file
@@ -0,0 +1,11 @@
|
||||
{"version":"1.0.1",
|
||||
"layers":[{
|
||||
"type":"cartodb",
|
||||
"options":{
|
||||
"sql":"select 1 as id, ST_SetSRID(ST_MakePoint(0,0),3857) as the_geom_webmercator",
|
||||
"cartocss":"#style{ marker-width: 12;}",
|
||||
"cartocss_version":"2.1.1",
|
||||
"Interactivity":"id"
|
||||
}
|
||||
}]
|
||||
}
|
||||
11
tools/examples/mapconfig_slow.js
Normal file
11
tools/examples/mapconfig_slow.js
Normal file
@@ -0,0 +1,11 @@
|
||||
{"version":"1.0.1",
|
||||
"layers":[{
|
||||
"type":"cartodb",
|
||||
"options":{
|
||||
"sql":"select 1 as id, ST_Transform(ST_SetSRID(ST_MakePoint(x/1000,x/2000),4326),3857) as the_geom_webmercator FROM generate_series(-170000,170000) x",
|
||||
"cartocss":"#style{ marker-width: 12;}",
|
||||
"cartocss_version":"2.1.1",
|
||||
"Interactivity":"id"
|
||||
}
|
||||
}]
|
||||
}
|
||||
10
tools/examples/mapconfig_torque.js
Normal file
10
tools/examples/mapconfig_torque.js
Normal file
@@ -0,0 +1,10 @@
|
||||
{"version":"1.0.1",
|
||||
"layers":[{
|
||||
"type":"torque",
|
||||
"options":{
|
||||
"sql":"select 1 as id, ST_SetSRID(ST_MakePoint(0,0),3857) as the_geom_webmercator",
|
||||
"cartocss":"Map{ -torque-time-attribute:'id'; -torque-aggregation-function:'count(id)'; -torque-frame-count:2; -torque-resolution:2}",
|
||||
"cartocss_version": "2.1.1"
|
||||
}
|
||||
}]
|
||||
}
|
||||
17
tools/examples/template_simple.js
Normal file
17
tools/examples/template_simple.js
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"version":"0.0.1",
|
||||
"name":"simple",
|
||||
"placeholders":{},
|
||||
"auth":{ "method":"open" },
|
||||
"layergroup":{
|
||||
"version":"1.0.1",
|
||||
"layers":[{
|
||||
"type":"cartodb",
|
||||
"options":{
|
||||
"sql":"select ST_SetSRID(ST_MakePoint(0,0),3857) as the_geom_webmercator",
|
||||
"cartocss":"#s{ marker-width: 12;}",
|
||||
"cartocss_version":"2.1.1"
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
59
tools/flush_cache
Executable file
59
tools/flush_cache
Executable file
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
var path = require('path');
|
||||
var request = require('request');
|
||||
|
||||
function usage(me, exitcode) {
|
||||
console.log("Usage: " + me + " [--env <environment>] <username> <tablename>");
|
||||
process.exit(exitcode);
|
||||
}
|
||||
|
||||
var node_path = process.argv.shift();
|
||||
var script_path = process.argv.shift();
|
||||
var basedir = path.dirname(script_path);
|
||||
var me = path.basename(script_path);
|
||||
|
||||
var ENV = 'development.js';
|
||||
var username, table;
|
||||
var arg;
|
||||
while ( arg = process.argv.shift() ) {
|
||||
if ( arg == '--env' ) {
|
||||
ENV = process.argv.shift();
|
||||
}
|
||||
else if ( ! username ) {
|
||||
username = arg;
|
||||
}
|
||||
else if ( ! table ) {
|
||||
table = arg;
|
||||
}
|
||||
else {
|
||||
console.warn("Unused parameter " + arg);
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! table ) {
|
||||
usage(me, 1);
|
||||
}
|
||||
|
||||
global.environment = require('../config/environments/' + ENV);
|
||||
|
||||
// _after_ setting global.environment
|
||||
var serverOptions = require('../lib/cartodb/server_options');
|
||||
|
||||
var host = global.environment.host;
|
||||
var port = global.environment.port;
|
||||
var re = ''+serverOptions.re_userFromHost;
|
||||
var hostname = re.replace(/^\/\^/, '')
|
||||
.replace(/\/$$/, '')
|
||||
.replace(/\\/g,'')
|
||||
.replace(/\([^)]*\)/,username)
|
||||
;
|
||||
//console.log("re: " + re);
|
||||
//console.log("hostname: " + hostname);
|
||||
|
||||
var url = 'http://' + host + ':' + port + '/tiles/' + table + '/flush_cache';
|
||||
request.del({ url: url, headers: { host: hostname } },
|
||||
function(err, res, body) {
|
||||
if ( err ) throw err;
|
||||
console.log(res.body);
|
||||
});
|
||||
53
tools/instanciate_template
Executable file
53
tools/instanciate_template
Executable file
@@ -0,0 +1,53 @@
|
||||
#!/bin/sh
|
||||
|
||||
verbose=no
|
||||
tiler_url=http://dev.localhost.lan:8181/tiles/template
|
||||
apikey=${CDB_APIKEY}
|
||||
|
||||
while test -n "$1"; do
|
||||
if test "$1" = "-v"; then
|
||||
verbose=yes
|
||||
elif test "$1" = "-k"; then
|
||||
shift
|
||||
apikey="$1"
|
||||
elif test "$1" = "-u"; then
|
||||
shift
|
||||
tiler_url="$1"
|
||||
elif test -z "$tpl"; then
|
||||
tpl="$1"
|
||||
elif test -z "$cfg"; then
|
||||
cfg="$1"
|
||||
else
|
||||
echo "Unused parameter $1" >&2
|
||||
fi
|
||||
shift
|
||||
done
|
||||
|
||||
if test -z "$tpl"; then
|
||||
echo "Usage: $0 [-v] [-k <api_key>] [-u <tiler_url>] <template_id> [<template_params>]" >&2
|
||||
echo "Default <tiler_url> is ${tiler_url}" >&2
|
||||
echo "Default <api_key> is read from CDB_APIKEY env variable" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if test -z "$cfg"; then
|
||||
cfg="/dev/null"
|
||||
fi
|
||||
|
||||
tiler_url="${tiler_url}/${tpl}"
|
||||
|
||||
cmd="curl -X POST -skH Content-Type:application/json --data-binary @- ${tiler_url}?api_key=${apikey}"
|
||||
if test x${verbose} = xyes; then
|
||||
cmd="${cmd} -v"
|
||||
fi
|
||||
res=`cat ${cfg} | tr '\n' ' ' | ${cmd}`
|
||||
if test $? -gt 0; then
|
||||
echo "curl command failed: ${cmd}"
|
||||
fi
|
||||
|
||||
if test x${verbose} = xyes; then
|
||||
echo "${res}"
|
||||
fi
|
||||
|
||||
tok=`echo "$res" | sed 's/.*"layergroupid":"\([^"]*\)".*/\1/'`
|
||||
echo $tok
|
||||
45
tools/list_templates
Executable file
45
tools/list_templates
Executable file
@@ -0,0 +1,45 @@
|
||||
#!/bin/sh
|
||||
|
||||
verbose=no
|
||||
tiler_url=http://dev.localhost.lan:8181/tiles/template
|
||||
apikey=${CDB_APIKEY}
|
||||
|
||||
while test -n "$1"; do
|
||||
if test "$1" = "-v"; then
|
||||
verbose=yes
|
||||
elif test "$1" = "-k"; then
|
||||
shift
|
||||
apikey="$1"
|
||||
elif test "$1" = "-u"; then
|
||||
shift
|
||||
tiler_url="$1"
|
||||
elif test "$1" = "-h" -o "$1" = "-?"; then
|
||||
echo "Usage: $0 [-v] [-k <api_key>] [-u <tiler_url>]" >&2
|
||||
echo "Default <tiler_url> is ${tiler_url}" >&2
|
||||
echo "Default <api_key> is read from CDB_APIKEY env variable" >&2
|
||||
exit 0
|
||||
else
|
||||
echo "Unused parameter $1" >&2
|
||||
fi
|
||||
shift
|
||||
done
|
||||
|
||||
|
||||
cmd="curl -X GET -sk ${tiler_url}?api_key=${apikey}"
|
||||
if test x${verbose} = xyes; then
|
||||
cmd="${cmd} -v"
|
||||
fi
|
||||
res=`${cmd}`
|
||||
if test $? -gt 0; then
|
||||
echo "curl command failed: ${cmd}"
|
||||
fi
|
||||
|
||||
if test x${verbose} = xyes; then
|
||||
echo "${res}"
|
||||
fi
|
||||
|
||||
node <<EOF
|
||||
var parsed = JSON.parse('$res');
|
||||
console.dir(parsed);
|
||||
EOF
|
||||
|
||||
@@ -68,7 +68,7 @@ for pid in ${pids}; do
|
||||
log=$(grep "${pid}" "${tmpreport}" | grep -w 1w | awk '{print $9}')
|
||||
if test -e "${log}"; then
|
||||
kill -USR2 "${pid}"
|
||||
cnt=$(tac ${log} | sed -n -e '/ItemKey/p;/^RenderCache/q' | wc -l)
|
||||
cnt=$(tac ${log} | sed -n -e '/ItemKey/p;/ RenderCache /q' | wc -l)
|
||||
if test $cnt -gt $maxcache; then maxcache=$cnt; fi
|
||||
else
|
||||
# report the error...
|
||||
|
||||
54
tools/performance/stresstest_templates.sh
Executable file
54
tools/performance/stresstest_templates.sh
Executable file
@@ -0,0 +1,54 @@
|
||||
#!/bin/sh
|
||||
|
||||
verbose=no
|
||||
tiler_url=http://dev.localhost.lan:8181/tiles/template
|
||||
apikey=${CDB_APIKEY}
|
||||
|
||||
while test -n "$1"; do
|
||||
if test "$1" = "-k"; then
|
||||
shift
|
||||
apikey="$1"
|
||||
elif test "$1" = "-u"; then
|
||||
shift
|
||||
tiler_url="$1"
|
||||
elif test -z "$tpl"; then
|
||||
tpl="$1"
|
||||
else
|
||||
echo "Unused parameter $1" >&2
|
||||
fi
|
||||
shift
|
||||
done
|
||||
|
||||
if test -z "$tpl"; then
|
||||
echo "Usage: $0 [-v] [-k <api_key>] [-u <tiler_url>] <template_config>" >&2
|
||||
echo "Default <tiler_url> is ${tiler_url}" >&2
|
||||
echo "Default <api_key> is read from CDB_APIKEY env variable" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
basedir=$(cd $(dirname $0); cd ..; pwd)
|
||||
export CDB_APIKEY=${apikey}
|
||||
max=3000000
|
||||
i=0
|
||||
while test "$i" -le "$max"; do
|
||||
tpln=`cat ${tpl} | sed "s/\"name\":\"\(.*\)\"/\"name\":\"\1${i}\"/"`
|
||||
tpl_id=`echo ${tpln} | ${basedir}/create_template -u ${tiler_url} /dev/stdin`
|
||||
if test $? -ne 0; then
|
||||
echo $tpl_id >&2
|
||||
break
|
||||
fi
|
||||
tpl_id=`echo ${tpln} | ${basedir}/update_template -u ${tiler_url} ${tpl_id} /dev/stdin`
|
||||
if test $? -ne 0; then
|
||||
echo $tpl_id >&2
|
||||
break
|
||||
fi
|
||||
out=`${basedir}/delete_template -u ${tiler_url} ${tpl_id}`
|
||||
if test $? -ne 0; then
|
||||
echo $out >&2
|
||||
break
|
||||
fi
|
||||
i=$((i+1))
|
||||
if test `expr $i % 100` -eq 0; then
|
||||
echo -n "."
|
||||
fi
|
||||
done
|
||||
167
tools/show_style
167
tools/show_style
@@ -1,29 +1,148 @@
|
||||
#!/bin/sh
|
||||
#!/usr/bin/env node
|
||||
|
||||
# TODO: port to node, if you really need it
|
||||
|
||||
REDIS_PORT=6379 # default port
|
||||
var path = require('path');
|
||||
var redis = require('redis');
|
||||
var Step = require('step');
|
||||
|
||||
|
||||
if test -z "$1"; then
|
||||
echo "Usage: $0 <username> [<tablename>|~<token>]" >&2
|
||||
exit 1
|
||||
fi
|
||||
function usage(me, exitcode) {
|
||||
console.log("Usage: " + me + " [--env <environment>] <username> [<tablename>|~<token>]");
|
||||
process.exit(exitcode);
|
||||
}
|
||||
|
||||
username="$1"
|
||||
token="$2"
|
||||
var node_path = process.argv.shift();
|
||||
var script_path = process.argv.shift();
|
||||
var basedir = path.dirname(script_path);
|
||||
var me = path.basename(script_path);
|
||||
|
||||
var ENV = 'development.js';
|
||||
var username, token;
|
||||
var arg;
|
||||
while ( arg = process.argv.shift() ) {
|
||||
if ( arg == '--env' ) {
|
||||
ENV = process.argv.shift();
|
||||
}
|
||||
else if ( ! username ) {
|
||||
username = arg;
|
||||
}
|
||||
else if ( ! token ) {
|
||||
token = arg;
|
||||
}
|
||||
else {
|
||||
console.warn("Unused parameter " + arg);
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! username ) usage(me, 1);
|
||||
|
||||
console.log("Using environment " + ENV);
|
||||
|
||||
global.environment = require('../config/environments/' + ENV);
|
||||
// _after_ setting global.environment
|
||||
var serverOptions = require('../lib/cartodb/server_options')();
|
||||
|
||||
var client;
|
||||
var dbname;
|
||||
Step(
|
||||
function getClient() {
|
||||
client = redis.createClient(serverOptions.redis.port, serverOptions.redis.host);
|
||||
client.on('connect', this);
|
||||
},
|
||||
function getUserMeta(err) {
|
||||
if ( err ) throw err;
|
||||
client.select(5);
|
||||
client.hgetall('rails:users:' + username, this);
|
||||
},
|
||||
function readDB(err, data) {
|
||||
if ( err ) throw err;
|
||||
if ( ! data )
|
||||
throw new Error('Username ' + username + ' unknown by redis on port '
|
||||
+ serverOptions.redis.port + ' (try CARTODB/script/restore_redis?)');
|
||||
//console.log("Data:"); console.dir(data);
|
||||
dbname = data['database_name'];
|
||||
console.log("Database name for user " + username + ": " + dbname);
|
||||
client.select(0);
|
||||
return null;
|
||||
},
|
||||
function showTokens(err) {
|
||||
if ( err ) throw err;
|
||||
if ( token ) return null;
|
||||
var next = this;
|
||||
Step(
|
||||
function getTokens() {
|
||||
client.keys('map_style|' + dbname + '|*', this);
|
||||
},
|
||||
function showTokens(err, data) {
|
||||
if (err) throw err;
|
||||
if ( data ) console.log(data.join('\n'));
|
||||
return null;
|
||||
},
|
||||
function showTokensFinish(err) {
|
||||
next(err);
|
||||
}
|
||||
);
|
||||
},
|
||||
function showStyle(err) {
|
||||
if ( err ) throw err;
|
||||
if ( ! token ) return null;
|
||||
var next = this;
|
||||
Step(
|
||||
function getStyle() {
|
||||
client.get('map_style|' + dbname + '|' + token, this);
|
||||
},
|
||||
function showStyle(err, data) {
|
||||
if ( err ) throw err;
|
||||
if ( ! data ) {
|
||||
throw new Error(token + ': no such map style known by redis on port '
|
||||
+ serverOptions.redis.port);
|
||||
}
|
||||
//console.log("data: " + data);
|
||||
var x=JSON.parse(data);
|
||||
printMapnikStyle(x, this);
|
||||
},
|
||||
function showStyleFinish(err) {
|
||||
next(err);
|
||||
}
|
||||
);
|
||||
},
|
||||
function finish(err) {
|
||||
if ( err ) {
|
||||
console.error(err.message)
|
||||
process.exit(1);
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
);
|
||||
|
||||
function printMapnikStyle(x, callback) {
|
||||
console.log('style: ' + x.style);
|
||||
console.log('version: ' + x.version);
|
||||
var grainstore = require(basedir + '/../node_modules/windshaft/node_modules/grainstore/lib/grainstore');
|
||||
var mml_store = new grainstore.MMLStore(serverOptions.redis, serverOptions.grainstore);
|
||||
var builderconfig = {dbname:dbname};
|
||||
if ( token.match(/^~/) ) {
|
||||
builderconfig.token = token.substring(1);
|
||||
} else {
|
||||
builderconfig.table = token;
|
||||
}
|
||||
var mml_builder;
|
||||
Step(
|
||||
function getBuilder() {
|
||||
mml_builder = mml_store.mml_builder(builderconfig, this);
|
||||
},
|
||||
function getXML(err, builder) {
|
||||
if ( err ) throw err;
|
||||
mml_builder.toXML(this);
|
||||
},
|
||||
function showXML(err, xml) {
|
||||
if ( err ) throw err;
|
||||
console.log('- XML - ');
|
||||
console.log(xml);
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
callback(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
dbname=`redis-cli -p ${REDIS_PORT} -n 5 hget "rails:users:${username}" "database_name"`
|
||||
if test $? -ne 0; then
|
||||
exit 1
|
||||
fi
|
||||
if test -z "${dbname}"; then
|
||||
echo "Username ${username} unknown by redis (try CARTODB/script/restore_redis?)" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Database name for user ${username}: ${dbname}" # only if verbose?
|
||||
if test -n "$token"; then
|
||||
redis-cli get "map_style|${dbname}|${token}" | sed -e 's/\\n/\n/g' -e 's/\\//g'
|
||||
else
|
||||
redis-cli keys "map_style|${dbname}|*"
|
||||
fi
|
||||
|
||||
47
tools/update_template
Executable file
47
tools/update_template
Executable file
@@ -0,0 +1,47 @@
|
||||
#!/bin/sh
|
||||
|
||||
verbose=no
|
||||
tiler_url=http://dev.localhost.lan:8181/tiles/template
|
||||
apikey=${CDB_APIKEY}
|
||||
|
||||
while test -n "$1"; do
|
||||
if test "$1" = "-v"; then
|
||||
verbose=yes
|
||||
elif test "$1" = "-k"; then
|
||||
shift
|
||||
apikey="$1"
|
||||
elif test "$1" = "-u"; then
|
||||
shift
|
||||
tiler_url="$1"
|
||||
elif test -z "$tpl"; then
|
||||
tpl="$1"
|
||||
elif test -z "$cfg"; then
|
||||
cfg="$1"
|
||||
else
|
||||
echo "Unused parameter $1" >&2
|
||||
fi
|
||||
shift
|
||||
done
|
||||
|
||||
if test -z "$cfg"; then
|
||||
echo "Usage: $0 [-v] [-k <api_key>] [-u <tiler_url>] <template_id> <template_config>" >&2
|
||||
echo "Default <tiler_url> is ${tiler_url}" >&2
|
||||
echo "Default <api_key> is read from CDB_APIKEY env variable" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cmd="curl -X PUT -skH Content-Type:application/json --data-binary @- ${tiler_url}/${tpl}?api_key=${apikey}"
|
||||
if test x${verbose} = xyes; then
|
||||
cmd="${cmd} -v"
|
||||
fi
|
||||
res=`cat ${cfg} | tr '\n' ' ' | ${cmd}`
|
||||
if test $? -gt 0; then
|
||||
echo "curl command failed: ${cmd}"
|
||||
fi
|
||||
|
||||
if test x${verbose} = xyes; then
|
||||
echo "${res}"
|
||||
fi
|
||||
|
||||
tok=`echo "$res" | sed 's/.*"template_id":"\([^"]*\)".*/\1/'`
|
||||
echo $tok
|
||||
Reference in New Issue
Block a user