Compare commits
1275 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd65c6dd0e | ||
|
|
0c670cfdfd | ||
|
|
27ff1ac4f6 | ||
|
|
3f06de93f7 | ||
|
|
0da6495330 | ||
|
|
bf24347328 | ||
|
|
7a5d73f9df | ||
|
|
0d9f34fd48 | ||
|
|
da55a3bdd2 | ||
|
|
333334e598 | ||
|
|
7168e4410c | ||
|
|
3ff8571f4a | ||
|
|
75ddcbbd01 | ||
|
|
9b3e18f333 | ||
|
|
fcb0a4a7e6 | ||
|
|
94e38cef9d | ||
|
|
0df725112b | ||
|
|
9ea2029f81 | ||
|
|
b59e0a00a0 | ||
|
|
790571fd2c | ||
|
|
6ecebae110 | ||
|
|
849470a3c0 | ||
|
|
74d0a6f183 | ||
|
|
61c134215a | ||
|
|
b7218d8832 | ||
|
|
63e19427af | ||
|
|
d4f8578fd6 | ||
|
|
eaccd062d3 | ||
|
|
5f2d5931f4 | ||
|
|
ce40fa608e | ||
|
|
0979c75852 | ||
|
|
f01e8e0866 | ||
|
|
a4e303ab63 | ||
|
|
c5137c9c29 | ||
|
|
9bce88f9b1 | ||
|
|
68c70effec | ||
|
|
ebae218219 | ||
|
|
6685b759b2 | ||
|
|
539b0496bf | ||
|
|
3c33fac8f4 | ||
|
|
9613f76ef5 | ||
|
|
3f0d344313 | ||
|
|
823ee63c25 | ||
|
|
d5d76f9c63 | ||
|
|
dfc9a6fbb4 | ||
|
|
2bd9aece35 | ||
|
|
21870c9fa2 | ||
|
|
7c315b3afd | ||
|
|
da0cdb081d | ||
|
|
9bd0a3f1c9 | ||
|
|
83992895e4 | ||
|
|
aa3b336e46 | ||
|
|
e17b374fde | ||
|
|
61158b62f1 | ||
|
|
88ed43a92e | ||
|
|
e5fff6b452 | ||
|
|
044d49c53a | ||
|
|
69abf8d9b1 | ||
|
|
14e13899a6 | ||
|
|
488c246222 | ||
|
|
654905a79c | ||
|
|
12cb199803 | ||
|
|
8759cf726b | ||
|
|
7a45c9e434 | ||
|
|
9ee69dea55 | ||
|
|
ebe38e977f | ||
|
|
40ad143c3e | ||
|
|
875159fa5f | ||
|
|
c97c65de34 | ||
|
|
25ae09b2c5 | ||
|
|
853c2b4b85 | ||
|
|
682db1ca75 | ||
|
|
d56bd8de72 | ||
|
|
1df91aee6f | ||
|
|
b64eaed7ed | ||
|
|
03dae8a93a | ||
|
|
6b93fa0575 | ||
|
|
5a9a2d7449 | ||
|
|
8d200686fd | ||
|
|
1c2f84b0cb | ||
|
|
513fa2af01 | ||
|
|
7580081a64 | ||
|
|
1a66f96379 | ||
|
|
fde680450f | ||
|
|
6843692f01 | ||
|
|
1f3a073f21 | ||
|
|
7b4d41464f | ||
|
|
7ae2910061 | ||
|
|
ed3517e733 | ||
|
|
6ac3b4c005 | ||
|
|
26545af9ae | ||
|
|
1ee96f14ce | ||
|
|
2250e6d608 | ||
|
|
5ad27e4bf5 | ||
|
|
5f765712b4 | ||
|
|
cb2e330e0b | ||
|
|
6de911e5bb | ||
|
|
9edec8ef3f | ||
|
|
8e8ab09bec | ||
|
|
c06cba81f4 | ||
|
|
ad5514dd02 | ||
|
|
a5b9ca706c | ||
|
|
5a476f9354 | ||
|
|
403039b695 | ||
|
|
5ee19cc2ed | ||
|
|
8c3f9c7ba0 | ||
|
|
b95a001e0b | ||
|
|
d180305e8b | ||
|
|
ef8fcf7e93 | ||
|
|
e7bd5dd644 | ||
|
|
8503a5c7c9 | ||
|
|
2de0e5d52b | ||
|
|
b9e4b0a90c | ||
|
|
8fb3dc7529 | ||
|
|
a897e36b91 | ||
|
|
446c432484 | ||
|
|
c49f3aaba5 | ||
|
|
fed29b3b50 | ||
|
|
e7d134d70c | ||
|
|
62dbce4311 | ||
|
|
5b5f7fc700 | ||
|
|
026a0750e3 | ||
|
|
7045f41252 | ||
|
|
eaf6775d9d | ||
|
|
ba2a9b81e9 | ||
|
|
5577600903 | ||
|
|
a0a455b225 | ||
|
|
0019ab495b | ||
|
|
cbebac1cb1 | ||
|
|
e2fd4aca60 | ||
|
|
0c578a193c | ||
|
|
84f579f0ec | ||
|
|
1bf2809355 | ||
|
|
e91bc91057 | ||
|
|
4f9b6be45b | ||
|
|
95aa74ee34 | ||
|
|
e516300825 | ||
|
|
2d84d38b90 | ||
|
|
7cd78be094 | ||
|
|
c7a10b048a | ||
|
|
d143b0235b | ||
|
|
a876c82660 | ||
|
|
a6c1aefecc | ||
|
|
9b122280e1 | ||
|
|
4777d1e93c | ||
|
|
97e8b54b8c | ||
|
|
136f68765a | ||
|
|
0062ec99f1 | ||
|
|
98bc95bc58 | ||
|
|
47cc1de89b | ||
|
|
69c623b5b2 | ||
|
|
ab9ae60958 | ||
|
|
907ed478cf | ||
|
|
2eb7529efb | ||
|
|
1782f240ce | ||
|
|
2ea880ce2c | ||
|
|
775b2b75a6 | ||
|
|
2d050eb43c | ||
|
|
7934d659fb | ||
|
|
21b5ed9c8a | ||
|
|
da70839f78 | ||
|
|
2e1f08d764 | ||
|
|
21072645a4 | ||
|
|
e3c6569302 | ||
|
|
091352e75b | ||
|
|
bf7044d723 | ||
|
|
38e4812b43 | ||
|
|
b8395010a3 | ||
|
|
f0eeb393d6 | ||
|
|
a9ab9f8b5c | ||
|
|
f019f34601 | ||
|
|
400e51f13a | ||
|
|
3234c37d62 | ||
|
|
e0413c3302 | ||
|
|
cd79fc606b | ||
|
|
a2ac1c23f1 | ||
|
|
69f99daa60 | ||
|
|
f1e8c9a709 | ||
|
|
1fc0545b5a | ||
|
|
b599e67c35 | ||
|
|
85804f9854 | ||
|
|
7df3658d41 | ||
|
|
c92e786a5f | ||
|
|
d6ef0b7457 | ||
|
|
434d6d4110 | ||
|
|
55a78899a4 | ||
|
|
124133ceca | ||
|
|
2b9f2ee66c | ||
|
|
70d1a30c64 | ||
|
|
2161bbf8e9 | ||
|
|
41521b6776 | ||
|
|
ec2fcad2e0 | ||
|
|
ecc67b1d0f | ||
|
|
4ea1199014 | ||
|
|
e7544e84c2 | ||
|
|
ada616ee6a | ||
|
|
000a248ab4 | ||
|
|
ff515f8c12 | ||
|
|
848bfacc2d | ||
|
|
da4b1d5a0f | ||
|
|
b2d9e5e822 | ||
|
|
d0313a4228 | ||
|
|
1f53884722 | ||
|
|
a664a1c550 | ||
|
|
d210643d63 | ||
|
|
4be0a70362 | ||
|
|
09e0f86936 | ||
|
|
e3b7027b24 | ||
|
|
0f30b7d7ef | ||
|
|
3012b99e15 | ||
|
|
f683e39aea | ||
|
|
985973dfda | ||
|
|
07bc281e25 | ||
|
|
1d433bf5b2 | ||
|
|
d5e20ef558 | ||
|
|
36ea58e750 | ||
|
|
e1e5f87123 | ||
|
|
ea3d2124dc | ||
|
|
415d0c42d5 | ||
|
|
c19f652ff3 | ||
|
|
f5f7be627f | ||
|
|
09b3f0a862 | ||
|
|
d9ab1e8810 | ||
|
|
23f5be6c33 | ||
|
|
07297f6bda | ||
|
|
02bc7b9fbf | ||
|
|
0e3f72ce0b | ||
|
|
f311f6d4df | ||
|
|
65c6559b1a | ||
|
|
53d92fe70e | ||
|
|
6d32199c53 | ||
|
|
25e4e3bd33 | ||
|
|
0321884795 | ||
|
|
5f6185dd51 | ||
|
|
63ba75f703 | ||
|
|
9b4acf99d5 | ||
|
|
9ba53dc4cf | ||
|
|
2f44dfe1da | ||
|
|
b891ae19f4 | ||
|
|
00cf83dc45 | ||
|
|
72294fbd25 | ||
|
|
5af09fc2bf | ||
|
|
c1c6d493b7 | ||
|
|
a30ed5ce04 | ||
|
|
7c35d5cd32 | ||
|
|
2e67633ca8 | ||
|
|
87782b400d | ||
|
|
b6d3785599 | ||
|
|
645a2cd442 | ||
|
|
8c09dfd230 | ||
|
|
336491b54c | ||
|
|
4365c1dbc2 | ||
|
|
3c56c1fab3 | ||
|
|
0a331cee37 | ||
|
|
d7f5c40645 | ||
|
|
5df24e7f27 | ||
|
|
406a1ffb0b | ||
|
|
438ecd5598 | ||
|
|
bd1c24ee1c | ||
|
|
e561f77d4d | ||
|
|
d03a2c64a6 | ||
|
|
fda8afdaf2 | ||
|
|
e4da13189d | ||
|
|
f35d328dbf | ||
|
|
d09998cce1 | ||
|
|
7a01d75cd8 | ||
|
|
83e5d889a7 | ||
|
|
f1eed600d5 | ||
|
|
75a980ff1d | ||
|
|
9d9aafbcb3 | ||
|
|
8e9d9113d7 | ||
|
|
62661c633c | ||
|
|
38e35a6d61 | ||
|
|
0d5242d12b | ||
|
|
edde869a68 | ||
|
|
6d4eb23696 | ||
|
|
1979697551 | ||
|
|
661df294e1 | ||
|
|
39dc9a316d | ||
|
|
cfe434c8ed | ||
|
|
53d7276136 | ||
|
|
e95167a049 | ||
|
|
e82131f4e8 | ||
|
|
cbd44192c9 | ||
|
|
f006d09f31 | ||
|
|
620160c44e | ||
|
|
6ae2c2630a | ||
|
|
a287c84500 | ||
|
|
62e435fd9e | ||
|
|
65702de64d | ||
|
|
ef2b45621b | ||
|
|
1771313bea | ||
|
|
b0624582d9 | ||
|
|
10acdc4615 | ||
|
|
d96ed3511e | ||
|
|
e0dba85f67 | ||
|
|
989e752959 | ||
|
|
71efe2109c | ||
|
|
5da239a2eb | ||
|
|
5c2e5c0d05 | ||
|
|
27d6d636cf | ||
|
|
5db7f002e6 | ||
|
|
ac1f8f8497 | ||
|
|
ee6bd8c561 | ||
|
|
7f20e296a3 | ||
|
|
2e577343d2 | ||
|
|
dc14248de2 | ||
|
|
9536669053 | ||
|
|
11db363bfb | ||
|
|
d2961430f3 | ||
|
|
2b7bd58fd5 | ||
|
|
055932d38e | ||
|
|
c65a29acf4 | ||
|
|
27cda49fd8 | ||
|
|
2d7b706507 | ||
|
|
3ea3638fe9 | ||
|
|
1f9387bb68 | ||
|
|
00542bbc57 | ||
|
|
5be8afbbeb | ||
|
|
9f7dcc2354 | ||
|
|
21b3f44441 | ||
|
|
f295f847d1 | ||
|
|
0478905689 | ||
|
|
d311dd4245 | ||
|
|
b25bb03cdf | ||
|
|
afa625e3d2 | ||
|
|
e08b1ea1a0 | ||
|
|
c6d328ee07 | ||
|
|
8d10d0f760 | ||
|
|
a2a09979e4 | ||
|
|
597cb5286d | ||
|
|
a0243e0bf7 | ||
|
|
0f668aabf1 | ||
|
|
59dfd11e5b | ||
|
|
b1b57d6f24 | ||
|
|
636591ecbb | ||
|
|
a4eade31a2 | ||
|
|
ba0f394a48 | ||
|
|
75c4153f9b | ||
|
|
8e038b0323 | ||
|
|
0a994c731c | ||
|
|
13ae1b4067 | ||
|
|
742a9744ea | ||
|
|
8ed864ad18 | ||
|
|
87638168ff | ||
|
|
8364da683a | ||
|
|
6eec5822f0 | ||
|
|
d667dbcc2f | ||
|
|
40de1a8f86 | ||
|
|
b53c25e514 | ||
|
|
81919706ea | ||
|
|
d38fc16b57 | ||
|
|
90b22b2718 | ||
|
|
04af57cab9 | ||
|
|
d40b15454b | ||
|
|
e1e925bd9e | ||
|
|
151968ae13 | ||
|
|
547782eea5 | ||
|
|
6bd967e9fb | ||
|
|
13f5fda1b8 | ||
|
|
673bd4f3f2 | ||
|
|
d56affae2d | ||
|
|
09527b6808 | ||
|
|
d065ace036 | ||
|
|
2736b93c69 | ||
|
|
10b7ab307e | ||
|
|
56c24a738a | ||
|
|
fa8b27231c | ||
|
|
c17af23a40 | ||
|
|
fbecc11aa5 | ||
|
|
8cacc3bb9e | ||
|
|
a82af16347 | ||
|
|
5018d32af6 | ||
|
|
2c7bc6adde | ||
|
|
58f9f5f7a8 | ||
|
|
e4e633cf86 | ||
|
|
4ca5c5fa3c | ||
|
|
1bb0d8738e | ||
|
|
4949616c4e | ||
|
|
12c5d835c5 | ||
|
|
87eaeb0074 | ||
|
|
8b07156a2d | ||
|
|
358b296750 | ||
|
|
d0ef87b0cf | ||
|
|
e28fe1fdc0 | ||
|
|
aecb07b008 | ||
|
|
5573dfda84 | ||
|
|
7a22973258 | ||
|
|
f099a69df3 | ||
|
|
938b6579c0 | ||
|
|
e445d0de01 | ||
|
|
7a35b9695f | ||
|
|
9523d40937 | ||
|
|
697323dbbc | ||
|
|
efe090f5b0 | ||
|
|
ee1454d91c | ||
|
|
38242813be | ||
|
|
f9373dd8d0 | ||
|
|
0e4e56f333 | ||
|
|
c1d4da870f | ||
|
|
3c26f1f986 | ||
|
|
e9195967a4 | ||
|
|
30c6a390ac | ||
|
|
3a97af767f | ||
|
|
57dd36a476 | ||
|
|
6ab6fd91e4 | ||
|
|
c41c223b84 | ||
|
|
7e2be7b30f | ||
|
|
e690170689 | ||
|
|
81f1b0dcf8 | ||
|
|
146a2b2606 | ||
|
|
ff811ac1b5 | ||
|
|
6a39893e20 | ||
|
|
11d9f5dd76 | ||
|
|
571a635fed | ||
|
|
6e70518146 | ||
|
|
fabb438cf0 | ||
|
|
0abd6a2293 | ||
|
|
272e8cd221 | ||
|
|
885accdadf | ||
|
|
f5a3b77737 | ||
|
|
56abcfd2f4 | ||
|
|
2e84d18c3c | ||
|
|
ecd570323b | ||
|
|
20eb92a3b1 | ||
|
|
8d22ed7594 | ||
|
|
b26fe87430 | ||
|
|
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 | ||
|
|
e9f5c60719 | ||
|
|
9b06ac833a | ||
|
|
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 | ||
|
|
17fc934aa3 | ||
|
|
27eaad932a | ||
|
|
602b255ce5 | ||
|
|
efc6d5c857 | ||
|
|
633e8d164b | ||
|
|
db951234aa | ||
|
|
60617e7641 | ||
|
|
7b43a0f0bd | ||
|
|
06e68c1518 | ||
|
|
b4045353ea | ||
|
|
22130f37df | ||
|
|
bce024961f | ||
|
|
3819d0d47b | ||
|
|
7cb69d1db9 | ||
|
|
ec97381820 | ||
|
|
25c46962cb | ||
|
|
23da8538a6 | ||
|
|
381b9a9edf | ||
|
|
76c056c7a1 | ||
|
|
4b5899ff1a | ||
|
|
afd4c3b460 | ||
|
|
8b7cc64567 | ||
|
|
308246f324 | ||
|
|
b0b40933d8 | ||
|
|
0e13228f5c | ||
|
|
65c7c5fc9c | ||
|
|
60242c80f4 | ||
|
|
5213216fc4 | ||
|
|
1c65cec6ed | ||
|
|
316c9c209d | ||
|
|
563764dc4f | ||
|
|
8c51c97102 | ||
|
|
6416af3f16 | ||
|
|
00c18ab8dd | ||
|
|
632d75a7c8 | ||
|
|
d7b1ff9a80 | ||
|
|
10a66fbd66 | ||
|
|
4b1d6cd729 | ||
|
|
e984811eae | ||
|
|
eb83851bb7 | ||
|
|
850d4cd6ba | ||
|
|
6cb8c85da0 | ||
|
|
3d4af14315 | ||
|
|
c07616f889 | ||
|
|
d53327bc33 | ||
|
|
f20c98e49c | ||
|
|
9807a5e12b | ||
|
|
70f535d13a | ||
|
|
8d326dba2f | ||
|
|
8d0da4d517 | ||
|
|
63296a87cb | ||
|
|
b67a487c48 | ||
|
|
d977f83bd1 | ||
|
|
5b6919e0c6 | ||
|
|
92a3d52885 | ||
|
|
627b3f084d | ||
|
|
3763232c14 | ||
|
|
3e4f98cb51 | ||
|
|
977d051518 | ||
|
|
d7ae28095f | ||
|
|
b98a32c296 | ||
|
|
5ab4caea7d | ||
|
|
9ce88bcc98 | ||
|
|
94ddbf69d8 | ||
|
|
c9e3cc00be | ||
|
|
efa79b243c | ||
|
|
06f781334d | ||
|
|
f0fc44aac9 | ||
|
|
b678f82be8 | ||
|
|
44a8db03e2 | ||
|
|
4a12ed1f19 | ||
|
|
de24e7fc34 | ||
|
|
1b6a662a72 | ||
|
|
6f8fd69101 | ||
|
|
78a6f4de1b | ||
|
|
afb875c32a | ||
|
|
369b0f6110 | ||
|
|
1c910ec513 | ||
|
|
08d1d73b4f | ||
|
|
83e6e0d457 | ||
|
|
e5af3b90f4 | ||
|
|
7c82498f8f | ||
|
|
9162d2cd43 | ||
|
|
a0b6f467b1 | ||
|
|
782edd9506 | ||
|
|
7ed4320493 | ||
|
|
113b70cf98 | ||
|
|
106e95940d | ||
|
|
4329ffd61a | ||
|
|
3383c44eb7 | ||
|
|
aea107f1af | ||
|
|
2b2c22cdd5 | ||
|
|
001bf97d69 | ||
|
|
2da8c44914 | ||
|
|
e53122de7e | ||
|
|
4e7f92c60d | ||
|
|
d95ac85b17 | ||
|
|
3ff3dc2c97 | ||
|
|
4605bd1e1d | ||
|
|
dfc4a02398 | ||
|
|
899d8a3c64 | ||
|
|
402fc90e63 | ||
|
|
fcd6d55ba4 | ||
|
|
4622dac81b | ||
|
|
e8cbc666e2 | ||
|
|
49aad435b9 | ||
|
|
23f6c82f0e | ||
|
|
4995011f1e | ||
|
|
e038aa7522 | ||
|
|
c77dce105c | ||
|
|
2fa09aee88 | ||
|
|
a7afdac67e | ||
|
|
f6d50fafb1 | ||
|
|
f076b0c4d1 | ||
|
|
9b04abb36c | ||
|
|
14e3ead06e | ||
|
|
b98a0f9104 | ||
|
|
dc188817a8 | ||
|
|
b81c83aace | ||
|
|
9dcf6a1acf | ||
|
|
c3f53a7987 | ||
|
|
3101dbd433 | ||
|
|
3c5e358c32 | ||
|
|
219caf7eab | ||
|
|
fb33583df9 | ||
|
|
a79b999e7a | ||
|
|
031c6ee0cd | ||
|
|
b3f8515fd5 | ||
|
|
6e6c1101fe | ||
|
|
39c0d3a9a4 | ||
|
|
6476b975d4 | ||
|
|
60b578ae56 | ||
|
|
7dd7d09cf5 | ||
|
|
c314640c3b | ||
|
|
d1d2074146 | ||
|
|
56319a5ac0 | ||
|
|
6b71cde56e | ||
|
|
7d34cbbd63 | ||
|
|
cb57dfb27d | ||
|
|
0b56dd09d9 | ||
|
|
2d35a2c269 | ||
|
|
6c40d936ef | ||
|
|
61f4212ba2 | ||
|
|
e408d04fc5 | ||
|
|
2510a47262 | ||
|
|
8d5baebf1d | ||
|
|
42b3d0ab9c | ||
|
|
8d4f033a56 | ||
|
|
dd19d74149 | ||
|
|
ac49abe750 | ||
|
|
b130b67f24 | ||
|
|
093d3de66e | ||
|
|
fea30dcea4 | ||
|
|
6e8bfffff1 | ||
|
|
0ff403fa83 | ||
|
|
ab1a0e0339 | ||
|
|
290e005e14 | ||
|
|
d1979a5265 | ||
|
|
9db49a25e4 | ||
|
|
a69afe4cd7 | ||
|
|
556aaf3330 | ||
|
|
7b1184db8f | ||
|
|
e1070d3998 | ||
|
|
cc91e0dcff | ||
|
|
081cc41a34 | ||
|
|
9d52b8b11f | ||
|
|
a8a3e739ad | ||
|
|
b884fe00ea | ||
|
|
4f8b855f1f | ||
|
|
6f5e3837e3 | ||
|
|
de1df4f33c | ||
|
|
7231864e1c | ||
|
|
e813209768 | ||
|
|
69508f05d7 | ||
|
|
2f70640340 | ||
|
|
02d4473766 | ||
|
|
72cf824235 | ||
|
|
a3d09339de | ||
|
|
a0cd4354a7 | ||
|
|
820c836050 | ||
|
|
bbdc29faae | ||
|
|
c976506c67 | ||
|
|
11025eb7c4 | ||
|
|
08c75843de | ||
|
|
c3169745ee | ||
|
|
32007403c0 | ||
|
|
59a1911950 | ||
|
|
2be0ebb808 | ||
|
|
6ccb7f6f15 | ||
|
|
bb59524914 | ||
|
|
9645d0fb58 | ||
|
|
635b5db3e6 | ||
|
|
19436a8b14 | ||
|
|
1ebe862594 | ||
|
|
2f0ef03cd3 | ||
|
|
597008d9d4 | ||
|
|
f9b78e2cb2 | ||
|
|
33e68d9069 | ||
|
|
b5658a9f43 | ||
|
|
4b8c165261 | ||
|
|
cd6002879e | ||
|
|
43bb96cc60 | ||
|
|
52303e7821 | ||
|
|
37cba2836c | ||
|
|
8ef952f138 | ||
|
|
b5db3a8138 | ||
|
|
d1d321194d | ||
|
|
8120e6579a | ||
|
|
f9e0b8708c | ||
|
|
6401278317 | ||
|
|
1873322a90 | ||
|
|
2d2a14e9a6 | ||
|
|
1da7484c2a | ||
|
|
2bc09a61cf | ||
|
|
d9e6aeb254 | ||
|
|
1a60c55fea | ||
|
|
ab8cb5bbb3 | ||
|
|
4c6d74b69e | ||
|
|
20dca2e8f8 | ||
|
|
90d726c0cb | ||
|
|
00dccd4c27 | ||
|
|
d691f94978 | ||
|
|
11910bb218 | ||
|
|
f0c655294f | ||
|
|
8e3c900580 | ||
|
|
65e4cf1510 | ||
|
|
41d7000daf | ||
|
|
0e37b32e52 | ||
|
|
9ad574efdc | ||
|
|
8d5c52ce1b | ||
|
|
926b47b009 | ||
|
|
bb00bf1c05 | ||
|
|
bc51b14485 | ||
|
|
9cdd2800fa | ||
|
|
0697873a64 | ||
|
|
6a1933bed9 | ||
|
|
703c63d5d6 | ||
|
|
89ca4e1a5a | ||
|
|
f8e88153f4 | ||
|
|
961269fa1f | ||
|
|
90f6e464d2 | ||
|
|
a42f03c224 | ||
|
|
34df118160 | ||
|
|
b6f28d7dd2 | ||
|
|
43298f58fb | ||
|
|
5d62c6b588 | ||
|
|
4216d43db2 | ||
|
|
f85ca16c62 | ||
|
|
14953e992f | ||
|
|
0122c6a386 |
10
.gitignore
vendored
@@ -1,3 +1,11 @@
|
||||
node_modules
|
||||
node_modules*
|
||||
config.status*
|
||||
config/environments/*.js
|
||||
.idea
|
||||
tools/munin/windshaft.conf
|
||||
logs/
|
||||
pids/
|
||||
redis.pid
|
||||
test.log
|
||||
npm-debug.log
|
||||
coverage/
|
||||
|
||||
4
.jshintignore
Normal file
@@ -0,0 +1,4 @@
|
||||
test/results/
|
||||
test/monkey/
|
||||
test/benchmark.js
|
||||
test/support/
|
||||
98
.jshintrc
Normal file
@@ -0,0 +1,98 @@
|
||||
{
|
||||
// // JSHint Default Configuration File (as on JSHint website)
|
||||
// // See http://jshint.com/docs/ for more details
|
||||
//
|
||||
// "maxerr" : 50, // {int} Maximum error before stopping
|
||||
//
|
||||
// // Enforcing
|
||||
// "bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.)
|
||||
// "camelcase" : false, // true: Identifiers must be in camelCase
|
||||
// "curly" : true, // true: Require {} for every new block or scope
|
||||
// "eqeqeq" : true, // true: Require triple equals (===) for comparison
|
||||
"forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty()
|
||||
"freeze" : true, // true: prohibits overwriting prototypes of native objects such as Array, Date etc.
|
||||
"immed" : true, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());`
|
||||
// "indent" : 4, // {int} Number of spaces to use for indentation
|
||||
// "latedef" : false, // true: Require variables/functions to be defined before being used
|
||||
"newcap" : true, // true: Require capitalization of all constructor functions e.g. `new F()`
|
||||
"noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee`
|
||||
// "noempty" : true, // true: Prohibit use of empty blocks
|
||||
"nonbsp" : true, // true: Prohibit "non-breaking whitespace" characters.
|
||||
"nonew" : true, // true: Prohibit use of constructors for side-effects (without assignment)
|
||||
// "plusplus" : false, // true: Prohibit use of `++` & `--`
|
||||
// "quotmark" : false, // Quotation mark consistency:
|
||||
// // false : do nothing (default)
|
||||
// // true : ensure whatever is used is consistent
|
||||
// // "single" : require single quotes
|
||||
// // "double" : require double quotes
|
||||
"undef" : true, // true: Require all non-global variables to be declared (prevents global leaks)
|
||||
"unused" : true, // true: Require all defined variables be used
|
||||
// "strict" : true, // true: Requires all functions run in ES5 Strict Mode
|
||||
// "maxparams" : false, // {int} Max number of formal params allowed per function
|
||||
// "maxdepth" : false, // {int} Max depth of nested blocks (within functions)
|
||||
// "maxstatements" : false, // {int} Max number statements per function
|
||||
"maxcomplexity" : 8, // {int} Max cyclomatic complexity per function
|
||||
"maxlen" : 120, // {int} Max number of characters per line
|
||||
//
|
||||
// // Relaxing
|
||||
// "asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons)
|
||||
// "boss" : false, // true: Tolerate assignments where comparisons would be expected
|
||||
"debug" : false, // true: Allow debugger statements e.g. browser breakpoints.
|
||||
// "eqnull" : false, // true: Tolerate use of `== null`
|
||||
// "es5" : false, // true: Allow ES5 syntax (ex: getters and setters)
|
||||
// "esnext" : false, // true: Allow ES.next (ES6) syntax (ex: `const`)
|
||||
// "moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features)
|
||||
// // (ex: `for each`, multiple try/catch, function expression…)
|
||||
// "evil" : false, // true: Tolerate use of `eval` and `new Function()`
|
||||
// "expr" : false, // true: Tolerate `ExpressionStatement` as Programs
|
||||
// "funcscope" : false, // true: Tolerate defining variables inside control statements
|
||||
// "globalstrict" : false, // true: Allow global "use strict" (also enables 'strict')
|
||||
// "iterator" : false, // true: Tolerate using the `__iterator__` property
|
||||
// "lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block
|
||||
// "laxbreak" : false, // true: Tolerate possibly unsafe line breakings
|
||||
// "laxcomma" : false, // true: Tolerate comma-first style coding
|
||||
// "loopfunc" : false, // true: Tolerate functions being defined in loops
|
||||
// "multistr" : false, // true: Tolerate multi-line strings
|
||||
// "noyield" : false, // true: Tolerate generator functions with no yield statement in them.
|
||||
// "notypeof" : false, // true: Tolerate invalid typeof operator values
|
||||
// "proto" : false, // true: Tolerate using the `__proto__` property
|
||||
// "scripturl" : false, // true: Tolerate script-targeted URLs
|
||||
// "shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;`
|
||||
// "sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation
|
||||
// "supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;`
|
||||
// "validthis" : false, // true: Tolerate using this in a non-constructor function
|
||||
//
|
||||
// // Environments
|
||||
// "browser" : true, // Web Browser (window, document, etc)
|
||||
// "browserify" : false, // Browserify (node.js code in the browser)
|
||||
// "couch" : false, // CouchDB
|
||||
// "devel" : true, // Development/debugging (alert, confirm, etc)
|
||||
// "dojo" : false, // Dojo Toolkit
|
||||
// "jasmine" : false, // Jasmine
|
||||
// "jquery" : false, // jQuery
|
||||
// "mocha" : true, // Mocha
|
||||
// "mootools" : false, // MooTools
|
||||
"node" : true, // Node.js
|
||||
// "nonstandard" : false, // Widely adopted globals (escape, unescape, etc)
|
||||
// "prototypejs" : false, // Prototype and Scriptaculous
|
||||
// "qunit" : false, // QUnit
|
||||
// "rhino" : false, // Rhino
|
||||
// "shelljs" : false, // ShellJS
|
||||
// "worker" : false, // Web Workers
|
||||
// "wsh" : false, // Windows Scripting Host
|
||||
// "yui" : false, // Yahoo User Interface
|
||||
|
||||
// Custom Globals
|
||||
"globals" : { // additional predefined global variables
|
||||
"describe": true,
|
||||
"before": true,
|
||||
"after": true,
|
||||
"beforeEach": true,
|
||||
"afterEach": true,
|
||||
"it": true,
|
||||
"suite": true,
|
||||
"suiteSetup": true,
|
||||
"test": true,
|
||||
"suiteTeardown": true
|
||||
}
|
||||
}
|
||||
22
.travis.yml
Normal file
@@ -0,0 +1,22 @@
|
||||
addons:
|
||||
postgresql: "9.3"
|
||||
|
||||
before_install:
|
||||
- sudo apt-get update
|
||||
- sudo apt-get install -y pkg-config libcairo2-dev libjpeg8-dev libgif-dev
|
||||
- sudo apt-get install postgresql-plpython-9.3
|
||||
- createdb template_postgis
|
||||
- psql -c "CREATE EXTENSION postgis" template_postgis
|
||||
|
||||
env:
|
||||
- NPROCS=1 JOBS=1 PGUSER=postgres
|
||||
|
||||
language: node_js
|
||||
node_js:
|
||||
- "0.10"
|
||||
|
||||
notifications:
|
||||
irc:
|
||||
channels:
|
||||
- "irc.freenode.org#cartodb"
|
||||
use_notice: true
|
||||
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
@@ -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
@@ -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.
|
||||
40
Makefile
@@ -1,11 +1,41 @@
|
||||
SHELL=/bin/bash
|
||||
|
||||
pre-install:
|
||||
@$(SHELL) ./scripts/check-node-canvas.sh
|
||||
|
||||
all:
|
||||
npm install
|
||||
@$(SHELL) ./scripts/install.sh
|
||||
|
||||
clean:
|
||||
rm -rf node_modules/*
|
||||
|
||||
config/environments/test.js: config/environments/test.js.example
|
||||
./configure
|
||||
distclean: clean
|
||||
rm config.status*
|
||||
|
||||
check: config/environments/test.js
|
||||
./run_tests.sh
|
||||
config.status--test:
|
||||
./configure --environment=test
|
||||
|
||||
config/environments/test.js: config.status--test
|
||||
./config.status--test
|
||||
|
||||
test: config/environments/test.js
|
||||
@echo "***tests***"
|
||||
@$(SHELL) ./run_tests.sh ${RUNTESTFLAGS} \
|
||||
test/unit/cartodb/*.js \
|
||||
test/unit/cartodb/cache/model/*.js \
|
||||
test/integration/*.js \
|
||||
test/acceptance/*.js \
|
||||
test/acceptance/cache/*.js
|
||||
|
||||
jshint:
|
||||
@echo "***jshint***"
|
||||
@./node_modules/.bin/jshint lib/ test/ app.js
|
||||
|
||||
test-all: jshint test
|
||||
|
||||
coverage:
|
||||
@RUNTESTFLAGS=--with-coverage make test
|
||||
|
||||
check: test
|
||||
|
||||
.PHONY: pre-install test jshint coverage
|
||||
|
||||
96
README.md
@@ -1,33 +1,38 @@
|
||||
Windshaft-CartoDB
|
||||
==================
|
||||
|
||||
NOTE: requires node-0.8.x
|
||||
[](https://travis-ci.org/CartoDB/Windshaft-cartodb)
|
||||
|
||||
This is the CartoDB map tiler. It extends Windshaft with some extra
|
||||
functionality and custom filters for authentication
|
||||
This is the [CartoDB Maps API](http://docs.cartodb.com/cartodb-platform/maps-api.html) tiler. It extends
|
||||
[Windshaft](https://github.com/CartoDB/Windshaft) with some extra functionality and custom filters for authentication.
|
||||
|
||||
* reads dbname from subdomain and cartodb redis for pretty tile urls
|
||||
* configures windshaft to publish ``cartodb_id`` as the interactivity layer
|
||||
* configures windshaft to publish `cartodb_id` as the interactivity layer
|
||||
* 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 a [template maps API](https://github.com/CartoDB/Windshaft-cartodb/blob/master/docs/Template-maps.md)
|
||||
|
||||
Requirements
|
||||
------------
|
||||
- Core
|
||||
- Node.js >=0.8
|
||||
- npm >=1.2.1
|
||||
- PostgreSQL >8.3.x, PostGIS >1.5.x
|
||||
- Redis >2.4.0 (http://www.redis.io)
|
||||
- Mapnik 2.0.1, 2.0.2, 2.1.0, 2.2.0, 2.3.0. See Installing Mapnik.
|
||||
- Windshaft: check [Windshaft dependencies and installation notes](https://github.com/CartoDB/Windshaft#dependencies)
|
||||
- libcairo2-dev, libpango1.0-dev, libjpeg8-dev and libgif-dev for server side canvas support
|
||||
|
||||
[core]
|
||||
- node-0.6.x+
|
||||
- PostgreSQL-8.3+
|
||||
- PostGIS-1.5.0+
|
||||
- Redis 2.2.0+ (http://www.redis.io)
|
||||
- Mapnik 2.0 or 2.1
|
||||
- For cache control (optional)
|
||||
- CartoDB-SQL-API 1.0.0+
|
||||
- CartoDB 0.9.5+ (for `CDB_QueryTables`)
|
||||
- Varnish (http://www.varnish-cache.org)
|
||||
|
||||
[for cache control]
|
||||
- CartoDB-SQL-API 1.0.0+
|
||||
- CartoDB 0.9.5+ (for ``CDB_QueryTables``)
|
||||
- Varnish (https://www.varnish-cache.org)
|
||||
- For running the testsuite
|
||||
- ImageMagick (http://www.imagemagick.org)
|
||||
|
||||
Configure
|
||||
---------
|
||||
@@ -54,6 +59,14 @@ happen to have startup errors you may need to force rebuilding those
|
||||
modules. At any time just wipe out the node_modules/ directory and run
|
||||
```npm install``` again.
|
||||
|
||||
Upgrading
|
||||
---------
|
||||
|
||||
Checkout your commit/branch. If you need to reinstall dependencies (you can check [NEWS](NEWS.md)) do the following:
|
||||
|
||||
```
|
||||
rm -rf node_modules; npm install
|
||||
```
|
||||
|
||||
Run
|
||||
---
|
||||
@@ -69,49 +82,16 @@ there may be out-of-sync records in there.
|
||||
Take a look: http://redis.io/commands
|
||||
|
||||
|
||||
URLs
|
||||
----
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
**TILES**
|
||||
|
||||
[GET] subdomain.cartodb.com/tiles/:table_name/:z/:x/:y.[png|png8|grid.json]
|
||||
|
||||
Args:
|
||||
|
||||
* sql - plain SQL arguments
|
||||
* interactivity - specify the column to use in UTFGrid
|
||||
* cache_buster - if needed you can add a cachebuster to make sure you're
|
||||
rendering new
|
||||
* geom_type - override the cartodb default
|
||||
* style - override the default map style with Carto
|
||||
The [docs directory](https://github.com/CartoDB/Windshaft-cartodb/tree/master/docs) contains different documentation
|
||||
resources, from higher level to more detailed ones:
|
||||
The [Maps API](https://github.com/CartoDB/Windshaft-cartodb/blob/master/docs/Map-API.md) defined the endpoints and their
|
||||
expected parameters and outputs.
|
||||
|
||||
|
||||
**STYLE**
|
||||
Examples
|
||||
--------
|
||||
|
||||
[GET/POST] subdomain.cartodb.com/tiles/:table_name/style
|
||||
|
||||
Args:
|
||||
|
||||
* style - the style in CartoCSS you want to set
|
||||
|
||||
|
||||
**INFOWINDOW**
|
||||
|
||||
[GET] subdomain.cartodb.com/tiles/:table_name/infowindow
|
||||
|
||||
Args:
|
||||
|
||||
* infowindow - returns contents of infowindow from CartoDB.
|
||||
|
||||
|
||||
**MAP METADATA**
|
||||
|
||||
[GET] subdomain.cartodb.com/tiles/:table_name/map_metadata
|
||||
|
||||
Args:
|
||||
|
||||
* infowindow - returns contents of infowindow from CartoDB.
|
||||
|
||||
|
||||
All GET requests are wrappable with JSONP using callback argument,
|
||||
including the UTFGrid map tile call.
|
||||
[CartoDB's Map Gallery](http://cartodb.com/gallery/) showcases several examples of visualisations built on top of this.
|
||||
|
||||
109
app.js
@@ -7,31 +7,108 @@
|
||||
* environments: [development, production]
|
||||
*/
|
||||
|
||||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
var RedisPool = require('redis-mpool');
|
||||
var _ = require('underscore');
|
||||
|
||||
var ENV;
|
||||
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'){
|
||||
if (ENV != 'development' && ENV != 'production' && ENV != 'staging' ){
|
||||
console.error("\nnode app.js [environment]");
|
||||
console.error("environments: [development, production]\n");
|
||||
console.error("environments: development, production, staging\n");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
var _ = require('underscore')
|
||||
, Step = require('step')
|
||||
, CartodbWindshaft = require('./lib/cartodb/cartodb_windshaft');
|
||||
|
||||
// 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');
|
||||
var 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' } }
|
||||
);
|
||||
}
|
||||
|
||||
global.log4js.configure(log4js_config, { cwd: __dirname });
|
||||
global.logger = global.log4js.getLogger();
|
||||
|
||||
var redisOpts = _.extend(global.environment.redis, { name: 'windshaft', unwatchOnRelease: false, noReadyCheck: true }),
|
||||
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 cartodbWindshaft = require('./lib/cartodb/cartodb_windshaft'),
|
||||
serverOptions = require('./lib/cartodb/server_options')(redisPool);
|
||||
|
||||
var Windshaft = require('windshaft');
|
||||
var serverOptions = require('./lib/cartodb/server_options');
|
||||
var ws = cartodbWindshaft(serverOptions);
|
||||
|
||||
ws = CartodbWindshaft(serverOptions);
|
||||
ws.listen(global.environment.port);
|
||||
console.log("Windshaft tileserver started on port " + global.environment.port);
|
||||
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
|
||||
// 1 is good if you have max 16 filedescriptors
|
||||
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 %s started on %s:%s (%s)",
|
||||
version, global.environment.host, global.environment.port, ENV
|
||||
);
|
||||
});
|
||||
|
||||
process.on('SIGHUP', function() {
|
||||
global.log4js.clearAndShutdownAppenders(function() {
|
||||
global.log4js.configure(log4js_config);
|
||||
global.logger = global.log4js.getLogger();
|
||||
console.log('Log files reloaded');
|
||||
});
|
||||
});
|
||||
|
||||
process.on('uncaughtException', function(err) {
|
||||
global.logger.error('Uncaught exception: ' + err.stack);
|
||||
});
|
||||
|
||||
BIN
assets/default-placeholder.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
assets/default-placeholder@2x.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
assets/render-timeout-fallback.png
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
BIN
assets/render-timeout-fallback@2x.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
52
cluster.js
@@ -1,52 +0,0 @@
|
||||
/*
|
||||
* Windshaft-CartoDB
|
||||
* ===============
|
||||
*
|
||||
* ./app.js [environment]
|
||||
*
|
||||
* environments: [development, production]
|
||||
*/
|
||||
|
||||
var Cluster = require('cluster2');
|
||||
|
||||
// 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");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
var _ = require('underscore')
|
||||
, Step = require('step')
|
||||
, CartodbWindshaft = require('./lib/cartodb/cartodb_windshaft');
|
||||
|
||||
|
||||
// set environment specific variables
|
||||
global.settings = require(__dirname + '/config/settings');
|
||||
global.environment = require(__dirname + '/config/environments/' + ENV);
|
||||
_.extend(global.settings, global.environment);
|
||||
|
||||
// Include cart_data.js 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 ws = CartodbWindshaft(serverOptions);
|
||||
|
||||
//.use(cluster.logger('logs'))
|
||||
//.use(cluster.stats())
|
||||
//.use(cluster.pidfiles('pids'))
|
||||
var cluster = new Cluster({
|
||||
port: global.environment.port,
|
||||
monPort: global.environment.port+1,
|
||||
noWorkers: 1 // .set('workers', 1)
|
||||
});
|
||||
|
||||
cluster.listen(function(cb) {
|
||||
cb(ws);
|
||||
});
|
||||
|
||||
console.log("Windshaft tileserver started on port " + global.environment.port);
|
||||
@@ -2,42 +2,220 @@ 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|/user/:user/api/v1/map/named|/tiles/template)'
|
||||
// Base url for the Detached Maps API
|
||||
// "maps" is the the new API,
|
||||
// "tiles/layergroup" is for compatibility with versions up to 1.6.x
|
||||
,base_url_detached: '(?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)'
|
||||
|
||||
// Maximum number of connections for one process
|
||||
// 128 is a good value with a limit of 1024 open file descriptors
|
||||
,maxConnections:128
|
||||
// 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] \033[90m:method\033[0m \033[36m:req[Host]:url\033[0m \033[90m:status :response-time ms -> :res[Content-Type]\033[0m'
|
||||
,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: "-20005048.4188,-20005048.4188,20005048.4188,20005048.4188",
|
||||
extent: "-20037508.3,-20037508.3,20037508.3,20037508.3",
|
||||
/* experimental
|
||||
geometry_field: "the_geom",
|
||||
extent: "-180,-90,180,90",
|
||||
srid: 4326,
|
||||
*/
|
||||
simplify: true
|
||||
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,
|
||||
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
|
||||
mapnik: {
|
||||
// The size of the pool of internal mapnik renderers
|
||||
// Check the configuration of uv_threadpool_size to use suitable value
|
||||
poolSize: 8,
|
||||
|
||||
// Metatile is the number of tiles-per-side that are going
|
||||
// to be rendered at once. If all of them will be requested
|
||||
// we'd have saved time. If only one will be used, we'd have
|
||||
// wasted time.
|
||||
metatile: 2,
|
||||
|
||||
// Override metatile behaviour depending on the format
|
||||
formatMetatile: {
|
||||
png: 2,
|
||||
'grid.json': 1
|
||||
},
|
||||
|
||||
// Buffer size is the tickness in pixel of a buffer
|
||||
// around the rendered (meta?)tile.
|
||||
//
|
||||
// This is important for labels and other marker that overlap tile boundaries.
|
||||
// Setting to 128 ensures no render artifacts.
|
||||
// 64 may have artifacts but is faster.
|
||||
// Less important if we can turn metatiling on.
|
||||
bufferSize: 64,
|
||||
|
||||
// SQL queries will be wrapped with ST_SnapToGrid
|
||||
// Snapping all points of the geometry to a regular grid
|
||||
snapToGrid: false,
|
||||
|
||||
// SQL queries will be wrapped with ST_ClipByBox2D
|
||||
// Returning the portion of a geometry falling within a rectangle
|
||||
// It will only work if snapToGrid is enabled
|
||||
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
|
||||
|
||||
limits: {
|
||||
// Time in milliseconds a render request can take before it fails, some notes:
|
||||
// - 0 means no render limit
|
||||
// - it considers metatiling, naive implementation: (render timeout) * (number of tiles in metatile)
|
||||
render: 0,
|
||||
// As the render request will finish even if timed out, whether it should be placed in the internal
|
||||
// cache or it should be fully discarded. When placed in the internal cache another attempt to retrieve
|
||||
// the same tile will result in an immediate response, however that will use a lot of more application
|
||||
// memory. If we want to enforce this behaviour we have to implement a cache eviction policy for the
|
||||
// internal cache.
|
||||
cacheOnTimeout: true
|
||||
}
|
||||
},
|
||||
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
|
||||
'.*', // will enable any URL
|
||||
'http://{s}.example.com/{z}/{x}/{y}.png'
|
||||
],
|
||||
// image to use as placeholder when urlTemplate is not in the whitelist
|
||||
// if provided the http renderer will use it instead of throw an error
|
||||
fallbackImage: {
|
||||
type: 'fs', // 'fs' and 'url' supported
|
||||
src: __dirname + '/../../assets/default-placeholder.png'
|
||||
}
|
||||
}
|
||||
}
|
||||
,millstone: {
|
||||
// Needs to be writable by server user
|
||||
cache_basedir: '/tmp/cdb-tiler-dev/millstone-dev'
|
||||
}
|
||||
,redis: {
|
||||
host: '127.0.0.1',
|
||||
port: 6379,
|
||||
idleTimeoutMillis: 1,
|
||||
reapIntervalMillis: 1
|
||||
}
|
||||
,sqlapi: {
|
||||
protocol: 'http',
|
||||
host: 'localhost.lan',
|
||||
port: 8080,
|
||||
version: 'v1'
|
||||
// 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 2 pools involved in serving
|
||||
// windshaft-cartodb requests so multiply this number
|
||||
// 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
|
||||
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
|
||||
}
|
||||
}
|
||||
,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
|
||||
}
|
||||
// this [OPTIONAL] configuration enables invalidating by surrogate key in fastly
|
||||
,fastly: {
|
||||
// whether the invalidation is enabled or not
|
||||
enabled: false,
|
||||
// the fastly api key
|
||||
apiKey: 'wadus_api_key',
|
||||
// the service that will get surrogate key invalidation
|
||||
serviceId: 'wadus_service_id'
|
||||
}
|
||||
// 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
|
||||
}
|
||||
,disabled_file: 'pids/disabled'
|
||||
|
||||
// Use this as a feature flags enabling/disabling mechanism
|
||||
,enabledFeatures: {
|
||||
// whether it should intercept tile render errors an act based on them, enabled by default.
|
||||
onTileErrorStrategy: true,
|
||||
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API
|
||||
cdbQueryTablesFromPostgres: true
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,34 +2,220 @@ 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|/user/:user/api/v1/map/named|/tiles/template)'
|
||||
// Base url for the Detached Maps API
|
||||
// "maps" is the the new API,
|
||||
// "tiles/layergroup" is for compatibility with versions up to 1.6.x
|
||||
,base_url_detached: '(?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)'
|
||||
|
||||
// Maximum number of connections for one process
|
||||
// 128 is a good value with a limit of 1024 open file descriptors
|
||||
,maxConnections:128
|
||||
// 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] \033[90m:method\033[0m \033[36m:req[Host]:url\033[0m \033[90m:status :response-time ms -> :res[Content-Type]\033[0m'
|
||||
,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: "-20005048.4188,-20005048.4188,20005048.4188,20005048.4188",
|
||||
simplify: true
|
||||
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,
|
||||
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
|
||||
mapnik: {
|
||||
// The size of the pool of internal mapnik renderers
|
||||
// Check the configuration of uv_threadpool_size to use suitable value
|
||||
poolSize: 8,
|
||||
|
||||
// Metatile is the number of tiles-per-side that are going
|
||||
// to be rendered at once. If all of them will be requested
|
||||
// we'd have saved time. If only one will be used, we'd have
|
||||
// wasted time.
|
||||
metatile: 2,
|
||||
|
||||
// Override metatile behaviour depending on the format
|
||||
formatMetatile: {
|
||||
png: 2,
|
||||
'grid.json': 1
|
||||
},
|
||||
|
||||
// Buffer size is the tickness in pixel of a buffer
|
||||
// around the rendered (meta?)tile.
|
||||
//
|
||||
// This is important for labels and other marker that overlap tile boundaries.
|
||||
// Setting to 128 ensures no render artifacts.
|
||||
// 64 may have artifacts but is faster.
|
||||
// Less important if we can turn metatiling on.
|
||||
bufferSize: 64,
|
||||
|
||||
// SQL queries will be wrapped with ST_SnapToGrid
|
||||
// Snapping all points of the geometry to a regular grid
|
||||
snapToGrid: false,
|
||||
|
||||
// SQL queries will be wrapped with ST_ClipByBox2D
|
||||
// Returning the portion of a geometry falling within a rectangle
|
||||
// It will only work if snapToGrid is enabled
|
||||
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
|
||||
|
||||
limits: {
|
||||
// Time in milliseconds a render request can take before it fails, some notes:
|
||||
// - 0 means no render limit
|
||||
// - it considers metatiling, naive implementation: (render timeout) * (number of tiles in metatile)
|
||||
render: 0,
|
||||
// As the render request will finish even if timed out, whether it should be placed in the internal
|
||||
// cache or it should be fully discarded. When placed in the internal cache another attempt to retrieve
|
||||
// the same tile will result in an immediate response, however that will use a lot of more application
|
||||
// memory. If we want to enforce this behaviour we have to implement a cache eviction policy for the
|
||||
// internal cache.
|
||||
cacheOnTimeout: true
|
||||
}
|
||||
},
|
||||
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
|
||||
'.*', // will enable any URL
|
||||
'http://{s}.example.com/{z}/{x}/{y}.png'
|
||||
],
|
||||
// image to use as placeholder when urlTemplate is not in the whitelist
|
||||
// if provided the http renderer will use it instead of throw an error
|
||||
fallbackImage: {
|
||||
type: 'fs', // 'fs' and 'url' supported
|
||||
src: __dirname + '/../../assets/default-placeholder.png'
|
||||
}
|
||||
}
|
||||
}
|
||||
,millstone: {
|
||||
// Needs to be writable by server user
|
||||
cache_basedir: '/home/ubuntu/tile_assets/'
|
||||
}
|
||||
,redis: {
|
||||
host: '127.0.0.1',
|
||||
port: 6379
|
||||
}
|
||||
,sqlapi: {
|
||||
protocol: 'https',
|
||||
host: 'cartodb.com',
|
||||
port: 8080,
|
||||
version: 'v2'
|
||||
port: 6379,
|
||||
// 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 2 pools involved in serving
|
||||
// windshaft-cartodb requests so multiply this number
|
||||
// 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
|
||||
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
|
||||
}
|
||||
}
|
||||
,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
|
||||
}
|
||||
// this [OPTIONAL] configuration enables invalidating by surrogate key in fastly
|
||||
,fastly: {
|
||||
// whether the invalidation is enabled or not
|
||||
enabled: false,
|
||||
// the fastly api key
|
||||
apiKey: 'wadus_api_key',
|
||||
// the service that will get surrogate key invalidation
|
||||
serviceId: 'wadus_service_id'
|
||||
}
|
||||
// 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'
|
||||
}
|
||||
}
|
||||
// Settings for the health check available at /health
|
||||
,health: {
|
||||
enabled: true,
|
||||
username: 'localhost',
|
||||
z: 0,
|
||||
x: 0,
|
||||
y: 0
|
||||
}
|
||||
,disabled_file: 'pids/disabled'
|
||||
|
||||
// Use this as a feature flags enabling/disabling mechanism
|
||||
,enabledFeatures: {
|
||||
// whether it should intercept tile render errors an act based on them, enabled by default.
|
||||
onTileErrorStrategy: true,
|
||||
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API
|
||||
cdbQueryTablesFromPostgres: true
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,34 +2,220 @@ 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/map/named|/user/:user/api/v1/map/named|/tiles/template)'
|
||||
// Base url for the Detached Maps API
|
||||
// "/api/v1/maps" is the the new API,
|
||||
// "/tiles/layergroup" is for compatibility with versions up to 1.6.x
|
||||
,base_url_detached: '(?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)'
|
||||
|
||||
// Maximum number of connections for one process
|
||||
// 128 is a good value with a limit of 1024 open file descriptors
|
||||
,maxConnections:128
|
||||
// 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] \033[90m:method\033[0m \033[36m:req[Host]:url\033[0m \033[90m:status :response-time ms -> :res[Content-Type]\033[0m'
|
||||
,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: "-20005048.4188,-20005048.4188,20005048.4188,20005048.4188",
|
||||
simplify: true
|
||||
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,
|
||||
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
|
||||
mapnik: {
|
||||
// The size of the pool of internal mapnik renderers
|
||||
// Check the configuration of uv_threadpool_size to use suitable value
|
||||
poolSize: 8,
|
||||
|
||||
// Metatile is the number of tiles-per-side that are going
|
||||
// to be rendered at once. If all of them will be requested
|
||||
// we'd have saved time. If only one will be used, we'd have
|
||||
// wasted time.
|
||||
metatile: 2,
|
||||
|
||||
// Override metatile behaviour depending on the format
|
||||
formatMetatile: {
|
||||
png: 2,
|
||||
'grid.json': 1
|
||||
},
|
||||
|
||||
// Buffer size is the tickness in pixel of a buffer
|
||||
// around the rendered (meta?)tile.
|
||||
//
|
||||
// This is important for labels and other marker that overlap tile boundaries.
|
||||
// Setting to 128 ensures no render artifacts.
|
||||
// 64 may have artifacts but is faster.
|
||||
// Less important if we can turn metatiling on.
|
||||
bufferSize: 64,
|
||||
|
||||
// SQL queries will be wrapped with ST_SnapToGrid
|
||||
// Snapping all points of the geometry to a regular grid
|
||||
snapToGrid: false,
|
||||
|
||||
// SQL queries will be wrapped with ST_ClipByBox2D
|
||||
// Returning the portion of a geometry falling within a rectangle
|
||||
// It will only work if snapToGrid is enabled
|
||||
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
|
||||
|
||||
limits: {
|
||||
// Time in milliseconds a render request can take before it fails, some notes:
|
||||
// - 0 means no render limit
|
||||
// - it considers metatiling, naive implementation: (render timeout) * (number of tiles in metatile)
|
||||
render: 0,
|
||||
// As the render request will finish even if timed out, whether it should be placed in the internal
|
||||
// cache or it should be fully discarded. When placed in the internal cache another attempt to retrieve
|
||||
// the same tile will result in an immediate response, however that will use a lot of more application
|
||||
// memory. If we want to enforce this behaviour we have to implement a cache eviction policy for the
|
||||
// internal cache.
|
||||
cacheOnTimeout: true
|
||||
}
|
||||
},
|
||||
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
|
||||
'.*', // will enable any URL
|
||||
'http://{s}.example.com/{z}/{x}/{y}.png'
|
||||
],
|
||||
// image to use as placeholder when urlTemplate is not in the whitelist
|
||||
// if provided the http renderer will use it instead of throw an error
|
||||
fallbackImage: {
|
||||
type: 'fs', // 'fs' and 'url' supported
|
||||
src: __dirname + '/../../assets/default-placeholder.png'
|
||||
}
|
||||
}
|
||||
}
|
||||
,millstone: {
|
||||
// Needs to be writable by server user
|
||||
cache_basedir: '/home/ubuntu/tile_assets/'
|
||||
}
|
||||
,redis: {
|
||||
host: '127.0.0.1',
|
||||
port: 6379
|
||||
}
|
||||
,sqlapi: {
|
||||
protocol: 'https',
|
||||
host: 'cartodb.com',
|
||||
port: 8080,
|
||||
version: 'v2'
|
||||
port: 6379,
|
||||
// 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 2 pools involved in serving
|
||||
// windshaft-cartodb requests so multiply this number
|
||||
// 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
|
||||
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
|
||||
}
|
||||
}
|
||||
,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
|
||||
}
|
||||
// this [OPTIONAL] configuration enables invalidating by surrogate key in fastly
|
||||
,fastly: {
|
||||
// whether the invalidation is enabled or not
|
||||
enabled: false,
|
||||
// the fastly api key
|
||||
apiKey: 'wadus_api_key',
|
||||
// the service that will get surrogate key invalidation
|
||||
serviceId: 'wadus_service_id'
|
||||
}
|
||||
// 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'
|
||||
}
|
||||
}
|
||||
// Settings for the health check available at /health
|
||||
,health: {
|
||||
enabled: false,
|
||||
username: 'localhost',
|
||||
z: 0,
|
||||
x: 0,
|
||||
y: 0
|
||||
}
|
||||
,disabled_file: 'pids/disabled'
|
||||
|
||||
// Use this as a feature flags enabling/disabling mechanism
|
||||
,enabledFeatures: {
|
||||
// whether it should intercept tile render errors an act based on them, enabled by default.
|
||||
onTileErrorStrategy: true,
|
||||
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API
|
||||
cdbQueryTablesFromPostgres: true
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,37 +2,216 @@ 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|/user/:user/api/v1/map/named|/tiles/template)'
|
||||
// Base url for the Detached Maps API
|
||||
// "maps" is the the new API,
|
||||
// "tiles/layergroup" is for compatibility with versions up to 1.6.x
|
||||
,base_url_detached: '(?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)'
|
||||
|
||||
// Maximum number of connections for one process
|
||||
// 128 is a good value with a limit of 1024 open file descriptors
|
||||
,maxConnections:128
|
||||
// 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]'
|
||||
,postgres_auth_user: 'test_cartodb_user_<%= user_id %>'
|
||||
,log_format: '[:date] :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: '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: {
|
||||
user: "publicuser",
|
||||
// Parameters to pass to datasource plugin of mapnik
|
||||
// See http://github.com/mapnik/mapnik/wiki/PostGIS
|
||||
user: "test_windshaft_publicuser",
|
||||
password: "public",
|
||||
host: '127.0.0.1',
|
||||
port: 5432,
|
||||
srid: 4326,
|
||||
extent: "-20005048.4188,-20005048.4188,20005048.4188,20005048.4188",
|
||||
simplify: true
|
||||
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: ''
|
||||
,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,
|
||||
statsInterval: 5000, // milliseconds between each report to statsd about number of renderers and mapnik pool status
|
||||
mapnik: {
|
||||
// The size of the pool of internal mapnik renderers
|
||||
// Check the configuration of uv_threadpool_size to use suitable value
|
||||
poolSize: 8,
|
||||
|
||||
// Metatile is the number of tiles-per-side that are going
|
||||
// to be rendered at once. If all of them will be requested
|
||||
// we'd have saved time. If only one will be used, we'd have
|
||||
// wasted time.
|
||||
metatile: 2,
|
||||
|
||||
// Override metatile behaviour depending on the format
|
||||
formatMetatile: {
|
||||
png: 2,
|
||||
'grid.json': 1
|
||||
},
|
||||
|
||||
// Buffer size is the tickness in pixel of a buffer
|
||||
// around the rendered (meta?)tile.
|
||||
//
|
||||
// This is important for labels and other marker that overlap tile boundaries.
|
||||
// Setting to 128 ensures no render artifacts.
|
||||
// 64 may have artifacts but is faster.
|
||||
// Less important if we can turn metatiling on.
|
||||
bufferSize: 64,
|
||||
|
||||
// SQL queries will be wrapped with ST_SnapToGrid
|
||||
// Snapping all points of the geometry to a regular grid
|
||||
snapToGrid: false,
|
||||
|
||||
// SQL queries will be wrapped with ST_ClipByBox2D
|
||||
// Returning the portion of a geometry falling within a rectangle
|
||||
// It will only work if snapToGrid is enabled
|
||||
clipByBox2d: false, // this requires postgis >=2.2 and geos >=3.5
|
||||
|
||||
limits: {
|
||||
// Time in milliseconds a render request can take before it fails, some notes:
|
||||
// - 0 means no render limit
|
||||
// - it considers metatiling, naive implementation: (render timeout) * (number of tiles in metatile)
|
||||
render: 0,
|
||||
// As the render request will finish even if timed out, whether it should be placed in the internal
|
||||
// cache or it should be fully discarded. When placed in the internal cache another attempt to retrieve
|
||||
// the same tile will result in an immediate response, however that will use a lot of more application
|
||||
// memory. If we want to enforce this behaviour we have to implement a cache eviction policy for the
|
||||
// internal cache.
|
||||
cacheOnTimeout: true
|
||||
}
|
||||
},
|
||||
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
|
||||
'.*', // will enable any URL
|
||||
'http://{s}.example.com/{z}/{x}/{y}.png',
|
||||
// for testing purposes
|
||||
'http://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png'
|
||||
],
|
||||
// image to use as placeholder when urlTemplate is not in the whitelist
|
||||
// if provided the http renderer will use it instead of throw an error
|
||||
fallbackImage: {
|
||||
type: 'fs', // 'fs' and 'url' supported
|
||||
src: __dirname + '/../../assets/default-placeholder.png'
|
||||
}
|
||||
}
|
||||
}
|
||||
,millstone: {
|
||||
// Needs to be writable by server user
|
||||
cache_basedir: '/tmp/cdb-tiler-test/millstone'
|
||||
}
|
||||
,redis: {
|
||||
host: '127.0.0.1',
|
||||
port: 6333,
|
||||
idleTimeoutMillis: 1,
|
||||
reapIntervalMillis: 1
|
||||
}
|
||||
,sqlapi: {
|
||||
protocol: 'http',
|
||||
host: 'localhost.lan',
|
||||
port: 8080,
|
||||
version: 'v1'
|
||||
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 2 pools involved in serving
|
||||
// windshaft-cartodb requests so multiply this number
|
||||
// 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
|
||||
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
|
||||
}
|
||||
}
|
||||
,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
|
||||
}
|
||||
// this [OPTIONAL] configuration enables invalidating by surrogate key in fastly
|
||||
,fastly: {
|
||||
// whether the invalidation is enabled or not
|
||||
enabled: false,
|
||||
// the fastly api key
|
||||
apiKey: 'wadus_api_key',
|
||||
// the service that will get surrogate key invalidation
|
||||
serviceId: 'wadus_service_id'
|
||||
}
|
||||
// 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
|
||||
}
|
||||
,disabled_file: 'pids/disabled'
|
||||
|
||||
// Use this as a feature flags enabling/disabling mechanism
|
||||
,enabledFeatures: {
|
||||
// whether it should intercept tile render errors an act based on them, enabled by default.
|
||||
onTileErrorStrategy: true,
|
||||
// whether the affected tables for a given SQL must query directly postgresql or use the SQL API
|
||||
cdbQueryTablesFromPostgres: true
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
module.exports.oneDay = 86400000;
|
||||
66
configure
vendored
@@ -17,16 +17,26 @@
|
||||
# --strk(2012-07-23)
|
||||
#
|
||||
|
||||
ENVDIR=config/environments
|
||||
|
||||
PGPORT=
|
||||
SQLAPI_PORT=
|
||||
MAPNIK_VERSION=
|
||||
ENVIRONMENT=development
|
||||
|
||||
STATUS="$0 $*"
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 [OPTION]"
|
||||
echo
|
||||
echo "Configuration:"
|
||||
echo " --help display this help and exit"
|
||||
echo " --with-pgport=NUM access PostgreSQL server on TCP port NUM"
|
||||
echo " --help display this help and exit"
|
||||
echo " --with-pgport=NUM access PostgreSQL server on TCP port NUM [$PGPORT]"
|
||||
echo " --with-sqlapi-port=NUM access SQL-API server on TCP port NUM [$SQLAPI_PORT]"
|
||||
echo " --with-mapnik-version=STRING set mapnik version string [$MAPNIK_VERSION]"
|
||||
echo " --environment=STRING set output environment name [$ENVIRONMENT]"
|
||||
}
|
||||
|
||||
PGPORT=5432
|
||||
|
||||
while test -n "$1"; do
|
||||
case "$1" in
|
||||
--help|-h)
|
||||
@@ -36,20 +46,46 @@ while test -n "$1"; do
|
||||
--with-pgport=*)
|
||||
PGPORT=`echo "$1" | cut -d= -f2`
|
||||
;;
|
||||
--with-sqlapi-port=*)
|
||||
SQLAPI_PORT=`echo "$1" | cut -d= -f2`
|
||||
;;
|
||||
--with-mapnik-version=*)
|
||||
MAPNIK_VERSION=`echo "$1" | cut -d= -f2`
|
||||
;;
|
||||
--environment=*)
|
||||
ENVIRONMENT=`echo "$1" | cut -d= -f2`
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option '$1'" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
echo "Unused option '$1'" >&2
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
echo "PGPORT: $PGPORT"
|
||||
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"
|
||||
|
||||
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;}" < "${ENVEX}" \
|
||||
| sed "s/mapnik_version:.*/mapnik_version: '$MAPNIK_VERSION'/" \
|
||||
| sed -n "1h;1!H;\${;g;s/\(,sqlapi: {[^}]*port: *'\?\)[^',]*\('\?,\)/\1$SQLAPI_PORT\2/;p;}" \
|
||||
> "$o"
|
||||
|
||||
STATUSFILE=config.status--${ENVIRONMENT}
|
||||
echo "Writing ${STATUSFILE}"
|
||||
echo ${STATUS} > ${STATUSFILE} && chmod +x ${STATUSFILE}
|
||||
|
||||
# TODO: allow specifying configuration settings !
|
||||
for f in config/environments/*.example; do
|
||||
o=`dirname "$f"`/`basename "$f" .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" > "$o"
|
||||
done
|
||||
|
||||
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 }
|
||||
|
||||
977
docs/Map-API.md
Normal file
@@ -0,0 +1,977 @@
|
||||
## 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**
|
||||
You can create maps using your CartoDB public data. Any client can change the read-only SQL and CartoCSS parameters that generate the map tiles. These maps can be created from a JavaScript application alone and no authenticated calls are needed. See [this CartoDB.js example]({{ '/cartodb-platform/cartodb-js.html' | prepend: site.baseurl }}).
|
||||
|
||||
- **Named maps**
|
||||
There are also maps that have access to your private data. These maps require an owner to setup and modify any SQL and CartoCSS parameters and are not modifiable without new setup calls.
|
||||
|
||||
## Quickstart
|
||||
|
||||
### Anonymous maps
|
||||
|
||||
Here is an example of how to create an anonymous map with JavaScript:
|
||||
|
||||
```javascript
|
||||
var mapconfig = {
|
||||
"version": "1.3.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: 'https://documentation.cartodb.com/api/v1/map',
|
||||
data: JSON.stringify(mapconfig),
|
||||
success: function(data) {
|
||||
var templateUrl = 'https://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": "mapnik",
|
||||
"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 'https://{account}.cartodb.com/api/v1/map/named/:template_id' -H 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
The response will return JSON with properties for the `layergroupid`, the timestamp (`last_updated`) of the last data modification and some key/value pairs with `metadata` for the `layers`.
|
||||
Note: all `layers` in `metadata` will always have a `type` string and a `meta` dictionary with the key/value pairs.
|
||||
|
||||
Here is an example response:
|
||||
|
||||
```javascript
|
||||
{
|
||||
"layergroupid": "c01a54877c62831bb51720263f91fb33:0",
|
||||
"last_updated": "1970-01-01T00:00:00.000Z",
|
||||
"metadata": {
|
||||
"layers": [
|
||||
{
|
||||
"type": "mapnik",
|
||||
"meta": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can use the `layergroupid` to instantiate a URL template for accessing tiles on the client. Here we use the `layergroupid` from the example response above in this URL template:
|
||||
|
||||
```bash
|
||||
https://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. Using HTTPS is mandatory 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.3.0",
|
||||
"layers": [{
|
||||
"type": "mapnik",
|
||||
"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.44.1/doc/MapConfig-1.3.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
|
||||
https://{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**
|
||||
Includes information about the layers.
|
||||
-
|
||||
|
||||
- **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 'https://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",
|
||||
"metadata": {
|
||||
"layers": [
|
||||
{
|
||||
"type": "mapnik",
|
||||
"meta": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
"cdn_url": {
|
||||
"http": "http://cdb.com",
|
||||
"https": "https://cdb.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### Retrieve resources from the layergroup
|
||||
|
||||
###### Mapnik tiles can be accessed using:
|
||||
|
||||
These tiles will get just the mapnik layers. To get individual layers see next section.
|
||||
|
||||
```bash
|
||||
https://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/{z}/{x}/{y}.png
|
||||
```
|
||||
|
||||
###### Individual layers
|
||||
|
||||
The MapConfig specification holds the layers definition in a 0-based index. Layers can be requested individually in different formats depending on the layer type.
|
||||
|
||||
Individual layers can be accessed using that 0-based index. For UTF grid tiles:
|
||||
|
||||
```bash
|
||||
https://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/:layer/{z}/{x}/{y}.grid.json
|
||||
```
|
||||
|
||||
In this case, `:layer` as 0 returns the UTF grid tiles/attributes for layer 0, the only layer in the example MapConfig.
|
||||
|
||||
If the MapConfig had a Torque layer at index 1 it could be possible to request it with:
|
||||
|
||||
```bash
|
||||
https://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/1/{z}/{x}/{y}.torque.json
|
||||
```
|
||||
|
||||
###### Attributes defined in `attributes` section:
|
||||
|
||||
```bash
|
||||
https://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 }
|
||||
```
|
||||
|
||||
###### Blending and layer selection
|
||||
|
||||
```bash
|
||||
https://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/:layer_filter/{z}/{x}/{y}.png
|
||||
```
|
||||
|
||||
Note: currently format is limited to `png`.
|
||||
|
||||
`:layer_filter` can be used to select some layers to be rendered together. `:layer_filter` supports two formats:
|
||||
|
||||
- `all` alias
|
||||
|
||||
Using `all` as `:layer_filter` will blend all layers in the layergroup
|
||||
|
||||
```bash
|
||||
https://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/all/{z}/{x}/{y}.png
|
||||
```
|
||||
|
||||
- Filter by layer index
|
||||
|
||||
A list of comma separated layer indexes can be used to just render a subset of layers. For example `0,3,4` will filter and blend layers with indexes 0, 3, and 4.
|
||||
|
||||
```bash
|
||||
https://documentation.cartodb.com/api/v1/map/c01a54877c62831bb51720263f91fb33:0/0,3,4/{z}/{x}/{y}.png
|
||||
```
|
||||
|
||||
Some notes about filtering:
|
||||
|
||||
- Invalid index values or out of bounds indexes will end in `Invalid layer filtering` errors.
|
||||
- Once a mapnik layer is selected, all mapnik layers will get blended. As this may change in the future **it is
|
||||
recommended** to always select all mapnik layers if you want to select at least one so you will get a consistent
|
||||
behavior in the future.
|
||||
- Ordering is not considered. So right now filtering layers 0,3,4 is the very same thing as filtering 3,4,0. As this
|
||||
may change in the future **it is recommended** to always select the layers in ascending order so you will get a
|
||||
consistent behavior in the future.
|
||||
|
||||
### 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 and attempts to interact with it in a way that requires new tiles (e.g. zoom). The client will need to go through the steps of creating the map again to fix the problem.
|
||||
|
||||
|
||||
## 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: 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).
|
||||
|
||||
### 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 %>"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"view": {
|
||||
"zoom": 4,
|
||||
"center": {
|
||||
"lng": 0,
|
||||
"lat": 0
|
||||
},
|
||||
"bounds": {
|
||||
"west": -45,
|
||||
"south": -45,
|
||||
"east": 45,
|
||||
"north": 45
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### Arguments
|
||||
|
||||
- **name**: There can be at most _one_ template with the same name for any user. Valid names start with a letter or a number, and only contain letters, numbers, dashes (-) or underscores (_).
|
||||
- **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. Variables 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/0.44.1/doc/MapConfig-1.3.0.md) for more info.
|
||||
- **view** (optional): extra keys to specify the compelling area for the map. It can be used to have a static preview of a named map without having to instantiate it. It is possible to specify it with `center` + `zoom` or with a bounding box `bbox`. Center+zoom takes precedence over bounding box.
|
||||
- **zoom** The zoom level to use
|
||||
- **center**
|
||||
- **lng** The longitude to use for the center
|
||||
- **lat** The latitude to use for the center
|
||||
- **bounds**
|
||||
- **west**: LowerCorner longitude for the bounding box, in decimal degrees (aka most western)
|
||||
- **south**: LowerCorner latitude for the bounding box, in decimal degrees (aka most southern)
|
||||
- **east**: UpperCorner longitude for the bounding box, in decimal degrees (aka most eastern)
|
||||
- **north**: UpperCorner latitude for the bounding box, in decimal degrees (aka most northern)
|
||||
|
||||
#### 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 new 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
|
||||
{
|
||||
"errors" : ["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
|
||||
{
|
||||
"errors" : ["error string here"]
|
||||
}
|
||||
```
|
||||
|
||||
### Delete
|
||||
|
||||
Delete the specified template map from the server and it 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
|
||||
{
|
||||
"errors" : ["Some error string here"]
|
||||
}
|
||||
```
|
||||
|
||||
On success, a 204 (No Content) response will be issued. Otherwise a 4xx response 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
|
||||
{
|
||||
"errors" : ["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
|
||||
{
|
||||
"errors" : ["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);
|
||||
|
||||
```
|
||||
|
||||
[CartoDB.js](http://docs.cartodb.com/cartodb-platform/cartodb-js.html) has methods for accessing your named maps.
|
||||
|
||||
1. [layer.setParams()](http://docs.cartodb.com/cartodb-platform/cartodb-js.html#layersetparamskey-value) allows you to change the template variables (in the placeholders object) via JavaScript
|
||||
2. [layer.setAuthToken()](http://docs.cartodb.com/cartodb-platform/cartodb-js.html#layersetauthtokenauthtoken) allows you to set the auth tokens to create the layer
|
||||
|
||||
## Static Maps API
|
||||
|
||||
The Static Maps API can be initiated using both named and anonymous maps using the 'layergroupid' token. The API can be used to create static images of parts of maps and thumbnails for use in web design, graphic design, print, field work, and many other applications that require standard image formats.
|
||||
|
||||
### Maps API endpoints
|
||||
|
||||
Begin by instantiating either a named or anonymous map using the `layergroupid token` as demonstrated in the Maps API documentation above. The `layergroupid` token calls to the map and allows for parameters in the definition to generate static images.
|
||||
|
||||
#### Zoom + center
|
||||
|
||||
##### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```bash
|
||||
GET /api/v1/map/static/center/:token/:z/:lat/:lng/:width/:height.:format
|
||||
```
|
||||
|
||||
##### Params
|
||||
|
||||
* **:token**: the layergroupid token from the map instantiation
|
||||
* **:z**: the zoom level of the map
|
||||
* **:lat**: the latitude for the center of the map
|
||||
* **:lng**: the longitude for the center of the map
|
||||
* **:width**: the width in pixels for the output image
|
||||
* **:height**: the height in pixels for the output image
|
||||
* **:format**: the format for the image, supported types: `png`, `jpg`
|
||||
* **jpg** will have a default quality of 85.
|
||||
|
||||
#### Bounding Box
|
||||
|
||||
##### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```bash
|
||||
GET /api/v1/map/static/bbox/:token/:bbox/:width/:height.:format`
|
||||
```
|
||||
|
||||
##### Params
|
||||
|
||||
* **:token**: the layergroupid token from the map instantiation
|
||||
* **:bbox**: the bounding box in WGS 84 (EPSG:4326), comma separated values for:
|
||||
- LowerCorner longitude, in decimal degrees (aka most western)
|
||||
- LowerCorner latitude, in decimal degrees (aka most southern)
|
||||
- UpperCorner longitude, in decimal degrees (aka most eastern)
|
||||
- UpperCorner latitude, in decimal degrees (aka most northern)
|
||||
* **:width**: the width in pixels for the output image
|
||||
* **:height**: the height in pixels for the output image
|
||||
* **:format**: the format for the image, supported types: `png`, `jpg`
|
||||
* **jpg** will have a default quality of 85.
|
||||
|
||||
Note: you can see this endpoint as:
|
||||
|
||||
```bash
|
||||
GET /api/v1/map/static/bbox/:token/:west,:south,:east,:north/:width/:height.:format`
|
||||
```
|
||||
|
||||
#### Named map
|
||||
|
||||
##### Definition
|
||||
|
||||
<div class="code-title notitle code-request"></div>
|
||||
```bash
|
||||
GET /api/v1/map/static/named/:name/:width/:height.:format
|
||||
```
|
||||
|
||||
##### Params
|
||||
|
||||
* **:name**: the name of the named map
|
||||
* **:width**: the width in pixels for the output image
|
||||
* **:height**: the height in pixels for the output image
|
||||
* **:format**: the format for the image, supported types: `png`, `jpg`
|
||||
* **jpg** will have a default quality of 85.
|
||||
|
||||
A named maps static image will get its constraints from the [view in the template](#Arguments), if `view` is not present it will estimate the extent based on the involved tables otherwise it fallback to `"zoom": 1`, `"lng": 0` and `"lat": 0`.
|
||||
|
||||
####Layers
|
||||
|
||||
The Static Maps API allows for multiple layers of incorporation into the `MapConfig` to allow for maximum versatility in creating a static map. The examples below were used to generate the static image example in the next section, and appear in the specific order designated.
|
||||
|
||||
**Basemaps**
|
||||
|
||||
```javascript
|
||||
{
|
||||
"type": "http",
|
||||
"options": {
|
||||
"urlTemplate": "http://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png",
|
||||
"subdomains": [
|
||||
"a",
|
||||
"b",
|
||||
"c"
|
||||
]
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
By manipulating the `"urlTemplate"` custom basemaps can be used in generating static images. Supported map types for the Static Maps API are:
|
||||
|
||||
'http://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png',
|
||||
'http://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png',
|
||||
'http://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png',
|
||||
'http://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png',
|
||||
|
||||
**Mapnik**
|
||||
|
||||
```javascript
|
||||
{
|
||||
"type": "mapnik",
|
||||
"options": {
|
||||
"sql": "select null::geometry the_geom_webmercator",
|
||||
"cartocss": "#layer {\n\tpolygon-fill: #FF3300;\n\tpolygon-opacity: 0;\n\tline-color: #333;\n\tline-width: 0;\n\tline-opacity: 0;\n}",
|
||||
"cartocss_version": "2.2.0"
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
**CartoDB**
|
||||
|
||||
As described in the [Mapconfig documentation](https://github.com/CartoDB/Windshaft/blob/0.44.1/doc/MapConfig-1.3.0.md), a "cartodb" type layer is now just an alias to a "mapnik" type layer as above, intended for backwards compatibility.
|
||||
|
||||
```javascript
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"sql": "select * from park",
|
||||
"cartocss": "/** simple visualization */\n\n#park{\n polygon-fill: #229A00;\n polygon-opacity: 0.7;\n line-color: #FFF;\n line-width: 0;\n line-opacity: 1;\n}",
|
||||
"cartocss_version": "2.1.1"
|
||||
}
|
||||
},
|
||||
```
|
||||
|
||||
Additionally, static images from Torque maps and other map layers can be used together to generate highly customizable and versatile static maps.
|
||||
|
||||
|
||||
#### Caching
|
||||
|
||||
It is important to note that generated images are cached from the live data referenced with the `layergroupid token` on the specified CartoDB account. This means that if the data changes, the cached image will also change. When linking dynamically, it is important to take into consideration the state of the data and longevity of the static image to avoid broken images or changes in how the image is displayed. To obtain a static snapshot of the map as it is today and preserve the image long-term regardless of changes in data, the image must be saved and stored locally.
|
||||
|
||||
#### Limits
|
||||
|
||||
* While images can encompass an entirety of a map, the default limit for pixel range is 8192 x 8192.
|
||||
* Image resolution by default is set to 72 DPI
|
||||
* JPEG quality by default is 85%
|
||||
* Timeout limits for generating static maps are the same across the CartoDB Editor and Platform. It is important to ensure timely processing of queries.
|
||||
|
||||
|
||||
### Examples
|
||||
|
||||
After instantiating a map from a CartoDB account:
|
||||
|
||||
<div class="code-title code-request with-result">REQUEST</div>
|
||||
```bash
|
||||
GET /api/v1/map/static/center/4b615ff367e498e770e7d05e99181873:1420231989550.8699/14/40.71502926732618/-73.96039009094238/600/400.png
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
<p class="wrap-border"><img src="https://raw.githubusercontent.com/namessanti/Pictures/master/static_api.png" alt="static-api"/></p>
|
||||
|
||||
#### MapConfig
|
||||
|
||||
For this map, the multiple layers, order, and stylings are defined by the MapConfig.
|
||||
|
||||
```javascript
|
||||
{
|
||||
"version": "1.3.0",
|
||||
"layers": [
|
||||
{
|
||||
"type": "http",
|
||||
"options": {
|
||||
"urlTemplate": "http://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png",
|
||||
"subdomains": [
|
||||
"a",
|
||||
"b",
|
||||
"c"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "mapnik",
|
||||
"options": {
|
||||
"sql": "select null::geometry the_geom_webmercator",
|
||||
"cartocss": "#layer {\n\tpolygon-fill: #FF3300;\n\tpolygon-opacity: 0;\n\tline-color: #333;\n\tline-width: 0;\n\tline-opacity: 0;\n}",
|
||||
"cartocss_version": "2.2.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"sql": "select * from park",
|
||||
"cartocss": "/** simple visualization */\n\n#park{\n polygon-fill: #229A00;\n polygon-opacity: 0.7;\n line-color: #FFF;\n line-width: 0;\n line-opacity: 1;\n}",
|
||||
"cartocss_version": "2.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"sql": "select * from residential_zoning_2009",
|
||||
"cartocss": "/** simple visualization */\n\n#residential_zoning_2009{\n polygon-fill: #c7eae5;\n polygon-opacity: 1;\n line-color: #FFF;\n line-width: 0.2;\n line-opacity: 0.5;\n}",
|
||||
"cartocss_version": "2.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"sql": "select * from nycha_developments_july2011",
|
||||
"cartocss": "/** simple visualization */\n\n#nycha_developments_july2011{\n polygon-fill: #ef3b2c;\n polygon-opacity: 0.7;\n line-color: #FFF;\n line-width: 0;\n line-opacity: 1;\n}",
|
||||
"cartocss_version": "2.1.1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
56
docs/MapConfig-NamedMaps-extension.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# 1. Purpose
|
||||
|
||||
This specification describes an extension for
|
||||
[MapConfig 1.3.0](https://github.com/CartoDB/Windshaft/blob/master/doc/MapConfig-1.3.0.md) version.
|
||||
|
||||
|
||||
# 2. Changes over specification
|
||||
|
||||
This extension introduces a new layer type so it's possible to use a named map by its name as a layer.
|
||||
|
||||
## 2.1 Named layers definition
|
||||
|
||||
```javascript
|
||||
{
|
||||
// REQUIRED
|
||||
// string, `named` is the only supported value
|
||||
type: "named",
|
||||
|
||||
// REQUIRED
|
||||
// object, set `named` map layers configuration
|
||||
options: {
|
||||
|
||||
// REQUIRED
|
||||
// string, the name for the named map to use
|
||||
name: "world_borders",
|
||||
|
||||
// OPTIONAL
|
||||
// object, the replacement values for the named map's template placeholders
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/blob/master/docs/Map-API.md#instantiate-1 for more details
|
||||
config: {
|
||||
"color": "#000"
|
||||
},
|
||||
|
||||
// OPTIONAL
|
||||
// string array, the authorized tokens in case the named map has auth method set to `token`
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/blob/master/docs/Map-API.md#named-maps-1 for more details
|
||||
auth_tokens: [
|
||||
"token1",
|
||||
"token2"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 2.2 Limitations
|
||||
|
||||
1. A Named Map will not allow to have `named` type layers inside their templates layergroup's layers definition.
|
||||
2. A `named` layer does not allow Named Maps form other accounts, it's only possible to use Named Maps from the very
|
||||
same user account.
|
||||
|
||||
|
||||
# History
|
||||
|
||||
## 1.0.0
|
||||
|
||||
- Initial version
|
||||
28
docs/MultiLayer-API.md
Normal file
@@ -0,0 +1,28 @@
|
||||
The Windshaft-CartoDB MultiLayer API extends the [Windshaft MultiLayer API](https://github.com/CartoDB/Windshaft/blob/master/doc/Multilayer-API.md) 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.
|
||||
104
docs/Routes.md
Normal file
@@ -0,0 +1,104 @@
|
||||
This document list all routes available in Windshaft-cartodb Maps API server.
|
||||
|
||||
## Routes list
|
||||
|
||||
1. `GET (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)/:token/:z/:x/:y@:scale_factor?x.:format {:user(f),:token(f),:z(f),:x(f),:y(f),:scale_factor(t),:format(f)} (1)`
|
||||
<br/>Notes: Mapnik retina tiles [0]
|
||||
|
||||
1. `GET (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)/:token/:z/:x/:y.:format {:user(f),:token(f),:z(f),:x(f),:y(f),:format(f)} (1)`
|
||||
<br/>Notes: Mapnik tiles [0]
|
||||
|
||||
1. `GET (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)/:token/:layer/:z/:x/:y.(:format) {:user(f),:token(f),:layer(f),:z(f),:x(f),:y(f),:format(f)} (1)`
|
||||
<br/>Notes: Per :layer rendering based on :format [0]
|
||||
|
||||
1. `GET (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup) {:user(f)} (1)`
|
||||
<br/>Notes: Map instantiation [0]
|
||||
|
||||
1. `GET (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)/:token/:layer/attributes/:fid {:user(f),:token(f),:layer(f),:fid(f)} (1)`
|
||||
<br/>Notes: Endpoint for info windows data, alternative for sql api when tables are private [0]
|
||||
|
||||
1. `GET (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)/static/center/:token/:z/:lat/:lng/:width/:height.:format {:user(f),:token(f),:z(f),:lat(f),:lng(f),:width(f),:height(f),:format(f)} (1)`
|
||||
<br/>Notes: Static Maps API [0]
|
||||
|
||||
1. `GET (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)/static/bbox/:token/:west,:south,:east,:north/:width/:height.:format {:user(f),:token(f),:west(f),:south(f),:east(f),:north(f),:width(f),:height(f),:format(f)} (1)`
|
||||
<br/>Notes: Static Maps API [0]
|
||||
|
||||
1. `GET / {} (1)`
|
||||
<br/>Notes: Welcome message
|
||||
|
||||
1. `GET /version {} (1)`
|
||||
<br/>Notes: Return relevant module versions: mapnik, grainstore, etc
|
||||
|
||||
1. `GET (?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)/:template_id/jsonp {:user(f),:template_id(f)} (1)`
|
||||
<br/>Notes: Named maps JSONP instantiation [1]
|
||||
|
||||
1. `GET (?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)/:template_id {:user(f),:template_id(f)} (1)`
|
||||
<br/>Notes: Named map retrieval (w/ API KEY) [1]
|
||||
|
||||
1. `GET (?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template) {:user(f)} (1)`
|
||||
<br/>Notes: List named maps (w/ API KEY) [1]
|
||||
|
||||
1. `GET (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup)/static/named/:template_id/:width/:height.:format {:user(f),:template_id(f),:width(f),:height(f),:format(f)} (1)`
|
||||
<br/>Notes: Static map for named maps
|
||||
|
||||
1. `GET /health {} (1)`
|
||||
<br/>Notes: Healt check
|
||||
|
||||
1. `OPTIONS (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup) {:user(f)} (1)`
|
||||
<br/>Notes: CORS [0]
|
||||
|
||||
1. `OPTIONS (?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)/:template_id {:user(f),:template_id(f)} (1)`
|
||||
<br/>Notes: CORS [1]
|
||||
|
||||
1. `POST (?:/api/v1/map|/user/:user/api/v1/map|/tiles/layergroup) {:user(f)} (1)`
|
||||
<br/>Notes: Map instantiation [0]
|
||||
|
||||
1. `POST (?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template) {:user(f)} (1)`
|
||||
<br/>Notes: Create named map (w/ API KEY) [1]
|
||||
|
||||
1. `POST (?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)/:template_id {:user(f),:template_id(f)} (1)`
|
||||
<br/>Notes: Instantiate named map [1]
|
||||
|
||||
1. `PUT (?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)/:template_id {:user(f),:template_id(f)} (1)`
|
||||
<br/>Notes: Update a named map (w/ API KEY) [1]
|
||||
|
||||
1. `DELETE (?:/api/v1/map/named|/user/:user/api/v1/map/named|/tiles/template)/:template_id {:user(f),:template_id(f)} (1)`
|
||||
<br/>Notes: Delete named map (w/ API KEY) [1]
|
||||
|
||||
## Optional deprecated routes
|
||||
|
||||
- [0] `/tiles/layergroup` is deprecated and `/api/v1/map` should be used but we keep it for now.
|
||||
- [1] `/tiles/template` is deprecated and `/api/v1/map/named` should be used but we keep it for now.
|
||||
|
||||
## How to generate the list of routes
|
||||
|
||||
Something like the following patch should do the trick
|
||||
|
||||
```javascript
|
||||
diff --git a/lib/cartodb/cartodb_windshaft.js b/lib/cartodb/cartodb_windshaft.js
|
||||
index b9429a2..e6cc5f9 100644
|
||||
--- a/lib/cartodb/cartodb_windshaft.js
|
||||
+++ b/lib/cartodb/cartodb_windshaft.js
|
||||
@@ -212,6 +212,20 @@ var CartodbWindshaft = function(serverOptions) {
|
||||
}
|
||||
});
|
||||
|
||||
+ var format = require('util').format;
|
||||
+ var routesNotes = Object.keys(ws.routes.routes)
|
||||
+ .map(function(method) { return ws.routes.routes[method]; })
|
||||
+ .reduce(function(previous, current) { current.map(function(r) { previous.push(r) }); return previous;}, [])
|
||||
+ .map(function(route) {
|
||||
+ return format("\n1. `%s %s {%s} (%d)`\n<br/>Notes: [DEPRECATED]? ",
|
||||
+ route.method.toUpperCase(),
|
||||
+ route.path,
|
||||
+ route.keys.map(function(k) { return format(':%s(%s)', k.name, k.optional ? 't' : 'f'); } ).join(','),
|
||||
+ route.callbacks.length
|
||||
+ );
|
||||
+ });
|
||||
+ console.log(routesNotes.join('\n'));
|
||||
+
|
||||
return ws;
|
||||
};
|
||||
|
||||
|
||||
```
|
||||
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"
|
||||
}
|
||||
```
|
||||
42
docs/metrics.md
Normal file
@@ -0,0 +1,42 @@
|
||||
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*
|
||||
- **authorizedByCert**: time to authorize a template instantiation
|
||||
- **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
|
||||
|
||||
75
lib/cartodb/api/query_tables_api.js
Normal file
@@ -0,0 +1,75 @@
|
||||
function QueryTablesApi(pgQueryRunner) {
|
||||
this.pgQueryRunner = pgQueryRunner;
|
||||
}
|
||||
|
||||
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.getAffectedTablesInQuery = function (username, sql, callback) {
|
||||
|
||||
var query = 'SELECT CDB_QueryTables($windshaft$' + prepareSql(sql) + '$windshaft$)';
|
||||
|
||||
this.pgQueryRunner.run(username, 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, 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(' ');
|
||||
|
||||
this.pgQueryRunner.run(username, 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 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')
|
||||
;
|
||||
}
|
||||
54
lib/cartodb/api/tables_extent_api.js
Normal file
@@ -0,0 +1,54 @@
|
||||
function TablesExtentApi(pgQueryRunner) {
|
||||
this.pgQueryRunner = pgQueryRunner;
|
||||
}
|
||||
|
||||
module.exports = TablesExtentApi;
|
||||
|
||||
/**
|
||||
* Given a username and a list of tables it will return the estimated extent in SRID 4326 for all the tables based on
|
||||
* the_geom_webmercator (SRID 3857) column.
|
||||
*
|
||||
* @param {String} username
|
||||
* @param {Array} tableNames The named can be schema qualified, so this accepts both `schema_name.table_name` and
|
||||
* `table_name` format as valid input
|
||||
* @param {Function} callback function(err, result) {Object} result with `west`, `south`, `east`, `north`
|
||||
*/
|
||||
TablesExtentApi.prototype.getBounds = function (username, tableNames, callback) {
|
||||
var estimatedExtentSQLs = tableNames.map(function(tableName) {
|
||||
var schemaTable = tableName.split('.');
|
||||
if (schemaTable.length > 1) {
|
||||
return "ST_EstimatedExtent('" + schemaTable[0] + "', '" + schemaTable[1] + "', 'the_geom_webmercator')";
|
||||
}
|
||||
return "ST_EstimatedExtent('" + schemaTable[0] + "', 'the_geom_webmercator')";
|
||||
});
|
||||
|
||||
var query = [
|
||||
"WITH ext as (" +
|
||||
"SELECT ST_Transform(ST_SetSRID(ST_Extent(ST_Union(ARRAY[",
|
||||
estimatedExtentSQLs.join(','),
|
||||
"])), 3857), 4326) geom)",
|
||||
"SELECT",
|
||||
"ST_XMin(geom) west,",
|
||||
"ST_YMin(geom) south,",
|
||||
"ST_XMax(geom) east,",
|
||||
"ST_YMax(geom) north",
|
||||
"FROM ext"
|
||||
].join(' ');
|
||||
|
||||
this.pgQueryRunner.run(username, query, handleBoundsResult, callback);
|
||||
};
|
||||
|
||||
function handleBoundsResult(err, rows, callback) {
|
||||
if (err){
|
||||
var msg = err.message ? err.message : err;
|
||||
callback(new Error('could not fetch source tables: ' + msg));
|
||||
return;
|
||||
}
|
||||
var result = null;
|
||||
if (rows.length > 0) {
|
||||
result = {
|
||||
bounds: rows[0]
|
||||
};
|
||||
}
|
||||
callback(null, result);
|
||||
}
|
||||
96
lib/cartodb/backends/pg_connection.js
Normal file
@@ -0,0 +1,96 @@
|
||||
var step = require('step');
|
||||
var _ = require('underscore');
|
||||
|
||||
function PgConnection(metadataBackend) {
|
||||
this.metadataBackend = metadataBackend;
|
||||
}
|
||||
|
||||
module.exports = PgConnection;
|
||||
|
||||
|
||||
// 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)
|
||||
//
|
||||
PgConnection.prototype.setDBAuth = function(username, params, callback) {
|
||||
var self = this;
|
||||
|
||||
var user_params = {};
|
||||
var auth_user = global.environment.postgres_auth_user;
|
||||
var auth_pass = global.environment.postgres_auth_pass;
|
||||
step(
|
||||
function getId() {
|
||||
self.metadataBackend.getUserId(username, this);
|
||||
},
|
||||
function(err, user_id) {
|
||||
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;
|
||||
|
||||
self.metadataBackend.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)
|
||||
//
|
||||
PgConnection.prototype.setDBConn = function(dbowner, params, callback) {
|
||||
var self = this;
|
||||
// Add default database connection parameters
|
||||
// if none given
|
||||
_.defaults(params, {
|
||||
dbuser: global.environment.postgres.user,
|
||||
dbpassword: global.environment.postgres.password,
|
||||
dbhost: global.environment.postgres.host,
|
||||
dbport: global.environment.postgres.port
|
||||
});
|
||||
step(
|
||||
function getConnectionParams() {
|
||||
self.metadataBackend.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);
|
||||
}
|
||||
);
|
||||
};
|
||||
44
lib/cartodb/backends/pg_query_runner.js
Normal file
@@ -0,0 +1,44 @@
|
||||
var PSQL = require('cartodb-psql');
|
||||
var step = require('step');
|
||||
|
||||
function PgQueryRunner(pgConnection) {
|
||||
this.pgConnection = pgConnection;
|
||||
}
|
||||
|
||||
module.exports = PgQueryRunner;
|
||||
|
||||
|
||||
PgQueryRunner.prototype.run = function(username, query, queryHandler, callback) {
|
||||
var self = this;
|
||||
|
||||
var params = {};
|
||||
|
||||
step(
|
||||
function setAuth() {
|
||||
self.pgConnection.setDBAuth(username, params, this);
|
||||
},
|
||||
function setConn(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
self.pgConnection.setDBConn(username, params, this);
|
||||
},
|
||||
function executeQuery(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
var psql = new PSQL({
|
||||
user: params.dbuser,
|
||||
pass: params.dbpass,
|
||||
host: params.dbhost,
|
||||
port: params.dbport,
|
||||
dbname: params.dbname
|
||||
});
|
||||
psql.query(query, function(err, resultSet) {
|
||||
resultSet = resultSet || {};
|
||||
var rows = resultSet.rows || [];
|
||||
queryHandler(err, rows, callback);
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
16
lib/cartodb/cache/backend/fastly.js
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
var FastlyPurge = require('fastly-purge');
|
||||
|
||||
function FastlyCacheBackend(apiKey, serviceId, softPurge) {
|
||||
this.serviceId = serviceId;
|
||||
this.fastlyPurge = new FastlyPurge(apiKey, { softPurge: softPurge || true });
|
||||
}
|
||||
|
||||
module.exports = FastlyCacheBackend;
|
||||
|
||||
/**
|
||||
* @param cacheObject should respond to `key() -> String` method
|
||||
* @param {Function} callback
|
||||
*/
|
||||
FastlyCacheBackend.prototype.invalidate = function(cacheObject, callback) {
|
||||
this.fastlyPurge.key(this.serviceId, cacheObject.key(), callback);
|
||||
};
|
||||
30
lib/cartodb/cache/backend/varnish_http.js
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
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);
|
||||
}
|
||||
);
|
||||
};
|
||||
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);
|
||||
}
|
||||
41
lib/cartodb/cache/surrogate_keys_cache.js
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
var queue = require('queue-async');
|
||||
|
||||
/**
|
||||
* @param {Array|Object} cacheBackends each backend backend should respond to `invalidate(cacheObject, callback)` method
|
||||
* @constructor
|
||||
*/
|
||||
function SurrogateKeysCache(cacheBackends) {
|
||||
this.cacheBackends = Array.isArray(cacheBackends) ? cacheBackends : [cacheBackends];
|
||||
}
|
||||
|
||||
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) {
|
||||
var invalidationQueue = queue(this.cacheBackends.length);
|
||||
|
||||
this.cacheBackends.forEach(function(cacheBackend) {
|
||||
invalidationQueue.defer(function(cacheBackend, done) {
|
||||
cacheBackend.invalidate(cacheObject, done);
|
||||
}, cacheBackend);
|
||||
});
|
||||
|
||||
invalidationQueue.awaitAll(function(err, result) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
callback(null, result);
|
||||
});
|
||||
};
|
||||
@@ -1,89 +0,0 @@
|
||||
var _ = require('underscore'),
|
||||
Varnish = require('node-varnish'),
|
||||
request = require('request'),
|
||||
crypto = require('crypto'),
|
||||
channelCache = {},
|
||||
varnish_queue = null;
|
||||
|
||||
function init(host, port) {
|
||||
varnish_queue = new Varnish.VarnishQueue(host, port);
|
||||
}
|
||||
|
||||
function invalidate_db(dbname, table) {
|
||||
try{
|
||||
varnish_queue.run_cmd('purge obj.http.X-Cache-Channel ~ "^' + dbname + ':(.*'+ table +'.*)|(table)$"');
|
||||
console.log('[SUCCESS FLUSHING CACHE]');
|
||||
} catch (e) {
|
||||
console.log("[ERROR FLUSHING CACHE] Is enable_cache set to true? Failed for: " + 'purge obj.http.X-Cache-Channel ~ "^' + dbname + ':(.*'+ table +'.*)|(table)$"');
|
||||
}
|
||||
}
|
||||
|
||||
function generateCacheChannel(req, callback){
|
||||
var cacheChannel = "";
|
||||
|
||||
// use key to call sql api with sql request if present, else just return dbname and table name
|
||||
// base key
|
||||
var tableNames = req.params.table;
|
||||
var dbName = req.params.dbname;
|
||||
var username = req.headers.host.split('.')[0];
|
||||
|
||||
// replace tableNames with the results of the explain if present
|
||||
if (_.isString(req.params.sql) && req.params.sql != ''){
|
||||
// initialise MD5 key of sql for cache lookups
|
||||
var sql_md5 = generateMD5(req.params.sql);
|
||||
var api = global.environment.sqlapi;
|
||||
var qs = {};
|
||||
|
||||
// use cache if present
|
||||
if (!_.isNull(channelCache[sql_md5]) && !_.isUndefined(channelCache[sql_md5])) {
|
||||
callback(channelCache[sql_md5]);
|
||||
} else{
|
||||
// strip out windshaft/mapnik inserted sql if present
|
||||
var sql = req.params.sql.match(/^\((.*)\)\sas\scdbq$/);
|
||||
sql = (sql != null) ? sql[1] : req.params.sql;
|
||||
|
||||
// build up api string
|
||||
var sqlapi = api.protocol + '://' + username + '.' + api.host + ':' + api.port + '/api/' + api.version + '/sql'
|
||||
|
||||
// add query to querystring
|
||||
qs.q = 'SELECT CDB_QueryTables($windshaft$' + sql + '$windshaft$)';
|
||||
|
||||
// add api_key if present in tile request (means table is private)
|
||||
if (_.isString(req.params.map_key) && req.params.map_key != ''){
|
||||
qs.api_key = req.params.map_key;
|
||||
}
|
||||
|
||||
// call sql api
|
||||
request.get({url:sqlapi, qs:qs, json:true}, function(err, response, body){
|
||||
if (!err && response.statusCode == 200) {
|
||||
tableNames = body.rows[0].cdb_querytables.split(/^\{(.*)\}$/)[1];
|
||||
} else {
|
||||
//oops, no SQL API. Just cache using fallback 'table' key
|
||||
tableNames = 'table';
|
||||
}
|
||||
cacheChannel = buildCacheChannel(dbName,tableNames);
|
||||
channelCache[sql_md5] = cacheChannel; // store for caching
|
||||
callback(cacheChannel);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
cacheChannel = buildCacheChannel(dbName,tableNames);
|
||||
callback(cacheChannel);
|
||||
}
|
||||
}
|
||||
|
||||
function buildCacheChannel(dbName, tableNames){
|
||||
return dbName + ':' + tableNames;
|
||||
}
|
||||
|
||||
function generateMD5(data){
|
||||
var hash = crypto.createHash('md5');
|
||||
hash.update(data);
|
||||
return hash.digest('hex');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
init: init,
|
||||
invalidate_db: invalidate_db,
|
||||
generateCacheChannel: generateCacheChannel
|
||||
}
|
||||
@@ -1,243 +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 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 %>"
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 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 new Error(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){
|
||||
if (err) throw err;
|
||||
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){
|
||||
if (err) throw err;
|
||||
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){
|
||||
if (err) throw err;
|
||||
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){
|
||||
if (err) throw err;
|
||||
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);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
redisClient = data;
|
||||
redisArgs.push(this);
|
||||
redisClient[redisFunc.toUpperCase()].apply(redisClient, redisArgs);
|
||||
},
|
||||
function releaseRedisClient(err, data) {
|
||||
if (err) throw err;
|
||||
redis_pool.release(db, redisClient);
|
||||
callback(err, data);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return me;
|
||||
}();
|
||||
@@ -1,90 +1,218 @@
|
||||
var _ = require('underscore');
|
||||
var step = require('step');
|
||||
var Windshaft = require('windshaft');
|
||||
var os = require('os');
|
||||
var HealthCheck = require('./monitoring/health_check');
|
||||
|
||||
var _ = require('underscore')
|
||||
, Step = require('step')
|
||||
, Windshaft = require('windshaft')
|
||||
, Cache = require('./cache_validator');
|
||||
var SurrogateKeysCache = require('./cache/surrogate_keys_cache');
|
||||
var NamedMapsCacheEntry = require('./cache/model/named_maps_entry');
|
||||
var VarnishHttpCacheBackend = require('./cache/backend/varnish_http');
|
||||
var FastlyCacheBackend = require('./cache/backend/fastly');
|
||||
|
||||
if ( ! process.env.PGAPPNAME )
|
||||
process.env.PGAPPNAME='cartodb_tiler';
|
||||
|
||||
var CartodbWindshaft = function(serverOptions) {
|
||||
|
||||
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);
|
||||
serverOptions.afterStateChange = function(req, data, callback) {
|
||||
Cache.invalidate_db(req.params.dbname, req.params.table);
|
||||
callback(null, data);
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
serverOptions.beforeStateChange = function(req, callback) {
|
||||
var err = null;
|
||||
if ( ! req.hasOwnProperty('dbuser') ) {
|
||||
err = new Error("map state cannot be changed by unauthenticated request!");
|
||||
}
|
||||
callback(err, req);
|
||||
var redisPool = serverOptions.redis.pool ||
|
||||
require('redis-mpool')(_.extend(global.environment.redis, {name: 'windshaft:cartodb'}));
|
||||
|
||||
var cartoData = require('cartodb-redis')({pool: redisPool});
|
||||
|
||||
var templateMaps = serverOptions.templateMaps;
|
||||
|
||||
// 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 surrogateKeysCacheBackends = [];
|
||||
|
||||
if (serverOptions.varnish_purge_enabled) {
|
||||
surrogateKeysCacheBackends.push(
|
||||
new VarnishHttpCacheBackend(serverOptions.varnish_host, serverOptions.varnish_http_port)
|
||||
);
|
||||
}
|
||||
|
||||
if (!!serverOptions.fastly.enabled && !!serverOptions.fastly.apiKey && !!serverOptions.fastly.serviceId) {
|
||||
surrogateKeysCacheBackends.push(
|
||||
new FastlyCacheBackend(serverOptions.fastly.apiKey, serverOptions.fastly.serviceId)
|
||||
);
|
||||
}
|
||||
|
||||
var surrogateKeysCache = new SurrogateKeysCache(surrogateKeysCacheBackends);
|
||||
|
||||
function invalidateNamedMap (owner, templateName) {
|
||||
var startTime = Date.now();
|
||||
surrogateKeysCache.invalidate(new NamedMapsCacheEntry(owner, templateName), function(err) {
|
||||
var logMessage = JSON.stringify({
|
||||
username: owner,
|
||||
type: 'named_map_invalidation',
|
||||
elapsed: Date.now() - startTime,
|
||||
error: !!err ? JSON.stringify(err.message) : undefined
|
||||
});
|
||||
if (err) {
|
||||
console.warn(logMessage);
|
||||
} else {
|
||||
console.info(logMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
['update', 'delete'].forEach(function(eventType) {
|
||||
templateMaps.on(eventType, invalidateNamedMap);
|
||||
});
|
||||
|
||||
// boot
|
||||
var ws = new Windshaft.Server(serverOptions);
|
||||
|
||||
/**
|
||||
* Helper to allow access to the layer to be used in the maps infowindow popup.
|
||||
*/
|
||||
ws.get(serverOptions.base_url + '/infowindow', function(req, res){
|
||||
ws.doCORS(res);
|
||||
Step(
|
||||
function(){
|
||||
serverOptions.getInfowindow(req, this);
|
||||
},
|
||||
function(err, data){
|
||||
if (err){
|
||||
res.send({error: err.message}, 500);
|
||||
} else {
|
||||
res.send({infowindow: data}, 200);
|
||||
}
|
||||
}
|
||||
// Override getVersion to include cartodb-specific versions
|
||||
var wsversion = ws.getVersion;
|
||||
ws.getVersion = function() {
|
||||
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 = [
|
||||
'/',
|
||||
'/version',
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/issues/176
|
||||
serverOptions.base_url_mapconfig,
|
||||
serverOptions.base_url_mapconfig + '/static/named/:template_id/:width/:height.:format',
|
||||
template_baseurl,
|
||||
template_baseurl + '/:template_id',
|
||||
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 NamedMapsController = require('./controllers/named_maps'),
|
||||
namedMapsController = new NamedMapsController(
|
||||
ws,
|
||||
serverOptions,
|
||||
templateMaps,
|
||||
cartoData,
|
||||
template_baseurl,
|
||||
surrogateKeysCache
|
||||
);
|
||||
namedMapsController.register(ws);
|
||||
|
||||
var TablesExtentApi = require('./api/tables_extent_api');
|
||||
var tablesExtentApi = new TablesExtentApi(serverOptions.pgQueryRunner);
|
||||
|
||||
var NamedStaticMapsController = require('./controllers/named_static_maps');
|
||||
var namedStaticMapsController = new NamedStaticMapsController(
|
||||
ws,
|
||||
serverOptions,
|
||||
templateMaps,
|
||||
ws.staticMapBackend,
|
||||
surrogateKeysCache,
|
||||
tablesExtentApi
|
||||
);
|
||||
namedStaticMapsController.register(ws);
|
||||
|
||||
/*******************************************************************************************************************
|
||||
* END Routing
|
||||
******************************************************************************************************************/
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Helper to allow access to metadata to be used in embedded maps.
|
||||
*/
|
||||
ws.get(serverOptions.base_url + '/map_metadata', function(req, res){
|
||||
ws.doCORS(res);
|
||||
Step(
|
||||
function(){
|
||||
serverOptions.getMapMetadata(req, this);
|
||||
},
|
||||
function(err, data){
|
||||
if (err){
|
||||
res.send(err.message, 500);
|
||||
} else {
|
||||
res.send({map_metadata: data}, 200);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper API to allow per table tile cache (and sql cache) to be invalidated remotely.
|
||||
* TODO: Move?
|
||||
*/
|
||||
ws.del(serverOptions.base_url + '/flush_cache', function(req, res){
|
||||
ws.doCORS(res);
|
||||
Step(
|
||||
function(){
|
||||
serverOptions.flushCache(req, serverOptions.cache_enabled ? Cache : null, this);
|
||||
},
|
||||
function(err, data){
|
||||
if (err){
|
||||
res.send(500);
|
||||
} else {
|
||||
res.send({status: 'ok'}, 200);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
return ws;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = CartodbWindshaft;
|
||||
|
||||
372
lib/cartodb/controllers/named_maps.js
Normal file
@@ -0,0 +1,372 @@
|
||||
var step = require('step');
|
||||
var assert = require('assert');
|
||||
var _ = require('underscore');
|
||||
var templateName = require('../template_maps').templateName;
|
||||
var CdbRequest = require('../models/cdb_request');
|
||||
var NamedMapsCacheEntry = require('../cache/model/named_maps_entry');
|
||||
|
||||
function NamedMapsController(app, serverOptions, templateMaps, metadataBackend, templateBaseUrl, surrogateKeysCache) {
|
||||
this.app = app;
|
||||
this.serverOptions = serverOptions;
|
||||
this.templateMaps = templateMaps;
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.templateBaseUrl = templateBaseUrl;
|
||||
this.surrogateKeysCache = surrogateKeysCache;
|
||||
}
|
||||
|
||||
module.exports = NamedMapsController;
|
||||
|
||||
var cdbRequest = new CdbRequest();
|
||||
|
||||
NamedMapsController.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
|
||||
NamedMapsController.prototype.create = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
this.app.doCORS(res);
|
||||
|
||||
var cdbuser = cdbRequest.userByReq(req);
|
||||
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.serverOptions.authorizedByAPIKey(req, this);
|
||||
},
|
||||
function addTemplate(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated users can get template maps');
|
||||
ifInvalidContentType(req, 'template POST data must be of type application/json');
|
||||
var cfg = req.body;
|
||||
self.templateMaps.addTemplate(cdbuser, cfg, this);
|
||||
},
|
||||
function prepareResponse(err, tpl_id){
|
||||
assert.ifError(err);
|
||||
return { template_id: tpl_id };
|
||||
},
|
||||
finishFn(self.app, res, 'POST TEMPLATE')
|
||||
);
|
||||
};
|
||||
|
||||
// Update a template
|
||||
NamedMapsController.prototype.update = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
this.app.doCORS(res);
|
||||
|
||||
var cdbuser = cdbRequest.userByReq(req);
|
||||
var template;
|
||||
var tpl_id;
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.serverOptions.authorizedByAPIKey(req, this);
|
||||
},
|
||||
function updateTemplate(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated user can update templated maps');
|
||||
ifInvalidContentType(req, 'template PUT data must be of type application/json');
|
||||
|
||||
template = req.body;
|
||||
tpl_id = templateName(req.params.template_id);
|
||||
self.templateMaps.updTemplate(cdbuser, tpl_id, template, this);
|
||||
},
|
||||
function prepareResponse(err){
|
||||
assert.ifError(err);
|
||||
|
||||
return { template_id: tpl_id };
|
||||
},
|
||||
finishFn(self.app, res, 'PUT TEMPLATE')
|
||||
);
|
||||
};
|
||||
|
||||
// Get a specific template
|
||||
NamedMapsController.prototype.retrieve = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
if (req.profiler) {
|
||||
req.profiler.start('windshaft-cartodb.get_template');
|
||||
}
|
||||
|
||||
this.app.doCORS(res);
|
||||
|
||||
var cdbuser = cdbRequest.userByReq(req);
|
||||
var tpl_id;
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.serverOptions.authorizedByAPIKey(req, this);
|
||||
},
|
||||
function updateTemplate(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated users can get template maps');
|
||||
|
||||
tpl_id = templateName(req.params.template_id);
|
||||
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 };
|
||||
},
|
||||
finishFn(self.app, res, 'GET TEMPLATE')
|
||||
);
|
||||
};
|
||||
|
||||
// Delete a specific template
|
||||
NamedMapsController.prototype.destroy = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
if (req.profiler) {
|
||||
req.profiler.start('windshaft-cartodb.delete_template');
|
||||
}
|
||||
this.app.doCORS(res);
|
||||
|
||||
var cdbuser = cdbRequest.userByReq(req);
|
||||
var tpl_id;
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.serverOptions.authorizedByAPIKey(req, this);
|
||||
},
|
||||
function updateTemplate(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated users can delete template maps');
|
||||
|
||||
tpl_id = templateName(req.params.template_id);
|
||||
self.templateMaps.delTemplate(cdbuser, tpl_id, this);
|
||||
},
|
||||
function prepareResponse(err/*, tpl_val*/){
|
||||
if ( err ) throw err;
|
||||
return { status: 'ok' };
|
||||
},
|
||||
finishFn(self.app, res, 'DELETE TEMPLATE', ['', 204])
|
||||
);
|
||||
};
|
||||
|
||||
// Get a list of owned templates
|
||||
NamedMapsController.prototype.list = function(req, res) {
|
||||
var self = this;
|
||||
if ( req.profiler ) {
|
||||
req.profiler.start('windshaft-cartodb.get_template_list');
|
||||
}
|
||||
this.app.doCORS(res);
|
||||
|
||||
var cdbuser = cdbRequest.userByReq(req);
|
||||
|
||||
step(
|
||||
function checkPerms(){
|
||||
self.serverOptions.authorizedByAPIKey(req, this);
|
||||
},
|
||||
function listTemplates(err, authenticated) {
|
||||
assert.ifError(err);
|
||||
ifUnauthenticated(authenticated, 'Only authenticated user can list templated maps');
|
||||
|
||||
self.templateMaps.listTemplates(cdbuser, this);
|
||||
},
|
||||
function prepareResponse(err, tpl_ids){
|
||||
assert.ifError(err);
|
||||
return { template_ids: tpl_ids };
|
||||
},
|
||||
finishFn(self.app, res, 'GET TEMPLATE LIST')
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.instantiate = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
if (req.profiler) {
|
||||
req.profiler.start('windshaft-cartodb.instance_template_post');
|
||||
}
|
||||
step(
|
||||
function() {
|
||||
ifInvalidContentType(req, 'template POST data must be of type application/json');
|
||||
|
||||
self.instantiateTemplate(req, res, req.body, this);
|
||||
}, function(err, response) {
|
||||
self.finish_instantiation(err, response, res);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.options = function(req, res, next) {
|
||||
this.app.doCORS(res, "Content-Type");
|
||||
return next();
|
||||
};
|
||||
|
||||
/**
|
||||
* jsonp endpoint, allows to instantiate a template with a json call.
|
||||
* callback query argument is mandatory
|
||||
*/
|
||||
NamedMapsController.prototype.jsonp = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
if (req.profiler) {
|
||||
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);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
// Instantiate a template
|
||||
NamedMapsController.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 = cdbRequest.userByReq(req);
|
||||
// Format of template_id: [<template_owner>]@<template_id>
|
||||
var tpl_id = templateName(req.params.template_id);
|
||||
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: {
|
||||
user: req.params.user
|
||||
},
|
||||
headers: _.clone(req.headers),
|
||||
context: _.clone(req.context),
|
||||
method: req.method,
|
||||
res: res,
|
||||
profiler: req.profiler
|
||||
};
|
||||
self.serverOptions.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 ) {
|
||||
return callback(err, { errors: [''+err] });
|
||||
}
|
||||
var tplhash = self.templateMaps.fingerPrint(template).substring(0,8);
|
||||
layergroup.layergroupid = cdbuser + '@' + tplhash + '@' + layergroup.layergroupid;
|
||||
res.header('X-Layergroup-Id', layergroup.layergroupid);
|
||||
|
||||
self.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(cdbuser, template.name));
|
||||
|
||||
callback(null, layergroup);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
NamedMapsController.prototype.finish_instantiation = function(err, response, res) {
|
||||
if (err) {
|
||||
var statusCode = 400;
|
||||
response = { errors: [''+err] };
|
||||
if ( ! _.isUndefined(err.http_status) ) {
|
||||
statusCode = err.http_status;
|
||||
}
|
||||
this.app.sendError(res, response, statusCode, 'POST INSTANCE TEMPLATE', err);
|
||||
} else {
|
||||
this.app.sendResponse(res, [response, 200]);
|
||||
}
|
||||
};
|
||||
|
||||
function finishFn(app, res, description, okResponse) {
|
||||
return function finish(err, response){
|
||||
var statusCode = 200;
|
||||
if (err) {
|
||||
statusCode = 400;
|
||||
response = { errors: ['' + err] };
|
||||
if ( ! _.isUndefined(err.http_status) ) {
|
||||
statusCode = err.http_status;
|
||||
}
|
||||
app.sendError(res, response, statusCode, description, err);
|
||||
} else {
|
||||
app.sendResponse(res, okResponse || [response, statusCode]);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function ifUnauthenticated(authenticated, description) {
|
||||
if (authenticated !== 1) {
|
||||
var err = new Error(description);
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function ifInvalidContentType(req, description) {
|
||||
if ( ! req.headers['content-type'] || req.headers['content-type'].split(';')[0] != 'application/json' ) {
|
||||
throw new Error(description);
|
||||
}
|
||||
}
|
||||
248
lib/cartodb/controllers/named_static_maps.js
Normal file
@@ -0,0 +1,248 @@
|
||||
var step = require('step');
|
||||
var assert = require('assert');
|
||||
var templateName = require('../template_maps').templateName;
|
||||
var CdbRequest = require('../models/cdb_request');
|
||||
var NamedMapsCacheEntry = require('../cache/model/named_maps_entry');
|
||||
var _ = require('underscore');
|
||||
|
||||
function NamedStaticMapsController(app, serverOptions, templateMaps, staticMapBackend, surrogateKeysCache,
|
||||
tablesExtentApi) {
|
||||
this.app = app;
|
||||
this.serverOptions = serverOptions;
|
||||
this.templateMaps = templateMaps;
|
||||
this.staticMapBackend = staticMapBackend;
|
||||
this.surrogateKeysCache = surrogateKeysCache;
|
||||
this.tablesExtentApi = tablesExtentApi;
|
||||
}
|
||||
|
||||
module.exports = NamedStaticMapsController;
|
||||
|
||||
var cdbRequest = new CdbRequest();
|
||||
|
||||
NamedStaticMapsController.prototype.register = function(app) {
|
||||
app.get(app.base_url_mapconfig + '/static/named/:template_id/:width/:height.:format', this.named.bind(this));
|
||||
};
|
||||
|
||||
NamedStaticMapsController.prototype.named = function(req, res) {
|
||||
var self = this;
|
||||
|
||||
this.app.doCORS(res);
|
||||
var cdbUser = cdbRequest.userByReq(req);
|
||||
|
||||
var format = req.params.format === 'jpg' ? 'jpeg' : 'png';
|
||||
|
||||
var template;
|
||||
var layergroupConfig;
|
||||
var layergroupId;
|
||||
var fakeReq;
|
||||
var cacheChannel;
|
||||
|
||||
step(
|
||||
function reqParams() {
|
||||
self.app.req2params(req, this);
|
||||
},
|
||||
function getTemplate(err) {
|
||||
assert.ifError(err);
|
||||
self.templateMaps.getTemplate(cdbUser, templateName(req.params.template_id), this);
|
||||
},
|
||||
function checkExists(err, tpl) {
|
||||
assert.ifError(err);
|
||||
if (!tpl) {
|
||||
var notFoundErr = new Error(
|
||||
"Template '" + templateName(req.params.template_id) + "' of user '" + cdbUser + "' not found"
|
||||
);
|
||||
notFoundErr.http_status = 404;
|
||||
throw notFoundErr;
|
||||
}
|
||||
return tpl;
|
||||
},
|
||||
function checkAuthorized(err, tpl) {
|
||||
assert.ifError(err);
|
||||
|
||||
var authorized = false;
|
||||
try {
|
||||
authorized = self.templateMaps.isAuthorized(tpl, req.query.auth_token);
|
||||
} catch (err) {
|
||||
// we catch to add http_status
|
||||
var authorizationFailedErr = new Error('Failed to authorize template');
|
||||
authorizationFailedErr.http_status = 403;
|
||||
throw authorizationFailedErr;
|
||||
}
|
||||
if ( ! authorized ) {
|
||||
var unauthorizedErr = new Error('Unauthorized template instantiation');
|
||||
unauthorizedErr.http_status = 403;
|
||||
throw unauthorizedErr;
|
||||
}
|
||||
|
||||
return tpl;
|
||||
},
|
||||
function prepareParams(err, tpl) {
|
||||
assert.ifError(err);
|
||||
|
||||
template = tpl;
|
||||
|
||||
var templateParams = {};
|
||||
if (req.query.config) {
|
||||
try {
|
||||
templateParams = JSON.parse(req.query.config);
|
||||
} catch (e) {
|
||||
throw new Error('malformed config parameter, should be a valid JSON');
|
||||
}
|
||||
}
|
||||
|
||||
return templateParams;
|
||||
},
|
||||
function instantiateTemplate(err, templateParams) {
|
||||
assert.ifError(err);
|
||||
return self.templateMaps.instance(template, templateParams);
|
||||
},
|
||||
function prepareLayergroup(err, layergroup) {
|
||||
assert.ifError(err);
|
||||
layergroupConfig = layergroup;
|
||||
fakeReq = {
|
||||
query: {},
|
||||
params: {
|
||||
user: req.params.user
|
||||
},
|
||||
headers: _.clone(req.headers),
|
||||
context: _.clone(req.context),
|
||||
method: req.method,
|
||||
res: res,
|
||||
profiler: req.profiler
|
||||
};
|
||||
self.serverOptions.setDBParams(cdbUser, fakeReq.params, this);
|
||||
},
|
||||
function setApiKey(err){
|
||||
assert.ifError(err);
|
||||
self.app.createLayergroup(layergroupConfig, fakeReq, this);
|
||||
},
|
||||
function prepareResponse(err, layergroup) {
|
||||
assert.ifError(err);
|
||||
|
||||
// added by createLayergroup
|
||||
cacheChannel = res.header('X-Cache-Channel');
|
||||
res.removeHeader('X-Cache-Channel');
|
||||
self.surrogateKeysCache.tag(res, new NamedMapsCacheEntry(cdbUser, template.name));
|
||||
|
||||
layergroupId = layergroup.layergroupid.split(":")[0];
|
||||
|
||||
return null;
|
||||
},
|
||||
function staticImageOptions(err) {
|
||||
assert.ifError(err);
|
||||
getStaticImageOptions(template, this);
|
||||
},
|
||||
function estimateBounds(err, imageOpts) {
|
||||
assert.ifError(err);
|
||||
if (imageOpts) {
|
||||
return imageOpts;
|
||||
}
|
||||
|
||||
var defaultZoomCenter = {
|
||||
zoom: 1,
|
||||
center: {
|
||||
lng: 0,
|
||||
lat: 0
|
||||
}
|
||||
};
|
||||
|
||||
var dbTables = cacheChannel.split(':');
|
||||
if (dbTables.length <= 1 || dbTables[1].length === 0) {
|
||||
return defaultZoomCenter;
|
||||
}
|
||||
|
||||
var tableNames = dbTables[1].split(',');
|
||||
if (tableNames.length === 0) {
|
||||
return defaultZoomCenter;
|
||||
}
|
||||
|
||||
var next = this;
|
||||
self.tablesExtentApi.getBounds(cdbUser, tableNames, function(err, result) {
|
||||
next(null, result || defaultZoomCenter);
|
||||
});
|
||||
},
|
||||
function getImage(err, imageOpts) {
|
||||
assert.ifError(err);
|
||||
|
||||
var staticImageReq = {
|
||||
headers: _.clone(fakeReq.headers),
|
||||
params: _.extend(_.clone(fakeReq.params), {
|
||||
token: layergroupId,
|
||||
format: req.params.format
|
||||
})
|
||||
};
|
||||
|
||||
var width = +req.params.width;
|
||||
var height = +req.params.height;
|
||||
|
||||
if (!_.isUndefined(imageOpts.zoom) && imageOpts.center) {
|
||||
self.staticMapBackend.getImage(staticImageReq, width, height, imageOpts.zoom, imageOpts.center, this);
|
||||
} else {
|
||||
self.staticMapBackend.getImage(staticImageReq, width, height, imageOpts.bounds, this);
|
||||
}
|
||||
},
|
||||
function handleImage(err, image, headers, stats) {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('render-' + format);
|
||||
req.profiler.add(stats || {});
|
||||
}
|
||||
|
||||
if (err) {
|
||||
if (!err.error) {
|
||||
err.error = err.message;
|
||||
}
|
||||
self.app.sendError(res, err, self.app.findStatusCode(err), 'STATIC_VIZ_MAP', err);
|
||||
} else {
|
||||
res.setHeader('Content-Type', headers['Content-Type'] || 'image/' + format);
|
||||
res.setHeader('Cache-Control', 'public,max-age=7200,must-revalidate');
|
||||
self.app.sendResponse(res, [image, 200]);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
function getStaticImageOptions(template, callback) {
|
||||
if (template.view) {
|
||||
var zoomCenter = templateZoomCenter(template.view);
|
||||
if (zoomCenter) {
|
||||
return callback(null, zoomCenter);
|
||||
}
|
||||
|
||||
var bounds = templateBounds(template.view);
|
||||
if (bounds) {
|
||||
return callback(null, bounds);
|
||||
}
|
||||
}
|
||||
return callback(null, null);
|
||||
}
|
||||
|
||||
function templateZoomCenter(view) {
|
||||
if (!_.isUndefined(view.zoom) && view.center) {
|
||||
return {
|
||||
zoom: view.zoom,
|
||||
center: view.center
|
||||
};
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function templateBounds(view) {
|
||||
if (view.bounds) {
|
||||
var hasAllBounds = _.every(['west', 'south', 'east', 'north'], function(prop) {
|
||||
return !!view.bounds[prop];
|
||||
});
|
||||
if (hasAllBounds) {
|
||||
return {
|
||||
bounds: {
|
||||
west: view.bounds.west,
|
||||
south: view.bounds.south,
|
||||
east: view.bounds.east,
|
||||
north: view.bounds.north
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
26
lib/cartodb/models/cdb_request.js
Normal file
@@ -0,0 +1,26 @@
|
||||
function CdbRequest() {
|
||||
this.RE_USER_FROM_HOST = new RegExp(global.environment.user_from_host ||
|
||||
'^([^\\.]+)\\.' // would extract "strk" from "strk.cartodb.com"
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = CdbRequest;
|
||||
|
||||
|
||||
CdbRequest.prototype.userByReq = function(req) {
|
||||
var host = req.headers.host;
|
||||
if (req.params.user) {
|
||||
return req.params.user;
|
||||
}
|
||||
var mat = host.match(this.RE_USER_FROM_HOST);
|
||||
if ( ! mat ) {
|
||||
console.error("Pattern '" + this.RE_USER_FROM_HOST + "' does not match hostname '" + host + "'");
|
||||
return;
|
||||
}
|
||||
// console.log("Matches: "); console.dir(mat);
|
||||
if ( mat.length !== 2 ) {
|
||||
console.error("Pattern '" + this.RE_USER_FROM_HOST + "' gave unexpected matches against '" + host + "': ", mat);
|
||||
return;
|
||||
}
|
||||
return mat[1];
|
||||
};
|
||||
120
lib/cartodb/models/mapconfig_named_layers_adapter.js
Normal file
@@ -0,0 +1,120 @@
|
||||
var queue = require('queue-async');
|
||||
var _ = require('underscore');
|
||||
var Datasource = require('windshaft').Datasource;
|
||||
|
||||
function MapConfigNamedLayersAdapter(templateMaps) {
|
||||
this.templateMaps = templateMaps;
|
||||
}
|
||||
|
||||
module.exports = MapConfigNamedLayersAdapter;
|
||||
|
||||
MapConfigNamedLayersAdapter.prototype.getLayers = function(username, layers, dbMetadata, callback) {
|
||||
var self = this;
|
||||
|
||||
var adaptLayersQueue = queue(layers.length);
|
||||
|
||||
function adaptLayer(layer, done) {
|
||||
if (isNamedTypeLayer(layer)) {
|
||||
|
||||
if (!layer.options.name) {
|
||||
return done(new Error('Missing Named Map `name` in layer options'));
|
||||
}
|
||||
|
||||
var templateName = layer.options.name;
|
||||
var templateConfigParams = layer.options.config || {};
|
||||
var templateAuthTokens = layer.options.auth_tokens;
|
||||
|
||||
self.templateMaps.getTemplate(username, templateName, function(err, template) {
|
||||
if (err || !template) {
|
||||
return done(new Error("Template '" + templateName + "' of user '" + username + "' not found"));
|
||||
}
|
||||
|
||||
if (self.templateMaps.isAuthorized(template, templateAuthTokens)) {
|
||||
var nestedNamedLayers = template.layergroup.layers.filter(function(layer) {
|
||||
return layer.type === 'named';
|
||||
});
|
||||
|
||||
if (nestedNamedLayers.length > 0) {
|
||||
var nestedNamedMapsError = new Error('Nested named layers are not allowed');
|
||||
// nestedNamedMapsError.http_status = 400;
|
||||
return done(nestedNamedMapsError);
|
||||
}
|
||||
|
||||
try {
|
||||
var templateLayergroupConfig = self.templateMaps.instance(template, templateConfigParams);
|
||||
return done(null, {
|
||||
datasource: true,
|
||||
layers: templateLayergroupConfig.layers
|
||||
});
|
||||
} catch (err) {
|
||||
return done(err);
|
||||
}
|
||||
} else {
|
||||
var unauthorizedError = new Error("Unauthorized '" + templateName + "' template instantiation");
|
||||
unauthorizedError.http_status = 403;
|
||||
return done(unauthorizedError);
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
return done(null, {
|
||||
datasource: false,
|
||||
layers: [layer]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var datasourceBuilder = new Datasource.Builder();
|
||||
|
||||
function layersAdaptQueueFinish(err, layersResults) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (!layersResults || layersResults.length === 0) {
|
||||
return callback(new Error('Missing layers array from layergroup config'));
|
||||
}
|
||||
|
||||
var layers = [],
|
||||
currentLayerIndex = 0;
|
||||
|
||||
layersResults.forEach(function(layersResult) {
|
||||
|
||||
layersResult.layers.forEach(function(layer) {
|
||||
layers.push(layer);
|
||||
if (layersResult.datasource) {
|
||||
datasourceBuilder.withLayerDatasource(currentLayerIndex, {
|
||||
user: dbAuth.dbuser
|
||||
});
|
||||
}
|
||||
currentLayerIndex++;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
return callback(null, layers, datasourceBuilder.build());
|
||||
}
|
||||
|
||||
|
||||
var dbAuth = {};
|
||||
|
||||
if (_.some(layers, isNamedTypeLayer)) {
|
||||
// Lazy load dbAuth
|
||||
dbMetadata.setDBAuth(username, dbAuth, function(err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
layers.forEach(function(layer) {
|
||||
adaptLayersQueue.defer(adaptLayer, layer);
|
||||
});
|
||||
adaptLayersQueue.awaitAll(layersAdaptQueueFinish);
|
||||
});
|
||||
} else {
|
||||
return callback(null, layers, datasourceBuilder.build());
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
function isNamedTypeLayer(layer) {
|
||||
return layer.type === 'named';
|
||||
}
|
||||
45
lib/cartodb/monitoring/health_check.js
Normal file
@@ -0,0 +1,45 @@
|
||||
var fs = require('fs');
|
||||
var step = require('step');
|
||||
|
||||
function HealthCheck(metadataBackend, tilelive) {
|
||||
this.metadataBackend = metadataBackend;
|
||||
this.tilelive = tilelive;
|
||||
}
|
||||
|
||||
module.exports = HealthCheck;
|
||||
|
||||
|
||||
HealthCheck.prototype.check = function(config, callback) {
|
||||
|
||||
var result = {
|
||||
redis: {
|
||||
ok: false
|
||||
},
|
||||
mapnik: {
|
||||
ok: false
|
||||
},
|
||||
tile: {
|
||||
ok: false
|
||||
}
|
||||
};
|
||||
|
||||
step(
|
||||
function getManualDisable() {
|
||||
fs.readFile(global.environment.disabled_file, this);
|
||||
},
|
||||
function handleDisabledFile(err, data) {
|
||||
var next = this;
|
||||
if (err) {
|
||||
return next();
|
||||
}
|
||||
if (!!data) {
|
||||
err = new Error(data);
|
||||
err.http_status = 503;
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
function handleResult(err) {
|
||||
callback(err, result);
|
||||
}
|
||||
);
|
||||
};
|
||||
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,77 +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;
|
||||
});
|
||||
return callback(null, client);
|
||||
},
|
||||
destroy: function(client) {
|
||||
return client.quit();
|
||||
},
|
||||
max: options.max,
|
||||
idleTimeoutMillis: options.idleTimeoutMillis,
|
||||
reapIntervalMillis: options.reapIntervalMillis,
|
||||
log: options.log
|
||||
});
|
||||
};
|
||||
|
||||
return me;
|
||||
};
|
||||
|
||||
module.exports = RedisPool;
|
||||
@@ -1,21 +1,205 @@
|
||||
var _ = require('underscore')
|
||||
, Step = require('step')
|
||||
, cartoData = require('./carto_data')
|
||||
, Cache = require('./cache_validator');
|
||||
var _ = require('underscore');
|
||||
var step = require('step');
|
||||
var LZMA = require('lzma').LZMA;
|
||||
var assert = require('assert');
|
||||
var RedisPool = require('redis-mpool');
|
||||
|
||||
var QueryTablesApi = require('./api/query_tables_api');
|
||||
var PgQueryRunner = require('./backends/pg_query_runner');
|
||||
var PgConnection = require('./backends/pg_connection');
|
||||
var TemplateMaps = require('./template_maps.js');
|
||||
var MapConfigNamedLayersAdapter = require('./models/mapconfig_named_layers_adapter');
|
||||
var CdbRequest = require('./models/cdb_request');
|
||||
|
||||
var timeoutErrorTilePath = __dirname + '/../../assets/render-timeout-fallback.png';
|
||||
var timeoutErrorTile = require('fs').readFileSync(timeoutErrorTilePath, {encoding: null});
|
||||
|
||||
// Whitelist query parameters and attach format
|
||||
var REQUEST_QUERY_PARAMS_WHITELIST = [
|
||||
'config',
|
||||
'map_key',
|
||||
'api_key',
|
||||
'auth_token',
|
||||
'callback'
|
||||
];
|
||||
|
||||
module.exports = function(redisPool) {
|
||||
redisPool = redisPool || new RedisPool(_.extend(global.environment.redis, {name: 'windshaft:server_options'}));
|
||||
|
||||
var cartoData = require('cartodb-redis')({ pool: redisPool });
|
||||
var lzmaWorker = new LZMA();
|
||||
var pgConnection = new PgConnection(cartoData);
|
||||
var pgQueryRunner = new PgQueryRunner(pgConnection);
|
||||
var queryTablesApi = new QueryTablesApi(pgQueryRunner);
|
||||
var cdbRequest = new CdbRequest();
|
||||
|
||||
var rendererConfig = _.defaults(global.environment.renderer || {}, {
|
||||
cache_ttl: 60000, // milliseconds
|
||||
statsInterval: 60000,
|
||||
mapnik: {
|
||||
poolSize: 8,
|
||||
metatile: 2,
|
||||
bufferSize: 64,
|
||||
snapToGrid: false,
|
||||
clipByBox2d: false,
|
||||
limits: {}
|
||||
},
|
||||
http: {}
|
||||
});
|
||||
|
||||
module.exports = function(){
|
||||
var me = {
|
||||
base_url: '/tiles/:table',
|
||||
// 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
|
||||
cachedir: global.environment.millstone.cache_basedir,
|
||||
mapnik_version: global.environment.mapnik_version,
|
||||
mapnik_tile_format: global.environment.mapnik_tile_format || 'png',
|
||||
default_layergroup_ttl: global.environment.mapConfigTTL || 7200
|
||||
},
|
||||
statsd: global.environment.statsd,
|
||||
renderCache: {
|
||||
ttl: rendererConfig.cache_ttl,
|
||||
statsInterval: rendererConfig.statsInterval
|
||||
},
|
||||
renderer: {
|
||||
mapnik: rendererConfig.mapnik,
|
||||
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,
|
||||
fastly: global.environment.fastly || {},
|
||||
cache_enabled: global.environment.cache_enabled,
|
||||
log_format: global.environment.log_format
|
||||
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;
|
||||
|
||||
// Re-use redisPool
|
||||
me.redis.pool = redisPool;
|
||||
|
||||
// Re-use pgConnection
|
||||
me.pgConnection = pgConnection;
|
||||
// Re-use pgQueryRunner
|
||||
me.pgQueryRunner = pgQueryRunner;
|
||||
|
||||
var templateMaps = new TemplateMaps(redisPool, {
|
||||
max_user_templates: global.environment.maxUserTemplates
|
||||
});
|
||||
me.templateMaps = templateMaps;
|
||||
|
||||
var mapConfigNamedLayersAdapter = new MapConfigNamedLayersAdapter(templateMaps);
|
||||
|
||||
/* This whole block is about generating X-Cache-Channel { */
|
||||
|
||||
// TODO: review lifetime of elements of this cache
|
||||
// NOTE: by-token indices should only be dropped when
|
||||
// the corresponding layegroup is dropped, because
|
||||
// we have no SQL after layer creation.
|
||||
me.channelCache = {};
|
||||
|
||||
me.buildCacheChannel = function (dbName, tableNames){
|
||||
return dbName + ':' + tableNames.join(',');
|
||||
};
|
||||
|
||||
me.generateCacheChannel = function(app, req, callback){
|
||||
// Build channelCache key
|
||||
var dbName = req.params.dbname;
|
||||
var cacheKey = [ dbName, req.params.token ].join(':');
|
||||
|
||||
// no token means no tables associated
|
||||
if (!req.params.token) {
|
||||
return callback(null, this.buildCacheChannel(dbName, []));
|
||||
}
|
||||
|
||||
step(
|
||||
function checkCached() {
|
||||
if ( me.channelCache.hasOwnProperty(cacheKey) ) {
|
||||
return callback(null, me.channelCache[cacheKey]);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
function extractSQL(err) {
|
||||
assert.ifError(err);
|
||||
|
||||
// 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 mapStore = app.mapStore;
|
||||
step(
|
||||
function loadFromStore() {
|
||||
mapStore.load(req.params.token, this);
|
||||
},
|
||||
function getSQL(err, mapConfig) {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('mapStore_load');
|
||||
}
|
||||
assert.ifError(err);
|
||||
|
||||
var queries = mapConfig.getLayers()
|
||||
.map(function(lyr) {
|
||||
return lyr.options.sql;
|
||||
})
|
||||
.filter(function(sql) {
|
||||
return !!sql;
|
||||
});
|
||||
|
||||
return queries.length ? queries.join(';') : null;
|
||||
},
|
||||
this
|
||||
);
|
||||
},
|
||||
function findAffectedTables(err, sql) {
|
||||
assert.ifError(err);
|
||||
|
||||
if ( ! sql ) {
|
||||
throw new Error("this request doesn't need an X-Cache-Channel generated");
|
||||
}
|
||||
|
||||
queryTablesApi.getAffectedTablesInQuery(cdbRequest.userByReq(req), sql, this); // in addCacheChannel
|
||||
},
|
||||
function buildCacheChannel(err, tableNames) {
|
||||
assert.ifError(err);
|
||||
|
||||
if (req.profiler) {
|
||||
req.profiler.done('affectedTables');
|
||||
}
|
||||
|
||||
var cacheChannel = me.buildCacheChannel(dbName,tableNames);
|
||||
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
|
||||
@@ -27,19 +211,328 @@ 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 ttl = global.environment.varnish.ttl || 86400;
|
||||
Cache.generateCacheChannel(req, function(channel){
|
||||
res.header('X-Cache-Channel', channel);
|
||||
res.header('Last-Modified', new Date().toUTCString());
|
||||
res.header('Cache-Control', 'no-cache,max-age='+ttl+',must-revalidate, public');
|
||||
cb(null, channel); // add last-modified too ?
|
||||
if ( req.params.token ) {
|
||||
res.header('Cache-Control', 'public,max-age=31536000'); // 1 year
|
||||
} else {
|
||||
var ttl = global.environment.varnish.ttl || 86400;
|
||||
res.header('Cache-Control', 'no-cache,max-age='+ttl+',must-revalidate, public');
|
||||
}
|
||||
|
||||
// Set Last-Modified header
|
||||
var lastUpdated;
|
||||
if ( req.params.cache_buster ) {
|
||||
// Assuming cache_buster is a timestamp
|
||||
// FIXME: store lastModified in the cache channel instead
|
||||
lastUpdated = new Date(parseInt(req.params.cache_buster));
|
||||
} else {
|
||||
lastUpdated = new Date();
|
||||
}
|
||||
res.header('Last-Modified', lastUpdated.toUTCString());
|
||||
|
||||
me.generateCacheChannel(app, req, function(err, channel){
|
||||
if (req.profiler) {
|
||||
req.profiler.done('generateCacheChannel');
|
||||
req.profiler.end();
|
||||
}
|
||||
if ( ! err ) {
|
||||
res.header('X-Cache-Channel', 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
|
||||
cb(null, 'ERROR');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
if (global.environment.enabledFeatures.onTileErrorStrategy !== false) {
|
||||
me.renderer.onTileErrorStrategy = function(err, tile, headers, stats, format, callback) {
|
||||
if (err && err.message === 'Render timed out' && format === 'png') {
|
||||
return callback(null, timeoutErrorTile, { 'Content-Type': 'image/png' }, {});
|
||||
} else {
|
||||
return callback(err, tile, headers, stats);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
me.renderCache.beforeRendererCreate = function(req, callback) {
|
||||
var user = cdbRequest.userByReq(req);
|
||||
|
||||
var rendererOptions = {};
|
||||
|
||||
step(
|
||||
function getLimits(err) {
|
||||
assert.ifError(err);
|
||||
cartoData.getTilerRenderLimit(user, this);
|
||||
},
|
||||
function handleTilerLimits(err, renderLimit) {
|
||||
assert.ifError(err);
|
||||
rendererOptions.limits = {
|
||||
cacheOnTimeout: rendererConfig.mapnik.limits.cacheOnTimeout || false,
|
||||
render: renderLimit || rendererConfig.mapnik.limits.render || 0
|
||||
};
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
return callback(null, rendererOptions);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
me.beforeLayergroupCreate = function(req, requestMapConfig, callback) {
|
||||
mapConfigNamedLayersAdapter.getLayers(cdbRequest.userByReq(req), requestMapConfig.layers, pgConnection,
|
||||
function(err, layers, datasource) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
requestMapConfig.layers = layers;
|
||||
return callback(null, requestMapConfig, datasource);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
me.afterLayergroupCreate = function(req, mapconfig, response, callback) {
|
||||
var token = response.layergroupid;
|
||||
|
||||
var username = cdbRequest.userByReq(req);
|
||||
|
||||
var tasksleft = 2; // redis key and affectedTables
|
||||
var errors = [];
|
||||
|
||||
var done = function(err) {
|
||||
if ( err ) {
|
||||
errors.push('' + err);
|
||||
}
|
||||
if ( ! --tasksleft ) {
|
||||
err = errors.length ? new Error(errors.join('\n')) : null;
|
||||
callback(err);
|
||||
}
|
||||
};
|
||||
|
||||
// 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();
|
||||
});
|
||||
|
||||
var sql = mapconfig.layers.map(function(layer) {
|
||||
return layer.options.sql;
|
||||
}).join(';');
|
||||
|
||||
var dbName = req.params.dbname;
|
||||
var cacheKey = dbName + ':' + token;
|
||||
|
||||
step(
|
||||
function getAffectedTablesAndLastUpdatedTime() {
|
||||
queryTablesApi.getAffectedTablesAndLastUpdatedTime(username, sql, this);
|
||||
},
|
||||
function handleAffectedTablesAndLastUpdatedTime(err, result) {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('queryTablesAndLastUpdated');
|
||||
}
|
||||
assert.ifError(err);
|
||||
var cacheChannel = me.buildCacheChannel(dbName, result.affectedTables);
|
||||
me.channelCache[cacheKey] = cacheChannel;
|
||||
|
||||
// last update for layergroup cache buster
|
||||
response.layergroupid = response.layergroupid + ':' + result.lastUpdatedTime;
|
||||
response.last_updated = new Date(result.lastUpdatedTime).toISOString();
|
||||
|
||||
var res = req.res;
|
||||
if (res) {
|
||||
if (req.method === 'GET') {
|
||||
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);
|
||||
}
|
||||
|
||||
res.header('X-Layergroup-Id', response.layergroupid);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/* X-Cache-Channel generation } */
|
||||
|
||||
// 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 ) {
|
||||
return callback(null, null); // no signer requested
|
||||
}
|
||||
|
||||
var signer = req.params.signer;
|
||||
var layergroup_id = req.params.token;
|
||||
var auth_token = req.params.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) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var authorized = me.templateMaps.isAuthorized(mapConfig.obj().template, auth_token);
|
||||
|
||||
return 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 = cdbRequest.userByReq(req);
|
||||
step(
|
||||
function (){
|
||||
cartoData.getUserMapKey(user, this);
|
||||
},
|
||||
function checkApiKey(err, val){
|
||||
assert.ifError(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 = cdbRequest.userByReq(req);
|
||||
|
||||
step(
|
||||
function (){
|
||||
that.authorizedByAPIKey(req, this);
|
||||
},
|
||||
function checkApiKey(err, authorized){
|
||||
if (req.profiler) {
|
||||
req.profiler.done('authorizedByAPIKey');
|
||||
}
|
||||
assert.ifError(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;
|
||||
}
|
||||
|
||||
// authorized by api key, login as the given username and stop
|
||||
pgConnection.setDBAuth(user, req.params, function(err) {
|
||||
callback(err, true); // authorized (or error)
|
||||
});
|
||||
},
|
||||
function checkSignAuthorized(err, signed_by){
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if ( ! signed_by ) {
|
||||
// request not authorized by signer.
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
pgConnection.setDBAuth(signed_by, req.params, function(err) {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('setDBAuth');
|
||||
}
|
||||
callback(err, true); // authorized (or error)
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
me.setDBParams = function(cdbuser, params, callback) {
|
||||
step(
|
||||
function setAuth() {
|
||||
pgConnection.setDBAuth(cdbuser, params, this);
|
||||
},
|
||||
function setConn(err) {
|
||||
if ( err ) throw err;
|
||||
pgConnection.setDBConn(cdbuser, params, this);
|
||||
},
|
||||
function finish(err) {
|
||||
callback(err);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// jshint maxcomplexity:10
|
||||
/**
|
||||
* Whitelist input and get database name & default geometry type from
|
||||
* subdomain/user metadata held in CartoDB Redis
|
||||
@@ -48,120 +541,111 @@ module.exports = function(){
|
||||
*/
|
||||
me.req2params = function(req, callback){
|
||||
|
||||
// Whitelist query parameters and attach format
|
||||
var good_query = ['sql', 'geom_type', 'cache_buster','callback', 'interactivity', 'map_key', 'api_key', 'style'];
|
||||
var bad_query = _.difference(_.keys(req.query), good_query);
|
||||
if ( req.query.lzma ) {
|
||||
|
||||
_.each(bad_query, function(key){ delete req.query[key]; });
|
||||
req.params = _.extend({}, req.params); // shuffle things as request is a strange array/object
|
||||
// Decode (from base64)
|
||||
var lzma = new Buffer(req.query.lzma, 'base64')
|
||||
.toString('binary')
|
||||
.split('')
|
||||
.map(function(c) {
|
||||
return c.charCodeAt(0) - 128;
|
||||
});
|
||||
|
||||
// Decompress
|
||||
lzmaWorker.decompress(
|
||||
lzma,
|
||||
function(result) {
|
||||
if (req.profiler) {
|
||||
req.profiler.done('lzma');
|
||||
}
|
||||
try {
|
||||
delete req.query.lzma;
|
||||
_.extend(req.query, JSON.parse(result));
|
||||
me.req2params(req, callback);
|
||||
} catch (err) {
|
||||
callback(new Error('Error parsing lzma as JSON: ' + err));
|
||||
}
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
req.query = _.pick(req.query, REQUEST_QUERY_PARAMS_WHITELIST);
|
||||
req.params = _.extend({}, req.params); // shuffle things as request is a strange array/object
|
||||
|
||||
var user = cdbRequest.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
|
||||
_.extend(req.params, req.query);
|
||||
|
||||
// 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>)/;
|
||||
xml = xml.replace(me.rx_dbuser, "$1" + dbuser + "$2");
|
||||
callback(null, xml);
|
||||
if (req.profiler) {
|
||||
req.profiler.done('req2params.setup');
|
||||
}
|
||||
|
||||
var that = this;
|
||||
|
||||
Step(
|
||||
step(
|
||||
function getPrivacy(){
|
||||
cartoData.authorize(req, this);
|
||||
me.authorize(req, this);
|
||||
},
|
||||
function gatekeep(err, data){
|
||||
if(err) throw err;
|
||||
if(data === "0") throw new Error("Sorry, you are unauthorized (permission denied)");
|
||||
return data;
|
||||
function gatekeep(err, authorized){
|
||||
if (req.profiler) {
|
||||
req.profiler.done('authorize');
|
||||
}
|
||||
assert.ifError(err);
|
||||
if(!authorized) {
|
||||
err = new Error("Sorry, you are unauthorized (permission denied)");
|
||||
err.http_status = 403;
|
||||
throw err;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
function getDatabase(err, data){
|
||||
if(err) throw err;
|
||||
|
||||
cartoData.getDatabase(req, this);
|
||||
function getDatabase(err){
|
||||
assert.ifError(err);
|
||||
pgConnection.setDBConn(user, req.params, this);
|
||||
},
|
||||
function getGeometryType(err, data){
|
||||
if (err) throw err;
|
||||
_.extend(req.params, {dbname:data});
|
||||
|
||||
cartoData.getGeometryType(req, this);
|
||||
},
|
||||
function finishSetup(err, data){
|
||||
function finishSetup(err) {
|
||||
if ( err ) { callback(err, req); return; }
|
||||
|
||||
if (!_.isNull(data))
|
||||
_.extend(req.params, {geom_type: data});
|
||||
|
||||
that.addCacheChannel(req, function(err, chan) {
|
||||
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
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Little helper method to get the current list of infowindow variables and return to client
|
||||
* @param req
|
||||
* @param callback
|
||||
*/
|
||||
me.getInfowindow = function(req, callback){
|
||||
var that = this;
|
||||
|
||||
Step(
|
||||
function(){
|
||||
that.req2params(req, this);
|
||||
},
|
||||
function(err, data){
|
||||
if (err) callback(err, null);
|
||||
else cartoData.getInfowindow(data, callback);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Little helper method to get map metadata and return to client
|
||||
* @param req
|
||||
* @param callback
|
||||
*/
|
||||
me.getMapMetadata = function(req, callback){
|
||||
var that = this;
|
||||
|
||||
Step(
|
||||
function(){
|
||||
that.req2params(req, this);
|
||||
},
|
||||
function(err, data){
|
||||
if (err) callback(err, null);
|
||||
else cartoData.getMapMetadata(data, callback);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper to clear out tile cache on request
|
||||
* @param req
|
||||
* @param callback
|
||||
*/
|
||||
me.flushCache = function(req, Cache, callback){
|
||||
var that = this;
|
||||
|
||||
Step(
|
||||
function(){
|
||||
that.req2params(req, this);
|
||||
},
|
||||
function(err, data){
|
||||
if (err) throw err;
|
||||
if(Cache) {
|
||||
Cache.invalidate_db(req.params.dbname, req.params.table);
|
||||
}
|
||||
callback(null, true);
|
||||
callback(null, req);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
return me;
|
||||
}();
|
||||
};
|
||||
|
||||
506
lib/cartodb/template_maps.js
Normal file
@@ -0,0 +1,506 @@
|
||||
var crypto = require('crypto');
|
||||
var step = require('step');
|
||||
var _ = require('underscore');
|
||||
var 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}}");
|
||||
}
|
||||
|
||||
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 _reValidNameIdentifier = /^[a-z0-9][0-9a-z_\-]*$/i;
|
||||
var _reValidPlaceholderIdentifier = /^[a-z][0-9a-z_]*$/i;
|
||||
// jshint maxcomplexity:15
|
||||
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(_reValidNameIdentifier) ) {
|
||||
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(_reValidPlaceholderIdentifier)) {
|
||||
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);
|
||||
}
|
||||
|
||||
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 (_.isString(templateAuth) && templateAuth === 'open') {
|
||||
return true;
|
||||
}
|
||||
|
||||
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}$/;
|
||||
|
||||
function _replaceVars (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')
|
||||
;
|
||||
};
|
||||
|
||||
module.exports.templateName = function templateName(templateId) {
|
||||
var templateIdTokens = templateId.split('@');
|
||||
var name = templateIdTokens[0];
|
||||
|
||||
if (templateIdTokens.length > 1) {
|
||||
name = templateIdTokens[1];
|
||||
}
|
||||
|
||||
return name;
|
||||
};
|
||||
3140
npm-shrinkwrap.json
generated
61
package.json
@@ -1,38 +1,55 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "windshaft-cartodb",
|
||||
"version": "1.0.1",
|
||||
"version": "2.7.0",
|
||||
"description": "A map tile server for CartoDB",
|
||||
"url": "https://github.com/Vizzuality/Windshaft-cartodb",
|
||||
"keywords": [
|
||||
"cartodb"
|
||||
],
|
||||
"url": "https://github.com/CartoDB/Windshaft-cartodb",
|
||||
"licenses": [{
|
||||
"type": "BSD",
|
||||
"url": "https://github.com/Vizzuality/Windshaft-cartodb/blob/master/LICENCE"
|
||||
"url": "https://github.com/CartoDB/Windshaft-cartodb/blob/master/LICENCE"
|
||||
}],
|
||||
"repositories": [{
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/Vizzuality/Windshaft-cartodb.git"
|
||||
}],
|
||||
"author": {
|
||||
"name": "Simon Tokumine, Javi Santana, Vizzuality",
|
||||
"url": "http://vizzuality.com",
|
||||
"email": "simon@vizzuality.com"
|
||||
"url": "git://github.com/CartoDB/Windshaft-cartodb.git"
|
||||
},
|
||||
"author": "Vizzuality <contact@vizzuality.com> (http://vizzuality.com)",
|
||||
"contributors": [
|
||||
"Simon Tokumine <simon@vizzuality.com>",
|
||||
"Javi Santana <jsantana@vizzuality.com>",
|
||||
"Sandro Santilli <strk@vizzuality.com>"
|
||||
],
|
||||
"dependencies": {
|
||||
"cluster2": "git://github.com/CartoDB/cluster2.git#28cde11",
|
||||
"node-varnish": "0.1.1",
|
||||
"underscore" : "1.1.x",
|
||||
"grainstore" : "~0.6.2",
|
||||
"windshaft" : "~0.5.8",
|
||||
"step": "0.0.x",
|
||||
"generic-pool": "1.0.x",
|
||||
"redis": "0.7.2",
|
||||
"hiredis": "~0.1.14",
|
||||
"request": "2.9.202"
|
||||
"underscore" : "~1.6.0",
|
||||
"dot": "~1.0.2",
|
||||
"windshaft": "0.47.0",
|
||||
"step": "~0.0.5",
|
||||
"queue-async": "~1.0.7",
|
||||
"request": "~2.9.203",
|
||||
"cartodb-redis": "~0.13.0",
|
||||
"cartodb-psql": "~0.4.0",
|
||||
"fastly-purge": "~1.0.0",
|
||||
"redis-mpool": "~0.4.0",
|
||||
"lzma": "~1.3.7",
|
||||
"log4js": "https://github.com/CartoDB/log4js-node/tarball/cdb"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mocha": "1.2.1"
|
||||
"istanbul": "~0.3.6",
|
||||
"mocha": "~1.21.4",
|
||||
"nock": "~1.3.0",
|
||||
"jshint": "~2.6.0",
|
||||
"redis": "~0.8.6",
|
||||
"strftime": "~0.8.2",
|
||||
"semver": "~1.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "make check"
|
||||
"preinstall": "make pre-install",
|
||||
"test": "make test-all"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8 <0.11",
|
||||
"npm": ">=1.2.1"
|
||||
}
|
||||
}
|
||||
|
||||
122
run_tests.sh
@@ -1,11 +1,36 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Must match config.redis_pool.port in test/support/config.js
|
||||
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_COVERAGE=no # run tests with coverage
|
||||
|
||||
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() {
|
||||
echo "Cleaning up"
|
||||
kill ${PID_REDIS}
|
||||
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
|
||||
echo "Could not find a test redis pid to kill it"
|
||||
return;
|
||||
fi
|
||||
fi
|
||||
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() {
|
||||
@@ -22,21 +47,86 @@ die() {
|
||||
|
||||
trap 'cleanup_and_exit' 1 2 3 5 9 13
|
||||
|
||||
echo "Starting redis on port ${REDIS_PORT}"
|
||||
echo "port ${REDIS_PORT}" | redis-server - > test.log &
|
||||
PID_REDIS=$!
|
||||
while [ -n "$1" ]; do
|
||||
# This is kept for backward compatibility
|
||||
if test "$1" = "--nodrop"; then
|
||||
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
|
||||
elif test "$1" = "--with-coverage"; then
|
||||
OPT_COVERAGE=yes
|
||||
shift
|
||||
continue
|
||||
# This is kept for backward compatibility
|
||||
elif test "$1" = "--nocreate"; then
|
||||
OPT_CREATE_REDIS=no
|
||||
OPT_CREATE_PGSQL=no
|
||||
shift
|
||||
continue
|
||||
else
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Preparing the database"
|
||||
cd test/support; sh prepare_db.sh >> test.log || die "database preparation failure (see test.log)"; cd -;
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: $0 [<options>] <test> [<test>]" >&2
|
||||
echo "Options:" >&2
|
||||
echo " --nocreate do not create the test environment on start" >&2
|
||||
echo " --nodrop do not drop the test environment on exit" >&2
|
||||
echo " --with-coverage use istanbul to determine code coverage" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TESTS=$@
|
||||
|
||||
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
|
||||
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"
|
||||
mocha -u tdd \
|
||||
test/unit/cartodb/redis_pool.test.js \
|
||||
test/unit/cartodb/req2params.test.js \
|
||||
test/acceptance/cache_validator.js \
|
||||
test/acceptance/server.js
|
||||
|
||||
if test x"$OPT_COVERAGE" = xyes; then
|
||||
echo "Running tests with coverage"
|
||||
./node_modules/.bin/istanbul cover node_modules/.bin/_mocha -- -u tdd -t 5000 ${TESTS}
|
||||
else
|
||||
echo "Running tests"
|
||||
mocha -u tdd -t 5000 ${TESTS}
|
||||
fi
|
||||
ret=$?
|
||||
|
||||
cleanup
|
||||
|
||||
exit $ret
|
||||
|
||||
24
scripts/check-node-canvas.sh
Normal file
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
CAIRO_PKG_CONFIG=`pkg-config cairo --cflags-only-I 2> /dev/null`
|
||||
RESULT=$?
|
||||
|
||||
if [[ ${RESULT} -ne 0 ]]; then
|
||||
echo "###################################################################################"
|
||||
echo "# PREINSTALL HOOK ERROR #"
|
||||
echo "#---------------------------------------------------------------------------------#"
|
||||
echo "# #"
|
||||
echo "# node-canvas install error: some packages required by 'cairo' are not found #"
|
||||
echo "# #"
|
||||
echo -e "# Use '\033[1mmake all\033[0m', it will take care of common/known issues #"
|
||||
echo "# #"
|
||||
echo "# As an alternative try: #"
|
||||
echo "# Try to 'export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:/opt/X11/lib/pkgconfig' #"
|
||||
echo "# #"
|
||||
echo "# If problems persist visit: https://github.com/Automattic/node-canvas/wiki #"
|
||||
echo "# #"
|
||||
echo "###################################################################################"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
7
scripts/install.sh
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:/opt/X11/lib/pkgconfig
|
||||
fi
|
||||
|
||||
npm install
|
||||
18
scripts/lzma2config.js
Normal file
@@ -0,0 +1,18 @@
|
||||
if (process.argv.length !== 3) {
|
||||
console.error('Usage: node %s lzma_string', __filename);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
var LZMA = require('lzma').LZMA;
|
||||
var lzmaWorker = new LZMA();
|
||||
var lzmaInput = decodeURIComponent(process.argv[2]);
|
||||
var lzmaBuffer = new Buffer(lzmaInput, 'base64')
|
||||
.toString('binary')
|
||||
.split('')
|
||||
.map(function(c) {
|
||||
return c.charCodeAt(0) - 128
|
||||
});
|
||||
|
||||
lzmaWorker.decompress(lzmaBuffer, function(result) {
|
||||
console.log(JSON.stringify(JSON.parse(JSON.parse(result).config), null, 4));
|
||||
});
|
||||
313
test/acceptance/cache/surrogate_keys_invalidation.js
vendored
Normal file
@@ -0,0 +1,313 @@
|
||||
require(__dirname + '/../../support/test_helper');
|
||||
|
||||
var assert = require('../../support/assert');
|
||||
var redis = require('redis');
|
||||
var step = require('step');
|
||||
var FastlyPurge = require('fastly-purge');
|
||||
|
||||
var NamedMapsCacheEntry = require(__dirname + '/../../../lib/cartodb/cache/model/named_maps_entry');
|
||||
var CartodbWindshaft = require(__dirname + '/../../../lib/cartodb/cartodb_windshaft');
|
||||
|
||||
|
||||
describe('templates surrogate keys', function() {
|
||||
|
||||
var redisClient = redis.createClient(global.environment.redis.port);
|
||||
|
||||
// Enable Varnish purge for tests
|
||||
var varnishHost = global.environment.varnish.host;
|
||||
global.environment.varnish.host = '127.0.0.1';
|
||||
var varnishPurgeEnabled = global.environment.varnish.purge_enabled;
|
||||
global.environment.varnish.purge_enabled = true;
|
||||
|
||||
var fastlyConfig = global.environment.fastly;
|
||||
var FAKE_FASTLY_API_KEY = 'fastly-api-key';
|
||||
var FAKE_FASTLY_SERVICE_ID = 'fake-service-id';
|
||||
global.environment.fastly = {
|
||||
enabled: true,
|
||||
// the fastly api key
|
||||
apiKey: FAKE_FASTLY_API_KEY,
|
||||
// the service that will get surrogate key invalidation
|
||||
serviceId: FAKE_FASTLY_SERVICE_ID
|
||||
};
|
||||
|
||||
var serverOptions = require('../../../lib/cartodb/server_options')();
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
|
||||
var templateOwner = 'localhost',
|
||||
templateName = 'acceptance',
|
||||
expectedTemplateId = 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 };
|
||||
|
||||
var varnishHttpUrl = [
|
||||
'http://', global.environment.varnish.host, ':', global.environment.varnish.http_port
|
||||
].join('');
|
||||
|
||||
var cacheEntryKey = new NamedMapsCacheEntry(templateOwner, templateName).key();
|
||||
var invalidationMatchHeader = '\\b' + cacheEntryKey + '\\b';
|
||||
var fastlyPurgePath = '/service/' + FAKE_FASTLY_SERVICE_ID + '/purge/' + encodeURIComponent(cacheEntryKey);
|
||||
|
||||
var nock = require('nock');
|
||||
nock.enableNetConnect(/(127.0.0.1:5555|cartocdn.com)/);
|
||||
|
||||
after(function(done) {
|
||||
serverOptions.varnish_purge_enabled = false;
|
||||
global.environment.varnish.host = varnishHost;
|
||||
global.environment.varnish.purge_enabled = varnishPurgeEnabled;
|
||||
|
||||
global.environment.fastly = fastlyConfig;
|
||||
|
||||
nock.restore();
|
||||
done();
|
||||
});
|
||||
|
||||
function createTemplate(callback) {
|
||||
var postTemplateRequest = {
|
||||
url: '/api/v1/map/named?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) {
|
||||
callback(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
it("invalidates surrogate keys on template update", function(done) {
|
||||
|
||||
var scope = nock(varnishHttpUrl)
|
||||
.intercept('/key', 'PURGE')
|
||||
.matchHeader('Invalidation-Match', invalidationMatchHeader)
|
||||
.reply(204, '');
|
||||
|
||||
var fastlyScope = nock(FastlyPurge.FASTLY_API_ENDPOINT)
|
||||
.post(fastlyPurgePath)
|
||||
.matchHeader('Fastly-Key', FAKE_FASTLY_API_KEY)
|
||||
.matchHeader('Fastly-Soft-Purge', 1)
|
||||
.matchHeader('Accept', 'application/json')
|
||||
.reply(200, {
|
||||
status:'ok'
|
||||
});
|
||||
|
||||
step(
|
||||
function createTemplateToUpdate() {
|
||||
createTemplate(this);
|
||||
},
|
||||
function putValidTemplate(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
var updateTemplateRequest = {
|
||||
url: '/api/v1/map/named/' + 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.equal(scope.pendingMocks().length, 0);
|
||||
assert.equal(fastlyScope.pendingMocks().length, 0);
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("invalidates surrogate on template deletion", function(done) {
|
||||
|
||||
var scope = nock(varnishHttpUrl)
|
||||
.intercept('/key', 'PURGE')
|
||||
.matchHeader('Invalidation-Match', invalidationMatchHeader)
|
||||
.reply(204, '');
|
||||
|
||||
var fastlyScope = nock(FastlyPurge.FASTLY_API_ENDPOINT)
|
||||
.post(fastlyPurgePath)
|
||||
.matchHeader('Fastly-Key', FAKE_FASTLY_API_KEY)
|
||||
.matchHeader('Fastly-Soft-Purge', 1)
|
||||
.matchHeader('Accept', 'application/json')
|
||||
.reply(200, {
|
||||
status:'ok'
|
||||
});
|
||||
|
||||
step(
|
||||
function createTemplateToDelete() {
|
||||
createTemplate(this);
|
||||
},
|
||||
function deleteValidTemplate(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
var deleteTemplateRequest = {
|
||||
url: '/api/v1/map/named/' + 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.equal(scope.pendingMocks().length, 0);
|
||||
assert.equal(fastlyScope.pendingMocks().length, 0);
|
||||
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("should update template even if surrogate key invalidation fails", function(done) {
|
||||
|
||||
var scope = nock(varnishHttpUrl)
|
||||
.intercept('/key', 'PURGE')
|
||||
.matchHeader('Invalidation-Match', invalidationMatchHeader)
|
||||
.reply(503, '');
|
||||
|
||||
step(
|
||||
function createTemplateToUpdate() {
|
||||
createTemplate(this);
|
||||
},
|
||||
function putValidTemplate(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
var updateTemplateRequest = {
|
||||
url: '/api/v1/map/named/' + 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.equal(scope.pendingMocks().length, 0);
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,21 +0,0 @@
|
||||
var assert = require('../support/assert');
|
||||
require(__dirname + '/../support/test_helper');
|
||||
var CacheValidator = require(__dirname + '/../../lib/cartodb/cache_validator');
|
||||
|
||||
var VarnishEmu = require('../support/VarnishEmu');
|
||||
|
||||
suite('cache_validator', function() {
|
||||
|
||||
test('should call purge on varnish when invalidate database', function(done) {
|
||||
var varnish = new VarnishEmu(function(cmds) {
|
||||
assert.ok(cmds.length == 1);
|
||||
assert.equal('purge obj.http.X-Cache-Channel ~ \"^test_db:(.*test_cache.*)|(table)$\"\n', cmds[0].toString('utf8'));
|
||||
done();
|
||||
},
|
||||
function() {
|
||||
CacheValidator.init('localhost', 1337);
|
||||
CacheValidator.invalidate_db('test_db', 'test_cache');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
94
test/acceptance/health_check.js
Normal file
@@ -0,0 +1,94 @@
|
||||
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);
|
||||
|
||||
var metadataBackend = {};
|
||||
var tilelive = {};
|
||||
var HealthCheck = require('../../lib/cartodb/monitoring/health_check');
|
||||
var healthCheck = new HealthCheck(metadataBackend, tilelive);
|
||||
|
||||
describe('health checks', function () {
|
||||
|
||||
function resetHealthConfig() {
|
||||
global.environment.health = {
|
||||
enabled: true,
|
||||
username: 'localhost',
|
||||
z: 0,
|
||||
x: 0,
|
||||
y: 0
|
||||
};
|
||||
}
|
||||
|
||||
var healthCheckRequest = {
|
||||
url: '/health',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
}
|
||||
};
|
||||
|
||||
it('returns 200 and ok=true with enabled configuration', function (done) {
|
||||
resetHealthConfig();
|
||||
|
||||
assert.response(server,
|
||||
healthCheckRequest,
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function (res, err) {
|
||||
assert.ok(!err);
|
||||
|
||||
var parsed = JSON.parse(res.body);
|
||||
|
||||
assert.ok(parsed.enabled);
|
||||
assert.ok(parsed.ok);
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('error if disabled file exists', function(done) {
|
||||
var fs = require('fs');
|
||||
|
||||
var readFileFn = fs.readFile;
|
||||
fs.readFile = function(filename, callback) {
|
||||
callback(null, "Maintenance");
|
||||
};
|
||||
|
||||
healthCheck.check(null, function(err/*, result*/) {
|
||||
assert.equal(err.message, "Maintenance");
|
||||
assert.equal(err.http_status, 503);
|
||||
done();
|
||||
fs.readFile = readFileFn;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it('not err if disabled file does not exists', function(done) {
|
||||
resetHealthConfig();
|
||||
|
||||
global.environment.disabled_file = '/tmp/ftreftrgtrccre';
|
||||
|
||||
assert.response(server,
|
||||
healthCheckRequest,
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function (res, err) {
|
||||
assert.ok(!err);
|
||||
|
||||
var parsed = JSON.parse(res.body);
|
||||
|
||||
assert.equal(parsed.enabled, true);
|
||||
assert.equal(parsed.ok, true);
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
307
test/acceptance/limits.js
Normal file
@@ -0,0 +1,307 @@
|
||||
require('../support/test_helper');
|
||||
|
||||
var assert = require('../support/assert');
|
||||
var _ = require('underscore');
|
||||
var redis = require('redis');
|
||||
|
||||
var CartodbWindshaft = require('../../lib/cartodb/cartodb_windshaft');
|
||||
var serverOptions = require('../../lib/cartodb/server_options');
|
||||
|
||||
describe('render limits', function() {
|
||||
|
||||
var layergroupUrl = '/api/v1/map';
|
||||
|
||||
var redisClient = redis.createClient(global.environment.redis.port);
|
||||
after(function(done) {
|
||||
redisClient.keys("map_style|*", function(err, matches) {
|
||||
redisClient.del(matches, function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
var server;
|
||||
beforeEach(function() {
|
||||
server = new CartodbWindshaft(serverOptions());
|
||||
server.setMaxListeners(0);
|
||||
});
|
||||
|
||||
var keysToDelete = [];
|
||||
afterEach(function(done) {
|
||||
redisClient.DEL(keysToDelete, function() {
|
||||
keysToDelete = [];
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
var user = 'localhost';
|
||||
|
||||
var pointSleepSql = "SELECT pg_sleep(0.5)," +
|
||||
" 'SRID=3857;POINT(0 0)'::geometry the_geom_webmercator, 1 cartodb_id";
|
||||
var pointCartoCss = '#layer { marker-fill:red; }';
|
||||
var polygonSleepSql = "SELECT pg_sleep(0.5)," +
|
||||
" ST_Buffer('SRID=3857;POINT(0 0)'::geometry, 100000000) the_geom_webmercator, 1 cartodb_id";
|
||||
var polygonCartoCss = '#layer { polygon-fill:red; }';
|
||||
|
||||
function singleLayergroupConfig(sql, cartocss) {
|
||||
return {
|
||||
version: '1.0.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: sql,
|
||||
cartocss: cartocss,
|
||||
cartocss_version: '2.0.1'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
function createRequest(layergroup, userHost) {
|
||||
return {
|
||||
url: layergroupUrl,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: userHost,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(layergroup)
|
||||
};
|
||||
}
|
||||
|
||||
function withRenderLimit(user, renderLimit, callback) {
|
||||
redisClient.SELECT(5, function(err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
var userLimitsKey = 'limits:tiler:' + user;
|
||||
redisClient.HSET(userLimitsKey, 'render', renderLimit, function(err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
keysToDelete.push(userLimitsKey);
|
||||
return callback();
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
describe('with onTileErrorStrategy DISABLED', function() {
|
||||
var onTileErrorStrategyEnabled;
|
||||
before(function() {
|
||||
onTileErrorStrategyEnabled = global.environment.enabledFeatures.onTileErrorStrategy;
|
||||
global.environment.enabledFeatures.onTileErrorStrategy = false;
|
||||
});
|
||||
|
||||
after(function() {
|
||||
global.environment.enabledFeatures.onTileErrorStrategy = onTileErrorStrategyEnabled;
|
||||
});
|
||||
|
||||
it("layergroup creation fails if test tile is slow", function(done) {
|
||||
withRenderLimit(user, 50, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var layergroup = singleLayergroupConfig(polygonSleepSql, polygonCartoCss);
|
||||
assert.response(server,
|
||||
createRequest(layergroup, user),
|
||||
{
|
||||
status: 400
|
||||
},
|
||||
function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.deepEqual(parsed, { errors: [ 'Render timed out' ] });
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("layergroup creation does not fail if user limit is high enough even if test tile is slow", function(done) {
|
||||
withRenderLimit(user, 5000, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var layergroup = singleLayergroupConfig(polygonSleepSql, polygonCartoCss);
|
||||
assert.response(server,
|
||||
createRequest(layergroup, user),
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.layergroupid);
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("layergroup creation works if test tile is fast but tile request fails if they are slow", function(done) {
|
||||
withRenderLimit(user, 50, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var layergroup = singleLayergroupConfig(pointSleepSql, pointCartoCss);
|
||||
assert.response(server,
|
||||
createRequest(layergroup, user),
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
assert.response(server,
|
||||
{
|
||||
url: layergroupUrl + _.template('/<%= layergroupId %>/<%= z %>/<%= x %>/<%= y %>.png', {
|
||||
layergroupId: JSON.parse(res.body).layergroupid,
|
||||
z: 0,
|
||||
x: 0,
|
||||
y: 0
|
||||
}),
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
},
|
||||
encoding: 'binary'
|
||||
},
|
||||
{
|
||||
status: 400
|
||||
},
|
||||
function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.deepEqual(parsed, { errors: ['Render timed out'] });
|
||||
done();
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("tile request does not fail if user limit is high enough", function(done) {
|
||||
withRenderLimit(user, 5000, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var layergroup = singleLayergroupConfig(pointSleepSql, pointCartoCss);
|
||||
assert.response(server,
|
||||
createRequest(layergroup, user),
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
assert.response(server,
|
||||
{
|
||||
url: layergroupUrl + _.template('/<%= layergroupId %>/<%= z %>/<%= x %>/<%= y %>.png', {
|
||||
layergroupId: JSON.parse(res.body).layergroupid,
|
||||
z: 0,
|
||||
x: 0,
|
||||
y: 0
|
||||
}),
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
},
|
||||
encoding: 'binary'
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'image/png'
|
||||
}
|
||||
},
|
||||
function(res, err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('with onTileErrorStrategy', function() {
|
||||
|
||||
it("layergroup creation works even if test tile is slow", function(done) {
|
||||
withRenderLimit(user, 50, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var layergroup = singleLayergroupConfig(polygonSleepSql, polygonCartoCss);
|
||||
assert.response(server,
|
||||
createRequest(layergroup, user),
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.layergroupid);
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("layergroup creation and tile requests works even if they are slow but returns fallback", function(done) {
|
||||
withRenderLimit(user, 50, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var layergroup = singleLayergroupConfig(pointSleepSql, pointCartoCss);
|
||||
assert.response(server,
|
||||
createRequest(layergroup, user),
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
assert.response(server,
|
||||
{
|
||||
url: layergroupUrl + _.template('/<%= layergroupId %>/<%= z %>/<%= x %>/<%= y %>.png', {
|
||||
layergroupId: JSON.parse(res.body).layergroupid,
|
||||
z: 0,
|
||||
x: 0,
|
||||
y: 0
|
||||
}),
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
},
|
||||
encoding: 'binary'
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'image/png'
|
||||
}
|
||||
},
|
||||
function(res, err) {
|
||||
if (err) {
|
||||
done(err);
|
||||
}
|
||||
assert.imageEqualsFile(res.body, './test/fixtures/render-timeout-fallback.png', 25,
|
||||
function(imgErr/*, similarity*/) {
|
||||
done(imgErr);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
1401
test/acceptance/multilayer.js
Normal file
378
test/acceptance/multilayer_server.js
Normal file
@@ -0,0 +1,378 @@
|
||||
var testHelper = require('../support/test_helper');
|
||||
|
||||
var assert = require('../support/assert');
|
||||
|
||||
var redis = require('redis');
|
||||
var _ = require('underscore');
|
||||
|
||||
|
||||
var PgQueryRunner = require('../../lib/cartodb/backends/pg_query_runner');
|
||||
var CartodbWindshaft = require('../../lib/cartodb/cartodb_windshaft');
|
||||
var serverOptions = require('../../lib/cartodb/server_options')();
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
server.setMaxListeners(0);
|
||||
|
||||
describe('tests from old api translated to multilayer', function() {
|
||||
|
||||
var layergroupUrl = '/api/v1/map';
|
||||
|
||||
var redisClient = redis.createClient(global.environment.redis.port);
|
||||
after(function(done) {
|
||||
// This test will add map_style records, like
|
||||
// 'map_style|null|publicuser|my_table',
|
||||
redisClient.keys("map_style|*", function(err, matches) {
|
||||
redisClient.del(matches, function() {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
var wadusSql = 'select 1 as cartodb_id, null::geometry as the_geom_webmercator';
|
||||
var pointSql = "SELECT 'SRID=3857;POINT(0 0)'::geometry as the_geom_webmercator, 1::int as cartodb_id";
|
||||
|
||||
function singleLayergroupConfig(sql, cartocss) {
|
||||
return {
|
||||
version: '1.0.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: sql,
|
||||
cartocss: cartocss,
|
||||
cartocss_version: '2.0.1'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
function createRequest(layergroup, userHost, apiKey) {
|
||||
var url = layergroupUrl;
|
||||
if (apiKey) {
|
||||
url += '?api_key=' + apiKey;
|
||||
}
|
||||
return {
|
||||
url: url,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: userHost || 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(layergroup)
|
||||
};
|
||||
}
|
||||
|
||||
it("layergroup creation fails if CartoCSS is bogus", function(done) {
|
||||
var layergroup = singleLayergroupConfig(wadusSql, '#my_table3{');
|
||||
assert.response(server,
|
||||
createRequest(layergroup),
|
||||
{
|
||||
status: 400
|
||||
},
|
||||
function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.errors[0].match(/^style0/));
|
||||
assert.ok(parsed.errors[0].match(/missing closing/));
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("multiple bad styles returns 400 with all errors", function(done) {
|
||||
var layergroup = singleLayergroupConfig(wadusSql, '#my_table4{backgxxxxxround-color:#fff;foo:bar}');
|
||||
assert.response(server,
|
||||
createRequest(layergroup),
|
||||
{
|
||||
status: 400
|
||||
},
|
||||
function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.equal(parsed.errors.length, 1);
|
||||
assert.ok(parsed.errors[0].match(/^style0/));
|
||||
assert.ok(parsed.errors[0].match(/Unrecognized rule: backgxxxxxround-color/));
|
||||
assert.ok(parsed.errors[0].match(/Unrecognized rule: foo/));
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// Zoom is a special variable
|
||||
it("Specifying zoom level in CartoCSS does not need a 'zoom' variable in SQL output", function(done) {
|
||||
var layergroup = singleLayergroupConfig(pointSql, '#gadm4 [ zoom>=3] { marker-fill:red; }');
|
||||
|
||||
assert.response(server,
|
||||
createRequest(layergroup),
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.layergroupid);
|
||||
assert.equal(res.headers['x-layergroup-id'], parsed.layergroupid);
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/issues/88
|
||||
it("getting a tile from a user-specific database should return an expected tile", function(done) {
|
||||
var layergroup = singleLayergroupConfig(pointSql, '#layer { marker-fill:red; }');
|
||||
|
||||
var backupDBHost = global.environment.postgres.host;
|
||||
global.environment.postgres.host = '6.6.6.6';
|
||||
|
||||
assert.response(server,
|
||||
createRequest(layergroup, 'cartodb250user'),
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.layergroupid);
|
||||
assert.equal(res.headers['x-layergroup-id'], parsed.layergroupid);
|
||||
|
||||
global.environment.postgres.host = backupDBHost;
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// See https://github.com/CartoDB/Windshaft-cartodb/issues/89
|
||||
it("getting a tile with a user-specific database password", function(done) {
|
||||
var layergroup = singleLayergroupConfig(pointSql, '#layer { marker-fill:red; }');
|
||||
|
||||
var backupDBPass = global.environment.postgres_auth_pass;
|
||||
global.environment.postgres_auth_pass = '<%= user_password %>';
|
||||
|
||||
assert.response(server,
|
||||
createRequest(layergroup, 'cartodb250user', '4321'),
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.layergroupid);
|
||||
assert.equal(res.headers['x-layergroup-id'], parsed.layergroupid);
|
||||
|
||||
global.environment.postgres_auth_pass = backupDBPass;
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("creating a layergroup from lzma param", function(done){
|
||||
var params = {
|
||||
config: JSON.stringify(singleLayergroupConfig(pointSql, '#layer { marker-fill:red; }'))
|
||||
};
|
||||
|
||||
testHelper.lzma_compress_to_base64(JSON.stringify(params), 1, function(err, lzma) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.response(server,
|
||||
{
|
||||
url: layergroupUrl + '?lzma=' + encodeURIComponent(lzma),
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
},
|
||||
encoding: 'binary'
|
||||
},
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.layergroupid);
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("creating a layergroup from lzma param, invalid json input", function(done) {
|
||||
var params = {
|
||||
config: 'WADUS'
|
||||
};
|
||||
|
||||
testHelper.lzma_compress_to_base64(JSON.stringify(params), 1, function(err, lzma) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
assert.response(server,
|
||||
{
|
||||
url: layergroupUrl + '?lzma=' + encodeURIComponent(lzma),
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
},
|
||||
encoding: 'binary'
|
||||
},
|
||||
{
|
||||
status: 400
|
||||
},
|
||||
function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.deepEqual(parsed, { errors: [ 'Unexpected token W' ] });
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("uses queries postgresql to figure affected tables in query", function(done) {
|
||||
var tableName = 'gadm4';
|
||||
var expectedCacheChannel = _.template('<%= databaseName %>:public.<%= tableName %>', {
|
||||
databaseName: _.template(global.environment.postgres_auth_user, {user_id:1}) + '_db',
|
||||
tableName: tableName
|
||||
});
|
||||
|
||||
var layergroup = singleLayergroupConfig('select * from ' + tableName, '#gadm4 { marker-fill: red; }');
|
||||
|
||||
assert.response(server,
|
||||
{
|
||||
url: layergroupUrl + '?config=' + encodeURIComponent(JSON.stringify(layergroup)),
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
}
|
||||
},
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.layergroupid);
|
||||
|
||||
assert.ok(res.headers.hasOwnProperty('x-cache-channel'));
|
||||
assert.equal(res.headers['x-cache-channel'], expectedCacheChannel);
|
||||
|
||||
assert.equal(res.headers['x-layergroup-id'], parsed.layergroupid);
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// https://github.com/CartoDB/cartodb-postgresql/issues/86
|
||||
it.skip("should not fail with long table names because table name length limit", function(done) {
|
||||
var tableName = 'long_table_name_with_enough_chars_to_break_querytables_function';
|
||||
var expectedCacheChannel = _.template('<%= databaseName %>:public.<%= tableName %>', {
|
||||
databaseName: _.template(global.environment.postgres_auth_user, {user_id:1}) + '_db',
|
||||
tableName: tableName
|
||||
});
|
||||
|
||||
var layergroup = singleLayergroupConfig('select * from ' + tableName, '#layer { marker-fill: red; }');
|
||||
|
||||
assert.response(server,
|
||||
{
|
||||
url: layergroupUrl + '?config=' + encodeURIComponent(JSON.stringify(layergroup)),
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
}
|
||||
},
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.ok(parsed.layergroupid);
|
||||
|
||||
assert.ok(res.headers.hasOwnProperty('x-cache-channel'));
|
||||
assert.equal(res.headers['x-cache-channel'], expectedCacheChannel);
|
||||
|
||||
assert.equal(res.headers['x-layergroup-id'], parsed.layergroupid);
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("creates layergroup fails when postgresql queries fail to figure affected tables in query", function(done) {
|
||||
|
||||
var runQueryFn = PgQueryRunner.prototype.run;
|
||||
PgQueryRunner.prototype.run = function(username, query, queryHandler, callback) {
|
||||
return queryHandler(new Error('fake error message'), [], callback);
|
||||
};
|
||||
|
||||
var layergroup = singleLayergroupConfig('select * from gadm4', '#gadm4 { marker-fill: red; }');
|
||||
|
||||
assert.response(server,
|
||||
{
|
||||
url: layergroupUrl + '?config=' + encodeURIComponent(JSON.stringify(layergroup)),
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
}
|
||||
},
|
||||
{
|
||||
status: 400
|
||||
},
|
||||
function(res) {
|
||||
PgQueryRunner.prototype.run = runQueryFn;
|
||||
|
||||
assert.ok(!res.headers.hasOwnProperty('x-cache-channel'));
|
||||
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.deepEqual(parsed, {
|
||||
errors: ["Error: could not fetch affected tables and last updated time: fake error message"]
|
||||
});
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it("tile requests works when postgresql queries fail to figure affected tables in query", function(done) {
|
||||
var layergroup = singleLayergroupConfig('select * from gadm4', '#gadm4 { marker-fill: red; }');
|
||||
assert.response(server,
|
||||
{
|
||||
url: layergroupUrl + '?config=' + encodeURIComponent(JSON.stringify(layergroup)),
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
}
|
||||
},
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
var runQueryFn = PgQueryRunner.prototype.run;
|
||||
PgQueryRunner.prototype.run = function(username, query, queryHandler, callback) {
|
||||
return queryHandler(new Error('failed to query database for affected tables'), [], callback);
|
||||
};
|
||||
|
||||
// reset internal cacheChannel cache
|
||||
serverOptions.channelCache = {};
|
||||
|
||||
assert.response(server,
|
||||
{
|
||||
url: layergroupUrl + _.template('/<%= layergroupId %>/<%= z %>/<%= x %>/<%= y %>.png', {
|
||||
layergroupId: JSON.parse(res.body).layergroupid,
|
||||
z: 0,
|
||||
x: 0,
|
||||
y: 0
|
||||
}),
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
},
|
||||
encoding: 'binary'
|
||||
},
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res) {
|
||||
assert.ok(!res.headers.hasOwnProperty('x-cache-channel'));
|
||||
PgQueryRunner.prototype.run = runQueryFn;
|
||||
done();
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
737
test/acceptance/named_layers.js
Normal file
@@ -0,0 +1,737 @@
|
||||
var test_helper = require('../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);
|
||||
|
||||
var RedisPool = require('redis-mpool');
|
||||
var TemplateMaps = require('../../lib/cartodb/template_maps.js');
|
||||
|
||||
var step = require('step');
|
||||
|
||||
describe('named_layers', function() {
|
||||
// configure redis pool instance to use in tests
|
||||
var redisPool = new RedisPool(global.environment.redis);
|
||||
|
||||
var templateMaps = new TemplateMaps(redisPool, {
|
||||
max_user_templates: global.environment.maxUserTemplates
|
||||
});
|
||||
|
||||
var wadusLayer = {
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: 'select 1 cartodb_id, null::geometry the_geom_webmercator',
|
||||
cartocss: '#layer { marker-fill: <%= color %>; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
};
|
||||
|
||||
var username = 'localhost';
|
||||
|
||||
var templateName = 'valid_template';
|
||||
var template = {
|
||||
version: '0.0.1',
|
||||
name: templateName,
|
||||
auth: {
|
||||
method: 'open'
|
||||
},
|
||||
"placeholders": {
|
||||
"color": {
|
||||
"type": "css_color",
|
||||
"default": "#cc3300"
|
||||
}
|
||||
},
|
||||
layergroup: {
|
||||
layers: [
|
||||
wadusLayer,
|
||||
wadusLayer
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
var tokenAuthTemplateName = 'auth_valid_template';
|
||||
var tokenAuthTemplate = {
|
||||
version: '0.0.1',
|
||||
name: tokenAuthTemplateName,
|
||||
auth: {
|
||||
method: 'token',
|
||||
valid_tokens: ['valid1', 'valid2']
|
||||
},
|
||||
placeholders: {
|
||||
color: {
|
||||
"type": "css_color",
|
||||
"default": "#cc3300"
|
||||
}
|
||||
},
|
||||
layergroup: {
|
||||
layers: [
|
||||
wadusLayer
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var namedMapLayer = {
|
||||
type: 'named',
|
||||
options: {
|
||||
name: templateName,
|
||||
config: {},
|
||||
auth_tokens: []
|
||||
}
|
||||
};
|
||||
|
||||
var nestedNamedMapTemplateName = 'nested_template';
|
||||
var nestedNamedMapTemplate = {
|
||||
version: '0.0.1',
|
||||
name: nestedNamedMapTemplateName,
|
||||
auth: {
|
||||
method: 'open'
|
||||
},
|
||||
layergroup: {
|
||||
layers: [
|
||||
namedMapLayer
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
before(function(done) {
|
||||
global.environment.enabledFeatures = {cdbQueryTablesFromPostgres: true};
|
||||
templateMaps.addTemplate(username, nestedNamedMapTemplate, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
templateMaps.addTemplate(username, tokenAuthTemplate, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
templateMaps.addTemplate(username, template, function(err) {
|
||||
return done(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail for non-existing template name', function(done) {
|
||||
var layergroup = {
|
||||
version: '1.3.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'named',
|
||||
options: {
|
||||
name: 'nonexistent'
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
step(
|
||||
function createLayergroup() {
|
||||
var next = this;
|
||||
assert.response(server,
|
||||
{
|
||||
url: '/api/v1/map',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(layergroup)
|
||||
},
|
||||
{
|
||||
status: 400
|
||||
},
|
||||
function(res, err) {
|
||||
next(err, res);
|
||||
}
|
||||
);
|
||||
},
|
||||
function checkLayergroup(err, response) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
var parsedBody = JSON.parse(response.body);
|
||||
assert.deepEqual(parsedBody, { errors: ["Template 'nonexistent' of user 'localhost' not found"] });
|
||||
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should return 403 if not properly authorized', function(done) {
|
||||
|
||||
var layergroup = {
|
||||
version: '1.3.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'named',
|
||||
options: {
|
||||
name: tokenAuthTemplateName,
|
||||
config: {},
|
||||
auth_tokens: ['token1']
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
step(
|
||||
function createLayergroup() {
|
||||
var next = this;
|
||||
assert.response(server,
|
||||
{
|
||||
url: '/api/v1/map',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(layergroup)
|
||||
},
|
||||
{
|
||||
status: 403
|
||||
},
|
||||
function(res, err) {
|
||||
next(err, res);
|
||||
}
|
||||
);
|
||||
},
|
||||
function checkLayergroup(err, response) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
var parsedBody = JSON.parse(response.body);
|
||||
assert.deepEqual(
|
||||
parsedBody,
|
||||
{ errors: [ "Unauthorized 'auth_valid_template' template instantiation" ] }
|
||||
);
|
||||
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
it('should return 200 and layergroup if properly authorized', function(done) {
|
||||
|
||||
var layergroup = {
|
||||
version: '1.3.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'named',
|
||||
options: {
|
||||
name: tokenAuthTemplateName,
|
||||
config: {},
|
||||
auth_tokens: ['valid1']
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
step(
|
||||
function createLayergroup() {
|
||||
var next = this;
|
||||
assert.response(server,
|
||||
{
|
||||
url: '/api/v1/map',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(layergroup)
|
||||
},
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res, err) {
|
||||
next(err, res);
|
||||
}
|
||||
);
|
||||
},
|
||||
function checkLayergroup(err, response) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
var parsedBody = JSON.parse(response.body);
|
||||
assert.ok(parsedBody.layergroupid);
|
||||
assert.ok(parsedBody.last_updated);
|
||||
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
it('should return 400 for nested named map layers', function(done) {
|
||||
|
||||
var layergroup = {
|
||||
version: '1.3.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'named',
|
||||
options: {
|
||||
name: nestedNamedMapTemplateName
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
step(
|
||||
function createLayergroup() {
|
||||
var next = this;
|
||||
assert.response(server,
|
||||
{
|
||||
url: '/api/v1/map',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(layergroup)
|
||||
},
|
||||
{
|
||||
status: 400
|
||||
},
|
||||
function(res, err) {
|
||||
next(err, res);
|
||||
}
|
||||
);
|
||||
},
|
||||
function checkLayergroup(err, response) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
var parsedBody = JSON.parse(response.body);
|
||||
assert.deepEqual(parsedBody, { errors: [ 'Nested named layers are not allowed' ] });
|
||||
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
it('should return 200 and layergroup with private tables', function(done) {
|
||||
|
||||
var privateTableTemplateName = 'private_table_template';
|
||||
var privateTableTemplate = {
|
||||
version: '0.0.1',
|
||||
name: privateTableTemplateName,
|
||||
auth: {
|
||||
method: 'open'
|
||||
},
|
||||
layergroup: {
|
||||
layers: [
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: 'select * from test_table_private_1',
|
||||
cartocss: '#layer { marker-fill: #cc3300; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
var layergroup = {
|
||||
version: '1.3.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'named',
|
||||
options: {
|
||||
name: privateTableTemplateName
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
step(
|
||||
function createTemplate() {
|
||||
templateMaps.addTemplate(username, privateTableTemplate, this);
|
||||
},
|
||||
function createLayergroup(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
var next = this;
|
||||
assert.response(server,
|
||||
{
|
||||
url: '/api/v1/map',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(layergroup)
|
||||
},
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res, err) {
|
||||
next(err, res);
|
||||
}
|
||||
);
|
||||
},
|
||||
function checkLayergroup(err, response) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
var parsedBody = JSON.parse(response.body);
|
||||
assert.ok(parsedBody.layergroupid);
|
||||
assert.ok(parsedBody.last_updated);
|
||||
|
||||
return parsedBody.layergroupid;
|
||||
},
|
||||
function requestTile(err, layergroupId) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
var next = this;
|
||||
assert.response(server,
|
||||
{
|
||||
url: '/api/v1/map/' + layergroupId + '/0/0/0.png',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
},
|
||||
encoding: 'binary'
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'image/png'
|
||||
}
|
||||
},
|
||||
function(res, err) {
|
||||
next(err, res);
|
||||
}
|
||||
);
|
||||
},
|
||||
function handleTileResponse(err, res) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
test_helper.checkCache(res);
|
||||
return true;
|
||||
},
|
||||
function deleteTemplate(err) {
|
||||
var next = this;
|
||||
templateMaps.delTemplate(username, privateTableTemplate, function(/*delErr*/) {
|
||||
// ignore deletion error
|
||||
next(err);
|
||||
});
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
it('should return 200 and layergroup with private tables and interactivity', function(done) {
|
||||
|
||||
var privateTableTemplateNameInteractivity = 'private_table_template_interactivity';
|
||||
var privateTableTemplate = {
|
||||
"version": "0.0.1",
|
||||
"auth": {
|
||||
"method": "open"
|
||||
},
|
||||
"name": privateTableTemplateNameInteractivity,
|
||||
"layergroup": {
|
||||
"layers": [
|
||||
{
|
||||
"type": "cartodb",
|
||||
"options": {
|
||||
"attributes": {
|
||||
"columns": [
|
||||
"name"
|
||||
],
|
||||
"id": "cartodb_id"
|
||||
},
|
||||
"cartocss": "#layer { marker-fill: #cc3300; }",
|
||||
"cartocss_version": "2.3.0",
|
||||
"interactivity": "cartodb_id",
|
||||
"sql": "select * from test_table_private_1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
var layergroup = {
|
||||
version: '1.3.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'named',
|
||||
options: {
|
||||
name: privateTableTemplateNameInteractivity
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
step(
|
||||
function createTemplate() {
|
||||
templateMaps.addTemplate(username, privateTableTemplate, this);
|
||||
},
|
||||
function createLayergroup(err) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
var next = this;
|
||||
assert.response(server,
|
||||
{
|
||||
url: '/api/v1/map',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(layergroup)
|
||||
},
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res, err) {
|
||||
next(err, res);
|
||||
}
|
||||
);
|
||||
},
|
||||
function checkLayergroup(err, response) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
var parsedBody = JSON.parse(response.body);
|
||||
assert.ok(parsedBody.layergroupid);
|
||||
assert.ok(parsedBody.last_updated);
|
||||
|
||||
return parsedBody.layergroupid;
|
||||
},
|
||||
function requestTile(err, layergroupId) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
var next = this;
|
||||
assert.response(server,
|
||||
{
|
||||
url: '/api/v1/map/' + layergroupId + '/0/0/0.png',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
},
|
||||
encoding: 'binary'
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
headers: {
|
||||
'content-type': 'image/png'
|
||||
}
|
||||
},
|
||||
function(res, err) {
|
||||
next(err, res);
|
||||
}
|
||||
);
|
||||
},
|
||||
function handleTileResponse(err, res) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
test_helper.checkCache(res);
|
||||
return true;
|
||||
},
|
||||
function deleteTemplate(err) {
|
||||
var next = this;
|
||||
templateMaps.delTemplate(username, privateTableTemplate, function(/*delErr*/) {
|
||||
// ignore deletion error
|
||||
next(err);
|
||||
});
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
it('should return 403 when private table is accessed from non named layer', function(done) {
|
||||
|
||||
var layergroup = {
|
||||
version: '1.3.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: 'select * from test_table_private_1',
|
||||
cartocss: '#layer { marker-fill: #cc3300; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'named',
|
||||
options: {
|
||||
name: templateName
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
step(
|
||||
function createLayergroup() {
|
||||
var next = this;
|
||||
assert.response(server,
|
||||
{
|
||||
url: '/api/v1/map',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(layergroup)
|
||||
},
|
||||
{
|
||||
status: 403
|
||||
},
|
||||
function(res, err) {
|
||||
next(err, res);
|
||||
}
|
||||
);
|
||||
},
|
||||
function checkLayergroup(err, response) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
var parsedBody = JSON.parse(response.body);
|
||||
assert.ok(parsedBody.errors[0].match(/permission denied for relation test_table_private_1/));
|
||||
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
it('should return metadata for named layers', function(done) {
|
||||
|
||||
var layergroup = {
|
||||
version: '1.3.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'plain',
|
||||
options: {
|
||||
color: '#fabada'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: 'select * from test_table',
|
||||
cartocss: '#layer { marker-fill: #cc3300; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'named',
|
||||
options: {
|
||||
name: templateName
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'torque',
|
||||
options: {
|
||||
sql: "select * from test_table LIMIT 0",
|
||||
cartocss: "Map { -torque-frame-count:1; -torque-resolution:1; " +
|
||||
"-torque-aggregation-function:'count(*)'; -torque-time-attribute:'updated_at'; }"
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
step(
|
||||
function createLayergroup() {
|
||||
var next = this;
|
||||
assert.response(server,
|
||||
{
|
||||
url: '/api/v1/map',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(layergroup)
|
||||
},
|
||||
{
|
||||
status: 200
|
||||
},
|
||||
function(res, err) {
|
||||
next(err, res);
|
||||
}
|
||||
);
|
||||
},
|
||||
function checkLayergroup(err, response) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
var parsedBody = JSON.parse(response.body);
|
||||
assert.ok(parsedBody.metadata);
|
||||
assert.ok(parsedBody.metadata.layers);
|
||||
assert.equal(parsedBody.metadata.layers.length, 5);
|
||||
assert.equal(parsedBody.metadata.layers[0].type, 'plain');
|
||||
assert.equal(parsedBody.metadata.layers[1].type, 'mapnik');
|
||||
assert.equal(parsedBody.metadata.layers[2].type, 'mapnik');
|
||||
assert.equal(parsedBody.metadata.layers[3].type, 'mapnik');
|
||||
assert.equal(parsedBody.metadata.layers[4].type, 'torque');
|
||||
|
||||
return null;
|
||||
},
|
||||
function finish(err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
after(function(done) {
|
||||
global.environment.enabledFeatures = {cdbQueryTablesFromPostgres: false};
|
||||
templateMaps.delTemplate(username, nestedNamedMapTemplateName, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
templateMaps.delTemplate(username, tokenAuthTemplateName, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
templateMaps.delTemplate(username, templateName, function(err) {
|
||||
return done(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
189
test/acceptance/named_static_maps.js
Normal file
@@ -0,0 +1,189 @@
|
||||
var test_helper = require('../support/test_helper');
|
||||
var RedisPool = require('redis-mpool');
|
||||
var querystring = require('querystring');
|
||||
|
||||
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);
|
||||
var TemplateMaps = require('../../lib/cartodb/template_maps.js');
|
||||
var NamedMapsCacheEntry = require('../../lib/cartodb/cache/model/named_maps_entry');
|
||||
|
||||
describe('named static maps', function() {
|
||||
// configure redis pool instance to use in tests
|
||||
var redisPool = new RedisPool(global.environment.redis);
|
||||
|
||||
var templateMaps = new TemplateMaps(redisPool, {
|
||||
max_user_templates: global.environment.maxUserTemplates
|
||||
});
|
||||
|
||||
var wadusLayer = {
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: 'select 1 cartodb_id, null::geometry the_geom_webmercator',
|
||||
cartocss: '#layer { marker-fill: <%= color %>; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
};
|
||||
|
||||
var username = 'localhost';
|
||||
|
||||
var templateName = 'valid_template';
|
||||
var template = {
|
||||
version: '0.0.1',
|
||||
name: templateName,
|
||||
auth: {
|
||||
method: 'open'
|
||||
},
|
||||
"placeholders": {
|
||||
"color": {
|
||||
"type": "css_color",
|
||||
"default": "#cc3300"
|
||||
}
|
||||
},
|
||||
layergroup: {
|
||||
layers: [
|
||||
wadusLayer
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
var tokenAuthTemplateName = 'auth_valid_template';
|
||||
var tokenAuthTemplate = {
|
||||
version: '0.0.1',
|
||||
name: tokenAuthTemplateName,
|
||||
auth: {
|
||||
method: 'token',
|
||||
valid_tokens: ['valid1', 'valid2']
|
||||
},
|
||||
placeholders: {
|
||||
color: {
|
||||
"type": "css_color",
|
||||
"default": "#cc3300"
|
||||
}
|
||||
},
|
||||
layergroup: {
|
||||
layers: [
|
||||
wadusLayer
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var namedMapLayer = {
|
||||
type: 'named',
|
||||
options: {
|
||||
name: templateName,
|
||||
config: {},
|
||||
auth_tokens: []
|
||||
}
|
||||
};
|
||||
|
||||
var nestedNamedMapTemplateName = 'nested_template';
|
||||
var nestedNamedMapTemplate = {
|
||||
version: '0.0.1',
|
||||
name: nestedNamedMapTemplateName,
|
||||
auth: {
|
||||
method: 'open'
|
||||
},
|
||||
layergroup: {
|
||||
layers: [
|
||||
namedMapLayer
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
before(function (done) {
|
||||
templateMaps.addTemplate(username, nestedNamedMapTemplate, function (err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
templateMaps.addTemplate(username, tokenAuthTemplate, function (err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
templateMaps.addTemplate(username, template, function (err) {
|
||||
return done(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
after(function (done) {
|
||||
templateMaps.delTemplate(username, nestedNamedMapTemplateName, function (err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
templateMaps.delTemplate(username, tokenAuthTemplateName, function (err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
templateMaps.delTemplate(username, templateName, function (err) {
|
||||
return done(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function getStaticMap(name, options, callback) {
|
||||
|
||||
var url = '/api/v1/map/static/named/' + name + '/640/480.png';
|
||||
if (options.params) {
|
||||
url = url + '?' + querystring.stringify(options.params);
|
||||
}
|
||||
var requestOptions = {
|
||||
url: url,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: username
|
||||
}
|
||||
};
|
||||
|
||||
console.log(url);
|
||||
|
||||
var statusCode = options.status || 200;
|
||||
|
||||
var expectedResponse = {
|
||||
status: statusCode,
|
||||
headers: {
|
||||
'Content-Type': statusCode === 200 ? 'image/png' : 'application/json; charset=utf-8'
|
||||
}
|
||||
};
|
||||
|
||||
assert.response(server,
|
||||
requestOptions,
|
||||
expectedResponse,
|
||||
function (res, err) {
|
||||
return callback(err, res);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
it('should return a 404 error for nonexistent template name', function (done) {
|
||||
var nonexistentName = 'nonexistent';
|
||||
getStaticMap(nonexistentName, { status: 404 }, function(err, res) {
|
||||
assert.ok(!err);
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.equal(parsed.error, "Template '" + nonexistentName + "' of user '" + username + "' not found");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 403 if not properly authorized', function(done) {
|
||||
getStaticMap(tokenAuthTemplateName, { status: 403 }, function(err, res) {
|
||||
assert.ok(!err);
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.equal(parsed.error, 'Unauthorized template instantiation');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 200 if properly authorized', function(done) {
|
||||
getStaticMap(tokenAuthTemplateName, { params: { auth_token: 'valid1' } }, function(err, res) {
|
||||
assert.ok(!err);
|
||||
test_helper.checkSurrogateKey(res, new NamedMapsCacheEntry(username, tokenAuthTemplateName).key());
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,671 +1,78 @@
|
||||
var assert = require('../support/assert');
|
||||
var tests = module.exports = {};
|
||||
var _ = require('underscore');
|
||||
var redis = require('redis');
|
||||
var querystring = require('querystring');
|
||||
require(__dirname + '/../support/test_helper');
|
||||
require('../support/test_helper');
|
||||
|
||||
var CartodbWindshaft = require(__dirname + '/../../lib/cartodb/cartodb_windshaft');
|
||||
var serverOptions = require(__dirname + '/../../lib/cartodb/server_options');
|
||||
var assert = require('../support/assert');
|
||||
var querystring = require('querystring');
|
||||
var step = require('step');
|
||||
|
||||
var CartodbWindshaft = require('../../lib/cartodb/cartodb_windshaft');
|
||||
var serverOptions = require('../../lib/cartodb/server_options')();
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
server.setMaxListeners(0);
|
||||
|
||||
|
||||
suite('server', function() {
|
||||
|
||||
var redis_client = redis.createClient(global.environment.redis.port);
|
||||
|
||||
suiteSetup(function(){
|
||||
});
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// GET UNSUPPORTED
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// TODO: I guess this should be a 404 instead...
|
||||
test("get call to server returns 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);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("get call to server returns 200", function(done){
|
||||
assert.response(server, {
|
||||
url: '/',
|
||||
url: '/version',
|
||||
method: 'GET'
|
||||
},{
|
||||
status: 200
|
||||
}, function() { done(); });
|
||||
});
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// GET STYLE
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
test("get'ing blank style returns default style", function(done){
|
||||
assert.response(server, {
|
||||
headers: {host: 'localhost'},
|
||||
url: '/tiles/my_table/style',
|
||||
method: 'GET'
|
||||
},{
|
||||
status: 200,
|
||||
headers: { 'X-Cache-Channel': 'cartodb_test_user_1_db:my_table' },
|
||||
body: '{"style":"#my_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;}"}'
|
||||
}, function() { done(); });
|
||||
});
|
||||
|
||||
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/43
|
||||
test("get'ing style of private table should fail when unauthenticated",
|
||||
function(done) {
|
||||
assert.response(server, {
|
||||
headers: {host: 'localhost'},
|
||||
url: '/tiles/test_table_private_1/style',
|
||||
method: 'GET'
|
||||
},{
|
||||
}, function(res) {
|
||||
// FIXME: should be 401 Unauthorized
|
||||
assert.equal(res.statusCode, 500, res.body);
|
||||
assert.deepEqual(JSON.parse(res.body),
|
||||
{error: 'Sorry, you are unauthorized (permission denied)'});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test("get'ing style of private table should succeed when authenticated",
|
||||
function(done) {
|
||||
assert.response(server, {
|
||||
headers: {host: 'localhost'},
|
||||
url: '/tiles/test_table_private_1/style?map_key=1234',
|
||||
method: 'GET'
|
||||
},{
|
||||
}, function(res) {
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
assert.deepEqual(res.body, '{"style":"#test_table_private_1 {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;}"}');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// POST STYLE
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
test("post'ing no style returns 400 with errors", function(done){
|
||||
assert.response(server, {
|
||||
headers: {host: 'localhost'},
|
||||
url: '/tiles/my_table/style',
|
||||
method: 'POST'
|
||||
},{
|
||||
status: 400,
|
||||
body: '{"error":"must send style information"}'
|
||||
}, function() { done(); });
|
||||
});
|
||||
|
||||
test("post'ing bad style returns 400 with error", function(done){
|
||||
assert.response(server, {
|
||||
url: '/tiles/my_table3/style?map_key=1234',
|
||||
method: 'POST',
|
||||
headers: {host: 'localhost', 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
data: querystring.stringify({style: '#my_table3{backgxxxxxround-color:#fff;}'})
|
||||
},{
|
||||
status: 500, // FIXME: should be 400 !
|
||||
body: /Unrecognized rule: backgxxxxxround-color/
|
||||
}, function() { done(); });
|
||||
});
|
||||
|
||||
test("post'ing unparseable style returns 400 with error", function(done){
|
||||
assert.response(server, {
|
||||
url: '/tiles/my_table3/style?map_key=1234',
|
||||
method: 'POST',
|
||||
headers: {host: 'localhost', 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
data: querystring.stringify({style: '#my_table3{'})
|
||||
},{
|
||||
status: 500, // FIXME: should be 400 !
|
||||
body: /Missing closing/
|
||||
}, function() { done(); });
|
||||
});
|
||||
|
||||
test("post'ing multiple bad styles returns 400 with error array", function(done){
|
||||
assert.response(server, {
|
||||
url: '/tiles/my_table4/style?map_key=1234',
|
||||
method: 'POST',
|
||||
headers: {host: 'localhost', 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
data: querystring.stringify({style: '#my_table4{backgxxxxxround-color:#fff;foo:bar}'})
|
||||
},{
|
||||
status: 500, // FIXME: should be 400 !
|
||||
status: 200
|
||||
}, function(res) {
|
||||
var parsed = JSON.parse(res.body);
|
||||
assert.equal(parsed.length, 2);
|
||||
assert.ok( RegExp(/Unrecognized rule: backgxxxxxround-color/).test(parsed[0]) );
|
||||
assert.ok( RegExp(/Unrecognized rule: foo/).test(parsed[1]) );
|
||||
assert.ok(parsed.hasOwnProperty('windshaft_cartodb'), "No 'windshaft_cartodb' version in " + parsed);
|
||||
console.log("Windshaft-cartodb: " + parsed.windshaft_cartodb);
|
||||
assert.ok(parsed.hasOwnProperty('windshaft'), "No 'windshaft' version in " + parsed);
|
||||
console.log("Windshaft: " + parsed.windshaft);
|
||||
assert.ok(parsed.hasOwnProperty('grainstore'), "No 'grainstore' version in " + parsed);
|
||||
console.log("Grainstore: " + parsed.grainstore);
|
||||
assert.ok(parsed.hasOwnProperty('node_mapnik'), "No 'node_mapnik' version in " + parsed);
|
||||
console.log("Node-mapnik: " + parsed.node_mapnik);
|
||||
assert.ok(parsed.hasOwnProperty('mapnik'), "No 'mapnik' version in " + parsed);
|
||||
console.log("Mapnik: " + parsed.mapnik);
|
||||
// TODO: check actual versions ?
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test("post'ing good style returns 200", function(done){
|
||||
assert.response(server, {
|
||||
url: '/tiles/my_table5/style?map_key=1234',
|
||||
method: 'POST',
|
||||
headers: {host: 'localhost', 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
data: querystring.stringify({style: 'Map {background-color:#fff;}'})
|
||||
},{
|
||||
}, function(res) {
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/38
|
||||
test("post'ing good style with auth passed as api_key returns 200", function(done){
|
||||
assert.response(server, {
|
||||
url: '/tiles/my_table5/style?api_key=1234',
|
||||
method: 'POST',
|
||||
headers: {host: 'localhost', 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
data: querystring.stringify({style: 'Map {background-color:#fff;}'})
|
||||
},{}, function(res) {
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
// See https://github.com/Vizzuality/cartodb-management/issues/155
|
||||
test("post'ing good style with no authentication returns an error", function(done){
|
||||
assert.response(server, {
|
||||
url: '/tiles/my_table5/style?map_key=1234',
|
||||
method: 'POST',
|
||||
headers: {host: 'localhost', 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
data: querystring.stringify({style: 'Map {background-color:#fff;}'})
|
||||
},{
|
||||
}, function(res) {
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
assert.response(server, {
|
||||
url: '/tiles/my_table5/style',
|
||||
method: 'POST',
|
||||
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
|
||||
assert.equal(res.statusCode, 500, res.body);
|
||||
assert.ok(res.body.indexOf('map state cannot be changed by unauthenticated request') != -1, res.body);
|
||||
|
||||
assert.response(server, {
|
||||
headers: {host: 'localhost'},
|
||||
url: '/tiles/my_table5/style',
|
||||
method: 'GET'
|
||||
},{
|
||||
status: 200,
|
||||
body: JSON.stringify({style: 'Map {background-color:#fff;}'})
|
||||
}, function() { done(); });
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("post'ing good style returns 200 then getting returns original style", function(done){
|
||||
var style = 'Map {background-color:#fff;}';
|
||||
assert.response(server, {
|
||||
url: '/tiles/my_table5/style?map_key=1234',
|
||||
method: 'POST',
|
||||
headers: {host: 'localhost', 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
data: querystring.stringify({style: style})
|
||||
},{
|
||||
}, function(res) {
|
||||
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
|
||||
assert.response(server, {
|
||||
headers: {host: 'localhost'},
|
||||
url: '/tiles/my_table5/style',
|
||||
method: 'GET'
|
||||
},{
|
||||
status: 200,
|
||||
body: JSON.stringify({style: style})
|
||||
}, function() { done(); });
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// DELETE STYLE
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Test that unauthenticated DELETE should fail
|
||||
// See https://github.com/Vizzuality/cartodb-management/issues/155
|
||||
test("delete'ing style with no authentication returns an error", function(done){
|
||||
assert.response(server, {
|
||||
url: '/tiles/my_table5/style',
|
||||
method: 'DELETE',
|
||||
headers: {host: 'localhost'},
|
||||
},{}, function(res) {
|
||||
// FIXME: should be 401 Unauthorized
|
||||
assert.equal(res.statusCode, 500, 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, {
|
||||
headers: {host: 'localhost'},
|
||||
url: '/tiles/my_table5/style?map_key=1234',
|
||||
method: 'GET'
|
||||
},{
|
||||
status: 200,
|
||||
body: JSON.stringify({style: 'Map {background-color:#fff;}'})
|
||||
}, function() { done(); });
|
||||
});
|
||||
});
|
||||
|
||||
test("delete'ing style returns 200 then getting returns default style", function(done){
|
||||
// this is the default style
|
||||
var style = '#my_table5 {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;}'
|
||||
assert.response(server, {
|
||||
url: '/tiles/my_table5/style?map_key=1234',
|
||||
method: 'DELETE',
|
||||
headers: {host: 'localhost'},
|
||||
},{}, function(res) {
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
|
||||
// Retrive style with authenticated request
|
||||
assert.response(server, {
|
||||
headers: {host: 'localhost'},
|
||||
url: '/tiles/my_table5/style?map_key=1234',
|
||||
method: 'GET'
|
||||
},{}, function(res) {
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
assert.deepEqual(JSON.parse(res.body).style, style);
|
||||
|
||||
// Now retrive style with unauthenticated request
|
||||
assert.response(server, {
|
||||
headers: {host: 'localhost'},
|
||||
url: '/tiles/my_table5/style',
|
||||
method: 'GET'
|
||||
}, {}, function(res) {
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
assert.deepEqual(JSON.parse(res.body).style, style);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/38
|
||||
test("delete'ing style with api_key is accepted", function(done){
|
||||
assert.response(server, {
|
||||
url: '/tiles/my_table5/style?api_key=1234',
|
||||
method: 'DELETE',
|
||||
headers: {host: 'localhost'},
|
||||
},{}, function(res) {
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// GET INFOWINDOW
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
test("get'ing blank infowindow returns blank", function(done){
|
||||
assert.response(server, {
|
||||
headers: {host: 'localhost'},
|
||||
url: '/tiles/my_tablez/infowindow',
|
||||
method: 'GET'
|
||||
},{
|
||||
status: 200,
|
||||
headers: { 'X-Cache-Channel': 'cartodb_test_user_1_db:my_tablez' },
|
||||
body: '{"infowindow":null}'
|
||||
}, function() { done(); });
|
||||
});
|
||||
|
||||
test("get'ing blank infowindow with callback returns blank with callback", function(done){
|
||||
assert.response(server, {
|
||||
headers: {host: 'localhost'},
|
||||
url: '/tiles/my_tablez/infowindow?callback=simon',
|
||||
method: 'GET'
|
||||
},{
|
||||
status: 200,
|
||||
body: 'simon({"infowindow":null});'
|
||||
}, function() { done(); });
|
||||
});
|
||||
|
||||
|
||||
test("get'ing completed infowindow with callback returns information with callback", function(done){
|
||||
assert.response(server, {
|
||||
headers: {host: 'localhost'},
|
||||
url: '/tiles/my_table/infowindow?callback=simon',
|
||||
method: 'GET'
|
||||
},{
|
||||
status: 200,
|
||||
body: 'simon({"infowindow":"this, that, the other"});'
|
||||
}, function() { done(); });
|
||||
});
|
||||
|
||||
test("get'ing infowindow of private table should fail when unauthenticated",
|
||||
function(done) {
|
||||
assert.response(server, {
|
||||
headers: {host: 'localhost'},
|
||||
url: '/tiles/test_table_private_1/infowindow',
|
||||
method: 'GET'
|
||||
},{}, function(res) {
|
||||
// FIXME: should be 401 Unauthorized
|
||||
assert.equal(res.statusCode, 500, res.body);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test("get'ing infowindow of private table should succeed when authenticated",
|
||||
function(done) {
|
||||
assert.response(server, {
|
||||
headers: {host: 'localhost'},
|
||||
url: '/tiles/test_table_private_1/infowindow?map_key=1234',
|
||||
method: 'GET'
|
||||
},{}, function(res) {
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// GET GRID
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
test("get'ing a json with default style should return an grid", function(done){
|
||||
assert.response(server, {
|
||||
headers: {host: 'localhost'},
|
||||
url: '/tiles/gadm4/6/31/24.grid.json',
|
||||
method: 'GET'
|
||||
},{
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'text/javascript; charset=utf-8; charset=utf-8',
|
||||
'X-Cache-Channel': 'cartodb_test_user_1_db:gadm4' }
|
||||
}, function() { done(); });
|
||||
});
|
||||
|
||||
test("get'ing a json with default style should return an grid", function(done){
|
||||
assert.response(server, {
|
||||
headers: {host: 'localhost'},
|
||||
url: '/tiles/gadm4/6/31/24.grid.json',
|
||||
method: 'GET'
|
||||
},{
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'text/javascript; charset=utf-8; charset=utf-8' }
|
||||
}, function() { done(); });
|
||||
});
|
||||
|
||||
test("get'ing a json with default style and sql should return a constrained grid", function(done){
|
||||
var sql = querystring.stringify({sql: "SELECT * FROM gadm4 WHERE codineprov = '08'"})
|
||||
assert.response(server, {
|
||||
headers: {host: 'localhost'},
|
||||
url: '/tiles/gadm4/6/31/24.grid.json?' + sql,
|
||||
method: 'GET'
|
||||
},{
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'text/javascript; charset=utf-8; charset=utf-8' }
|
||||
}, function() { done(); });
|
||||
});
|
||||
|
||||
test("get'ing the grid of a private table should fail when unauthenticated",
|
||||
function(done) {
|
||||
assert.response(server, {
|
||||
headers: {host: 'localhost'},
|
||||
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);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test("get'ing the grid of a private table should succeed when authenticated",
|
||||
function(done) {
|
||||
assert.response(server, {
|
||||
headers: {host: 'localhost'},
|
||||
url: '/tiles/test_table_private_1/6/31/24.grid.json?map_key=1234',
|
||||
method: 'GET'
|
||||
},{}, function(res) {
|
||||
assert.equal(res.statusCode, 200, res.body);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// GET TILE
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
test("get'ing a tile with default style should return an image", function(done){
|
||||
assert.response(server, {
|
||||
headers: {host: 'localhost'},
|
||||
url: '/tiles/gadm4/6/31/24.png?geom_type=polygon',
|
||||
method: 'GET'
|
||||
},{
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'image/png', 'X-Cache-Channel': 'cartodb_test_user_1_db:gadm4' }
|
||||
}, function() { done(); });
|
||||
});
|
||||
|
||||
test("get'ing a tile with default style and sql should return a constrained image", function(done){
|
||||
var sql = querystring.stringify({sql: "SELECT * FROM gadm4 WHERE codineprov = '08'"});
|
||||
assert.response(server, {
|
||||
headers: {host: 'localhost'},
|
||||
url: '/tiles/gadm4/6/31/24.png?' + sql,
|
||||
method: 'GET'
|
||||
},{
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'image/png' }
|
||||
}, function() { done(); });
|
||||
});
|
||||
|
||||
|
||||
test("get'ing a tile with default style and complex sql should return a constrained image", function(done){
|
||||
var sql = querystring.stringify({sql: "SELECT * FROM gadm4 WHERE codineprov = '08' AND codccaa > 60"})
|
||||
assert.response(server, {
|
||||
headers: {host: 'localhost'},
|
||||
url: '/tiles/gadm4/6/31/24.png?' + sql,
|
||||
method: 'GET'
|
||||
},{
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'image/png' }
|
||||
}, function() { done(); });
|
||||
});
|
||||
|
||||
test("get'ing a tile with data from private table should succeed when authenticated", function(done){
|
||||
// NOTE: may fail if grainstore < 0.3.0 is used by Windshaft
|
||||
var sql = querystring.stringify({sql: "SELECT * FROM test_table_private_1", map_key: 1234})
|
||||
assert.response(server, {
|
||||
headers: {host: 'localhost'},
|
||||
// NOTE: we encode a public table in the URL !
|
||||
url: '/tiles/gadm4/6/31/24.png?' + sql,
|
||||
method: 'GET'
|
||||
},{
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'image/png' }
|
||||
}, function() { done(); });
|
||||
});
|
||||
|
||||
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/38
|
||||
test("get'ing a tile with data from private table should succeed when authenticated with api_key", function(done){
|
||||
// NOTE: may fail if grainstore < 0.3.0 is used by Windshaft
|
||||
var sql = querystring.stringify({sql: "SELECT * FROM test_table_private_1", api_key: 1234})
|
||||
assert.response(server, {
|
||||
headers: {host: 'localhost'},
|
||||
url: '/tiles/gadm4/6/31/24.png?' + sql,
|
||||
method: 'GET'
|
||||
},{
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'image/png' }
|
||||
}, function() { done(); });
|
||||
});
|
||||
|
||||
test("get'ing a tile with data from private table should fail when unauthenticated", function(done){
|
||||
var sql = querystring.stringify({
|
||||
sql: "SELECT * FROM test_table_private_1",
|
||||
cache_buster:2 // this is to avoid getting the cached response
|
||||
});
|
||||
assert.response(server, {
|
||||
headers: {host: 'localhost'},
|
||||
url: '/tiles/gadm4/6/31/24.png?' + sql,
|
||||
method: 'GET'
|
||||
},{
|
||||
}, function(res) {
|
||||
// 401 Unauthorized
|
||||
assert.equal(res.statusCode, 401, res.statusCode + ': ' + res.body);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test("get'ing a tile with data from private table should fail when unauthenticated (uses old redis key)", function(done){
|
||||
var sql = querystring.stringify({
|
||||
sql: "SELECT * FROM test_table_private_1",
|
||||
cache_buster:3,
|
||||
// 1235 is written in rails:users:localhost:map_key SET
|
||||
// See https://github.com/Vizzuality/Windshaft-cartodb/issues/39
|
||||
map_key: 1235
|
||||
});
|
||||
assert.response(server, {
|
||||
headers: {host: 'localhost'},
|
||||
url: '/tiles/gadm4/6/31/24.png?' + sql,
|
||||
method: 'GET'
|
||||
},{
|
||||
}, function(res) {
|
||||
// 401 Unauthorized
|
||||
assert.equal(res.statusCode, 401, res.statusCode + ': ' + res.body);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// DELETE CACHE
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
test("forbids flushing cache without specifying table name", function(done) {
|
||||
assert.response(server, {
|
||||
headers: {host: 'localhost'},
|
||||
url: '/tiles/flush_cache',
|
||||
method: 'DELETE'
|
||||
},{}, function(res) {
|
||||
assert.equal(res.statusCode, 404, res.statusCode + ': ' + res.body);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test("allows flushing table cache by unauthenticated user", function(done) {
|
||||
assert.response(server, {
|
||||
headers: {host: 'localhost'},
|
||||
url: '/tiles/gadm4/flush_cache',
|
||||
method: 'DELETE'
|
||||
},{}, function(res) {
|
||||
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
|
||||
// TODO: also check that varnish is signalled (using VarnishEmu)
|
||||
// NOTE: requires enable_cache=1 in test.js
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// GET METADATA
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
test("does not provide metadata of private table to unauthenticated requests", function(done){
|
||||
assert.response(server, {
|
||||
headers: {host: 'localhost'},
|
||||
url: '/tiles/test_table_private_1/map_metadata',
|
||||
method: 'GET'
|
||||
},{}, function(res) {
|
||||
// FIXME: should be 401 instead
|
||||
assert.equal(res.statusCode, 500, res.statusCode + ': ' + res.body);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test("does provide metadata of private table to authenticated requests", function(done){
|
||||
assert.response(server, {
|
||||
headers: {host: 'localhost'},
|
||||
url: '/tiles/test_table_private_1/map_metadata?map_key=1234',
|
||||
method: 'GET'
|
||||
},{}, function(res) {
|
||||
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test("does provide metadata of public table to unauthenticated requests", function(done){
|
||||
assert.response(server, {
|
||||
headers: {host: 'localhost'},
|
||||
url: '/tiles/gadm4/map_metadata',
|
||||
method: 'GET'
|
||||
},{}, function(res) {
|
||||
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
|
||||
// TODO: show metadata ?
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test("does provide metadata of public table to authenticated requests", function(done){
|
||||
assert.response(server, {
|
||||
headers: {host: 'localhost'},
|
||||
url: '/tiles/gadm4/map_metadata?map_key=1234',
|
||||
method: 'GET'
|
||||
},{}, function(res) {
|
||||
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
|
||||
// TODO: show metadata ?
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// DELETE CACHE
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
test("forbids flushing cache without specifying table name", function(done) {
|
||||
assert.response(server, {
|
||||
headers: {host: 'localhost'},
|
||||
url: '/tiles/flush_cache',
|
||||
method: 'DELETE'
|
||||
},{}, function(res) {
|
||||
assert.equal(res.statusCode, 404, res.statusCode + ': ' + res.body);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
test("allows flushing table cache by unauthenticated user", function(done) {
|
||||
assert.response(server, {
|
||||
headers: {host: 'localhost'},
|
||||
url: '/tiles/gadm4/flush_cache',
|
||||
method: 'DELETE'
|
||||
},{}, function(res) {
|
||||
assert.equal(res.statusCode, 200, res.statusCode + ': ' + res.body);
|
||||
// TODO: also check that varnish is signalled (using VarnishEmu)
|
||||
// NOTE: requires enable_cache=1 in test.js
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Tear down
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
suiteTeardown(function(done) {
|
||||
// This test will add map_style records, like
|
||||
// 'map_style|null|publicuser|my_table',
|
||||
redis_client.keys("map_style|*", function(err, matches) {
|
||||
_.each(matches, function(k) { redis_client.del(k); });
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
suite('server old_api', function() {
|
||||
|
||||
// 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();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
2129
test/acceptance/templates.js
Normal file
295
test/acceptance/x_cache_channel.js
Normal file
@@ -0,0 +1,295 @@
|
||||
require('../support/test_helper');
|
||||
|
||||
var assert = require('../support/assert');
|
||||
var qs = require('querystring');
|
||||
|
||||
var CartodbWindshaft = require('../../lib/cartodb/cartodb_windshaft');
|
||||
var serverOptions = require('../../lib/cartodb/server_options')();
|
||||
var server = new CartodbWindshaft(serverOptions);
|
||||
server.setMaxListeners(0);
|
||||
|
||||
describe('get requests x-cache-channel', function() {
|
||||
|
||||
var statusOkResponse = {
|
||||
status: 200
|
||||
};
|
||||
|
||||
var mapConfig = {
|
||||
version: '1.3.0',
|
||||
layers: [
|
||||
{
|
||||
options: {
|
||||
sql: 'select * from test_table limit 2',
|
||||
cartocss: '#layer { marker-fill:red; }',
|
||||
cartocss_version: '2.3.0',
|
||||
attributes: {
|
||||
id:'cartodb_id',
|
||||
columns: [
|
||||
'name',
|
||||
'address'
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var layergroupRequest = {
|
||||
url: '/api/v1/map?config=' + encodeURIComponent(JSON.stringify(mapConfig)),
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
}
|
||||
};
|
||||
|
||||
function getRequest(url, addApiKey, callbackName) {
|
||||
var params = {};
|
||||
if (!!addApiKey) {
|
||||
params.api_key = '1234';
|
||||
}
|
||||
if (!!callbackName) {
|
||||
params.callback = callbackName;
|
||||
}
|
||||
|
||||
return {
|
||||
url: url + '?' + qs.stringify(params),
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function validateXCacheChannel(done, expectedCacheChannel) {
|
||||
return function(res, err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
assert.ok(res.headers['x-cache-channel']);
|
||||
if (expectedCacheChannel) {
|
||||
assert.equal(res.headers['x-cache-channel'], expectedCacheChannel);
|
||||
}
|
||||
|
||||
done();
|
||||
};
|
||||
}
|
||||
|
||||
function noXCacheChannelHeader(done) {
|
||||
return function(res, err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
assert.ok(
|
||||
!res.headers['x-cache-channel'],
|
||||
'did not expect x-cache-channel header, got: `' + res.headers['x-cache-channel'] + '`'
|
||||
);
|
||||
done();
|
||||
};
|
||||
}
|
||||
|
||||
function withLayergroupId(callback) {
|
||||
assert.response(
|
||||
server,
|
||||
layergroupRequest,
|
||||
statusOkResponse,
|
||||
function(res, err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
callback(null, JSON.parse(res.body).layergroupid);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
describe('header should be present', function() {
|
||||
|
||||
it('/api/v1/map Map instantiation', function(done) {
|
||||
assert.response(
|
||||
server,
|
||||
layergroupRequest,
|
||||
statusOkResponse,
|
||||
validateXCacheChannel(done, 'test_windshaft_cartodb_user_1_db:public.test_table')
|
||||
);
|
||||
});
|
||||
|
||||
it ('/api/v1/map/:token/:z/:x/:y@:scale_factor?x.:format Mapnik retina tiles', function(done) {
|
||||
withLayergroupId(function(err, layergroupId) {
|
||||
assert.response(
|
||||
server,
|
||||
getRequest('/api/v1/map/' + layergroupId + '/0/0/0@2x.png'),
|
||||
validateXCacheChannel(done, 'test_windshaft_cartodb_user_1_db:public.test_table')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it ('/api/v1/map/:token/:z/:x/:y@:scale_factor?x.:format Mapnik tiles', function(done) {
|
||||
withLayergroupId(function(err, layergroupId) {
|
||||
assert.response(
|
||||
server,
|
||||
getRequest('/api/v1/map/' + layergroupId + '/0/0/0.png'),
|
||||
validateXCacheChannel(done, 'test_windshaft_cartodb_user_1_db:public.test_table')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it ('/api/v1/map/:token/:layer/:z/:x/:y.(:format) Per :layer rendering', function(done) {
|
||||
withLayergroupId(function(err, layergroupId) {
|
||||
assert.response(
|
||||
server,
|
||||
getRequest('/api/v1/map/' + layergroupId + '/0/0/0/0.png'),
|
||||
validateXCacheChannel(done, 'test_windshaft_cartodb_user_1_db:public.test_table')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it ('/api/v1/map/:token/:layer/attributes/:fid endpoint for info windows', function(done) {
|
||||
withLayergroupId(function(err, layergroupId) {
|
||||
assert.response(
|
||||
server,
|
||||
getRequest('/api/v1/map/' + layergroupId + '/0/attributes/1'),
|
||||
validateXCacheChannel(done, 'test_windshaft_cartodb_user_1_db:public.test_table')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it ('/api/v1/map/static/center/:token/:z/:lat/:lng/:width/:height.:format static maps', function(done) {
|
||||
withLayergroupId(function(err, layergroupId) {
|
||||
assert.response(
|
||||
server,
|
||||
getRequest('/api/v1/map/static/center/' + layergroupId + '/0/0/0/400/300.png'),
|
||||
validateXCacheChannel(done, 'test_windshaft_cartodb_user_1_db:public.test_table')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it ('/api/v1/map/static/bbox/:token/:bbox/:width/:height.:format static maps', function(done) {
|
||||
withLayergroupId(function(err, layergroupId) {
|
||||
assert.response(
|
||||
server,
|
||||
getRequest('/api/v1/map/static/bbox/' + layergroupId + '/-45,-45,45,45/400/300.png'),
|
||||
validateXCacheChannel(done, 'test_windshaft_cartodb_user_1_db:public.test_table')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('header should NOT be present', function() {
|
||||
|
||||
it('/', function(done) {
|
||||
assert.response(
|
||||
server,
|
||||
getRequest('/'),
|
||||
statusOkResponse,
|
||||
noXCacheChannelHeader(done)
|
||||
);
|
||||
});
|
||||
|
||||
it('/version', function(done) {
|
||||
assert.response(
|
||||
server,
|
||||
getRequest('/version'),
|
||||
statusOkResponse,
|
||||
noXCacheChannelHeader(done)
|
||||
);
|
||||
});
|
||||
|
||||
it('/health', function(done) {
|
||||
assert.response(
|
||||
server,
|
||||
getRequest('/health'),
|
||||
statusOkResponse,
|
||||
noXCacheChannelHeader(done)
|
||||
);
|
||||
});
|
||||
|
||||
it('/api/v1/map/named list named maps', function(done) {
|
||||
assert.response(
|
||||
server,
|
||||
getRequest('/api/v1/map/named', true),
|
||||
statusOkResponse,
|
||||
noXCacheChannelHeader(done)
|
||||
);
|
||||
});
|
||||
|
||||
describe('with named maps', function() {
|
||||
|
||||
var templateName = 'x_cache';
|
||||
|
||||
before(function(done) {
|
||||
var template = {
|
||||
version: '0.0.1',
|
||||
name: templateName,
|
||||
auth: {
|
||||
method: 'open'
|
||||
},
|
||||
layergroup: mapConfig
|
||||
};
|
||||
|
||||
var namedMapRequest = {
|
||||
url: '/api/v1/map/named?api_key=1234',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
host: 'localhost',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
data: JSON.stringify(template)
|
||||
};
|
||||
|
||||
assert.response(
|
||||
server,
|
||||
namedMapRequest,
|
||||
statusOkResponse,
|
||||
function(res, err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
after(function(done) {
|
||||
assert.response(
|
||||
server,
|
||||
{
|
||||
url: '/api/v1/map/named/' + templateName + '?api_key=1234',
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
host: 'localhost'
|
||||
}
|
||||
},
|
||||
{
|
||||
status: 204
|
||||
},
|
||||
function(res, err) {
|
||||
done(err);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
it('/api/v1/map/named/:template_id Named map retrieval', function(done) {
|
||||
assert.response(
|
||||
server,
|
||||
getRequest('/api/v1/map/named/' + templateName, true),
|
||||
statusOkResponse,
|
||||
noXCacheChannelHeader(done)
|
||||
);
|
||||
});
|
||||
|
||||
it('/api/v1/map/named/:template_id/jsonp Named map retrieval', function(done) {
|
||||
assert.response(
|
||||
server,
|
||||
getRequest('/api/v1/map/named/' + templateName, true, 'cb'),
|
||||
statusOkResponse,
|
||||
noXCacheChannelHeader(done)
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
BIN
test/fixtures/blank.png
vendored
Normal file
|
After Width: | Height: | Size: 850 B |
BIN
test/fixtures/render-timeout-fallback.png
vendored
Normal file
|
After Width: | Height: | Size: 7.0 KiB |
1
test/fixtures/test_multilayer_bbox.grid.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"grid":[" "," "," "," "," "," "," "," "," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"," !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"],"keys":["","1"],"data":{"1":{"cartodb_id":1}}}
|
||||
BIN
test/fixtures/test_multilayer_bbox.png
vendored
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
1
test/fixtures/test_table_0_0_0_multilayer1.layer0.grid.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"grid":[" "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," !!! "," !!!!!! "," !!!!!!! "," !!!!!!!! "," !!!!!!!! "," !!!!!!!! "," !!!!!!! "," !!!!!! "," !!! "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "],"keys":["","2"],"data":{"2":{"cartodb_id":2}}}
|
||||
1
test/fixtures/test_table_0_0_0_multilayer1.layer1.grid.json
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"grid":[" "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," !! "," !!! "," !! "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "," "],"keys":["","2"],"data":{"2":{"cartodb_id":4}}}
|
||||
BIN
test/fixtures/test_table_0_0_0_multilayer1.png
vendored
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
test/fixtures/test_table_13_4011_3088_styled_black.png
vendored
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
test/fixtures/test_table_15_16046_12354_styled_black.png
vendored
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
test/fixtures/test_table_15_16046_12354_styled_blue.png
vendored
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
304
test/integration/mapconfig_named_layers_datasource.js
Normal file
@@ -0,0 +1,304 @@
|
||||
require('../support/test_helper');
|
||||
|
||||
var assert = require('assert');
|
||||
var RedisPool = require('redis-mpool');
|
||||
var TemplateMaps = require('../../lib/cartodb/template_maps.js');
|
||||
var PgConnection = require(__dirname + '/../../lib/cartodb/backends/pg_connection');
|
||||
var MapConfigNamedLayersAdapter = require('../../lib/cartodb/models/mapconfig_named_layers_adapter');
|
||||
|
||||
// configure redis pool instance to use in tests
|
||||
var redisPool = new RedisPool(global.environment.redis);
|
||||
var pgConnection = new PgConnection(require('cartodb-redis')({ pool: redisPool }));
|
||||
|
||||
var templateMaps = new TemplateMaps(redisPool, {
|
||||
max_user_templates: global.environment.maxUserTemplates
|
||||
});
|
||||
|
||||
var mapConfigNamedLayersAdapter = new MapConfigNamedLayersAdapter(templateMaps);
|
||||
|
||||
var wadusSql = 'select 1 wadusLayer, null::geometry the_geom_webmercator';
|
||||
var wadusLayer = {
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: wadusSql,
|
||||
cartocss: '#layer { marker-fill: black; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
};
|
||||
|
||||
var wadusTemplateSql = 'select 1 wadusTemplateLayer, null::geometry the_geom_webmercator';
|
||||
var wadusTemplateLayer = {
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: wadusTemplateSql,
|
||||
cartocss: '#layer { marker-fill: <%= color %>; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
};
|
||||
|
||||
var wadusMapnikSql = 'select 1 wadusMapnikLayer, null::geometry the_geom_webmercator';
|
||||
var wadusMapnikLayer = {
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: wadusMapnikSql,
|
||||
cartocss: '#layer { polygon-fill: <%= polygon_color %>; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
};
|
||||
|
||||
var username = 'localhost';
|
||||
|
||||
var templateName = 'valid_template';
|
||||
var template = {
|
||||
version: '0.0.1',
|
||||
name: templateName,
|
||||
auth: {
|
||||
method: 'open'
|
||||
},
|
||||
"placeholders": {
|
||||
"color": {
|
||||
"type": "css_color",
|
||||
"default": "#cc3300"
|
||||
}
|
||||
},
|
||||
layergroup: {
|
||||
layers: [
|
||||
wadusTemplateLayer
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
var multipleLayersTemplateName = 'multiple_valid_template';
|
||||
var multipleLayersTemplate = {
|
||||
version: '0.0.1',
|
||||
name: multipleLayersTemplateName,
|
||||
auth: {
|
||||
method: 'token',
|
||||
valid_tokens: ['valid1', 'valid2']
|
||||
},
|
||||
"placeholders": {
|
||||
"polygon_color": {
|
||||
"type": "css_color",
|
||||
"default": "green"
|
||||
},
|
||||
"color": {
|
||||
"type": "css_color",
|
||||
"default": "red"
|
||||
}
|
||||
},
|
||||
layergroup: {
|
||||
layers: [
|
||||
wadusMapnikLayer,
|
||||
wadusTemplateLayer
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
describe('named_layers datasources', function() {
|
||||
before(function(done) {
|
||||
templateMaps.addTemplate(username, template, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
templateMaps.addTemplate(username, multipleLayersTemplate, done);
|
||||
});
|
||||
});
|
||||
|
||||
function makeNamedMapLayerConfig(layers) {
|
||||
return {
|
||||
version: '1.3.0',
|
||||
layers: layers
|
||||
};
|
||||
}
|
||||
|
||||
var simpleNamedLayer = {
|
||||
type: 'named',
|
||||
options: {
|
||||
name: templateName
|
||||
}
|
||||
};
|
||||
|
||||
var multipleLayersNamedLayer = {
|
||||
type: 'named',
|
||||
options: {
|
||||
name: multipleLayersTemplateName,
|
||||
auth_tokens: ['valid2']
|
||||
}
|
||||
};
|
||||
|
||||
var testScenarios = [
|
||||
{
|
||||
desc: 'without datasource for non-named layers',
|
||||
config: makeNamedMapLayerConfig([wadusLayer]),
|
||||
test: function(err, layers, datasource, done) {
|
||||
assert.ok(!err);
|
||||
assert.equal(layers.length, 1);
|
||||
|
||||
assert.equal(layers[0].type, 'cartodb');
|
||||
assert.equal(layers[0].options.sql, wadusSql);
|
||||
assert.equal(datasource.getLayerDatasource(0), undefined);
|
||||
|
||||
done();
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
desc: 'with datasource for the named layer but not for the normal',
|
||||
config: makeNamedMapLayerConfig([wadusLayer, simpleNamedLayer]),
|
||||
test: function(err, layers, datasource, done) {
|
||||
assert.ok(!err);
|
||||
assert.equal(layers.length, 2);
|
||||
|
||||
assert.equal(layers[0].type, 'cartodb');
|
||||
assert.equal(layers[0].options.sql, wadusSql);
|
||||
assert.equal(datasource.getLayerDatasource(0), undefined);
|
||||
|
||||
assert.equal(layers[1].type, 'cartodb');
|
||||
assert.equal(layers[1].options.sql, wadusTemplateSql);
|
||||
var layerDatasource = datasource.getLayerDatasource(1);
|
||||
assert.notEqual(layerDatasource, undefined);
|
||||
assert.ok(layerDatasource.user);
|
||||
|
||||
done();
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
desc: 'with datasource for the multiple layers in the named but not for the normal',
|
||||
config: makeNamedMapLayerConfig([wadusLayer, multipleLayersNamedLayer]),
|
||||
test: function(err, layers, datasource, done) {
|
||||
assert.ok(!err);
|
||||
assert.equal(layers.length, 3);
|
||||
|
||||
assert.equal(layers[0].type, 'cartodb');
|
||||
assert.equal(layers[0].options.sql, wadusSql);
|
||||
assert.equal(datasource.getLayerDatasource(0), undefined);
|
||||
|
||||
assert.equal(layers[1].type, 'mapnik');
|
||||
assert.equal(layers[1].options.sql, wadusMapnikSql);
|
||||
var layerDatasource = datasource.getLayerDatasource(1);
|
||||
assert.notEqual(layerDatasource, undefined);
|
||||
assert.ok(layerDatasource.user);
|
||||
|
||||
assert.equal(layers[2].type, 'cartodb');
|
||||
assert.equal(layers[2].options.sql, wadusTemplateSql);
|
||||
layerDatasource = datasource.getLayerDatasource(2);
|
||||
assert.notEqual(layerDatasource, undefined);
|
||||
assert.ok(layerDatasource.user);
|
||||
|
||||
done();
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
desc: 'all with datasource because all are named',
|
||||
config: makeNamedMapLayerConfig([multipleLayersNamedLayer, simpleNamedLayer]),
|
||||
test: function(err, layers, datasource, done) {
|
||||
assert.ok(!err);
|
||||
assert.equal(layers.length, 3);
|
||||
|
||||
assert.equal(layers[0].type, 'mapnik');
|
||||
assert.equal(layers[0].options.sql, wadusMapnikSql);
|
||||
var layerDatasource = datasource.getLayerDatasource(0);
|
||||
assert.notEqual(layerDatasource, undefined);
|
||||
assert.ok(layerDatasource.user);
|
||||
|
||||
assert.equal(layers[1].type, 'cartodb');
|
||||
assert.equal(layers[1].options.sql, wadusTemplateSql);
|
||||
layerDatasource = datasource.getLayerDatasource(1);
|
||||
assert.notEqual(layerDatasource, undefined);
|
||||
assert.ok(layerDatasource.user);
|
||||
|
||||
assert.equal(layers[2].type, 'cartodb');
|
||||
assert.equal(layers[2].options.sql, wadusTemplateSql);
|
||||
layerDatasource = datasource.getLayerDatasource(2);
|
||||
assert.notEqual(layerDatasource, undefined);
|
||||
assert.ok(layerDatasource.user);
|
||||
|
||||
done();
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
desc: 'with a mix of datasource and no datasource depending if layers are named or not',
|
||||
config: makeNamedMapLayerConfig([
|
||||
simpleNamedLayer,
|
||||
multipleLayersNamedLayer,
|
||||
wadusLayer,
|
||||
simpleNamedLayer,
|
||||
wadusLayer,
|
||||
multipleLayersNamedLayer
|
||||
]),
|
||||
test: function(err, layers, datasource, done) {
|
||||
|
||||
assert.ok(!err);
|
||||
assert.equal(layers.length, 8);
|
||||
|
||||
assert.equal(layers[0].type, 'cartodb');
|
||||
assert.equal(layers[0].options.sql, wadusTemplateSql);
|
||||
var layerDatasource = datasource.getLayerDatasource(0);
|
||||
assert.notEqual(layerDatasource, undefined);
|
||||
assert.ok(layerDatasource.user);
|
||||
|
||||
assert.equal(layers[1].type, 'mapnik');
|
||||
assert.equal(layers[1].options.sql, wadusMapnikSql);
|
||||
layerDatasource = datasource.getLayerDatasource(1);
|
||||
assert.notEqual(layerDatasource, undefined);
|
||||
assert.ok(layerDatasource.user);
|
||||
|
||||
assert.equal(layers[2].type, 'cartodb');
|
||||
assert.equal(layers[2].options.sql, wadusTemplateSql);
|
||||
layerDatasource = datasource.getLayerDatasource(2);
|
||||
assert.notEqual(layerDatasource, undefined);
|
||||
assert.ok(layerDatasource.user);
|
||||
|
||||
assert.equal(layers[3].type, 'cartodb');
|
||||
assert.equal(layers[3].options.sql, wadusSql);
|
||||
assert.equal(datasource.getLayerDatasource(3), undefined);
|
||||
|
||||
assert.equal(layers[4].type, 'cartodb');
|
||||
assert.equal(layers[4].options.sql, wadusTemplateSql);
|
||||
layerDatasource = datasource.getLayerDatasource(4);
|
||||
assert.notEqual(layerDatasource, undefined);
|
||||
assert.ok(layerDatasource.user);
|
||||
|
||||
assert.equal(layers[5].type, 'cartodb');
|
||||
assert.equal(layers[5].options.sql, wadusSql);
|
||||
assert.equal(datasource.getLayerDatasource(5), undefined);
|
||||
|
||||
assert.equal(layers[6].type, 'mapnik');
|
||||
assert.equal(layers[6].options.sql, wadusMapnikSql);
|
||||
layerDatasource = datasource.getLayerDatasource(6);
|
||||
assert.notEqual(layerDatasource, undefined);
|
||||
assert.ok(layerDatasource.user);
|
||||
|
||||
assert.equal(layers[7].type, 'cartodb');
|
||||
assert.equal(layers[7].options.sql, wadusTemplateSql);
|
||||
layerDatasource = datasource.getLayerDatasource(7);
|
||||
assert.notEqual(layerDatasource, undefined);
|
||||
assert.ok(layerDatasource.user);
|
||||
|
||||
done();
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
testScenarios.forEach(function(testScenario) {
|
||||
it('should return a list of layers ' + testScenario.desc, function(done) {
|
||||
mapConfigNamedLayersAdapter.getLayers(username, testScenario.config.layers, pgConnection,
|
||||
function(err, layers, datasource) {
|
||||
testScenario.test(err, layers, datasource, done);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
after(function(done) {
|
||||
templateMaps.delTemplate(username, templateName, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
templateMaps.delTemplate(username, multipleLayersTemplateName, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
327
test/integration/mapconfig_named_layers_expanded.js
Normal file
@@ -0,0 +1,327 @@
|
||||
require('../support/test_helper');
|
||||
|
||||
var assert = require('assert');
|
||||
var RedisPool = require('redis-mpool');
|
||||
var TemplateMaps = require('../../lib/cartodb/template_maps.js');
|
||||
var PgConnection = require(__dirname + '/../../lib/cartodb/backends/pg_connection');
|
||||
var MapConfigNamedLayersAdapter = require('../../lib/cartodb/models/mapconfig_named_layers_adapter');
|
||||
|
||||
describe('mapconfig_named_layers_adapter', function() {
|
||||
|
||||
// configure redis pool instance to use in tests
|
||||
var redisPool = new RedisPool(global.environment.redis);
|
||||
var pgConnection = new PgConnection(require('cartodb-redis')({ pool: redisPool }));
|
||||
|
||||
var templateMaps = new TemplateMaps(redisPool, {
|
||||
max_user_templates: global.environment.maxUserTemplates
|
||||
});
|
||||
|
||||
var mapConfigNamedLayersAdapter = new MapConfigNamedLayersAdapter(templateMaps);
|
||||
|
||||
var wadusLayer = {
|
||||
type: 'cartodb',
|
||||
options: {
|
||||
sql: 'select 1 cartodb_id, null::geometry the_geom_webmercator',
|
||||
cartocss: '#layer { marker-fill: <%= color %>; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
};
|
||||
|
||||
var wadusMapnikLayer = {
|
||||
type: 'mapnik',
|
||||
options: {
|
||||
sql: 'select 1 cartodb_id, null::geometry the_geom_webmercator',
|
||||
cartocss: '#layer { polygon-fill: <%= polygon_color %>; }',
|
||||
cartocss_version: '2.3.0'
|
||||
}
|
||||
};
|
||||
|
||||
var username = 'localhost';
|
||||
|
||||
var templateName = 'valid_template';
|
||||
var template = {
|
||||
version: '0.0.1',
|
||||
name: templateName,
|
||||
auth: {
|
||||
method: 'open'
|
||||
},
|
||||
"placeholders": {
|
||||
"color": {
|
||||
"type": "css_color",
|
||||
"default": "#cc3300"
|
||||
}
|
||||
},
|
||||
layergroup: {
|
||||
layers: [
|
||||
wadusLayer
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
var tokenAuthTemplateName = 'auth_valid_template';
|
||||
var tokenAuthTemplate = {
|
||||
version: '0.0.1',
|
||||
name: tokenAuthTemplateName,
|
||||
auth: {
|
||||
method: 'token',
|
||||
valid_tokens: ['valid1', 'valid2']
|
||||
},
|
||||
layergroup: {
|
||||
layers: [
|
||||
wadusLayer
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
var multipleLayersTemplateName = 'multiple_valid_template';
|
||||
var multipleLayersTemplate = {
|
||||
version: '0.0.1',
|
||||
name: multipleLayersTemplateName,
|
||||
auth: {
|
||||
method: 'token',
|
||||
valid_tokens: ['valid1', 'valid2']
|
||||
},
|
||||
"placeholders": {
|
||||
"polygon_color": {
|
||||
"type": "css_color",
|
||||
"default": "green"
|
||||
},
|
||||
"color": {
|
||||
"type": "css_color",
|
||||
"default": "red"
|
||||
}
|
||||
},
|
||||
layergroup: {
|
||||
layers: [
|
||||
wadusMapnikLayer,
|
||||
wadusLayer
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
var namedMapLayer = {
|
||||
type: 'named',
|
||||
options: {
|
||||
name: templateName,
|
||||
config: {},
|
||||
auth_tokens: []
|
||||
}
|
||||
};
|
||||
|
||||
var nestedNamedMapTemplateName = 'nested_template';
|
||||
var nestedNamedMapTemplate = {
|
||||
version: '0.0.1',
|
||||
name: nestedNamedMapTemplateName,
|
||||
auth: {
|
||||
method: 'open'
|
||||
},
|
||||
layergroup: {
|
||||
layers: [
|
||||
namedMapLayer
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
function makeNamedMapLayerConfig(options) {
|
||||
return {
|
||||
version: '1.3.0',
|
||||
layers: [
|
||||
{
|
||||
type: 'named',
|
||||
options: options
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
before(function(done) {
|
||||
templateMaps.addTemplate(username, template, done);
|
||||
});
|
||||
|
||||
it('should fail for named map layer with missing name', function(done) {
|
||||
var missingNamedMapLayerConfig = makeNamedMapLayerConfig({
|
||||
config: {}
|
||||
});
|
||||
mapConfigNamedLayersAdapter.getLayers(username, missingNamedMapLayerConfig.layers, pgConnection,
|
||||
function(err, layers, datasource) {
|
||||
assert.ok(err);
|
||||
assert.ok(!layers);
|
||||
assert.ok(!datasource);
|
||||
assert.equal(err.message, 'Missing Named Map `name` in layer options');
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should fail for non-existing template name', function(done) {
|
||||
var missingTemplateName = 'wadus';
|
||||
var nonExistentNamedMapLayerConfig = makeNamedMapLayerConfig({
|
||||
name: missingTemplateName
|
||||
});
|
||||
mapConfigNamedLayersAdapter.getLayers(username, nonExistentNamedMapLayerConfig.layers, pgConnection,
|
||||
function(err, layers, datasource) {
|
||||
assert.ok(err);
|
||||
assert.ok(!layers);
|
||||
assert.ok(!datasource);
|
||||
assert.equal(
|
||||
err.message, "Template '" + missingTemplateName + "' of user '" + username + "' not found"
|
||||
);
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should fail if not properly authorized', function(done) {
|
||||
templateMaps.addTemplate(username, tokenAuthTemplate, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var nonAuthTokensNamedMapLayerConfig = makeNamedMapLayerConfig({
|
||||
name: tokenAuthTemplateName
|
||||
});
|
||||
mapConfigNamedLayersAdapter.getLayers(username, nonAuthTokensNamedMapLayerConfig.layers, pgConnection,
|
||||
function(err, layers, datasource) {
|
||||
assert.ok(err);
|
||||
assert.ok(!layers);
|
||||
assert.ok(!datasource);
|
||||
assert.equal(err.message, "Unauthorized '" + tokenAuthTemplateName + "' template instantiation");
|
||||
|
||||
templateMaps.delTemplate(username, tokenAuthTemplateName, done);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should fail for nested named map layers', function(done) {
|
||||
templateMaps.addTemplate(username, nestedNamedMapTemplate, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var nestedNamedMapLayerConfig = makeNamedMapLayerConfig({
|
||||
name: nestedNamedMapTemplateName
|
||||
});
|
||||
mapConfigNamedLayersAdapter.getLayers(username, nestedNamedMapLayerConfig.layers, pgConnection,
|
||||
function(err, layers, datasource) {
|
||||
assert.ok(err);
|
||||
assert.ok(!layers);
|
||||
assert.ok(!datasource);
|
||||
assert.equal(err.message, 'Nested named layers are not allowed');
|
||||
|
||||
templateMaps.delTemplate(username, nestedNamedMapTemplateName, done);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an expanded list of layers for a named map layer', function(done) {
|
||||
var validNamedMapMapLayerConfig = makeNamedMapLayerConfig({
|
||||
name: templateName
|
||||
});
|
||||
mapConfigNamedLayersAdapter.getLayers(username, validNamedMapMapLayerConfig.layers, pgConnection,
|
||||
function(err, layers, datasource) {
|
||||
assert.ok(!err);
|
||||
assert.ok(layers.length, 1);
|
||||
assert.ok(layers[0].type, 'cartodb');
|
||||
assert.notEqual(datasource.getLayerDatasource(0), undefined);
|
||||
|
||||
done();
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should return on auth=token with valid tokens provided', function(done) {
|
||||
templateMaps.addTemplate(username, tokenAuthTemplate, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var validAuthTokensNamedMapLayerConfig = makeNamedMapLayerConfig({
|
||||
name: tokenAuthTemplateName,
|
||||
auth_tokens: ['valid1']
|
||||
});
|
||||
mapConfigNamedLayersAdapter.getLayers(username, validAuthTokensNamedMapLayerConfig.layers, pgConnection,
|
||||
function(err, layers, datasource) {
|
||||
assert.ok(!err);
|
||||
assert.equal(layers.length, 1);
|
||||
assert.notEqual(datasource.getLayerDatasource(0), undefined);
|
||||
|
||||
templateMaps.delTemplate(username, tokenAuthTemplateName, done);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an expanded list of layers for a named map layer, multiple layers version', function(done) {
|
||||
templateMaps.addTemplate(username, multipleLayersTemplate, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var multipleLayersNamedMapLayerConfig = makeNamedMapLayerConfig({
|
||||
name: multipleLayersTemplateName,
|
||||
auth_tokens: ['valid2']
|
||||
});
|
||||
mapConfigNamedLayersAdapter.getLayers(username, multipleLayersNamedMapLayerConfig.layers, pgConnection,
|
||||
function(err, layers, datasource) {
|
||||
assert.ok(!err);
|
||||
assert.equal(layers.length, 2);
|
||||
|
||||
assert.equal(layers[0].type, 'mapnik');
|
||||
assert.equal(layers[0].options.cartocss, '#layer { polygon-fill: green; }');
|
||||
assert.notEqual(datasource.getLayerDatasource(0), undefined);
|
||||
|
||||
assert.equal(layers[1].type, 'cartodb');
|
||||
assert.equal(layers[1].options.cartocss, '#layer { marker-fill: red; }');
|
||||
assert.notEqual(datasource.getLayerDatasource(1), undefined);
|
||||
|
||||
templateMaps.delTemplate(username, multipleLayersTemplateName, done);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should replace template params with the given config', function(done) {
|
||||
templateMaps.addTemplate(username, multipleLayersTemplate, function(err) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
|
||||
var color = '#cc3300',
|
||||
polygonColor = '#ff9900';
|
||||
|
||||
var multipleLayersNamedMapLayerConfig = makeNamedMapLayerConfig({
|
||||
name: multipleLayersTemplateName,
|
||||
config: {
|
||||
polygon_color: polygonColor,
|
||||
color: color
|
||||
},
|
||||
auth_tokens: ['valid2']
|
||||
});
|
||||
mapConfigNamedLayersAdapter.getLayers(username, multipleLayersNamedMapLayerConfig.layers, pgConnection,
|
||||
function(err, layers, datasource) {
|
||||
assert.ok(!err);
|
||||
assert.equal(layers.length, 2);
|
||||
|
||||
assert.equal(layers[0].type, 'mapnik');
|
||||
assert.equal(layers[0].options.cartocss, '#layer { polygon-fill: ' + polygonColor + '; }');
|
||||
assert.notEqual(datasource.getLayerDatasource(0), undefined);
|
||||
|
||||
assert.equal(layers[1].type, 'cartodb');
|
||||
assert.equal(layers[1].options.cartocss, '#layer { marker-fill: ' + color + '; }');
|
||||
assert.notEqual(datasource.getLayerDatasource(1), undefined);
|
||||
|
||||
templateMaps.delTemplate(username, multipleLayersTemplateName, done);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
after(function(done) {
|
||||
templateMaps.delTemplate(username, templateName, done);
|
||||
});
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
var net = require('net');
|
||||
|
||||
module.exports = function(on_cmd_recieved, test_callback) {
|
||||
var self = this;
|
||||
var welcome_msg = 'hi, im a varnish emu, right?';
|
||||
|
||||
self.commands_recieved = [];
|
||||
|
||||
var server = net.createServer(function (socket) {
|
||||
var command = '';
|
||||
socket.write("200 " + welcome_msg.length + "\n");
|
||||
socket.write(welcome_msg);
|
||||
socket.on('data', function(data) {
|
||||
self.commands_recieved.push(data);
|
||||
on_cmd_recieved && on_cmd_recieved(self.commands_recieved);
|
||||
socket.write('200 0\n');
|
||||
});
|
||||
});
|
||||
server.listen(1337, "127.0.0.1");
|
||||
|
||||
server.on('listening', function(){
|
||||
test_callback();
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,36 +1,117 @@
|
||||
// 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');
|
||||
|
||||
assert.imageEqualsFile = function(buffer, file_b, callback) {
|
||||
// @param tolerance number of tolerated grid cell differences
|
||||
assert.utfgridEqualsFile = function(buffer, file_b, tolerance, callback) {
|
||||
fs.writeFileSync('/tmp/grid.json', buffer, 'binary'); // <-- to debug/update
|
||||
var expected_json = JSON.parse(fs.readFileSync(file_b, 'utf8'));
|
||||
|
||||
var err = null;
|
||||
|
||||
var Celldiff = function(x, y, ev, ov) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.ev = ev;
|
||||
this.ov = ov;
|
||||
};
|
||||
|
||||
Celldiff.prototype.toString = function() {
|
||||
return '(' + this.x + ',' + this.y + ')["' + this.ev + '" != "' + this.ov + '"]';
|
||||
};
|
||||
|
||||
try {
|
||||
var obtained_json = JSON.parse(buffer);
|
||||
|
||||
// compare grid
|
||||
var obtained_grid = obtained_json.grid;
|
||||
var expected_grid = expected_json.grid;
|
||||
var nrows = obtained_grid.length
|
||||
if (nrows != expected_grid.length) {
|
||||
throw new Error( "Obtained grid rows (" + nrows +
|
||||
") != expected grid rows (" + expected_grid.length + ")" );
|
||||
}
|
||||
var celldiff = [];
|
||||
for (var i=0; i<nrows; ++i) {
|
||||
var ocols = obtained_grid[i];
|
||||
var ecols = expected_grid[i];
|
||||
var ncols = ocols.length;
|
||||
if ( ncols != ecols.length ) {
|
||||
throw new Error( "Obtained grid cols (" + ncols +
|
||||
") != expected grid cols (" + ecols.length +
|
||||
") on row " + i );
|
||||
}
|
||||
for (var j=0; j<ncols; ++j) {
|
||||
var ocell = ocols[j];
|
||||
var ecell = ecols[j];
|
||||
if ( ocell !== ecell ) {
|
||||
celldiff.push(new Celldiff(i, j, ecell, ocell));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( celldiff.length > tolerance ) {
|
||||
throw new Error( celldiff.length + " cell differences: " + celldiff );
|
||||
}
|
||||
|
||||
assert.deepEqual(obtained_json.keys, expected_json.keys);
|
||||
} catch (e) { err = e; }
|
||||
|
||||
callback(err);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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/' + (Math.random() * 1e16);
|
||||
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;
|
||||
|
||||
exec('compare -metric PSNR "' + 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();
|
||||
if (stderr === 'inf') {
|
||||
fs.unlinkSync(file_a);
|
||||
callback(null);
|
||||
} else {
|
||||
var similarity = parseFloat(stderr);
|
||||
var err = new Error('Images not equal(' + similarity + '): ' +
|
||||
file_a + ' ' + file_b);
|
||||
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(testImageFilePath);
|
||||
callback(null);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -145,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
|
||||
@@ -163,9 +246,10 @@ assert.response = function(server, req, res, msg){
|
||||
assert.equal(
|
||||
response.statusCode,
|
||||
status,
|
||||
msg + 'Invalid response status code.\n'
|
||||
msg + colorize('Invalid response status code.\n'
|
||||
+ ' Expected: [green]{' + status + '}\n'
|
||||
+ ' Got: [red]{' + response.statusCode + '}'
|
||||
+ ' Got: [red]{' + response.statusCode + '}\n'
|
||||
+ ' Response body: ' + response.body)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -190,7 +274,6 @@ assert.response = function(server, req, res, msg){
|
||||
|
||||
// Callback
|
||||
callback(response);
|
||||
check();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -198,3 +281,16 @@ assert.response = function(server, req, res, msg){
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Colorize the given string using ansi-escape sequences.
|
||||
* Disabled when --boring is set.
|
||||
*
|
||||
* @param {String} str
|
||||
* @return {String}
|
||||
*/
|
||||
function colorize(str) {
|
||||
var colors = { bold: 1, red: 31, green: 32, yellow: 33 };
|
||||
return str.replace(/\[(\w+)\]\{([^]*?)\}/g, function(_, color, str) {
|
||||
return '\x1B[' + colors[color] + 'm' + str + '\x1B[0m';
|
||||
});
|
||||
}
|
||||
@@ -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,27 +10,109 @@
|
||||
# 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
|
||||
|
||||
psql -c "CREATE EXTENSION plpythonu;" ${TEST_DB}
|
||||
curl -L -s https://github.com/CartoDB/cartodb-postgresql/raw/cdb/scripts-available/CDB_QueryStatements.sql -o sql/CDB_QueryStatements.sql
|
||||
curl -L -s https://github.com/CartoDB/cartodb-postgresql/raw/cdb/scripts-available/CDB_QueryTables.sql -o sql/CDB_QueryTables.sql
|
||||
cat sql/CDB_QueryStatements.sql sql/CDB_QueryTables.sql |
|
||||
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"
|
||||
|
||||
echo "Finished preparing data. Run tests with expresso."
|
||||
|
||||
14
test/support/sql/CDB_QueryStatements.sql
Normal file
@@ -0,0 +1,14 @@
|
||||
-- Return an array of statements found in the given query text
|
||||
--
|
||||
-- Regexp curtesy of Hubert Lubaczewski (depesz)
|
||||
-- Implemented in plpython for performance reasons
|
||||
--
|
||||
CREATE OR REPLACE FUNCTION CDB_QueryStatements(query text)
|
||||
RETURNS SETOF TEXT AS $$
|
||||
import re
|
||||
pat = re.compile( r'''((?:[^'"$;]+|"[^"]*"|'[^']*'|(\$[^$]*\$).*?\2)+)''', re.DOTALL )
|
||||
for match in pat.findall(query):
|
||||
cleaned = match[0].strip()
|
||||
if ( cleaned ):
|
||||
yield cleaned
|
||||
$$ language 'plpythonu' IMMUTABLE STRICT;
|
||||
78
test/support/sql/CDB_QueryTables.sql
Normal file
@@ -0,0 +1,78 @@
|
||||
-- Return an array of table names scanned by a given query
|
||||
--
|
||||
-- Requires PostgreSQL 9.x+
|
||||
--
|
||||
CREATE OR REPLACE FUNCTION CDB_QueryTablesText(query text)
|
||||
RETURNS text[]
|
||||
AS $$
|
||||
DECLARE
|
||||
exp XML;
|
||||
tables text[];
|
||||
rec RECORD;
|
||||
rec2 RECORD;
|
||||
BEGIN
|
||||
|
||||
tables := '{}';
|
||||
|
||||
FOR rec IN SELECT CDB_QueryStatements(query) q LOOP
|
||||
|
||||
IF NOT ( rec.q ilike 'select%' or rec.q ilike 'with%' ) THEN
|
||||
--RAISE WARNING 'Skipping %', rec.q;
|
||||
CONTINUE;
|
||||
END IF;
|
||||
|
||||
BEGIN
|
||||
EXECUTE 'EXPLAIN (FORMAT XML, VERBOSE) ' || rec.q INTO STRICT exp;
|
||||
EXCEPTION WHEN others THEN
|
||||
-- TODO: if error is 'relation "xxxxxx" does not exist', take xxxxxx as
|
||||
-- the affected table ?
|
||||
RAISE WARNING 'CDB_QueryTables cannot explain query: % (%: %)', rec.q, SQLSTATE, SQLERRM;
|
||||
RAISE EXCEPTION '%', SQLERRM;
|
||||
CONTINUE;
|
||||
END;
|
||||
|
||||
-- Now need to extract all values of <Relation-Name>
|
||||
|
||||
-- RAISE DEBUG 'Explain: %', exp;
|
||||
|
||||
FOR rec2 IN WITH
|
||||
inp AS (
|
||||
SELECT
|
||||
xpath('//x:Relation-Name/text()', exp, ARRAY[ARRAY['x', 'http://www.postgresql.org/2009/explain']]) as x,
|
||||
xpath('//x:Relation-Name/../x:Schema/text()', exp, ARRAY[ARRAY['x', 'http://www.postgresql.org/2009/explain']]) as s
|
||||
)
|
||||
SELECT unnest(x) as p, unnest(s) as sc from inp
|
||||
LOOP
|
||||
-- RAISE DEBUG 'tab: %', rec2.p;
|
||||
-- RAISE DEBUG 'sc: %', rec2.sc;
|
||||
tables := array_append(tables, (rec2.sc || '.' || rec2.p));
|
||||
END LOOP;
|
||||
|
||||
-- RAISE DEBUG 'Tables: %', tables;
|
||||
|
||||
END LOOP;
|
||||
|
||||
-- RAISE DEBUG 'Tables: %', tables;
|
||||
|
||||
-- Remove duplicates and sort by name
|
||||
IF array_upper(tables, 1) > 0 THEN
|
||||
WITH dist as ( SELECT DISTINCT unnest(tables)::text as p ORDER BY p )
|
||||
SELECT array_agg(p) from dist into tables;
|
||||
END IF;
|
||||
|
||||
--RAISE DEBUG 'Tables: %', tables;
|
||||
|
||||
return tables;
|
||||
END
|
||||
$$ LANGUAGE 'plpgsql' VOLATILE STRICT;
|
||||
|
||||
|
||||
-- Keep CDB_QueryTables with same signature for backwards compatibility.
|
||||
-- It should probably be removed in the future.
|
||||
CREATE OR REPLACE FUNCTION CDB_QueryTables(query text)
|
||||
RETURNS name[]
|
||||
AS $$
|
||||
BEGIN
|
||||
RETURN CDB_QueryTablesText(query)::name[];
|
||||
END
|
||||
$$ LANGUAGE 'plpgsql' VOLATILE STRICT;
|
||||
@@ -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 (
|
||||
@@ -51,19 +53,20 @@ SELECT pg_catalog.setval('test_table_cartodb_id_seq', 60, true);
|
||||
|
||||
ALTER TABLE test_table ALTER COLUMN cartodb_id SET DEFAULT nextval('test_table_cartodb_id_seq'::regclass);
|
||||
|
||||
INSERT INTO test_table VALUES ('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.314252', 1, 'Hawai', 'Calle de Pérez Galdós 9, Madrid, Spain', '0101000020E6100000A6B73F170D990DC064E8D84125364440', '0101000020110F000076491621312319C122D4663F1DCC5241');
|
||||
INSERT INTO test_table VALUES ('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.319101', 2, 'El Estocolmo', 'Calle de la Palma 72, Madrid, Spain', '0101000020E6100000C90567F0F7AB0DC0AB07CC43A6364440', '0101000020110F0000C4356B29423319C15DD1092DADCC5241');
|
||||
INSERT INTO test_table VALUES ('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.324', 3, 'El Rey del Tallarín', 'Plaza Conde de Toreno 2, Madrid, Spain', '0101000020E610000021C8410933AD0DC0CB0EF10F5B364440', '0101000020110F000053E71AC64D3419C10F664E4659CC5241');
|
||||
INSERT INTO test_table VALUES ('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.329509', 4, 'El Lacón', 'Manuel Fernández y González 8, Madrid, Spain', '0101000020E6100000BC5983F755990DC07D923B6C22354440', '0101000020110F00005DACDB056F2319C1EC41A980FCCA5241');
|
||||
INSERT INTO test_table VALUES ('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.334931', 5, 'El Pico', 'Calle Divino Pastor 12, Madrid, Spain', '0101000020E61000003B6D8D08C6A10DC0371B2B31CF364440', '0101000020110F00005F716E91992A19C17DAAA4D6DACC5241');
|
||||
INSERT INTO test_table VALUES
|
||||
('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.314252', 1, 'Hawai', 'Calle de Pérez Galdós 9, Madrid, Spain', '0101000020E6100000A6B73F170D990DC064E8D84125364440', '0101000020110F000076491621312319C122D4663F1DCC5241'),
|
||||
('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.319101', 2, 'El Estocolmo', 'Calle de la Palma 72, Madrid, Spain', '0101000020E6100000C90567F0F7AB0DC0AB07CC43A6364440', '0101000020110F0000C4356B29423319C15DD1092DADCC5241'),
|
||||
('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.324', 3, 'El Rey del Tallarín', 'Plaza Conde de Toreno 2, Madrid, Spain', '0101000020E610000021C8410933AD0DC0CB0EF10F5B364440', '0101000020110F000053E71AC64D3419C10F664E4659CC5241'),
|
||||
('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.329509', 4, 'El Lacón', 'Manuel Fernández y González 8, Madrid, Spain', '0101000020E6100000BC5983F755990DC07D923B6C22354440', '0101000020110F00005DACDB056F2319C1EC41A980FCCA5241'),
|
||||
('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.334931', 5, 'El Pico', 'Calle Divino Pastor 12, Madrid, Spain', '0101000020E61000003B6D8D08C6A10DC0371B2B31CF364440', '0101000020110F00005F716E91992A19C17DAAA4D6DACC5241');
|
||||
|
||||
ALTER TABLE ONLY test_table ADD CONSTRAINT test_table_pkey PRIMARY KEY (cartodb_id);
|
||||
|
||||
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 (
|
||||
@@ -95,19 +98,20 @@ SELECT pg_catalog.setval('test_table_2_cartodb_id_seq', 60, true);
|
||||
|
||||
ALTER TABLE test_table_2 ALTER COLUMN cartodb_id SET DEFAULT nextval('test_table_2_cartodb_id_seq'::regclass);
|
||||
|
||||
INSERT INTO test_table_2 VALUES ('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.314252', 1, 'Hawai', 'Calle de Pérez Galdós 9, Madrid, Spain', '0101000020E6100000A6B73F170D990DC064E8D84125364440', '0101000020110F000076491621312319C122D4663F1DCC5241');
|
||||
INSERT INTO test_table_2 VALUES ('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.319101', 2, 'El Estocolmo', 'Calle de la Palma 72, Madrid, Spain', '0101000020E6100000C90567F0F7AB0DC0AB07CC43A6364440', '0101000020110F0000C4356B29423319C15DD1092DADCC5241');
|
||||
INSERT INTO test_table_2 VALUES ('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.324', 3, 'El Rey del Tallarín', 'Plaza Conde de Toreno 2, Madrid, Spain', '0101000020E610000021C8410933AD0DC0CB0EF10F5B364440', '0101000020110F000053E71AC64D3419C10F664E4659CC5241');
|
||||
INSERT INTO test_table_2 VALUES ('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.329509', 4, 'El Lacón', 'Manuel Fernández y González 8, Madrid, Spain', '0101000020E6100000BC5983F755990DC07D923B6C22354440', '0101000020110F00005DACDB056F2319C1EC41A980FCCA5241');
|
||||
INSERT INTO test_table_2 VALUES ('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.334931', 5, 'El Pico', 'Calle Divino Pastor 12, Madrid, Spain', '0101000020E61000003B6D8D08C6A10DC0371B2B31CF364440', '0101000020110F00005F716E91992A19C17DAAA4D6DACC5241');
|
||||
INSERT INTO test_table_2 VALUES
|
||||
('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.314252', 1, 'Hawai', 'Calle de Pérez Galdós 9, Madrid, Spain', '0101000020E6100000A6B73F170D990DC064E8D84125364440', '0101000020110F000076491621312319C122D4663F1DCC5241'),
|
||||
('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.319101', 2, 'El Estocolmo', 'Calle de la Palma 72, Madrid, Spain', '0101000020E6100000C90567F0F7AB0DC0AB07CC43A6364440', '0101000020110F0000C4356B29423319C15DD1092DADCC5241'),
|
||||
('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.324', 3, 'El Rey del Tallarín', 'Plaza Conde de Toreno 2, Madrid, Spain', '0101000020E610000021C8410933AD0DC0CB0EF10F5B364440', '0101000020110F000053E71AC64D3419C10F664E4659CC5241'),
|
||||
('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.329509', 4, 'El Lacón', 'Manuel Fernández y González 8, Madrid, Spain', '0101000020E6100000BC5983F755990DC07D923B6C22354440', '0101000020110F00005DACDB056F2319C1EC41A980FCCA5241'),
|
||||
('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.334931', 5, 'El Pico', 'Calle Divino Pastor 12, Madrid, Spain', '0101000020E61000003B6D8D08C6A10DC0371B2B31CF364440', '0101000020110F00005F716E91992A19C17DAAA4D6DACC5241');
|
||||
|
||||
ALTER TABLE ONLY test_table_2 ADD CONSTRAINT test_table_2_pkey PRIMARY KEY (cartodb_id);
|
||||
|
||||
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 (
|
||||
@@ -139,19 +143,20 @@ SELECT pg_catalog.setval('test_table_3_cartodb_id_seq', 60, true);
|
||||
|
||||
ALTER TABLE test_table_3 ALTER COLUMN cartodb_id SET DEFAULT nextval('test_table_3_cartodb_id_seq'::regclass);
|
||||
|
||||
INSERT INTO test_table_3 VALUES ('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.314252', 1, 'Hawai', 'Calle de Pérez Galdós 9, Madrid, Spain', '0101000020E6100000A6B73F170D990DC064E8D84125364440', '0101000020110F000076491621312319C122D4663F1DCC5241');
|
||||
INSERT INTO test_table_3 VALUES ('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.319101', 2, 'El Estocolmo', 'Calle de la Palma 72, Madrid, Spain', '0101000020E6100000C90567F0F7AB0DC0AB07CC43A6364440', '0101000020110F0000C4356B29423319C15DD1092DADCC5241');
|
||||
INSERT INTO test_table_3 VALUES ('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.324', 3, 'El Rey del Tallarín', 'Plaza Conde de Toreno 2, Madrid, Spain', '0101000020E610000021C8410933AD0DC0CB0EF10F5B364440', '0101000020110F000053E71AC64D3419C10F664E4659CC5241');
|
||||
INSERT INTO test_table_3 VALUES ('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.329509', 4, 'El Lacón', 'Manuel Fernández y González 8, Madrid, Spain', '0101000020E6100000BC5983F755990DC07D923B6C22354440', '0101000020110F00005DACDB056F2319C1EC41A980FCCA5241');
|
||||
INSERT INTO test_table_3 VALUES ('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.334931', 5, 'El Pico', 'Calle Divino Pastor 12, Madrid, Spain', '0101000020E61000003B6D8D08C6A10DC0371B2B31CF364440', '0101000020110F00005F716E91992A19C17DAAA4D6DACC5241');
|
||||
INSERT INTO test_table_3 VALUES
|
||||
('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.314252', 1, 'Hawai', 'Calle de Pérez Galdós 9, Madrid, Spain', '0101000020E6100000A6B73F170D990DC064E8D84125364440', '0101000020110F000076491621312319C122D4663F1DCC5241'),
|
||||
('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.319101', 2, 'El Estocolmo', 'Calle de la Palma 72, Madrid, Spain', '0101000020E6100000C90567F0F7AB0DC0AB07CC43A6364440', '0101000020110F0000C4356B29423319C15DD1092DADCC5241'),
|
||||
('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.324', 3, 'El Rey del Tallarín', 'Plaza Conde de Toreno 2, Madrid, Spain', '0101000020E610000021C8410933AD0DC0CB0EF10F5B364440', '0101000020110F000053E71AC64D3419C10F664E4659CC5241'),
|
||||
('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.329509', 4, 'El Lacón', 'Manuel Fernández y González 8, Madrid, Spain', '0101000020E6100000BC5983F755990DC07D923B6C22354440', '0101000020110F00005DACDB056F2319C1EC41A980FCCA5241'),
|
||||
('2011-09-21 14:02:21.358706', '2011-09-21 14:02:21.334931', 5, 'El Pico', 'Calle Divino Pastor 12, Madrid, Spain', '0101000020E61000003B6D8D08C6A10DC0371B2B31CF364440', '0101000020110F00005F716E91992A19C17DAAA4D6DACC5241');
|
||||
|
||||
ALTER TABLE ONLY test_table_3 ADD CONSTRAINT test_table_3_pkey PRIMARY KEY (cartodb_id);
|
||||
|
||||
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 (
|
||||
@@ -169,5 +174,46 @@ 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;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS
|
||||
CDB_TableMetadata (
|
||||
tabname regclass not null primary key,
|
||||
updated_at timestamp with time zone not null default now()
|
||||
);
|
||||
|
||||
INSERT INTO CDB_TableMetadata (tabname, updated_at) VALUES ('test_table'::regclass, '2009-02-13T23:31:30.123Z');
|
||||
INSERT INTO CDB_TableMetadata (tabname, updated_at) VALUES ('test_table_private_1'::regclass, '2009-02-13T23:31:30.123Z');
|
||||
|
||||
-- GRANT SELECT ON CDB_TableMetadata TO :PUBLICUSER;
|
||||
GRANT SELECT ON CDB_TableMetadata TO :TESTUSER;
|
||||
|
||||
-- long name table
|
||||
CREATE TABLE
|
||||
long_table_name_with_enough_chars_to_break_querytables_function
|
||||
(
|
||||
updated_at timestamp without time zone DEFAULT now(),
|
||||
created_at timestamp without time zone DEFAULT now(),
|
||||
cartodb_id integer NOT NULL,
|
||||
name character varying,
|
||||
address character varying,
|
||||
the_geom geometry,
|
||||
the_geom_webmercator geometry
|
||||
);
|
||||
|
||||
INSERT INTO long_table_name_with_enough_chars_to_break_querytables_function SELECT * from test_table;
|
||||
|
||||
ALTER TABLE ONLY long_table_name_with_enough_chars_to_break_querytables_function
|
||||
ADD CONSTRAINT long_table_name_with_enough_chars_to_break_querytables_func_pkey PRIMARY KEY (cartodb_id);
|
||||
|
||||
CREATE INDEX long_table_name_the_geom_idx
|
||||
ON long_table_name_with_enough_chars_to_break_querytables_function USING gist (the_geom);
|
||||
CREATE INDEX long_table_name_the_geom_webmercator_idx
|
||||
ON long_table_name_with_enough_chars_to_break_querytables_function USING gist (the_geom_webmercator);
|
||||
|
||||
GRANT ALL ON TABLE long_table_name_with_enough_chars_to_break_querytables_function TO :TESTUSER;
|
||||
GRANT SELECT ON TABLE long_table_name_with_enough_chars_to_break_querytables_function TO :PUBLICUSER;
|
||||
|
||||
INSERT INTO CDB_TableMetadata (tabname, updated_at) VALUES ('long_table_name_with_enough_chars_to_break_querytables_function'::regclass, '2009-02-13T23:31:30.123Z');
|
||||
|
||||
@@ -6,11 +6,60 @@
|
||||
*/
|
||||
|
||||
var _ = require('underscore');
|
||||
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) {
|
||||
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');
|
||||
callback(null, base64);
|
||||
},
|
||||
function(percent) {
|
||||
//console.log("Compressing: " + percent + "%");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// 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
@@ -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');
|
||||
});
|
||||
|
||||
});
|
||||
60
test/unit/cartodb/cdb_request.test.js
Normal file
@@ -0,0 +1,60 @@
|
||||
require('../../support/test_helper');
|
||||
var assert = require('assert');
|
||||
|
||||
var CdbRequest = require('../../../lib/cartodb/models/cdb_request');
|
||||
|
||||
describe('req2params', function() {
|
||||
|
||||
function createRequest(host, userParam) {
|
||||
var req = {
|
||||
params: {},
|
||||
headers: {
|
||||
host: host
|
||||
}
|
||||
};
|
||||
|
||||
if (userParam) {
|
||||
req.params.user = userParam;
|
||||
}
|
||||
|
||||
return req;
|
||||
}
|
||||
|
||||
it('extracts name from host header', function() {
|
||||
var cdbRequest = new CdbRequest();
|
||||
var user = cdbRequest.userByReq(createRequest('localhost'));
|
||||
|
||||
assert.equal(user, 'localhost');
|
||||
});
|
||||
|
||||
it('extracts name from subdomain host header in case of no config', function() {
|
||||
var userFromHostConfig = global.environment.user_from_host;
|
||||
global.environment.user_from_host = null;
|
||||
|
||||
var cdbRequest = new CdbRequest();
|
||||
var user = cdbRequest.userByReq(createRequest('development.localhost.lan'));
|
||||
|
||||
global.environment.user_from_host = userFromHostConfig;
|
||||
|
||||
assert.equal(user, 'development');
|
||||
});
|
||||
|
||||
it('considers user param before headers', function() {
|
||||
var cdbRequest = new CdbRequest();
|
||||
var user = cdbRequest.userByReq(createRequest('localhost', 'development'));
|
||||
|
||||
assert.equal(user, 'development');
|
||||
});
|
||||
|
||||
it('returns undefined when it cannot extract username', function() {
|
||||
var userFromHostConfig = global.environment.user_from_host;
|
||||
global.environment.user_from_host = null;
|
||||
|
||||
var cdbRequest = new CdbRequest();
|
||||
var user = cdbRequest.userByReq(createRequest('localhost'));
|
||||
|
||||
global.environment.user_from_host = userFromHostConfig;
|
||||
|
||||
assert.equal(user, undefined);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,13 +1,16 @@
|
||||
var assert = require('assert')
|
||||
, _ = require('underscore')
|
||||
, redis = require('redis')
|
||||
, test_helper = require('../../support/test_helper')
|
||||
, tests = module.exports = {};
|
||||
var assert = require('assert');
|
||||
var _ = require('underscore');
|
||||
var test_helper = require('../../support/test_helper');
|
||||
|
||||
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,52 +18,79 @@ 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.ok(!req.params.hasOwnProperty('interactivity'), 'request params do not have interactivity');
|
||||
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.ok(!req.params.hasOwnProperty('interactivity'), 'request params do not have interactivity');
|
||||
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.ok(!req.params.hasOwnProperty('interactivity'), 'request params do not have interactivity');
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
test('it should extend params with decoded lzma', function(done) {
|
||||
var qo = {
|
||||
config: {
|
||||
version: '1.3.0'
|
||||
}
|
||||
};
|
||||
test_helper.lzma_compress_to_base64(JSON.stringify(qo), 1, function(err, data) {
|
||||
var req = {
|
||||
headers: {
|
||||
host:'localhost'
|
||||
},
|
||||
query: {
|
||||
non_included: 'toberemoved',
|
||||
api_key: 'test',
|
||||
style: 'override',
|
||||
lzma: data
|
||||
}
|
||||
};
|
||||
opts.req2params(req, function(err, req) {
|
||||
if ( err ) {
|
||||
return done(err);
|
||||
}
|
||||
var query = req.params;
|
||||
assert.deepEqual(qo.config, query.config);
|
||||
assert.equal('test', query.api_key);
|
||||
assert.equal(undefined, query.non_included);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
525
test/unit/cartodb/template_maps.test.js
Normal file
@@ -0,0 +1,525 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
var assert = require('assert');
|
||||
var RedisPool = require('redis-mpool');
|
||||
var TemplateMaps = require('../../../lib/cartodb/template_maps.js');
|
||||
var step = require('step');
|
||||
var _ = require('underscore');
|
||||
|
||||
describe('template_maps', function() {
|
||||
|
||||
// configure redis pool instance to use in tests
|
||||
var redis_pool = new 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'
|
||||
}
|
||||
};
|
||||
|
||||
it('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);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('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);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('naming', function() {
|
||||
|
||||
function createTemplate(name) {
|
||||
return {
|
||||
version:'0.0.1',
|
||||
name: name,
|
||||
auth: {},
|
||||
layergroup: {
|
||||
layers: [
|
||||
wadusLayer
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
var templateMaps = new TemplateMaps(redis_pool);
|
||||
|
||||
var invalidNames = [ "ab|", "a b", "a@b", "-1ab", "_x", "", " x", "x " ];
|
||||
invalidNames.forEach(function(invalidName) {
|
||||
it('should NOT accept template with invalid name: ' + invalidName, function(done) {
|
||||
templateMaps.addTemplate('me', createTemplate(invalidName), function(err) {
|
||||
assert.ok(err, "Unexpected success with invalid name '" + invalidName + "'");
|
||||
assert.ok(
|
||||
err.message.match(/template.*name/i),
|
||||
"Unexpected error message with invalid name '" + invalidName + "': " + err.message
|
||||
);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
var validNames = [
|
||||
"AB", "1ab", "DFD19A1A-0AC6-11E5-B0CA-6476BA93D4F6", "25ad8300-0ac7-11e5-b93f-6476ba93d4f6"
|
||||
];
|
||||
validNames.forEach(function(validName) {
|
||||
it('should accept template with valid name: ' + validName, function(done) {
|
||||
templateMaps.addTemplate('me', createTemplate(validName), function(err) {
|
||||
assert.ok(!err, "Unexpected error with valid name '" + validName + "': " + err);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
it('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();
|
||||
});
|
||||
|
||||
it('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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('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
|
||||
it('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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('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);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('add multiple templates, list them', function(done) {
|
||||
var tmap = new TemplateMaps(redis_pool);
|
||||
assert.ok(tmap);
|
||||
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);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('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;
|
||||
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);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('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 { 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)
|
||||
err = null;
|
||||
try { 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)
|
||||
err = null;
|
||||
try { tmap.instance(tpl1, {color:'#1234567'}); }
|
||||
catch (e) { err = e; }
|
||||
assert.ok(err);
|
||||
assert.ok(err.message.match(/invalid css_color/i), err);
|
||||
|
||||
// Invalid number
|
||||
err = null;
|
||||
try { tmap.instance(tpl1, {zoom:'#'}); }
|
||||
catch (e) { err = e; }
|
||||
assert.ok(err);
|
||||
assert.ok(err.message.match(/invalid number/i), err);
|
||||
|
||||
// Invalid number 2
|
||||
err = null;
|
||||
try { tmap.instance(tpl1, {zoom:'23e'}); }
|
||||
catch (e) { err = e; }
|
||||
assert.ok(err);
|
||||
assert.ok(err.message.match(/invalid number/i), err);
|
||||
|
||||
// Valid number
|
||||
err = null;
|
||||
try { tmap.instance(tpl1, {zoom:'-.23e10'}); }
|
||||
catch (e) { err = e; }
|
||||
assert.ok(!err);
|
||||
});
|
||||
|
||||
// Can set a limit on the number of user templates
|
||||
it('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);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
105
test/unit/cartodb/template_maps_auth.test.js
Normal file
@@ -0,0 +1,105 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
var assert = require('assert');
|
||||
var RedisPool = require('redis-mpool');
|
||||
|
||||
var TemplateMaps = require('../../../lib/cartodb/template_maps');
|
||||
|
||||
describe('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) {
|
||||
it(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();
|
||||
});
|
||||
});
|
||||
|
||||
it("auth as 'open' string is authorized", function(done) {
|
||||
var template = {
|
||||
name: 'wadus_template',
|
||||
auth: 'open'
|
||||
};
|
||||
|
||||
assert.ok(templateMaps.isAuthorized(template));
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
107
test/unit/cartodb/template_maps_defaults.test.js
Normal file
@@ -0,0 +1,107 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
var assert = require('assert');
|
||||
var RedisPool = require('redis-mpool');
|
||||
var TemplateMaps = require('../../../lib/cartodb/template_maps.js');
|
||||
var _ = require('underscore');
|
||||
|
||||
describe('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) {
|
||||
it('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);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
113
test/unit/cartodb/valid_template_maps.test.js
Normal file
@@ -0,0 +1,113 @@
|
||||
require('../../support/test_helper');
|
||||
|
||||
var assert = require('assert');
|
||||
var RedisPool = require('redis-mpool');
|
||||
var TemplateMaps = require('../../../lib/cartodb/template_maps.js');
|
||||
var _ = require('underscore');
|
||||
|
||||
describe('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) {
|
||||
it(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();
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
8
tools/README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
Deprecated tools
|
||||
================
|
||||
|
||||
All tools and scripts found in this directory are deprecated and no longer maintained.
|
||||
|
||||
Use at your own peril.
|
||||
|
||||
In future releases they might get removed.
|
||||
81
tools/convert_database_styles
Executable file
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
var path = require('path');
|
||||
|
||||
var grainstore = require('../node_modules/windshaft/node_modules/grainstore');
|
||||
var mapnik = require('mapnik');
|
||||
var redis = require('redis');
|
||||
|
||||
function usage(me, exitcode) {
|
||||
console.log("Usage: " + me + " <database_name> <table_name> [<target_mapnik_version>]");
|
||||
process.exit(exitcode);
|
||||
}
|
||||
|
||||
var node_path = process.argv.shift();
|
||||
var script_path = process.argv.shift();
|
||||
var me = path.basename(script_path);
|
||||
|
||||
var database_name = process.argv.shift()
|
||||
var table_name = process.argv.shift()
|
||||
var MAPNIK_VERSION = process.argv.shift()
|
||||
|
||||
|
||||
if ( ! MAPNIK_VERSION ) {
|
||||
MAPNIK_VERSION = mapnik.versions.mapnik;
|
||||
}
|
||||
|
||||
if ( ! database_name || ! table_name) {
|
||||
usage(me, 1);
|
||||
}
|
||||
|
||||
var REDIS_PORT = 6379; // TODO: make a command line parameter
|
||||
|
||||
var dbnum = 0;
|
||||
|
||||
var mml_store = new grainstore.MMLStore({port:REDIS_PORT}, {mapnik_version:MAPNIK_VERSION});
|
||||
|
||||
var failures = [];
|
||||
|
||||
var client = redis.createClient(REDIS_PORT, 'localhost');
|
||||
client.on('connect', function() {
|
||||
client.select(dbnum);
|
||||
client.keys('map_style|' + database_name + '|' + table_name, function(err, matches) {
|
||||
|
||||
processNext = function() {
|
||||
if ( ! matches.length ) process.exit(failures.length);
|
||||
var k = matches.shift();
|
||||
|
||||
if ( /map_style\|.*\|.*\|/.test(k) ) {
|
||||
//console.warn("Key " + k + " is EXTENDED, skipping");
|
||||
processNext();
|
||||
}
|
||||
|
||||
var out = 'map_style|' + database_name + '|' + table_name + ': ';
|
||||
|
||||
var mml_builder = mml_store.mml_builder({
|
||||
dbname:database_name,
|
||||
table:table_name},
|
||||
function(err, payload) {
|
||||
if ( err ) {
|
||||
console.warn(out + err.message);
|
||||
failures.push(k); processNext();
|
||||
}
|
||||
else {
|
||||
mml_builder.resetStyle(function(err, data) {
|
||||
if ( err ) {
|
||||
console.warn(out + err.message);
|
||||
failures.push(k);
|
||||
}
|
||||
else console.log(out + 'OK');
|
||||
processNext();
|
||||
}, true);
|
||||
}
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
processNext();
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
40
tools/create_multilayer
Executable file
@@ -0,0 +1,40 @@
|
||||
#!/bin/sh
|
||||
|
||||
verbose=no
|
||||
tiler_url=http://dev.localhost.lan:8181/tiles/layergroup
|
||||
|
||||
# This is for direct windshaft connection
|
||||
#tiler_url=http://dev.localhost.lan:8083/database/cartodb_dev_user_1_db/layergroup
|
||||
|
||||
while test -n "$1"; do
|
||||
if test "$1" = "-v"; then
|
||||
verbose=yes
|
||||
elif test -z "$cfg"; then
|
||||
cfg="$1"
|
||||
else
|
||||
tiler_url="$1"
|
||||
fi
|
||||
shift
|
||||
done
|
||||
|
||||
if test -z "$cfg"; then
|
||||
echo "Usage: $0 [-v] <config_file> [<tiler_url>]" >&2
|
||||
echo "Default <tiler_url> is ${tiler_url}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cmd="curl -skH Content-Type:application/json --data-binary @- ${tiler_url}"
|
||||
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
|
||||
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
@@ -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
|
||||