Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f884da79fe | ||
|
|
10e72ba0c1 | ||
|
|
e1e0cea494 | ||
|
|
d07b5acf15 | ||
|
|
64d02d7375 | ||
|
|
5c822b7ec4 | ||
|
|
de97f646a4 | ||
|
|
a45c639a97 | ||
|
|
1e711051da | ||
|
|
79077c873f | ||
|
|
2ed163945b | ||
|
|
cf3cd4b8d0 | ||
|
|
38ea4378da | ||
|
|
226f1caa24 | ||
|
|
6475f254f7 | ||
|
|
d6cd5d0fb4 | ||
|
|
c73282c3f0 | ||
|
|
d7dfdaa03f | ||
|
|
6c8ce3929c | ||
|
|
f84bc6a0ff | ||
|
|
82ecf836c4 | ||
|
|
c820efb64a | ||
|
|
e30bf53afa | ||
|
|
5ce020d5bc | ||
|
|
fbf9bf21c2 | ||
|
|
d490bf09fd | ||
|
|
aa53623aac | ||
|
|
0fce7ca361 | ||
|
|
d0a71de2da | ||
|
|
40c2bcd4a1 | ||
|
|
0dc67bed0b | ||
|
|
2d00d7d248 | ||
|
|
2977d09779 | ||
|
|
630e0a1691 | ||
|
|
28d5278d62 | ||
|
|
e332f038ef | ||
|
|
905fcb05cc | ||
|
|
774d015eb5 | ||
|
|
e752224f14 | ||
|
|
add10bd10c | ||
|
|
52d3026133 | ||
|
|
5cdafb23e7 | ||
|
|
20b06efabe | ||
|
|
7508230760 | ||
|
|
a17dd41d6b | ||
|
|
0a1564ba5a | ||
|
|
078c6ec08c | ||
|
|
83cf15fb7e | ||
|
|
40dc46ed2e | ||
|
|
d9261fa674 | ||
|
|
856d450775 | ||
|
|
c1a8661ab3 | ||
|
|
aa15f4782e | ||
|
|
77cd9387d4 | ||
|
|
3cd3a3f8c6 | ||
|
|
78821a8fe7 | ||
|
|
5c372df487 | ||
|
|
fad47324c3 | ||
|
|
11a9849152 | ||
|
|
1f13e43abe | ||
|
|
fac797c766 | ||
|
|
07cb0b582e | ||
|
|
d54a2f48de | ||
|
|
bcce7b1c53 | ||
|
|
8d1961ce07 | ||
|
|
9a19dff83e | ||
|
|
54b9a0b8b3 | ||
|
|
9a3d453bf4 | ||
|
|
4f6c466031 | ||
|
|
a852b766eb | ||
|
|
14be3fb671 | ||
|
|
8a3686d86b | ||
|
|
009250006e | ||
|
|
38ad8c04e1 | ||
|
|
54cbf147e3 | ||
|
|
f9f0f79fa9 | ||
|
|
a867d79c42 | ||
|
|
4197d2f739 | ||
|
|
c10ba8e214 | ||
|
|
887948d259 | ||
|
|
c63786733a | ||
|
|
4fe696dd96 | ||
|
|
67936b343f | ||
|
|
b0e395f495 | ||
|
|
3b55a27ef3 | ||
|
|
43da81c30c | ||
|
|
dd2f5511d8 | ||
|
|
49c349c846 | ||
|
|
aaf77902f6 | ||
|
|
4c555cbd5d | ||
|
|
3e53796120 | ||
|
|
0a2b7064c7 | ||
|
|
ce378e6b3a | ||
|
|
feadf59bb3 | ||
|
|
ad7d80a430 | ||
|
|
e572d1d1a2 | ||
|
|
c06237cc56 | ||
|
|
c656a870f4 | ||
|
|
1b625cb77a | ||
|
|
394bd22148 | ||
|
|
963416ae29 | ||
|
|
d3b10e7b2a | ||
|
|
acf62320ef | ||
|
|
16f2849724 | ||
|
|
d945e8c06a | ||
|
|
bc1fb67df0 | ||
|
|
1442e02f5f | ||
|
|
68d2d834ba | ||
|
|
eb777cf823 | ||
|
|
1aed55be68 | ||
|
|
032f07f3f0 | ||
|
|
7629eff51d | ||
|
|
46fd856fc6 | ||
|
|
caaa9a3944 | ||
|
|
439a96c578 | ||
|
|
e731241b97 | ||
|
|
216dc833fd | ||
|
|
a8e02f766b | ||
|
|
cae1c67b00 | ||
|
|
b05ea14122 | ||
|
|
666114f81d | ||
|
|
3e99995e6a | ||
|
|
8213c58452 | ||
|
|
ee036f8397 | ||
|
|
5b6421d03a | ||
|
|
ba00cb98f4 | ||
|
|
6184921edc | ||
|
|
9e0ea113fb | ||
|
|
0d5d209bfb | ||
|
|
253f2bf537 | ||
|
|
2a2889020a | ||
|
|
6a79de67ce | ||
|
|
c49932d3a8 | ||
|
|
95d71281d6 | ||
|
|
a1e2fe308f | ||
|
|
a9d66d1dac | ||
|
|
f4135bb3ff | ||
|
|
d7868235d8 | ||
|
|
60f4446fd8 | ||
|
|
c893ac15dc | ||
|
|
ecada119f8 | ||
|
|
57e32ca0a3 | ||
|
|
eb3f7c696a | ||
|
|
09249f97ab | ||
|
|
754282c385 | ||
|
|
52ba757ebe | ||
|
|
7aadaa44e8 | ||
|
|
0139cd8261 | ||
|
|
7874bee879 | ||
|
|
12ad7d0585 | ||
|
|
9065ed65fa | ||
|
|
8aa79086d2 | ||
|
|
775c006806 | ||
|
|
be6c30a0fb | ||
|
|
8f80cd1469 | ||
|
|
5fd6b3956e | ||
|
|
f353711ed2 | ||
|
|
0a56d6ce88 | ||
|
|
43ad6d6aa0 | ||
|
|
5a0f83ecb8 | ||
|
|
4ddd5b4a76 | ||
|
|
1f69757835 | ||
|
|
e0df95844b | ||
|
|
5a660e0cbe | ||
|
|
566c6fe53f | ||
|
|
d93bb95d83 | ||
|
|
3773cce246 | ||
|
|
52c011f896 | ||
|
|
81cd9d4b15 | ||
|
|
781fa592ca | ||
|
|
1065021348 | ||
|
|
aecc53d8f9 | ||
|
|
a5465e20d0 | ||
|
|
9e567216e9 | ||
|
|
5e9dba5292 | ||
|
|
f7009635d6 | ||
|
|
e5c77385ae | ||
|
|
3103060f4b | ||
|
|
315b01ebf7 | ||
|
|
25f65065f4 | ||
|
|
60c0c11c1d | ||
|
|
cb99270a8f | ||
|
|
cca0b5ae66 | ||
|
|
2aa0e091bb | ||
|
|
78d45b4e32 | ||
|
|
ef42d1c87f | ||
|
|
aaa4fbc370 | ||
|
|
5e65b2afaf | ||
|
|
3408998629 | ||
|
|
b8c5d212ea | ||
|
|
c8e4f38984 | ||
|
|
44dd73adcc | ||
|
|
b2935ef799 | ||
|
|
0b1f8358c6 | ||
|
|
2c31c64325 | ||
|
|
425ab70ec7 | ||
|
|
2cce82bdd5 | ||
|
|
800873ed91 | ||
|
|
b702c5fbe4 | ||
|
|
cfd0ee35d3 | ||
|
|
31cd0bbff4 | ||
|
|
78ec8a095a | ||
|
|
39959fa330 | ||
|
|
fc3a01fabe | ||
|
|
03b810ffa5 | ||
|
|
177ec4df85 | ||
|
|
dbf645b899 | ||
|
|
e274706088 | ||
|
|
da632ac7d3 | ||
|
|
b78b096ae4 | ||
|
|
bbbc308b6f | ||
|
|
ecf54bef43 | ||
|
|
8c0d187044 | ||
|
|
ae8625a7b6 | ||
|
|
7c7a04a27e | ||
|
|
2289defe4b | ||
|
|
772d3c4e01 | ||
|
|
45e7aac218 | ||
|
|
5d126bf0a3 | ||
|
|
f9f572a5d2 | ||
|
|
88b5966a44 | ||
|
|
3d2880d40a | ||
|
|
958e052de5 | ||
|
|
793acd276a | ||
|
|
9be6747b66 | ||
|
|
3bee6b1e27 | ||
|
|
6fe1617348 | ||
|
|
2158f3d039 | ||
|
|
ca338ae401 | ||
|
|
5135fea797 | ||
|
|
df538bfd98 | ||
|
|
7ace8ace49 | ||
|
|
c8b39c6cde | ||
|
|
5f5a0c7764 | ||
|
|
922da4520f | ||
|
|
64e047e5d7 | ||
|
|
efecf007e8 | ||
|
|
b81e15d0c5 | ||
|
|
0fbb116dd2 | ||
|
|
e4b98d51be | ||
|
|
d37e14c4db | ||
|
|
cfaeff8a5a | ||
|
|
6398dd2b32 | ||
|
|
11385bbd9f | ||
|
|
858e4405ec | ||
|
|
46307ca64a | ||
|
|
fd67c8480c | ||
|
|
f54dc2e20e | ||
|
|
e356783cc9 | ||
|
|
b5080fa2d7 | ||
|
|
e564ed6e3e | ||
|
|
8d369a2195 | ||
|
|
92e28bd902 | ||
|
|
c27e7be028 | ||
|
|
9d3bbc736b | ||
|
|
38ecaa44db | ||
|
|
efd65ff1f4 | ||
|
|
d5e662567b | ||
|
|
334f500f7c | ||
|
|
bb52fb9b0e | ||
|
|
74ee6cd674 | ||
|
|
524c6a7c2c | ||
|
|
c9f7504d09 | ||
|
|
ba1fcfd1e3 | ||
|
|
554db21f49 | ||
|
|
ebc34561fa | ||
|
|
5ae9a2b1cf | ||
|
|
e2aa227597 | ||
|
|
81f42d4b8a | ||
|
|
90eaaefe12 | ||
|
|
cdce929159 | ||
|
|
79b865ba79 | ||
|
|
8e96e3b3ae | ||
|
|
0595e564da | ||
|
|
9bb0bc1750 | ||
|
|
950070ce4e | ||
|
|
3b290d524c | ||
|
|
906fac2dd6 | ||
|
|
377944502d | ||
|
|
a41380b965 | ||
|
|
b2a2504201 | ||
|
|
c45e85c5a6 | ||
|
|
2afe13fa89 | ||
|
|
36c24d9cbe | ||
|
|
d6ec171bcb | ||
|
|
06c9e246f4 | ||
|
|
b2b92e75cc | ||
|
|
506929c586 | ||
|
|
f75e77cf60 | ||
|
|
ba3ba9fd5b | ||
|
|
7de8d5446a | ||
|
|
4ac47df528 | ||
|
|
57bc3d805c | ||
|
|
3648986aa8 | ||
|
|
c050b61db8 | ||
|
|
92d00749a3 | ||
|
|
c5849ecbe0 | ||
|
|
fea38a4caf | ||
|
|
d24f0c8216 | ||
|
|
35a114a81b | ||
|
|
ed988100c6 | ||
|
|
b6a93aae1f | ||
|
|
70cae5beed | ||
|
|
0a24a83fa9 | ||
|
|
c8739aa8a3 | ||
|
|
d4950c6460 | ||
|
|
1d15060c63 | ||
|
|
401b98c5b4 | ||
|
|
d53949e4a4 | ||
|
|
421163dc75 | ||
|
|
eae169ca67 | ||
|
|
ec23067e3e | ||
|
|
bfacec3a67 | ||
|
|
e7b69cb99c | ||
|
|
fd7c23f7a1 | ||
|
|
84e03a16dd | ||
|
|
d556cea8ce | ||
|
|
8911afd079 | ||
|
|
d2df289696 | ||
|
|
2eb7c803e3 | ||
|
|
71a5748718 | ||
|
|
4ab5e63e58 | ||
|
|
c445105d55 | ||
|
|
2905753194 | ||
|
|
df49f482dc | ||
|
|
23e4c7fe6e | ||
|
|
5ac4c29a9e | ||
|
|
e0ba6b6d1c | ||
|
|
bdf1cbe111 | ||
|
|
9252ec2509 | ||
|
|
8230128ff6 | ||
|
|
6d7bf9faa9 | ||
|
|
28183a667c | ||
|
|
1eda7db0fb | ||
|
|
3cdc6d15d7 | ||
|
|
0708f46df3 | ||
|
|
7a7ceaf39a | ||
|
|
3e83bef9c4 | ||
|
|
169547aee6 | ||
|
|
f7745e1569 | ||
|
|
870f7a39b0 | ||
|
|
86c40e8df9 | ||
|
|
d87b622d64 | ||
|
|
7088e9d510 | ||
|
|
fe8b32d077 | ||
|
|
478c48cc84 | ||
|
|
c9c64fd71f | ||
|
|
93210f9380 | ||
|
|
c25473e001 | ||
|
|
f3cbab6f82 | ||
|
|
3d65d66b1b | ||
|
|
f582798276 | ||
|
|
ba87a60a47 | ||
|
|
356bff9cbd | ||
|
|
86574c89a8 | ||
|
|
734f8e6df2 | ||
|
|
df7d71db32 | ||
|
|
0c5d2e553e | ||
|
|
36ed7e2fb7 | ||
|
|
bac63f339d | ||
|
|
8d8074d500 | ||
|
|
63ebf6e92b | ||
|
|
d7f54e76f4 | ||
|
|
34d0f34620 | ||
|
|
14c882a566 | ||
|
|
ff56c3adf4 | ||
|
|
12967f8643 | ||
|
|
b056735535 | ||
|
|
f5afa419f1 | ||
|
|
7531c5490d | ||
|
|
bb62e559d6 | ||
|
|
e42bf30aeb | ||
|
|
2b8ffc8e61 | ||
|
|
48a3d1d516 | ||
|
|
5e6987b1d8 | ||
|
|
4dfcb59f73 | ||
|
|
620da2e776 | ||
|
|
f9688c3553 | ||
|
|
4ff2469f57 | ||
|
|
e3252a6e9e | ||
|
|
a9035bba2f | ||
|
|
4c8c3ccfb2 | ||
|
|
09f3e09c96 | ||
|
|
807364bd7b | ||
|
|
d14f745406 | ||
|
|
1cdc9169c7 | ||
|
|
9aa400c8f3 | ||
|
|
e4cdb8c491 | ||
|
|
c812a3fe3e | ||
|
|
1ab11de96c | ||
|
|
f7b173163c | ||
|
|
2be63b36d6 | ||
|
|
c60380b5fc | ||
|
|
d88b16e5ab | ||
|
|
867f2569a0 | ||
|
|
5a3fbd7111 | ||
|
|
a2349d90c0 | ||
|
|
980f27a6d1 | ||
|
|
ce96c88ba0 | ||
|
|
50b275d3e2 | ||
|
|
e90391b7ac | ||
|
|
5ad8800773 | ||
|
|
d2aa50be52 | ||
|
|
a8477b7822 | ||
|
|
115dae7484 | ||
|
|
e4006da9bc | ||
|
|
a47c59baca | ||
|
|
b191431530 | ||
|
|
9bdb4e5c4f | ||
|
|
07d619981e | ||
|
|
778b8d074d | ||
|
|
902e4e8921 | ||
|
|
d2719420f4 | ||
|
|
057e8d63bf | ||
|
|
f4e171aa2b | ||
|
|
7f1507b9fb | ||
|
|
cdcc175abf | ||
|
|
f995799e8f | ||
|
|
44e2b2fbb4 | ||
|
|
2eaf81334e | ||
|
|
c399b1f5f8 | ||
|
|
bc7d54c7d2 | ||
|
|
7589102c6c | ||
|
|
007f49a8bc | ||
|
|
a768bcbf20 | ||
|
|
cb145271ac | ||
|
|
9124dfd427 | ||
|
|
8e86177486 | ||
|
|
fe898c27ae | ||
|
|
f3f9030c48 | ||
|
|
73dc2692b1 | ||
|
|
87ffe39746 | ||
|
|
331e88ea39 | ||
|
|
f4becf99ad | ||
|
|
799daa7556 | ||
|
|
a3c1d9977f | ||
|
|
c803c4d0b6 | ||
|
|
8aef09574c | ||
|
|
efcc93ad89 | ||
|
|
d6834e8bf8 | ||
|
|
99d0b805df | ||
|
|
69070f341a | ||
|
|
dcf7ade5bc | ||
|
|
9021c96baa | ||
|
|
41a4396147 | ||
|
|
d9f7f6f509 | ||
|
|
bd62008e1c | ||
|
|
ded8981368 | ||
|
|
52d628f1ed | ||
|
|
9ca9a54d21 | ||
|
|
bf689565a5 | ||
|
|
9f1b5c0a81 | ||
|
|
87bd8e72cf | ||
|
|
8934190d43 | ||
|
|
c894b543b0 | ||
|
|
58f76f3d6d | ||
|
|
8980b09419 | ||
|
|
927ff6f1e5 | ||
|
|
8a722e2cfe | ||
|
|
271c329677 | ||
|
|
c63420f0e7 | ||
|
|
2fe2c9104b | ||
|
|
0ea36fc039 | ||
|
|
fc7113e52e | ||
|
|
c7cadcd776 | ||
|
|
fae1e168a7 | ||
|
|
d2791ea0e5 | ||
|
|
f4637c64c9 | ||
|
|
1b9a30cff4 | ||
|
|
5a627e76b2 | ||
|
|
d9e016be8b | ||
|
|
c3ade07ad6 | ||
|
|
05b5ecd23b | ||
|
|
dbbde6aae1 | ||
|
|
9ed30c0ba4 | ||
|
|
ec09b87940 | ||
|
|
6b5370f145 | ||
|
|
a25ad42644 | ||
|
|
995215c6a5 | ||
|
|
4b841dba61 | ||
|
|
9639cd5751 | ||
|
|
3c17f97c87 | ||
|
|
d97bf3b864 | ||
|
|
65f27653dc | ||
|
|
a134d8a493 | ||
|
|
3ded567f6d | ||
|
|
8b63d928ae | ||
|
|
8060fe3559 | ||
|
|
df5d9400fe | ||
|
|
6bc965e2a0 | ||
|
|
bfbec8fa36 | ||
|
|
51ee072f84 | ||
|
|
aa82d2e277 | ||
|
|
e13d912a79 | ||
|
|
826ae7459a | ||
|
|
cde9cd4ab7 | ||
|
|
b0a0c4768d | ||
|
|
4a9ca9bdae | ||
|
|
65a3ae08f0 | ||
|
|
bba6e96fd3 | ||
|
|
cc900787de | ||
|
|
01e65f5221 | ||
|
|
1e9031573c | ||
|
|
81ebdff977 | ||
|
|
9baad293e8 | ||
|
|
efc7a21427 | ||
|
|
b9c497a3ab | ||
|
|
5f43235370 | ||
|
|
57ff89be64 | ||
|
|
4f7ee6f36a | ||
|
|
14f3b25022 | ||
|
|
b99c8c897c | ||
|
|
ac3655df1d | ||
|
|
c858d158aa | ||
|
|
cc8a7ef0c2 | ||
|
|
c86e254a24 | ||
|
|
1c7ca51308 | ||
|
|
6a3f8ca828 | ||
|
|
031de4d35e | ||
|
|
595c2a5257 | ||
|
|
74f6e5225c | ||
|
|
c581148f1b | ||
|
|
0e20b47875 | ||
|
|
1cb00d3c62 | ||
|
|
bbc07d3f40 | ||
|
|
9c6597c351 | ||
|
|
defdcb85ef | ||
|
|
94115ff134 | ||
|
|
5a8d9b1246 | ||
|
|
32ff9dfb48 | ||
|
|
ab90bd24e0 | ||
|
|
c9f017c632 | ||
|
|
132ecd8d74 | ||
|
|
19c3ee2143 | ||
|
|
4fb0d1279c | ||
|
|
81f9bf290c | ||
|
|
dbef11869c | ||
|
|
b38564e623 | ||
|
|
2a2a4bdeac | ||
|
|
a65ac9b0f6 | ||
|
|
7ed8b10776 | ||
|
|
2f239b9048 | ||
|
|
c3854e7c74 | ||
|
|
b7dcc63e37 | ||
|
|
62243852b6 | ||
|
|
1b48531369 | ||
|
|
1981160536 | ||
|
|
f3eb8a317d | ||
|
|
7aedebecee | ||
|
|
d5afdc7c53 | ||
|
|
b7c261f520 | ||
|
|
942391c508 | ||
|
|
f333305491 | ||
|
|
88283801cb | ||
|
|
4e07846c5d | ||
|
|
bf58a3600a | ||
|
|
bae5afbad6 | ||
|
|
91689e28f0 | ||
|
|
041f73cae8 | ||
|
|
43c8bde8ce | ||
|
|
82dd1b732d | ||
|
|
41ac0d5d93 | ||
|
|
60db026dcc | ||
|
|
47b6f04a66 | ||
|
|
f228cccbae | ||
|
|
3820590649 | ||
|
|
8cc8531e49 | ||
|
|
65062364c4 | ||
|
|
ebe66cb985 | ||
|
|
9c172db534 | ||
|
|
de531b6b41 | ||
|
|
71cb8afc0d | ||
|
|
36dbd09459 | ||
|
|
fa4df95f55 | ||
|
|
4104f5fb75 | ||
|
|
abb6167caa | ||
|
|
15ea7dfe09 | ||
|
|
a2c2544271 | ||
|
|
2b22f889bd | ||
|
|
cd3013569e | ||
|
|
5dc37fc8e8 | ||
|
|
038d84b62d | ||
|
|
40f9a6068a | ||
|
|
5cd7d35966 | ||
|
|
71262fbc5c | ||
|
|
d16a4a9a2b | ||
|
|
2357961f48 | ||
|
|
162c4e38af | ||
|
|
0f95900822 | ||
|
|
8ac14447ef | ||
|
|
a1b3bca7fc | ||
|
|
8cde90b7fe | ||
|
|
3b83f536ca | ||
|
|
42d7cf8ae5 | ||
|
|
627c3c34b7 | ||
|
|
ee56dd5db1 | ||
|
|
bf91187e63 | ||
|
|
2982c5de2e | ||
|
|
add95c32ca | ||
|
|
243e4f0408 | ||
|
|
885ff60138 | ||
|
|
72a28a145d | ||
|
|
4a594d5e95 | ||
|
|
0ba133a0bd | ||
|
|
b05898d17f | ||
|
|
d4c6abbaec | ||
|
|
caaa34b5de | ||
|
|
c9a60373f6 | ||
|
|
ed5072f4b1 | ||
|
|
ed98ac9d7e | ||
|
|
d9f1ae6a46 | ||
|
|
5874482f03 | ||
|
|
0511e99dd4 | ||
|
|
4e4548d702 | ||
|
|
b3899e7aa4 | ||
|
|
6d9fcfa09f | ||
|
|
38e5b76996 | ||
|
|
ca4539e78d | ||
|
|
a54a61d00e | ||
|
|
142fc06b15 | ||
|
|
c7b1cda3ed | ||
|
|
fb2adea5e9 | ||
|
|
d9b15adb35 | ||
|
|
0bc7c034cf | ||
|
|
cbe0e5254e | ||
|
|
68bf17118d | ||
|
|
fdf6c510ec | ||
|
|
db95bd1c37 | ||
|
|
83cc358e1b | ||
|
|
11374c2fae | ||
|
|
27c8dabd8d | ||
|
|
9568c4605a | ||
|
|
6fe88ef53c | ||
|
|
0961a587c0 | ||
|
|
67c2ed7c62 | ||
|
|
f3a184af01 | ||
|
|
41d119b19f | ||
|
|
214a92f207 | ||
|
|
1759c1c75b | ||
|
|
2944cf94c7 | ||
|
|
59e0b006a5 | ||
|
|
1b12b112a1 | ||
|
|
88a481da8a | ||
|
|
d99909c403 | ||
|
|
f169661ce6 | ||
|
|
823348a72a | ||
|
|
5c75f0dcc2 | ||
|
|
601c802f23 | ||
|
|
b4ca70708a | ||
|
|
082c91c855 | ||
|
|
0f6a13191c | ||
|
|
6670a3214b | ||
|
|
63e0292e23 | ||
|
|
59f0fe5b32 | ||
|
|
387c387073 | ||
|
|
acd787a5af | ||
|
|
4ded01b104 | ||
|
|
c691b37f76 | ||
|
|
14ddecc81a | ||
|
|
3611eab39c | ||
|
|
e5350d2d50 | ||
|
|
249d4117e0 | ||
|
|
ba47a54293 | ||
|
|
f6b3a5cc22 | ||
|
|
6e0ef95320 | ||
|
|
082da78f4c | ||
|
|
21d280497b | ||
|
|
7ca0d46936 | ||
|
|
b566481477 | ||
|
|
fe497fc438 | ||
|
|
de73ced34a | ||
|
|
bc879507ae | ||
|
|
280b89d87c | ||
|
|
eee5d5d482 | ||
|
|
2e73886adc | ||
|
|
e199337a53 | ||
|
|
2417cfb02f | ||
|
|
2559d32912 | ||
|
|
c2df7d8198 | ||
|
|
c8edfac39e | ||
|
|
39d4646a4c | ||
|
|
40b1dccc54 | ||
|
|
32bac81d91 | ||
|
|
dccce946e3 | ||
|
|
915d568257 | ||
|
|
698bfe6e7b | ||
|
|
1f206a560b | ||
|
|
42f8f4fdcb | ||
|
|
c67cd5e6fc | ||
|
|
67e3886547 | ||
|
|
4b46c4eff3 | ||
|
|
cd09cc6c52 | ||
|
|
c2c7bbe9dd | ||
|
|
d40d704043 | ||
|
|
91d0c732f0 | ||
|
|
801e229c59 | ||
|
|
67ca814d0b | ||
|
|
fc791a6320 | ||
|
|
579a626de2 | ||
|
|
f99fd6cbad | ||
|
|
8c44410ce0 | ||
|
|
9118a4a780 | ||
|
|
a5372e4713 | ||
|
|
67f555d24e | ||
|
|
7f11c1a991 | ||
|
|
9c79a4174c | ||
|
|
376beac22f | ||
|
|
65e7f9e846 | ||
|
|
9f074cecce | ||
|
|
71f6061d30 | ||
|
|
3a466aea9a | ||
|
|
5baec4819f | ||
|
|
d3a8b5f6e1 | ||
|
|
498e77215a | ||
|
|
6750a5c44d | ||
|
|
e4516ab606 | ||
|
|
c3ccaf1a08 | ||
|
|
a7dab6d95a | ||
|
|
b50b67491d | ||
|
|
e6973f966b | ||
|
|
0ddd676ef0 | ||
|
|
c93e8f9e0c | ||
|
|
6557f1f9c0 | ||
|
|
17af56adb1 | ||
|
|
4344cc9202 | ||
|
|
125ea8f6d9 | ||
|
|
9c005fc683 | ||
|
|
674615041a | ||
|
|
15039553db | ||
|
|
ba86b479f8 | ||
|
|
6dd126461e | ||
|
|
97d8456382 | ||
|
|
ab6f90aa78 | ||
|
|
522308217a | ||
|
|
d173b7d8d6 | ||
|
|
b8e6450af1 | ||
|
|
45ebaf0ad8 | ||
|
|
1edbef0538 | ||
|
|
6efa304142 | ||
|
|
eed6015e70 | ||
|
|
94b516d074 | ||
|
|
56a7bc5030 | ||
|
|
af2788468e | ||
|
|
cc54135bf0 | ||
|
|
519550e9af | ||
|
|
93541035f6 | ||
|
|
568477b6af | ||
|
|
c7f0d42323 | ||
|
|
2015688312 | ||
|
|
8e02551956 | ||
|
|
96478ed016 | ||
|
|
2425cddaed | ||
|
|
b5185de706 | ||
|
|
56557c9c40 | ||
|
|
2cbe1ba903 | ||
|
|
69b459e7b6 | ||
|
|
add326a79d | ||
|
|
b7fc393e49 | ||
|
|
f2f639e484 | ||
|
|
9cfc66a6a4 | ||
|
|
bf141ca13f | ||
|
|
fe09637711 | ||
|
|
dbe3c163bd | ||
|
|
a52c114ff4 | ||
|
|
71fa87937b | ||
|
|
0c25f12825 | ||
|
|
093fd3c8f6 | ||
|
|
ebd474a7e6 | ||
|
|
11d62a8010 | ||
|
|
7069bb3c01 | ||
|
|
9254f119f6 | ||
|
|
ed58511de3 | ||
|
|
ab3375998d | ||
|
|
8979442e27 | ||
|
|
eb46cf2662 | ||
|
|
d218a9eaff | ||
|
|
7d6d015822 | ||
|
|
609c9aead8 | ||
|
|
10a70d4e52 | ||
|
|
de5742fc7d | ||
|
|
1a5f6cce46 | ||
|
|
f2cb91740e | ||
|
|
07cb47e6ac | ||
|
|
9adec12a67 | ||
|
|
787973f323 | ||
|
|
4bd5e6bd82 | ||
|
|
8e58b56d5c | ||
|
|
b13e95aefb | ||
|
|
3425352035 | ||
|
|
dbf99c6ac1 | ||
|
|
698fac8ff2 | ||
|
|
0d5520d91d | ||
|
|
944efb3a6a | ||
|
|
d91f47e582 | ||
|
|
2c52b96e04 | ||
|
|
e08bb3a197 | ||
|
|
b091586394 | ||
|
|
ca7ffd0a70 | ||
|
|
3f690ec78f | ||
|
|
6e04fe7454 | ||
|
|
65968b5320 | ||
|
|
ed8bf8272f | ||
|
|
3830887577 | ||
|
|
b6d8d89b32 | ||
|
|
814da058ec | ||
|
|
5d140f6fa0 | ||
|
|
51b43f5605 | ||
|
|
8fd9a9ab5d | ||
|
|
5f3b581562 | ||
|
|
139f9b987a | ||
|
|
63849f1275 | ||
|
|
9f8d1af2cd | ||
|
|
122fea1507 | ||
|
|
a7403fecc2 | ||
|
|
efc725fb24 | ||
|
|
4b4a622a17 | ||
|
|
8618ffa5fc | ||
|
|
2798ced9b9 | ||
|
|
a6696f3ba1 | ||
|
|
04eaeec7fc | ||
|
|
302d4e15ad | ||
|
|
18cc214c04 | ||
|
|
06e85357c7 | ||
|
|
eed0d3a66c | ||
|
|
d1d0c6772a | ||
|
|
7e3e19acbb | ||
|
|
b7b66b600d | ||
|
|
780970885e | ||
|
|
5f33f01094 | ||
|
|
719c4abba2 | ||
|
|
29858b8d0d | ||
|
|
b21a0f6192 | ||
|
|
f5331d1a77 | ||
|
|
5bb670b736 | ||
|
|
2eb8083869 | ||
|
|
99ecc7af70 | ||
|
|
ccad9eb9bc | ||
|
|
d9a608d9b0 | ||
|
|
f6b6abe734 | ||
|
|
e9efcc70a5 | ||
|
|
60398ad538 | ||
|
|
741f017a17 | ||
|
|
e4e84e5714 | ||
|
|
d395bb6052 | ||
|
|
17f04a886e | ||
|
|
807dae1768 | ||
|
|
7af54c5813 | ||
|
|
bd2160db26 | ||
|
|
a3f4548a81 | ||
|
|
8e4687fb53 | ||
|
|
189847927e | ||
|
|
6b2767d35c | ||
|
|
1f6a824dfb | ||
|
|
e8b4e3771c | ||
|
|
e272ea47be | ||
|
|
0ff05edd16 | ||
|
|
96a16a377f | ||
|
|
896b79f05b | ||
|
|
343e41c51d | ||
|
|
55b4f4fbe7 | ||
|
|
a5bc293372 | ||
|
|
43d71e95ac | ||
|
|
24064f8626 | ||
|
|
8588a42aa9 | ||
|
|
0f0d617951 | ||
|
|
69b09e312a | ||
|
|
784ebe57d7 | ||
|
|
08186ea51c | ||
|
|
d4aba5cb08 | ||
|
|
3a1844ec8e | ||
|
|
48c9ac36b1 | ||
|
|
e6874c866d | ||
|
|
289bb66f56 | ||
|
|
67344c8e0a | ||
|
|
222a53015e |
@@ -1,446 +0,0 @@
|
|||||||
version: 2
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-cache:
|
|
||||||
docker:
|
|
||||||
- image: circleci/buildpack-deps:trusty
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- restore_cache:
|
|
||||||
keys:
|
|
||||||
- v1-stack-dependencies-{{ checksum "postgrest.cabal" }}-{{ checksum "stack.yaml" }}
|
|
||||||
- run:
|
|
||||||
name: install stack & dependencies
|
|
||||||
command: |
|
|
||||||
curl -L https://github.com/commercialhaskell/stack/releases/download/v2.1.3/stack-2.1.3-linux-x86_64.tar.gz | tar zx -C /tmp
|
|
||||||
sudo mv /tmp/stack-2.1.3-linux-x86_64/stack /usr/bin
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libgmp-dev
|
|
||||||
sudo apt-get install -y --only-upgrade binutils
|
|
||||||
sudo apt-get install -y postgresql-client
|
|
||||||
stack setup
|
|
||||||
- run:
|
|
||||||
name: build src and tests dependencies
|
|
||||||
command: |
|
|
||||||
stack build --fast -j1 --only-dependencies
|
|
||||||
stack build --fast --test --no-run-tests --only-dependencies
|
|
||||||
- save_cache:
|
|
||||||
paths:
|
|
||||||
- "~/.stack"
|
|
||||||
- ".stack-work"
|
|
||||||
key: v1-stack-dependencies-{{ checksum "postgrest.cabal" }}-{{ checksum "stack.yaml" }}
|
|
||||||
- run:
|
|
||||||
name: build src and tests
|
|
||||||
command: |
|
|
||||||
stack build --fast -j1
|
|
||||||
stack build --fast --test --no-run-tests
|
|
||||||
|
|
||||||
style:
|
|
||||||
docker:
|
|
||||||
- image: nixos/nix:2.3
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- run:
|
|
||||||
name: Install linting and styling scripts
|
|
||||||
command: nix-env -f default.nix -iA lint style
|
|
||||||
- run:
|
|
||||||
name: Run linter
|
|
||||||
command: |
|
|
||||||
echo 'Note: For checking this locally, use `make lint`. Or, if using `nix`, run `nix-shell --run postgrest-lint`'
|
|
||||||
postgrest-lint
|
|
||||||
- run:
|
|
||||||
name: Run style check
|
|
||||||
command: |
|
|
||||||
echo 'Note: For checking this locally, use `make style`. Or, if using `nix`, run `nix-shell --run postgrest-style`'
|
|
||||||
postgrest-style-check
|
|
||||||
|
|
||||||
build-test-9.4:
|
|
||||||
docker:
|
|
||||||
- image: circleci/buildpack-deps:trusty
|
|
||||||
environment:
|
|
||||||
- PGHOST=localhost
|
|
||||||
- image: circleci/postgres:9.4.26
|
|
||||||
environment:
|
|
||||||
- POSTGRES_USER=circleci
|
|
||||||
- POSTGRES_DB=circleci
|
|
||||||
- POSTGRES_HOST_AUTH_METHOD=trust
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- restore_cache:
|
|
||||||
keys:
|
|
||||||
- v1-stack-dependencies-{{ checksum "postgrest.cabal" }}-{{ checksum "stack.yaml" }}
|
|
||||||
- run:
|
|
||||||
name: install ncat
|
|
||||||
command: |
|
|
||||||
# utility needed to test socket connection with curl < 7.40
|
|
||||||
sudo apt-get install nmap
|
|
||||||
- run:
|
|
||||||
name: install stack & dependencies
|
|
||||||
command: |
|
|
||||||
curl -L https://github.com/commercialhaskell/stack/releases/download/v2.1.3/stack-2.1.3-linux-x86_64.tar.gz | tar zx -C /tmp
|
|
||||||
sudo mv /tmp/stack-2.1.3-linux-x86_64/stack /usr/bin
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libgmp-dev
|
|
||||||
sudo apt-get install -y --only-upgrade binutils
|
|
||||||
sudo apt-get install -y postgresql-client
|
|
||||||
stack setup
|
|
||||||
- run:
|
|
||||||
name: build src and tests dependencies
|
|
||||||
command: |
|
|
||||||
stack build --fast -j1 --only-dependencies
|
|
||||||
stack build --fast --test --no-run-tests --only-dependencies
|
|
||||||
- run:
|
|
||||||
name: build src and tests
|
|
||||||
command: |
|
|
||||||
stack build --fast -j1
|
|
||||||
stack build --fast --test --no-run-tests
|
|
||||||
- run:
|
|
||||||
name: run tests
|
|
||||||
command: |
|
|
||||||
POSTGREST_TEST_CONNECTION=$(test/create_test_db "postgres://circleci@localhost" postgrest_test) stack test
|
|
||||||
test/io-tests.sh
|
|
||||||
|
|
||||||
build-test-9.6:
|
|
||||||
docker:
|
|
||||||
- image: circleci/buildpack-deps:trusty
|
|
||||||
environment:
|
|
||||||
- PGHOST=localhost
|
|
||||||
- image: circleci/postgres:9.6.17
|
|
||||||
environment:
|
|
||||||
- POSTGRES_USER=circleci
|
|
||||||
- POSTGRES_DB=circleci
|
|
||||||
- POSTGRES_HOST_AUTH_METHOD=trust
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- restore_cache:
|
|
||||||
keys:
|
|
||||||
- v1-stack-dependencies-{{ checksum "postgrest.cabal" }}-{{ checksum "stack.yaml" }}
|
|
||||||
- run:
|
|
||||||
name: install stack & dependencies
|
|
||||||
command: |
|
|
||||||
curl -L https://github.com/commercialhaskell/stack/releases/download/v2.1.3/stack-2.1.3-linux-x86_64.tar.gz | tar zx -C /tmp
|
|
||||||
sudo mv /tmp/stack-2.1.3-linux-x86_64/stack /usr/bin
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libgmp-dev
|
|
||||||
sudo apt-get install -y postgresql-client
|
|
||||||
stack setup
|
|
||||||
- run:
|
|
||||||
name: build src and tests
|
|
||||||
command: |
|
|
||||||
stack build --fast -j1
|
|
||||||
stack build --fast --test --no-run-tests
|
|
||||||
- run:
|
|
||||||
name: run tests
|
|
||||||
command: POSTGREST_TEST_CONNECTION=$(test/create_test_db "postgres://circleci@localhost" postgrest_test) stack test postgrest:spec
|
|
||||||
|
|
||||||
build-test-10:
|
|
||||||
docker:
|
|
||||||
- image: circleci/buildpack-deps:trusty
|
|
||||||
environment:
|
|
||||||
- PGHOST=localhost
|
|
||||||
- image: circleci/postgres:10.12
|
|
||||||
environment:
|
|
||||||
- POSTGRES_USER=circleci
|
|
||||||
- POSTGRES_DB=circleci
|
|
||||||
- POSTGRES_HOST_AUTH_METHOD=trust
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- restore_cache:
|
|
||||||
keys:
|
|
||||||
- v1-stack-dependencies-{{ checksum "postgrest.cabal" }}-{{ checksum "stack.yaml" }}
|
|
||||||
- run:
|
|
||||||
name: install stack & dependencies
|
|
||||||
command: |
|
|
||||||
curl -L https://github.com/commercialhaskell/stack/releases/download/v2.1.3/stack-2.1.3-linux-x86_64.tar.gz | tar zx -C /tmp
|
|
||||||
sudo mv /tmp/stack-2.1.3-linux-x86_64/stack /usr/bin
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libgmp-dev
|
|
||||||
sudo apt-get install -y postgresql-client
|
|
||||||
stack setup
|
|
||||||
- run:
|
|
||||||
name: build src and tests
|
|
||||||
command: |
|
|
||||||
stack build --fast -j1
|
|
||||||
stack build --fast --test --no-run-tests
|
|
||||||
- run:
|
|
||||||
name: run tests
|
|
||||||
command: POSTGREST_TEST_CONNECTION=$(test/create_test_db "postgres://circleci@localhost" postgrest_test) stack test postgrest:spec
|
|
||||||
|
|
||||||
build-test-11:
|
|
||||||
docker:
|
|
||||||
- image: circleci/buildpack-deps:trusty
|
|
||||||
environment:
|
|
||||||
- PGHOST=localhost
|
|
||||||
- image: circleci/postgres:11.7
|
|
||||||
environment:
|
|
||||||
- POSTGRES_USER=circleci
|
|
||||||
- POSTGRES_DB=circleci
|
|
||||||
- POSTGRES_HOST_AUTH_METHOD=trust
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- restore_cache:
|
|
||||||
keys:
|
|
||||||
- v1-stack-dependencies-{{ checksum "postgrest.cabal" }}-{{ checksum "stack.yaml" }}
|
|
||||||
- run:
|
|
||||||
name: install stack & dependencies
|
|
||||||
command: |
|
|
||||||
curl -L https://github.com/commercialhaskell/stack/releases/download/v2.1.3/stack-2.1.3-linux-x86_64.tar.gz | tar zx -C /tmp
|
|
||||||
sudo mv /tmp/stack-2.1.3-linux-x86_64/stack /usr/bin
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libgmp-dev
|
|
||||||
sudo apt-get install -y postgresql-client
|
|
||||||
stack setup
|
|
||||||
- run:
|
|
||||||
name: build src and tests
|
|
||||||
command: |
|
|
||||||
stack build --fast -j1
|
|
||||||
stack build --fast --test --no-run-tests
|
|
||||||
- run:
|
|
||||||
name: run tests
|
|
||||||
command: POSTGREST_TEST_CONNECTION=$(test/create_test_db "postgres://circleci@localhost" postgrest_test) stack test postgrest:spec
|
|
||||||
|
|
||||||
build-test-12:
|
|
||||||
docker:
|
|
||||||
- image: circleci/buildpack-deps:trusty
|
|
||||||
environment:
|
|
||||||
- PGHOST=localhost
|
|
||||||
- image: circleci/postgres:12.2
|
|
||||||
environment:
|
|
||||||
- POSTGRES_USER=circleci
|
|
||||||
- POSTGRES_DB=circleci
|
|
||||||
- POSTGRES_HOST_AUTH_METHOD=trust
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- restore_cache:
|
|
||||||
keys:
|
|
||||||
- v1-stack-dependencies-{{ checksum "postgrest.cabal" }}-{{ checksum "stack.yaml" }}
|
|
||||||
- run:
|
|
||||||
name: install stack & dependencies
|
|
||||||
command: |
|
|
||||||
curl -L https://github.com/commercialhaskell/stack/releases/download/v2.1.3/stack-2.1.3-linux-x86_64.tar.gz | tar zx -C /tmp
|
|
||||||
sudo mv /tmp/stack-2.1.3-linux-x86_64/stack /usr/bin
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libgmp-dev
|
|
||||||
sudo apt-get install -y postgresql-client
|
|
||||||
stack setup
|
|
||||||
- run:
|
|
||||||
name: build src and tests
|
|
||||||
command: |
|
|
||||||
stack build --fast -j1
|
|
||||||
stack build --fast --test --no-run-tests
|
|
||||||
- run:
|
|
||||||
name: run tests
|
|
||||||
command: POSTGREST_TEST_CONNECTION=$(test/create_test_db "postgres://circleci@localhost" postgrest_test) stack test postgrest:spec
|
|
||||||
|
|
||||||
build-prof-test:
|
|
||||||
docker:
|
|
||||||
- image: circleci/buildpack-deps:trusty
|
|
||||||
environment:
|
|
||||||
- PGHOST=localhost
|
|
||||||
- TERM=xterm
|
|
||||||
- image: circleci/postgres:9.6.17
|
|
||||||
environment:
|
|
||||||
- POSTGRES_USER=circleci
|
|
||||||
- POSTGRES_DB=circleci
|
|
||||||
- POSTGRES_HOST_AUTH_METHOD=trust
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- restore_cache:
|
|
||||||
keys:
|
|
||||||
- v1-stack-prof-dependencies-{{ checksum "postgrest.cabal" }}-{{ checksum "stack.yaml" }}
|
|
||||||
- run:
|
|
||||||
name: install stack & dependencies
|
|
||||||
command: |
|
|
||||||
curl -L https://github.com/commercialhaskell/stack/releases/download/v2.1.3/stack-2.1.3-linux-x86_64.tar.gz | tar zx -C /tmp
|
|
||||||
sudo mv /tmp/stack-2.1.3-linux-x86_64/stack /usr/bin
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libgmp-dev
|
|
||||||
sudo apt-get install -y postgresql-client
|
|
||||||
stack setup
|
|
||||||
- run:
|
|
||||||
name: build dependencies with profiling enabled
|
|
||||||
command: |
|
|
||||||
stack build --profile -j1 --only-dependencies
|
|
||||||
- save_cache:
|
|
||||||
paths:
|
|
||||||
- "~/.stack"
|
|
||||||
- ".stack-work"
|
|
||||||
key: v1-stack-prof-dependencies-{{ checksum "postgrest.cabal" }}-{{ checksum "stack.yaml" }}
|
|
||||||
- run:
|
|
||||||
name: build with profiling enabled
|
|
||||||
command: |
|
|
||||||
stack build --profile -j1
|
|
||||||
- run:
|
|
||||||
name: run memory usage tests
|
|
||||||
command: |
|
|
||||||
test/create_test_db "postgres://circleci@localhost" postgrest_test
|
|
||||||
test/memory-tests.sh
|
|
||||||
|
|
||||||
release:
|
|
||||||
docker:
|
|
||||||
- image: circleci/golang:1.9
|
|
||||||
steps:
|
|
||||||
- attach_workspace:
|
|
||||||
at: /tmp/workspace
|
|
||||||
- checkout
|
|
||||||
- run:
|
|
||||||
name: add body and tars to github release
|
|
||||||
command: |
|
|
||||||
go get -u github.com/tcnksm/ghr
|
|
||||||
START=$(echo $CIRCLE_TAG | cut -c2-)
|
|
||||||
END='## \['
|
|
||||||
BODY=$(sed -n "1,/$START/d;/$END/q;p" CHANGELOG.md)
|
|
||||||
ghr -t $GITHUB_TOKEN -u $CIRCLE_PROJECT_USERNAME -r $CIRCLE_PROJECT_REPONAME -b "$BODY" --replace $CIRCLE_TAG /tmp/workspace/bin
|
|
||||||
- setup_remote_docker
|
|
||||||
- run:
|
|
||||||
name: publish docker image
|
|
||||||
command: |
|
|
||||||
docker build --build-arg POSTGREST_VERSION=$CIRCLE_TAG -t postgrest ./docker/
|
|
||||||
docker login -u $DOCKER_USER -p $DOCKER_PASS
|
|
||||||
docker tag postgrest postgrest/postgrest:$CIRCLE_TAG
|
|
||||||
docker push postgrest/postgrest:$CIRCLE_TAG
|
|
||||||
docker tag postgrest postgrest/postgrest:latest
|
|
||||||
docker push postgrest/postgrest:latest
|
|
||||||
|
|
||||||
build-nix:
|
|
||||||
machine: true
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- run:
|
|
||||||
name: Install Nix
|
|
||||||
command: |
|
|
||||||
curl -L https://nixos.org/nix/install | sh
|
|
||||||
echo "source $HOME/.nix-profile/etc/profile.d/nix.sh" >> $BASH_ENV
|
|
||||||
- run:
|
|
||||||
name: Install and use the Cachix binary cache
|
|
||||||
command: |
|
|
||||||
nix-env -iA cachix -f https://cachix.org/api/v1/install
|
|
||||||
cachix use postgrest
|
|
||||||
- run:
|
|
||||||
name: Build the dynamic PostgREST executable
|
|
||||||
command: |
|
|
||||||
# We build the dynamic postgrest executable first in order to fail fast if anything
|
|
||||||
# should be wrong with the source code.
|
|
||||||
nix-build -A postgrest
|
|
||||||
- restore_cache:
|
|
||||||
keys:
|
|
||||||
- v1-cabal-nix-dependencies-{{ checksum "postgrest.cabal" }}
|
|
||||||
- run:
|
|
||||||
name: Run the spec tests against all supported PostgreSQL versions
|
|
||||||
command: |
|
|
||||||
# Our utility scripts use cabal to run the tests, which will pick up all the libraries that
|
|
||||||
# are built with Nix. So this should only ever build the PostgREST itself and be reasonably
|
|
||||||
# quick. We accelerate that part further by caching `~/.cabal` and `dist-newstyle`.
|
|
||||||
nix-shell --run postgrest-test-spec-all
|
|
||||||
- save_cache:
|
|
||||||
paths:
|
|
||||||
- "~/.cabal"
|
|
||||||
- "dist-newstyle"
|
|
||||||
key: v1-cabal-nix-dependencies-{{ checksum "postgrest.cabal" }}
|
|
||||||
- run:
|
|
||||||
name: Build all derivations from default.nix and push results to Cachix
|
|
||||||
command: |
|
|
||||||
# Only push to the cache when CircleCI makes the CACHIX_SIGNING_KEY available (e.g. not for
|
|
||||||
# pull requests).
|
|
||||||
if [ -n "${CACHIX_SIGNING_KEY:-""}" ]; then
|
|
||||||
echo "Building and caching all derivations..."
|
|
||||||
nix-build | cachix push postgrest
|
|
||||||
else
|
|
||||||
echo "Building all derivations (caching skipped for outside pull requests)..."
|
|
||||||
nix-build
|
|
||||||
fi
|
|
||||||
|
|
||||||
build-static-bin:
|
|
||||||
machine: true
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- run:
|
|
||||||
name: Install Nix
|
|
||||||
command: |
|
|
||||||
curl -L https://nixos.org/nix/install | sh
|
|
||||||
echo "source $HOME/.nix-profile/etc/profile.d/nix.sh" >> $BASH_ENV
|
|
||||||
- run:
|
|
||||||
name: Install and use the Cachix binary cache
|
|
||||||
command: |
|
|
||||||
nix-env -iA cachix -f https://cachix.org/api/v1/install
|
|
||||||
cachix use postgrest
|
|
||||||
- run:
|
|
||||||
name: Build the PostgREST statically linked binary
|
|
||||||
command: |
|
|
||||||
nix-build -A postgrestStatic
|
|
||||||
- run:
|
|
||||||
name: compress binary
|
|
||||||
command: |
|
|
||||||
mkdir -p /tmp/workspace/bin
|
|
||||||
cd /tmp/workspace/bin
|
|
||||||
tar cvJf postgrest-$CIRCLE_TAG-linux-x64-static.tar.xz -C ~/project/result/bin postgrest
|
|
||||||
- persist_to_workspace:
|
|
||||||
root: /tmp/workspace
|
|
||||||
paths:
|
|
||||||
- bin/*
|
|
||||||
|
|
||||||
workflows:
|
|
||||||
version: 2
|
|
||||||
build-test-release:
|
|
||||||
jobs:
|
|
||||||
- build-cache:
|
|
||||||
filters:
|
|
||||||
tags:
|
|
||||||
only: /v[0-9]+(\.[0-9]+)*/
|
|
||||||
- style:
|
|
||||||
filters:
|
|
||||||
tags:
|
|
||||||
only: /v[0-9]+(\.[0-9]+)*/
|
|
||||||
- build-test-9.4:
|
|
||||||
filters:
|
|
||||||
tags:
|
|
||||||
only: /v[0-9]+(\.[0-9]+)*/
|
|
||||||
requires:
|
|
||||||
- build-cache
|
|
||||||
- build-test-9.6:
|
|
||||||
filters:
|
|
||||||
tags:
|
|
||||||
only: /v[0-9]+(\.[0-9]+)*/
|
|
||||||
requires:
|
|
||||||
- build-cache
|
|
||||||
- build-test-10:
|
|
||||||
filters:
|
|
||||||
tags:
|
|
||||||
only: /v[0-9]+(\.[0-9]+)*/
|
|
||||||
requires:
|
|
||||||
- build-cache
|
|
||||||
- build-test-11:
|
|
||||||
filters:
|
|
||||||
tags:
|
|
||||||
only: /v[0-9]+(\.[0-9]+)*/
|
|
||||||
requires:
|
|
||||||
- build-cache
|
|
||||||
- build-test-12:
|
|
||||||
filters:
|
|
||||||
tags:
|
|
||||||
only: /v[0-9]+(\.[0-9]+)*/
|
|
||||||
requires:
|
|
||||||
- build-cache
|
|
||||||
- build-prof-test:
|
|
||||||
filters:
|
|
||||||
tags:
|
|
||||||
only: /v[0-9]+(\.[0-9]+)*/
|
|
||||||
- build-static-bin:
|
|
||||||
requires:
|
|
||||||
- build-nix
|
|
||||||
- build-prof-test
|
|
||||||
filters:
|
|
||||||
tags:
|
|
||||||
only: /v[0-9]+(\.[0-9]+)*/
|
|
||||||
branches:
|
|
||||||
ignore: /.*/
|
|
||||||
- release:
|
|
||||||
requires:
|
|
||||||
- build-static-bin
|
|
||||||
filters:
|
|
||||||
tags:
|
|
||||||
only: /v[0-9]+(\.[0-9]+)*/
|
|
||||||
- build-nix:
|
|
||||||
filters:
|
|
||||||
tags:
|
|
||||||
only: /v[0-9]+(\.[0-9]+)*/
|
|
||||||
+11
-51
@@ -1,55 +1,15 @@
|
|||||||
freebsd_instance:
|
freebsd_instance:
|
||||||
image: freebsd-12-1-release-amd64
|
image_family: freebsd-13-1
|
||||||
|
|
||||||
build_task:
|
build_task:
|
||||||
env:
|
name: Build FreeBSD (Stack)
|
||||||
ASSUME_ALWAYS_YES: YES
|
install_script: pkg install -y postgresql13-client hs-stack git
|
||||||
install_script:
|
|
||||||
## TODO: Some of these workaround steps might be unneeded now
|
|
||||||
## gmake required, otherwise bsd make is used and some cryptic "No operator" errors are shown
|
|
||||||
- pkg install -y gmake
|
|
||||||
## gcc required, otherwise warning: macro expansion producing 'defined' has undefined behavior [-Wexpansion-to-defined]
|
|
||||||
- pkg install -y gcc
|
|
||||||
- pkg install -y libiconv
|
|
||||||
- pkg install -y gmp
|
|
||||||
- pkg install -y postgresql95-client
|
|
||||||
- pkg install -y pkgconf
|
|
||||||
- pkg install -y ghc
|
|
||||||
- pkg install -y hs-cabal-install
|
|
||||||
cabal_cache:
|
|
||||||
folder: ~/.cabal
|
|
||||||
fingerprint_script: cat postgrest.cabal
|
|
||||||
build_script:
|
|
||||||
- cabal v2-update
|
|
||||||
- cabal v2-build
|
|
||||||
binaries_artifacts:
|
|
||||||
path: "dist-newstyle/build/x86_64-freebsd/ghc-8.6.5/postgrest-7.0.1/x/postgrest/build/postgrest/*"
|
|
||||||
#binary_script:
|
|
||||||
#- mkdir bin
|
|
||||||
## cabal v2-install --installdir=.
|
|
||||||
## the installdir option is not working on this cabal version(form hs-cabal-install), so the binary must be extrated from the path
|
|
||||||
## this is too brittle so for now we don't use it
|
|
||||||
#- ldd /tmp/cirrus-ci-build/dist-newstyle/build/x86_64-freebsd/ghc-8.6.5/postgrest-7.0.1/x/postgrest/build/postgrest/postgrest
|
|
||||||
#- mv /tmp/cirrus-ci-build/dist-newstyle/build/x86_64-freebsd/ghc-8.6.5/postgrest-7.0.1/x/postgrest/build/postgrest/postgrest bin/
|
|
||||||
## https://cirrus-ci.org/guide/writing-tasks/#artifacts-instruction
|
|
||||||
#binaries_artifacts:
|
|
||||||
#path: bin/
|
|
||||||
cleanup_script:
|
|
||||||
- rm -rf ~/.cabal/logs
|
|
||||||
- rm ~/.cabal/config
|
|
||||||
|
|
||||||
#publish_task:
|
stack_cache:
|
||||||
#skip: $CIRRUS_TAG == ""
|
folders: /.stack
|
||||||
#env:
|
fingerprint_script: cat postgrest.cabal stack.yaml.lock
|
||||||
#TOKEN: ENCRYPTED[c99babbf04e8a33962ffbbc75bf20d3abe408f9bede5ed5d8956cd24da6fdb34266c920984cc43f8106ef3423fba0e98]
|
reupload_on_changes: false
|
||||||
#depends_on:
|
|
||||||
#- build
|
build_script: stack build -j 1 --local-bin-path . --copy-bins
|
||||||
#script:
|
bin_artifacts:
|
||||||
## https://cirrus-ci.org/guide/writing-tasks/#artifacts-instruction
|
path: postgrest
|
||||||
#- wget https://api.cirrus-ci.com/v1/artifact/github/$CIRRUS_REPO_FULL_NAME/build/binaries/bin/postgrest
|
|
||||||
#- tar cjf postgrest-$CIRRUS_TAG-freebsd.tar.xz postgrest
|
|
||||||
### The CIRRUS_RELEASE env var is not the same as the tag. It's an id for the release.
|
|
||||||
#- curl -X POST --data-binary @$NAME \
|
|
||||||
#-H "Authorization:token ${TOKEN}" \
|
|
||||||
#-H "Content-Type:application/octet-stream" \
|
|
||||||
#"https://uploads.github.com/repos/$CIRRUS_REPO_FULL_NAME/releases/${CIRRUS_RELEASE}/assets?name=${NAME}"
|
|
||||||
|
|||||||
+13
-15
@@ -15,14 +15,14 @@ your contributions.
|
|||||||
|
|
||||||
## Issues
|
## Issues
|
||||||
|
|
||||||
|
For questions on how to use PostgREST, please use
|
||||||
|
[GitHub discussions](https://github.com/PostgREST/postgrest/discussions).
|
||||||
|
|
||||||
### Reporting an Issue
|
### Reporting an Issue
|
||||||
|
|
||||||
* Make sure you test against the latest released version. It is possible
|
* Make sure you test against the latest [stable release](https://github.com/PostgREST/postgrest/releases/latest)
|
||||||
we already fixed the bug you're experiencing.
|
and also against the latest [nightly release](https://github.com/PostgREST/postgrest/releases/tag/nightly).
|
||||||
|
It is possible we already fixed the bug you're experiencing.
|
||||||
* Also check the `CHANGELOG.md` to see if any unreleased changes affect
|
|
||||||
the issue. The very newest changes can take a little while to be released
|
|
||||||
as a new official version.
|
|
||||||
|
|
||||||
* Provide steps to reproduce the issue, including your OS version and
|
* Provide steps to reproduce the issue, including your OS version and
|
||||||
the specific database schema that you are using.
|
the specific database schema that you are using.
|
||||||
@@ -32,11 +32,14 @@ your contributions.
|
|||||||
then [find your logs](http://blog.endpoint.com/2014/11/dear-postgresql-where-are-my-logs.html).
|
then [find your logs](http://blog.endpoint.com/2014/11/dear-postgresql-where-are-my-logs.html).
|
||||||
|
|
||||||
* If your database schema has changed while the PostgREST server is running,
|
* If your database schema has changed while the PostgREST server is running,
|
||||||
[send the server a `SIGUSR1` signal](http://postgrest.org/en/v5.2/admin.html#schema-reloading) or restart it to ensure the schema cache
|
[send the server a `SIGUSR1` signal](http://postgrest.org/en/latest/admin.html#schema-reloading) or restart it to ensure the schema cache
|
||||||
is not stale. This sometimes fixes apparent bugs.
|
is not stale. This sometimes fixes apparent bugs.
|
||||||
|
|
||||||
## Code
|
## Code
|
||||||
|
|
||||||
|
We have a fully nix-based development environment with many tools for a smooth development workflow available.
|
||||||
|
Check the [development docs](https://github.com/PostgREST/postgrest/blob/main/nix/README.md) on how to set it up and use it.
|
||||||
|
|
||||||
### Haskell Conventions
|
### Haskell Conventions
|
||||||
|
|
||||||
* All contributions must pass the tests before being merged. When
|
* All contributions must pass the tests before being merged. When
|
||||||
@@ -44,14 +47,9 @@ your contributions.
|
|||||||
|
|
||||||
* All code must also pass [hlint](http://community.haskell.org/~ndm/hlint/) and [stylish-haskell](https://github.com/jaspervdj/stylish-haskell)
|
* All code must also pass [hlint](http://community.haskell.org/~ndm/hlint/) and [stylish-haskell](https://github.com/jaspervdj/stylish-haskell)
|
||||||
with no warnings. This helps enforce a uniform style for all committers. Continuous integration will check this as well on every
|
with no warnings. This helps enforce a uniform style for all committers. Continuous integration will check this as well on every
|
||||||
pull request. There's a useful Makefile that helps with checking this locally. You can run `make commit-check` to do this manually but
|
pull request. There are useful tools in the nix-shell that help with checking this locally. You can run `postgrest-check` to do this manually but
|
||||||
we recommend adding it to `.git/hooks/pre-commit` to automatically check this before doing a commit.
|
we recommend adding it to `.git/hooks/pre-commit` as `nix-shell --run postgrest-check` to automatically check this before doing a commit.
|
||||||
|
|
||||||
* For help building the Haskell code on your computer check out the [building from
|
|
||||||
source](https://postgrest.com/en/stable/install.html#build-from-source) docs section.
|
|
||||||
|
|
||||||
### Running Tests
|
### Running Tests
|
||||||
|
|
||||||
For instructions on running tests, see the official docs hosted here:
|
For instructions on running tests, see the [development docs](https://github.com/PostgREST/postgrest/blob/main/nix/README.md#testing).
|
||||||
|
|
||||||
https://postgrest.com/en/stable/install.html#postgrest-test-suite
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<!--
|
<!--
|
||||||
Before reporting a bug:
|
Before reporting a bug:
|
||||||
If your database schema has changed while the PostgREST server is running, send the server a SIGUSR1 signal or restart it(http://postgrest.org/en/v5.2/admin.html#schema-reloading) to ensure the schema cache is not stale. This sometimes fixes apparent bugs.
|
If your database schema has changed while the PostgREST server is running,
|
||||||
|
send the server a SIGUSR1 signal or restart it(http://postgrest.org/en/stable/admin.html#schema-reloading)
|
||||||
|
to ensure the schema cache is not stale. This sometimes fixes apparent bugs.
|
||||||
-->
|
-->
|
||||||
### Environment
|
### Environment
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<!--
|
||||||
|
When submitting a new feature or fix:
|
||||||
|
|
||||||
|
- Add a new entry to the CHANGELOG - https://github.com/PostgREST/postgrest/blob/main/CHANGELOG.md#unreleased
|
||||||
|
- If relevant, update the docs - https://github.com/PostgREST/postgrest-docs
|
||||||
|
-->
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
name: Setup Nix
|
||||||
|
|
||||||
|
description: Installs nix, sets up cachix and installs a subset of tooling.
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
authToken:
|
||||||
|
description: Token to pass to cachix
|
||||||
|
tools:
|
||||||
|
description: Tools to install with nix-env -iA <tools>
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- uses: cachix/install-nix-action@v23
|
||||||
|
with:
|
||||||
|
install_url: https://releases.nixos.org/nix/nix-2.13.3/install
|
||||||
|
- uses: cachix/cachix-action@v12
|
||||||
|
with:
|
||||||
|
name: postgrest
|
||||||
|
authToken: ${{ inputs.authToken }}
|
||||||
|
- if: ${{ inputs.tools }}
|
||||||
|
run: nix-env -f default.nix -iA ${{ inputs.tools }}
|
||||||
|
shell: bash
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
codecov:
|
||||||
|
branch: main
|
||||||
|
require_ci_to_pass: false
|
||||||
|
|
||||||
|
comment: false
|
||||||
|
|
||||||
|
coverage:
|
||||||
|
status:
|
||||||
|
project:
|
||||||
|
default:
|
||||||
|
target: auto
|
||||||
|
threshold: 1%
|
||||||
|
only_pulls: false
|
||||||
|
patch:
|
||||||
|
default:
|
||||||
|
target: auto
|
||||||
|
threshold: 1%
|
||||||
|
only_pulls: true
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: /.github/actions/setup-nix
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
Executable
+57
@@ -0,0 +1,57 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Get the FreeBSD PostgREST executable built by CirrusCI for the given GITHUB_COMMIT in GITHUB_REPOSITORY
|
||||||
|
|
||||||
|
# We use the GitHub API for 'check suites' to find the corresponding CirrusCI job, see:
|
||||||
|
# https://docs.github.com/en/rest/reference/checks#list-check-suites-for-a-git-reference
|
||||||
|
|
||||||
|
cirrus_artifact_name=bin
|
||||||
|
gh_auth_header="Authorization: Bearer $GITHUB_TOKEN"
|
||||||
|
gh_accept_header="Accept: application/vnd.github.v3+json"
|
||||||
|
|
||||||
|
get_gh_check_runs_url() {
|
||||||
|
gh_checks_list_url="https://api.github.com/repos/$GITHUB_REPOSITORY/commits/$GITHUB_COMMIT/check-suites"
|
||||||
|
>&2 echo "Getting list of check-suites from $gh_checks_list_url ..."
|
||||||
|
curl --fail -H "$gh_auth_header" -H "$gh_accept_header" "$gh_checks_list_url" \
|
||||||
|
| jq -r '.check_suites[] | select(.app.slug == "cirrus-ci") | .check_runs_url'
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_cirrusci() {
|
||||||
|
gh_check_runs_url="$(get_gh_check_runs_url)"
|
||||||
|
>&2 echo "Waiting to CirrusCI run to complete (two hours maximum)..."
|
||||||
|
for _ in $(seq 1 120); do
|
||||||
|
echo "Checking for CirrusCI task status at $gh_check_runs_url ..."
|
||||||
|
status=$(curl --fail -H "$gh_auth_header" "$gh_check_runs_url" | jq -r '.check_runs[] | .status')
|
||||||
|
if [ "$status" == "completed" ]; then
|
||||||
|
break
|
||||||
|
else
|
||||||
|
echo "CirrusCI task is still $status, waiting..."
|
||||||
|
sleep 60
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# The CirrusCI taskid can change if a new check run is started for the same commit,
|
||||||
|
# e.g. when pushing both a branch and tag. We make sure that we have the very
|
||||||
|
# latest taskid by re-loading the 'gh_check_runs_url' and the 'check run' itself.
|
||||||
|
get_cirrus_taskid() {
|
||||||
|
gh_check_runs_url="$(get_gh_check_runs_url)"
|
||||||
|
>&2 echo "Getting the CirrusCI task id from $gh_check_runs_url ..."
|
||||||
|
curl --fail -H "$gh_auth_header" -H "$gh_accept_header" "$gh_check_runs_url" \
|
||||||
|
| jq -r '.check_runs[] | .external_id'
|
||||||
|
}
|
||||||
|
|
||||||
|
download_artifact() {
|
||||||
|
cirrus_task_id="$(get_cirrus_taskid)"
|
||||||
|
cirrus_artifact_url="https://api.cirrus-ci.com/v1/artifact/task/$cirrus_task_id/$cirrus_artifact_name.zip"
|
||||||
|
>&2 echo "Attemping to download the CirrusCI artifact from $cirrus_artifact_url ..."
|
||||||
|
curl --fail "$cirrus_artifact_url" -o freebsd.zip
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_cirrusci
|
||||||
|
download_artifact
|
||||||
|
|
||||||
|
echo "Unpacking executable..."
|
||||||
|
unzip freebsd.zip -d .
|
||||||
|
rm -rf freebsd.zip
|
||||||
Executable
+12
@@ -0,0 +1,12 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Tag a release that will be built with Github Actions. The version is
|
||||||
|
# read from 'postgrest.cabal'.
|
||||||
|
|
||||||
|
version="$(grep -oP '^version:\s*\K.*' postgrest.cabal)"
|
||||||
|
|
||||||
|
echo "Tagging version v$version"
|
||||||
|
git tag -f "v$version"
|
||||||
|
|
||||||
|
echo "Pushing tag..."
|
||||||
|
git push -f origin "refs/tags/v$version"
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# This script builds PostgREST in a remote ARM server
|
||||||
|
|
||||||
|
[ -z "$1" ] && { echo "Missing 1st argument: PostgREST github commit SHA"; exit 1; }
|
||||||
|
[ -z "$2" ] && { echo "Missing 2nd argument: Build environment directory name"; exit 1; }
|
||||||
|
[ -z "$3" ] && { echo "Missing 3rd argument: GHC version"; exit 1; }
|
||||||
|
|
||||||
|
PGRST_GITHUB_COMMIT="$1"
|
||||||
|
SCRIPT_DIR="$2"
|
||||||
|
|
||||||
|
DOCKER_BUILD_DIR="$SCRIPT_DIR/docker-env"
|
||||||
|
# latest is a shortcut documented on https://www.haskell.org/ghcup/guide/#tags-and-shortcuts
|
||||||
|
CABAL_VERSION="latest"
|
||||||
|
GHC_VERSION="$3"
|
||||||
|
|
||||||
|
install_packages() {
|
||||||
|
sudo apt-get update -y
|
||||||
|
sudo apt-get upgrade -y
|
||||||
|
sudo apt-get install -y git build-essential curl libffi-dev libffi7 libgmp-dev libgmp10 libncurses-dev libncurses5 libtinfo5 llvm libnuma-dev zlib1g-dev libpq-dev jq gcc
|
||||||
|
sudo apt-get clean
|
||||||
|
}
|
||||||
|
|
||||||
|
install_ghcup() {
|
||||||
|
export BOOTSTRAP_HASKELL_NONINTERACTIVE=1
|
||||||
|
export BOOTSTRAP_HASKELL_MINIMAL=1
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh
|
||||||
|
source ~/.ghcup/env
|
||||||
|
}
|
||||||
|
|
||||||
|
install_cabal() {
|
||||||
|
ghcup upgrade
|
||||||
|
ghcup install cabal $CABAL_VERSION
|
||||||
|
ghcup set cabal $CABAL_VERSION
|
||||||
|
}
|
||||||
|
|
||||||
|
install_ghc() {
|
||||||
|
ghcup upgrade
|
||||||
|
ghcup install ghc $GHC_VERSION
|
||||||
|
ghcup set ghc $GHC_VERSION
|
||||||
|
}
|
||||||
|
|
||||||
|
install_packages
|
||||||
|
|
||||||
|
# Add ghcup to the PATH for this session
|
||||||
|
[ -f ~/.ghcup/env ] && source ~/.ghcup/env
|
||||||
|
|
||||||
|
ghcup --version || install_ghcup
|
||||||
|
ghcup set cabal $CABAL_VERSION || install_cabal
|
||||||
|
ghcup set ghc $GHC_VERSION || install_ghc
|
||||||
|
|
||||||
|
cd ~/$SCRIPT_DIR
|
||||||
|
|
||||||
|
# Clone the repository and build the project
|
||||||
|
git clone https://github.com/PostgREST/postgrest.git
|
||||||
|
cd postgrest
|
||||||
|
git checkout $PGRST_GITHUB_COMMIT
|
||||||
|
cabal v2-update && cabal v2-build
|
||||||
|
|
||||||
|
# Copy the built binary to the Dockerfile directory
|
||||||
|
PGRST_BIN=$(cabal exec which postgrest | tail -1)
|
||||||
|
cp $PGRST_BIN ~/$DOCKER_BUILD_DIR
|
||||||
|
|
||||||
|
# Move and compress the built binary
|
||||||
|
mkdir -p ~/$SCRIPT_DIR/result
|
||||||
|
mv $PGRST_BIN ~/$SCRIPT_DIR/result
|
||||||
|
cd ~/$SCRIPT_DIR
|
||||||
|
tar -cJf result.tar.xz result
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# PostgREST docker hub image
|
||||||
|
|
||||||
|
FROM ubuntu:focal AS postgrest
|
||||||
|
|
||||||
|
RUN apt-get update -y \
|
||||||
|
&& apt install -y --no-install-recommends libpq-dev zlib1g-dev jq gcc libnuma-dev \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY postgrest /usr/bin/postgrest
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
USER 1000
|
||||||
|
|
||||||
|
# Use the array form to avoid running the command using bash, which does not handle `SIGTERM` properly.
|
||||||
|
# See https://docs.docker.com/compose/faq/#why-do-my-services-take-10-seconds-to-recreate-or-stop
|
||||||
|
CMD ["postgrest"]
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# This script publishes the Docker ARM images to Docker Hub.
|
||||||
|
|
||||||
|
[ -z "$1" ] && { echo "Missing 1st argument: PostgREST github commit SHA"; exit 1; }
|
||||||
|
[ -z "$2" ] && { echo "Missing 2nd argument: Docker repo"; exit 1; }
|
||||||
|
[ -z "$3" ] && { echo "Missing 3rd argument: Docker username"; exit 1; }
|
||||||
|
[ -z "$4" ] && { echo "Missing 4th argument: Docker password"; exit 1; }
|
||||||
|
[ -z "$5" ] && { echo "Missing 5th argument: Build environment directory name"; exit 1; }
|
||||||
|
[ -z "$6" ] && { echo "Missing 6th argument: PostgREST version"; exit 1; }
|
||||||
|
|
||||||
|
PGRST_GITHUB_COMMIT="$1"
|
||||||
|
DOCKER_REPO="$2"
|
||||||
|
DOCKER_USER="$3"
|
||||||
|
DOCKER_PASS="$4"
|
||||||
|
SCRIPT_DIR="$5"
|
||||||
|
PGRST_VERSION="v$6"
|
||||||
|
IS_PRERELEASE="$7"
|
||||||
|
|
||||||
|
DOCKER_BUILD_DIR="$SCRIPT_DIR/docker-env"
|
||||||
|
|
||||||
|
clean_env()
|
||||||
|
{
|
||||||
|
sudo docker logout
|
||||||
|
}
|
||||||
|
|
||||||
|
# Login to Docker
|
||||||
|
sudo docker logout
|
||||||
|
{ echo $DOCKER_PASS | sudo docker login -u $DOCKER_USER --password-stdin; } || { echo "Couldn't login to docker"; exit 1; }
|
||||||
|
|
||||||
|
trap clean_env sigint sigterm exit
|
||||||
|
|
||||||
|
# Move to the docker build environment
|
||||||
|
cd ~/$DOCKER_BUILD_DIR
|
||||||
|
|
||||||
|
# Push final images to Docker hub
|
||||||
|
# NOTE: This command publishes a separate ARM image because the builds cannot
|
||||||
|
# be added to the manifest if they are not in the registry beforehand.
|
||||||
|
# This image must be manually deleted from Docker Hub at the end of the process.
|
||||||
|
sudo docker buildx build --build-arg PGRST_GITHUB_COMMIT=$PGRST_GITHUB_COMMIT \
|
||||||
|
-t $DOCKER_REPO/postgrest:$PGRST_VERSION-arm \
|
||||||
|
--push .
|
||||||
|
|
||||||
|
# Add the arm images to the manifest
|
||||||
|
# NOTE: This assumes that there already is a `postgrest:<version>` image
|
||||||
|
# for the amd64 architecture pushed to Docker Hub
|
||||||
|
sudo docker buildx imagetools create --append -t $DOCKER_REPO/postgrest:$PGRST_VERSION $DOCKER_REPO/postgrest:$PGRST_VERSION-arm
|
||||||
|
[ -z $IS_PRERELEASE ] && sudo docker buildx imagetools create --append -t $DOCKER_REPO/postgrest:latest $DOCKER_REPO/postgrest:$PGRST_VERSION-arm
|
||||||
|
|
||||||
|
sudo docker logout
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
name: Cachix
|
||||||
|
|
||||||
|
# This workflow serves to
|
||||||
|
# - keep cachix up to date with the main branch
|
||||||
|
# - incrementally update cachix for large dependency
|
||||||
|
# updates, e.g. after running postgrest-nixpkgs-upgrade,
|
||||||
|
# which can cause the main CI workflow to time out
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- rel-*
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
Seed-Cachix:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: Linux
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
- os: MacOS
|
||||||
|
runs-on: macos-latest
|
||||||
|
name: Seed ${{ matrix.os }}
|
||||||
|
runs-on: ${{ matrix.runs-on }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Setup Nix Environment
|
||||||
|
uses: ./.github/actions/setup-nix
|
||||||
|
with:
|
||||||
|
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||||
|
|
||||||
|
- name: Install cachix tooling
|
||||||
|
run: |
|
||||||
|
nix-env -f default.nix -iA devTools.pushCachix.bin
|
||||||
|
postgrest-push-cachix
|
||||||
|
|
||||||
|
- name: Seed dynamic postgrest build
|
||||||
|
run: |
|
||||||
|
nix-build -A postgrestPackage
|
||||||
|
postgrest-push-cachix
|
||||||
|
|
||||||
|
- name: Seed style tools
|
||||||
|
run: |
|
||||||
|
nix-build -A style
|
||||||
|
postgrest-push-cachix
|
||||||
|
|
||||||
|
- name: Seed test tools
|
||||||
|
run: |
|
||||||
|
nix-build -A tests
|
||||||
|
postgrest-push-cachix
|
||||||
|
|
||||||
|
- name: Seed static toolchain
|
||||||
|
if: matrix.os == 'Linux'
|
||||||
|
run: |
|
||||||
|
nix-build -A packagesStatic.haskellPackages.hello
|
||||||
|
postgrest-push-cachix
|
||||||
|
|
||||||
|
- name: Seed static postgresql build (for libpq)
|
||||||
|
if: matrix.os == 'Linux'
|
||||||
|
run: |
|
||||||
|
nix-build -A packagesStatic.pkgs.postgresql
|
||||||
|
postgrest-push-cachix
|
||||||
|
|
||||||
|
- name: Seed static postgrest build
|
||||||
|
if: matrix.os == 'Linux'
|
||||||
|
run: |
|
||||||
|
nix-build -A postgrestStatic
|
||||||
|
postgrest-push-cachix
|
||||||
|
|
||||||
|
- name: Build and push everything to Cachix
|
||||||
|
run: |
|
||||||
|
nix-build
|
||||||
|
postgrest-push-cachix
|
||||||
@@ -0,0 +1,535 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- rel-*
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- rel-*
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
Lint-Style:
|
||||||
|
name: Lint & check code style
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Setup Nix Environment
|
||||||
|
uses: ./.github/actions/setup-nix
|
||||||
|
with:
|
||||||
|
tools: style
|
||||||
|
- name: Run linter (check locally with `nix-shell --run postgrest-lint`)
|
||||||
|
run: postgrest-lint
|
||||||
|
- name: Run style check (auto-format with `nix-shell --run postgrest-style`)
|
||||||
|
run: postgrest-style-check
|
||||||
|
|
||||||
|
|
||||||
|
Test-Nix:
|
||||||
|
name: Test (Nix)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
# Hack for enabling color output, see:
|
||||||
|
# https://github.com/actions/runner/issues/241#issuecomment-842566950
|
||||||
|
shell: script -qec "bash --noprofile --norc -eo pipefail {0}"
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Setup Nix Environment
|
||||||
|
uses: ./.github/actions/setup-nix
|
||||||
|
with:
|
||||||
|
tools: tests
|
||||||
|
|
||||||
|
- name: Run coverage (IO tests and Spec tests against PostgreSQL 15)
|
||||||
|
run: postgrest-coverage
|
||||||
|
- name: Upload coverage to codecov
|
||||||
|
uses: codecov/codecov-action@v3.1.4
|
||||||
|
with:
|
||||||
|
files: ./coverage/codecov.json
|
||||||
|
|
||||||
|
- name: Run doctests
|
||||||
|
if: always()
|
||||||
|
run: nix-shell --run postgrest-test-doctests
|
||||||
|
|
||||||
|
- name: Check the spec tests for idempotence
|
||||||
|
if: always()
|
||||||
|
run: postgrest-test-spec-idempotence
|
||||||
|
|
||||||
|
|
||||||
|
Test-Pg-Nix:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
pgVersion: [9.6, 10, 11, 12, 13, 14, 15, 16]
|
||||||
|
name: Test PG ${{ matrix.pgVersion }} (Nix)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
# Hack for enabling color output, see:
|
||||||
|
# https://github.com/actions/runner/issues/241#issuecomment-842566950
|
||||||
|
shell: script -qec "bash --noprofile --norc -eo pipefail {0}"
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Setup Nix Environment
|
||||||
|
uses: ./.github/actions/setup-nix
|
||||||
|
with:
|
||||||
|
tools: tests withTools
|
||||||
|
|
||||||
|
- name: Run spec tests
|
||||||
|
if: always()
|
||||||
|
run: postgrest-with-postgresql-${{ matrix.pgVersion }} postgrest-test-spec
|
||||||
|
|
||||||
|
- name: Run IO tests
|
||||||
|
if: always()
|
||||||
|
run: postgrest-with-postgresql-${{ matrix.pgVersion }} -f test/io/fixtures.sql postgrest-test-io -vv
|
||||||
|
|
||||||
|
|
||||||
|
Test-Memory-Nix:
|
||||||
|
name: Test memory (Nix)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Setup Nix Environment
|
||||||
|
uses: ./.github/actions/setup-nix
|
||||||
|
with:
|
||||||
|
tools: memory
|
||||||
|
- name: Run memory tests
|
||||||
|
run: postgrest-test-memory
|
||||||
|
|
||||||
|
|
||||||
|
Build-Static-Nix:
|
||||||
|
name: Build Linux static (Nix)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Setup Nix Environment
|
||||||
|
uses: ./.github/actions/setup-nix
|
||||||
|
with:
|
||||||
|
tools: tests
|
||||||
|
|
||||||
|
- name: Build static executable
|
||||||
|
run: nix-build -A postgrestStatic
|
||||||
|
- name: Check static executable
|
||||||
|
run: postgrest-check-static result/bin/postgrest
|
||||||
|
- name: Save built executable as artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: postgrest-linux-static-x64
|
||||||
|
path: result/bin/postgrest
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Build Docker image
|
||||||
|
run: nix-build -A docker.image --out-link postgrest-docker.tar.gz
|
||||||
|
- name: Save built Docker image as artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: postgrest-docker-x64
|
||||||
|
path: postgrest-docker.tar.gz
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
|
||||||
|
Build-Macos-Nix:
|
||||||
|
name: Build MacOS (Nix)
|
||||||
|
runs-on: macos-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Setup Nix Environment
|
||||||
|
uses: ./.github/actions/setup-nix
|
||||||
|
|
||||||
|
- name: Build everything
|
||||||
|
run: |
|
||||||
|
nix-build
|
||||||
|
|
||||||
|
|
||||||
|
Build-Stack:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- name: Linux
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
cache: |
|
||||||
|
~/.stack
|
||||||
|
.stack-work
|
||||||
|
artifact: postgrest-ubuntu-x64
|
||||||
|
|
||||||
|
- name: MacOS
|
||||||
|
runs-on: macos-latest
|
||||||
|
cache: |
|
||||||
|
~/.stack
|
||||||
|
.stack-work
|
||||||
|
artifact: postgrest-macos-x64
|
||||||
|
|
||||||
|
- name: Windows
|
||||||
|
runs-on: windows-latest
|
||||||
|
cache: |
|
||||||
|
~\AppData\Roaming\stack
|
||||||
|
~\AppData\Local\Programs\stack
|
||||||
|
.stack-work
|
||||||
|
deps: Add-Content $env:GITHUB_PATH $env:PGBIN
|
||||||
|
artifact: postgrest-windows-x64
|
||||||
|
|
||||||
|
name: Build ${{ matrix.name }} (Stack)
|
||||||
|
runs-on: ${{ matrix.runs-on }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Stack working files cache
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ${{ matrix.cache }}
|
||||||
|
key: ${{ runner.os }}-${{ hashFiles('stack.yaml.lock') }}
|
||||||
|
- name: Install dependencies
|
||||||
|
if: ${{ matrix.deps }}
|
||||||
|
run: ${{ matrix.deps }}
|
||||||
|
- name: Build with Stack
|
||||||
|
run: stack build --local-bin-path result --copy-bins
|
||||||
|
- name: Save built executable as artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.artifact }}
|
||||||
|
path: |
|
||||||
|
result/postgrest
|
||||||
|
result/postgrest.exe
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
Get-FreeBSD-CirrusCI:
|
||||||
|
name: Get FreeBSD build from CirrusCI
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Get FreeBSD executable from CirrusCI
|
||||||
|
env:
|
||||||
|
# GITHUB_SHA does weird things for pull request, so we roll our own:
|
||||||
|
GITHUB_COMMIT: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: .github/get_cirrusci_freebsd
|
||||||
|
- name: Save executable as artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: postgrest-freebsd-x64
|
||||||
|
path: postgrest
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
Build-Cabal:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
ghc: ['9.0.2', '9.2.4']
|
||||||
|
fail-fast: false
|
||||||
|
name: Build Linux (Cabal, GHC ${{ matrix.ghc }})
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Workaround runner image issue
|
||||||
|
# https://github.com/actions/runner-images/issues/7061
|
||||||
|
run: sudo chown -R "$USER" /usr/local/.ghcup
|
||||||
|
- name: ghcup
|
||||||
|
run: |
|
||||||
|
ghcup install ghc ${{ matrix.ghc }}
|
||||||
|
ghcup set ghc ${{ matrix.ghc }}
|
||||||
|
- name: Copy cabal.project
|
||||||
|
run: |
|
||||||
|
cp cabal.project.non-nix cabal.project
|
||||||
|
- name: Cache
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/.cabal
|
||||||
|
key: ${{ runner.os }}-${{ matrix.ghc }}-${{ hashFiles('**/*.cabal') }}-${{ hashFiles('**/cabal.project') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-${{ matrix.ghc }}-
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
cabal update
|
||||||
|
cabal build --only-dependencies --enable-tests --enable-benchmarks
|
||||||
|
- name: Build
|
||||||
|
run: cabal build --enable-tests --enable-benchmarks all
|
||||||
|
|
||||||
|
Build-Cabal-Arm:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
ghc: ['9.2.4']
|
||||||
|
fail-fast: false
|
||||||
|
name: Build aarch64 (Cabal, GHC ${{ matrix.ghc }})
|
||||||
|
if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || startsWith(github.ref, 'refs/heads/rel-') }}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
remotepath: ${{ steps.Remote-Dir.outputs.remotepath }}
|
||||||
|
env:
|
||||||
|
GITHUB_COMMIT: ${{ github.sha }}
|
||||||
|
GHC_VERSION: ${{ matrix.ghc }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- id: Remote-Dir
|
||||||
|
name: Unique directory name for the remote build
|
||||||
|
run: echo "remotepath=postgrest-build-$(uuidgen)" >> "$GITHUB_OUTPUT"
|
||||||
|
- name: Copy script files to the remote server
|
||||||
|
uses: appleboy/scp-action@master
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.SSH_ARM_HOST }}
|
||||||
|
username: ubuntu
|
||||||
|
key: ${{ secrets.SSH_ARM_PRIVATE_KEY }}
|
||||||
|
fingerprint: ${{ secrets.SSH_ARM_FINGERPRINT }}
|
||||||
|
source: ".github/scripts/arm/*"
|
||||||
|
target: ${{ steps.Remote-Dir.outputs.remotepath }}
|
||||||
|
strip_components: 3
|
||||||
|
- name: Build ARM
|
||||||
|
uses: appleboy/ssh-action@master
|
||||||
|
env:
|
||||||
|
REMOTE_DIR: ${{ steps.Remote-Dir.outputs.remotepath }}
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.SSH_ARM_HOST }}
|
||||||
|
username: ubuntu
|
||||||
|
key: ${{ secrets.SSH_ARM_PRIVATE_KEY }}
|
||||||
|
fingerprint: ${{ secrets.SSH_ARM_FINGERPRINT }}
|
||||||
|
command_timeout: 120m
|
||||||
|
script_stop: true
|
||||||
|
envs: GITHUB_COMMIT,REMOTE_DIR,GHC_VERSION
|
||||||
|
script: bash ~/$REMOTE_DIR/build.sh "$GITHUB_COMMIT" "$REMOTE_DIR" "GHC_VERSION"
|
||||||
|
- name: Download binaries from remote server
|
||||||
|
uses: nicklasfrahm/scp-action@main
|
||||||
|
with:
|
||||||
|
direction: download
|
||||||
|
host: ${{ secrets.SSH_ARM_HOST }}
|
||||||
|
username: ubuntu
|
||||||
|
key: ${{ secrets.SSH_ARM_PRIVATE_KEY }}
|
||||||
|
fingerprint: ${{ secrets.SSH_ARM_FINGERPRINT }}
|
||||||
|
source: "${{ steps.Remote-Dir.outputs.remotepath }}/result.tar.xz"
|
||||||
|
target: "result.tar.xz"
|
||||||
|
- name: Extract downloaded binaries
|
||||||
|
run: tar -xvf result.tar.xz && rm result.tar.xz
|
||||||
|
- name: Save aarch64 executable as artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: postgrest-ubuntu-aarch64
|
||||||
|
path: result/postgrest
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
|
||||||
|
Prepare-Release:
|
||||||
|
name: Prepare release
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- Lint-Style
|
||||||
|
- Test-Nix
|
||||||
|
- Test-Pg-Nix
|
||||||
|
- Test-Memory-Nix
|
||||||
|
- Build-Static-Nix
|
||||||
|
- Build-Stack
|
||||||
|
#- Get-FreeBSD-CirrusCI
|
||||||
|
- Build-Cabal-Arm
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.Identify-Version.outputs.version }}
|
||||||
|
isprerelease: ${{ steps.Identify-Version.outputs.isprerelease }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- id: Identify-Version
|
||||||
|
name: Identify the version to be released
|
||||||
|
run: |
|
||||||
|
tag_version="${GITHUB_REF##*/}"
|
||||||
|
cabal_version="$(grep -oP '^version:\s*\K.*' postgrest.cabal)"
|
||||||
|
|
||||||
|
if [ "$tag_version" != "v$cabal_version" ]; then
|
||||||
|
echo "Tagged version ($tag_version) does not match the one in postgrest.cabal (v$cabal_version). Aborting release..."
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "Version to be released is $cabal_version"
|
||||||
|
echo "version=$cabal_version" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$cabal_version" != *.*.*.* ]]; then
|
||||||
|
echo "Version is for a full release (version does not have four components)"
|
||||||
|
else
|
||||||
|
echo "Version is for a pre-release (version has four components, e.g., 1.1.1.1)"
|
||||||
|
echo "isprerelease=1" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
- name: Identify changes from CHANGELOG.md
|
||||||
|
run: |
|
||||||
|
version="${{ steps.Identify-Version.outputs.version }}"
|
||||||
|
isprerelease="${{ steps.Identify-Version.outputs.isprerelease }}"
|
||||||
|
|
||||||
|
if [ -n "$isprerelease" ]; then
|
||||||
|
echo "Getting unreleased changes..."
|
||||||
|
sed -n "1,/## Unreleased/d;/## \[/q;p" CHANGELOG.md > CHANGES.md
|
||||||
|
else
|
||||||
|
echo "Full release (no '-' in version), getting changes for version $version ..."
|
||||||
|
sed -n "1,/## \[$version\]/d;/## \[/q;p" CHANGELOG.md > CHANGES.md
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Relevant extract from CHANGELOG.md:"
|
||||||
|
cat CHANGES.md
|
||||||
|
- name: Save CHANGES.md as artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: release-changes
|
||||||
|
path: CHANGES.md
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
|
||||||
|
Release-GitHub:
|
||||||
|
name: Release on GitHub
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: Prepare-Release
|
||||||
|
env:
|
||||||
|
VERSION: ${{ needs.Prepare-Release.outputs.version }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Download all artifacts
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
path: artifacts
|
||||||
|
- name: Create release bundle with archives for all builds
|
||||||
|
run: |
|
||||||
|
find artifacts -type f -iname postgrest -exec chmod +x {} \;
|
||||||
|
|
||||||
|
mkdir -p release-bundle
|
||||||
|
|
||||||
|
tar cJvf "release-bundle/postgrest-v$VERSION-linux-static-x64.tar.xz" \
|
||||||
|
-C artifacts/postgrest-linux-static-x64 postgrest
|
||||||
|
|
||||||
|
# No need to release Ubuntu, as the static Linux binary built with Nix
|
||||||
|
# covers all Linux use-cases
|
||||||
|
#tar cfJv "release-bundle/postgrest-v$VERSION-ubuntu-x64.tar.xz" \
|
||||||
|
# -C artifacts/postgrest-ubuntu-x64 postgrest
|
||||||
|
|
||||||
|
tar cJvf "release-bundle/postgrest-v$VERSION-macos-x64.tar.xz" \
|
||||||
|
-C artifacts/postgrest-macos-x64 postgrest
|
||||||
|
|
||||||
|
# TODO: Fix timeouts for FreeBSD builds in Cirrus
|
||||||
|
#tar cJvf "release-bundle/postgrest-v$VERSION-freebsd-x64.tar.xz" \
|
||||||
|
# -C artifacts/postgrest-freebsd-x64 postgrest
|
||||||
|
|
||||||
|
tar cJvf "release-bundle/postgrest-v$VERSION-ubuntu-aarch64.tar.xz" \
|
||||||
|
-C artifacts/postgrest-ubuntu-aarch64 postgrest
|
||||||
|
|
||||||
|
zip "release-bundle/postgrest-v$VERSION-windows-x64.zip" \
|
||||||
|
artifacts/postgrest-windows-x64/postgrest.exe
|
||||||
|
|
||||||
|
- name: Save release bundle
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: release-bundle
|
||||||
|
path: release-bundle
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Publish release on GitHub
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
isprerelease="${{ needs.Prepare-Release.outputs.isprerelease }}"
|
||||||
|
echo "Releasing version v$VERSION on GitHub (isprerelease=$isprerelease)..."
|
||||||
|
|
||||||
|
gh release delete "v$VERSION" || true
|
||||||
|
gh release create "v$VERSION" \
|
||||||
|
-F artifacts/release-changes/CHANGES.md \
|
||||||
|
${isprerelease:+"--prerelease"} \
|
||||||
|
release-bundle/*
|
||||||
|
|
||||||
|
|
||||||
|
Release-Docker:
|
||||||
|
name: Release on Docker Hub
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- Prepare-Release
|
||||||
|
env:
|
||||||
|
GITHUB_COMMIT: ${{ github.sha }}
|
||||||
|
DOCKER_REPO: postgrest
|
||||||
|
DOCKER_USER: stevechavez
|
||||||
|
DOCKER_PASS: ${{ secrets.DOCKER_PASS }}
|
||||||
|
VERSION: ${{ needs.Prepare-Release.outputs.version }}
|
||||||
|
ISPRERELEASE: ${{ needs.Prepare-Release.outputs.isprerelease }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Setup Nix Environment
|
||||||
|
uses: ./.github/actions/setup-nix
|
||||||
|
with:
|
||||||
|
tools: release
|
||||||
|
- name: Download Docker image
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: postgrest-docker-x64
|
||||||
|
- name: Publish images on Docker Hub
|
||||||
|
run: |
|
||||||
|
docker login -u "$DOCKER_USER" -p "$DOCKER_PASS"
|
||||||
|
docker load -i postgrest-docker.tar.gz
|
||||||
|
|
||||||
|
docker tag postgrest:latest "$DOCKER_REPO/postgrest:v$VERSION"
|
||||||
|
docker push "$DOCKER_REPO/postgrest:v$VERSION"
|
||||||
|
|
||||||
|
# Only tag 'latest' for full releases
|
||||||
|
if [[ -z "$ISPRERELEASE" ]]; then
|
||||||
|
echo "Pushing to 'latest' tag for full release of v$VERSION ..."
|
||||||
|
docker tag postgrest:latest "$DOCKER_REPO"/postgrest:latest
|
||||||
|
docker push "$DOCKER_REPO"/postgrest:latest
|
||||||
|
else
|
||||||
|
echo "Skipping pushing to 'latest' tag for v$VERSION pre-release..."
|
||||||
|
fi
|
||||||
|
# TODO: Enable dockerhub description update again, once a solution for the permission problem is found:
|
||||||
|
# https://github.com/docker/hub-feedback/issues/1927
|
||||||
|
# - name: Update descriptions on Docker Hub
|
||||||
|
# env:
|
||||||
|
# DOCKER_PASS: ${{ secrets.DOCKER_PASS }}
|
||||||
|
# run: |
|
||||||
|
# if [[ -z "$ISPRERELEASE" ]]; then
|
||||||
|
# echo "Updating description on Docker Hub..."
|
||||||
|
# postgrest-release-dockerhub-description
|
||||||
|
# else
|
||||||
|
# echo "Skipping updating description for pre-release..."
|
||||||
|
# fi
|
||||||
|
|
||||||
|
Release-Docker-Arm:
|
||||||
|
name: Release Arm Builds on Docker Hub
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- Build-Cabal-Arm
|
||||||
|
- Prepare-Release
|
||||||
|
- Release-Docker
|
||||||
|
env:
|
||||||
|
GITHUB_COMMIT: ${{ github.sha }}
|
||||||
|
DOCKER_REPO: postgrest
|
||||||
|
DOCKER_USER: stevechavez
|
||||||
|
DOCKER_PASS: ${{ secrets.DOCKER_PASS }}
|
||||||
|
VERSION: ${{ needs.Prepare-Release.outputs.version }}
|
||||||
|
ISPRERELEASE: ${{ needs.Prepare-Release.outputs.isprerelease }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Publish images for ARM builds on Docker Hub
|
||||||
|
uses: appleboy/ssh-action@master
|
||||||
|
env:
|
||||||
|
REMOTE_DIR: ${{ needs.Build-Cabal-Arm.outputs.remotepath }}
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.SSH_ARM_HOST }}
|
||||||
|
username: ubuntu
|
||||||
|
key: ${{ secrets.SSH_ARM_PRIVATE_KEY }}
|
||||||
|
fingerprint: ${{ secrets.SSH_ARM_FINGERPRINT }}
|
||||||
|
script_stop: true
|
||||||
|
envs: GITHUB_COMMIT,DOCKER_REPO,DOCKER_USER,DOCKER_PASS,REMOTE_DIR,VERSION,ISPRERELEASE
|
||||||
|
script: bash ~/$REMOTE_DIR/docker-publish.sh "$GITHUB_COMMIT" "$DOCKER_REPO" "$DOCKER_USER" "$DOCKER_PASS" "$REMOTE_DIR" "$VERSION" "$ISPRERELEASE"
|
||||||
|
|
||||||
|
Clean-Arm-Server:
|
||||||
|
name: Remove copied files from server
|
||||||
|
needs:
|
||||||
|
- Build-Cabal-Arm
|
||||||
|
- Release-Docker-Arm
|
||||||
|
if: success() ||
|
||||||
|
needs.Build-Cabal-Arm.result == 'failure' ||
|
||||||
|
needs.Build-Cabal-Arm.result == 'cancelled' ||
|
||||||
|
(needs.Build-Cabal-Arm.result == 'success' && !startsWith(github.ref, 'refs/tags/v'))
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
REMOTE_DIR: ${{ needs.Build-Cabal-Arm.outputs.remotepath }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Remove uploaded files from server
|
||||||
|
uses: appleboy/ssh-action@master
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.SSH_ARM_HOST }}
|
||||||
|
username: ubuntu
|
||||||
|
key: ${{ secrets.SSH_ARM_PRIVATE_KEY }}
|
||||||
|
fingerprint: ${{ secrets.SSH_ARM_FINGERPRINT }}
|
||||||
|
envs: REMOTE_DIR
|
||||||
|
script: rm -rf $REMOTE_DIR
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
name: Loadtest
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
Loadtest-Nix:
|
||||||
|
name: Loadtest (Nix)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Setup Nix Environment
|
||||||
|
uses: ./.github/actions/setup-nix
|
||||||
|
with:
|
||||||
|
tools: loadtest
|
||||||
|
- name: Run loadtest
|
||||||
|
run: |
|
||||||
|
postgrest-loadtest-against main
|
||||||
|
postgrest-loadtest-report > loadtest/loadtest.md
|
||||||
|
- name: Upload report
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: loadtest.md
|
||||||
|
path: loadtest/loadtest.md
|
||||||
|
if-no-files-found: error
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
name: Upload Reports
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["Loadtest"]
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
upload:
|
||||||
|
name: Loadtest
|
||||||
|
permissions:
|
||||||
|
checks: write
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||||
|
steps:
|
||||||
|
- name: Download from Artifacts
|
||||||
|
uses: dawidd6/action-download-artifact@v2
|
||||||
|
with:
|
||||||
|
workflow: ${{ github.event.workflow.name }}
|
||||||
|
run_id: ${{github.event.workflow_run.id }}
|
||||||
|
name: loadtest.md
|
||||||
|
path: artifacts
|
||||||
|
- name: Upload to GitHub Checks
|
||||||
|
uses: LouisBrunner/checks-action@v1.6.2
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
sha: ${{ github.event.workflow_run.head_sha }}
|
||||||
|
name: Loadtest Results
|
||||||
|
conclusion: neutral
|
||||||
|
output: |
|
||||||
|
{"summary":""}
|
||||||
|
output_text_description_file: artifacts/loadtest.md
|
||||||
@@ -15,3 +15,11 @@ site
|
|||||||
*.swp
|
*.swp
|
||||||
result*
|
result*
|
||||||
dist-newstyle
|
dist-newstyle
|
||||||
|
postgrest.hp
|
||||||
|
postgrest.prof
|
||||||
|
__pycache__
|
||||||
|
*.tix
|
||||||
|
coverage
|
||||||
|
.hpc
|
||||||
|
loadtest
|
||||||
|
.history
|
||||||
|
|||||||
-70
@@ -1,70 +0,0 @@
|
|||||||
## Travis is only used for building an OSX binary ,
|
|
||||||
## no tests are run here.
|
|
||||||
language: generic
|
|
||||||
|
|
||||||
sudo: false
|
|
||||||
|
|
||||||
os:
|
|
||||||
- osx
|
|
||||||
|
|
||||||
cache:
|
|
||||||
timeout: 1000
|
|
||||||
directories:
|
|
||||||
- $HOME/.stack
|
|
||||||
- $HOME/.local/bin
|
|
||||||
|
|
||||||
before_install:
|
|
||||||
- mkdir -p "$HOME/.local/bin"
|
|
||||||
- export PATH="$PATH:$HOME/.local/bin"
|
|
||||||
|
|
||||||
install:
|
|
||||||
- |
|
|
||||||
if test -f "$HOME/.local/bin/stack"
|
|
||||||
then
|
|
||||||
echo 'Stack is already installed.'
|
|
||||||
else
|
|
||||||
echo "Installing Stack..."
|
|
||||||
travis_retry curl -L https://www.stackage.org/stack/osx-x86_64 > stack.tar.gz
|
|
||||||
gunzip stack.tar.gz
|
|
||||||
tar -x -f stack.tar --strip-components 1
|
|
||||||
mv stack "$HOME/.local/bin/"
|
|
||||||
rm stack.tar
|
|
||||||
fi
|
|
||||||
- |
|
|
||||||
if test -f "$HOME/.local/bin/ghr"
|
|
||||||
then
|
|
||||||
echo 'ghr is already installed.'
|
|
||||||
else
|
|
||||||
echo "Installing ghr..."
|
|
||||||
travis_retry curl -L https://github.com/tcnksm/ghr/releases/download/v0.5.4/ghr_v0.5.4_darwin_386.zip > ghr.zip
|
|
||||||
unzip ghr.zip -d "$HOME/.local/bin"
|
|
||||||
rm ghr.zip
|
|
||||||
fi
|
|
||||||
|
|
||||||
script:
|
|
||||||
## Building the whole project can take longer than 50 minutes. Since Travis has a global timeout of 50 minutes
|
|
||||||
## we compile for 30 minutes tops(`gtimeout 1800`) and quit compiling with no error.
|
|
||||||
## Since we CACHE the compile results we can continue compiling from where we left off
|
|
||||||
## on the next commit.
|
|
||||||
- gtimeout 1800 stack build --no-terminal --only-snapshot --install-ghc || true
|
|
||||||
- |
|
|
||||||
if test ! "$TRAVIS_TAG"
|
|
||||||
then
|
|
||||||
echo 'No tag pushed. Skip building binary.'
|
|
||||||
else
|
|
||||||
stack build --no-terminal --copy-bins --local-bin-path .
|
|
||||||
fi
|
|
||||||
- |
|
|
||||||
if test ! "$TRAVIS_TAG"
|
|
||||||
then
|
|
||||||
echo 'No tag pushed. Skipping release.'
|
|
||||||
else
|
|
||||||
OWNER="$(echo "$TRAVIS_REPO_SLUG" | cut -f1 -d/)"
|
|
||||||
REPO="$(echo "$TRAVIS_REPO_SLUG" | cut -f2 -d/)"
|
|
||||||
START=$(echo $TRAVIS_TAG | cut -c2-)
|
|
||||||
END='## \['
|
|
||||||
BODY=$(sed -n "1,/$START/d;/$END/q;p" CHANGELOG.md)
|
|
||||||
strip postgrest
|
|
||||||
tar cJf postgrest-$TRAVIS_TAG-osx.tar.xz postgrest
|
|
||||||
ghr -t $GITHUB_TOKEN -u $OWNER -r $REPO -b "$BODY"--replace $TRAVIS_TAG postgrest-$TRAVIS_TAG-osx.tar.xz
|
|
||||||
fi
|
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# Architecture
|
||||||
|
|
||||||
|
This document describes the high-level architecture of PostgREST.
|
||||||
|
|
||||||
|
## Bird's Eye View
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
postgrest :: Request -> Either Error SQLStatement -> Response
|
||||||
|
```
|
||||||
|
|
||||||
|
On the highest level, PostgREST processes an HTTP request, if it's accepted it builds a SQL statement for it, executes it, and produces a response.
|
||||||
|
|
||||||
|
## Code Map
|
||||||
|
|
||||||
|
This section talks briefly about various important modules.
|
||||||
|
|
||||||
|
The starting point of the program is `main/Main.hs`, which calls `src/PostgREST/CLI.hs` which then calls `src/PostgREST/App.hs`.
|
||||||
|
|
||||||
|
`App.hs` is then in charge of composing the different modules.
|
||||||
|
|
||||||
|
### ApiRequest.hs
|
||||||
|
|
||||||
|
PostgREST operates over two types of resources: database relations(tables or views) and database functions; providing different representations(depending on the media type)
|
||||||
|
for them.
|
||||||
|
|
||||||
|
This module is in charge of representing the operation over an `ApiRequest` type. It parses the URL querystring following PostgREST syntax, the request headers, and the request body
|
||||||
|
(if possible it avoids parsing the body and sends it directly to the db).
|
||||||
|
|
||||||
|
A request might be rejected at this level if it's invalid, e.g. providing an unknown media type to PostgREST or using an unknown HTTP method.
|
||||||
|
|
||||||
|
### Plan.hs
|
||||||
|
|
||||||
|
Using the Schema Cache, this module enables more complex functionality(like resource embedding) by enriching the ApiRequest. It generates Plan types(`ReadPlan`, `MutatePlan`)
|
||||||
|
that then will be used to generate a SQL statement.
|
||||||
|
|
||||||
|
A request might be rejected at this level if it's invalid, e.g. by doing resource embedding on a nonexistent resource.
|
||||||
|
|
||||||
|
An OPTIONS request doesn't require a plan to be generated.
|
||||||
|
|
||||||
|
### Query.hs
|
||||||
|
|
||||||
|
This module constructs single SQL statements that can be parametrized and prepared. Only at this stage a PostgreSQL connection from the pool is used.
|
||||||
|
|
||||||
|
A query might fail(and be rollbacked) at this level if it doesn't comply to certain conditions, e.g. by not returning a single row when a ``Accept: application/vnd.pgrst.object`` header is specified.
|
||||||
|
|
||||||
|
An OPTIONS request doesn't require a query to be executed.
|
||||||
|
|
||||||
|
### Response.hs
|
||||||
|
|
||||||
|
This module constructs the HTTP response body with the right headers.
|
||||||
|
|
||||||
|
It builds the OpenAPI response using the schema cache.
|
||||||
|
|
||||||
|
### Auth.hs
|
||||||
|
|
||||||
|
This module provides functions to deal with JWT authorization.
|
||||||
|
|
||||||
|
### SchemaCache.hs
|
||||||
|
|
||||||
|
This queries the PostgreSQL system catalogs and caches the metadata into a SchemaCache type,
|
||||||
|
|
||||||
|
### AppState.hs
|
||||||
|
|
||||||
|
The state of the App which is kept across requests.
|
||||||
|
|
||||||
|
This spawns threads which are used to execute concurrent jobs.
|
||||||
|
|
||||||
|
Jobs include connection recover and a listener for the PostgreSQL LISTEN command.
|
||||||
+30
-6
@@ -8,18 +8,36 @@ PostgREST ongoing development is only possible thanks to our Sponsors and Backer
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" valign="middle">
|
<td align="center" valign="middle">
|
||||||
<a href="https://www.cybertec-postgresql.com/en/" target="_blank">
|
<a href="https://www.cybertec-postgresql.com/en/?utm_source=postgrest.org&utm_medium=referral&utm_campaign=postgrest" target="_blank">
|
||||||
<img width="222px" src="static/cybertec.png">
|
<img width="222px" src="static/cybertec-new.png">
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" valign="middle">
|
<td align="center" valign="middle">
|
||||||
<a href="https://www.2ndquadrant.com/en/?utm_campaign=External%20Websites&utm_source=PostgREST&utm_medium=Logo" target="_blank">
|
<a href="https://www.2ndquadrant.com/en/?utm_campaign=External%20Websites&utm_source=PostgREST&utm_medium=Logo" target="_blank">
|
||||||
<img width="222px" src="static/2ndquadrant.png">
|
<img width="296px" src="static/2ndquadrant.png">
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" valign="middle">
|
<td align="center" valign="middle">
|
||||||
<a href="https://tryretool.com/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
<a href="https://tryretool.com/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||||
<img width="222px" src="static/retool.png">
|
<img width="296px" src="static/retool.png">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr></tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="middle">
|
||||||
|
<a href="https://gnuhost.eu/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||||
|
<img width="296px" src="static/gnuhost.png">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" valign="middle">
|
||||||
|
<a href="https://supabase.io?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage" target="_blank">
|
||||||
|
<img width="296px" src="static/supabase.png">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" valign="middle">
|
||||||
|
<a href="https://oblivious.ai/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||||
|
<img width="296px" src="static/oblivious.jpg">
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -28,23 +46,27 @@ PostgREST ongoing development is only possible thanks to our Sponsors and Backer
|
|||||||
|
|
||||||
## Lead Backers
|
## Lead Backers
|
||||||
|
|
||||||
- [Daniel Babiak](https://github.com/d-babiak)
|
- [Roboflow](https://github.com/roboflow)
|
||||||
- Evans Fernandes
|
- Evans Fernandes
|
||||||
- [Jan Sommer](https://github.com/nerfpops)
|
- [Jan Sommer](https://github.com/nerfpops)
|
||||||
|
- [Franz Gusenbauer](https://www.igutech.at/)
|
||||||
|
|
||||||
## Backers
|
## Backers
|
||||||
|
|
||||||
|
- Zac Miller
|
||||||
- Tsingson Qin
|
- Tsingson Qin
|
||||||
- Michel Pelletier
|
- Michel Pelletier
|
||||||
- Jay Hannah
|
- Jay Hannah
|
||||||
- Robert Stolarz
|
- Robert Stolarz
|
||||||
- Kofi Gumbs
|
|
||||||
- Nicholas DiBiase
|
- Nicholas DiBiase
|
||||||
- Christopher Reid
|
- Christopher Reid
|
||||||
- Nathan Bouscal
|
- Nathan Bouscal
|
||||||
- Daniel Rafaj
|
- Daniel Rafaj
|
||||||
- David Fenko
|
- David Fenko
|
||||||
- Remo Rechkemmer
|
- Remo Rechkemmer
|
||||||
|
- Severin Ibarluzea
|
||||||
|
- Tom Saleeba
|
||||||
|
- Pawel Tyll
|
||||||
|
|
||||||
## Former Backers
|
## Former Backers
|
||||||
|
|
||||||
@@ -61,3 +83,5 @@ PostgREST ongoing development is only possible thanks to our Sponsors and Backer
|
|||||||
</table>
|
</table>
|
||||||
|
|
||||||
- [Christiaan Westerbeek](https://devotis.nl)
|
- [Christiaan Westerbeek](https://devotis.nl)
|
||||||
|
- [Daniel Babiak](https://github.com/dbabiak)
|
||||||
|
- Kofi Gumbs
|
||||||
|
|||||||
+415
-1
@@ -5,10 +5,420 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
### Added
|
## [11.2.2] - 2023-10-25
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
- #2824, Fix regression by reverting fix that returned 206 when first position = length in a `Range` header - @laurenceisla, @strengthless
|
||||||
|
|
||||||
|
## [11.2.1] - 2023-10-03
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- #2899, Fix `application/vnd.pgrst.array` not accepted as a valid mediatype - @taimoorzaeem
|
||||||
|
- #2524, Fix schema cache and configuration reloading with `NOTIFY` not working on Windows - @diogob, @laurenceisla
|
||||||
|
- #2915, Fix duplicate headers in response - @taimoorzaeem
|
||||||
|
- #2824, Fix range request with first position same as length return status 206 - @taimoorzaeem
|
||||||
|
- #2939, Fix wrong `Preference-Applied` with `Prefer: tx=commit` when transaction is rollbacked - @steve-chavez
|
||||||
|
- #2939, Fix `count=exact` not being included in `Preference-Applied` - @steve-chavez
|
||||||
|
- #2800, Fix not including to-one embed resources that had a `NULL` value in any of the selected fields when doing null filtering on them - @laurenceisla
|
||||||
|
- #2846, Fix error when requesting `Prefer: count=<type>` and doing null filtering on embedded resources - @laurenceisla
|
||||||
|
- #2959, Fix setting `default_transaction_isolation` unnecessarily - @steve-chavez
|
||||||
|
- #2929, Fix arrow filtering on RPC returning dynamic TABLE with composite type - @steve-chavez
|
||||||
|
- #2963, Fix RPCs not embedding correctly when using overloaded functions for computed relationships - @laurenceisla
|
||||||
|
- #2970, Fix regression that rejects URI connection strings with certain unescaped characters in the password - @laurenceisla, @steve-chavez
|
||||||
|
|
||||||
|
## [11.2.0] - 2023-08-10
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- #2523, Data representations - @aljungberg
|
||||||
|
+ Allows for flexible API output formatting and input parsing on a per-column type basis using regular SQL functions configured in the database
|
||||||
|
+ Enables greater flexibility in the form and shape of your APIs, both for output and input, making PostgREST a more versatile general-purpose API server
|
||||||
|
+ Examples include base64 encode/decode your binary data (like a `bytea` column containing an image), choose whether to present a timestamp column as seconds since the Unix epoch or as an ISO 8601 string, or represent fixed precision decimals as strings, not doubles, to preserve precision
|
||||||
|
+ ...and accept the same in `POST/PUT/PATCH` by configuring the reverse transformation(s)
|
||||||
|
+ Other use-cases include custom representation of enums, arrays, nested objects, CSS hex colour strings, gzip compressed fields, metric to imperial conversions, and much more
|
||||||
|
+ Works when using the `select` parameter to select only a subset of columns, embedding through complex joins, renaming fields, with views and computed columns
|
||||||
|
+ Works when filtering on a formatted column without extra indexes by parsing to the canonical representation
|
||||||
|
+ Works for data `RETURNING` operations, such as requesting the full body in a POST/PUT/PATCH with `Prefer: return=representation`
|
||||||
|
+ Works for batch updates and inserts
|
||||||
|
+ Completely optional, define the functions in the database and they will be used automatically everywhere
|
||||||
|
+ Data representations preserve the ability to write to the original column and require no extra storage or complex triggers (compared to using `GENERATED ALWAYS` columns)
|
||||||
|
+ Note: data representations require Postgres 10 (Postgres 11 if using `IN` predicates); data representations are not implemented for RPC
|
||||||
|
- #2647, Allow to verify the PostgREST version in SQL: `select distinct application_name from pg_stat_activity`. - @laurenceisla
|
||||||
|
- #2856, Add the `--version` CLI option that prints the version information - @laurenceisla
|
||||||
|
- #1655, Improve `details` field of the singular error response - @taimoorzaeem
|
||||||
|
- #740, Add `Preference-Applied` in response for `Prefer: return=representation/headers-only/minimal` - @taimoorzaeem
|
||||||
|
- #1601, Add optional `nulls=stripped` parameter for mediatypes `application/vnd.pgrst.array+json` and `application/vnd.pgrst.object+json` - @taimoorzaeem
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- #2821, Fix OPTIONS not accepting all available media types - @steve-chavez
|
||||||
|
- #2834, Fix compilation on Ubuntu by being compatible with GHC 9.0.2 - @steve-chavez
|
||||||
|
- #2840, Fix `Prefer: missing=default` with DOMAIN default values - @steve-chavez
|
||||||
|
- #2849, Fix HEAD unnecessarily executing aggregates - @steve-chavez
|
||||||
|
- #2594, Fix unused index on jsonb/jsonb arrow filter and order (``/bets?data->>contractId=eq.1`` and ``/bets?order=data->>contractId``) - @steve-chavez
|
||||||
|
- #2861, Fix character and bit columns with fixed length not inserting/updating properly - @laurenceisla
|
||||||
|
+ Fixes the error "value too long for type character(1)" when the char length of the column was bigger than one.
|
||||||
|
- #2862, Fix null filtering on embedded resource when using a column name equal to the relation name - @steve-chavez
|
||||||
|
- #1586, Fix function parameters of type character and bit not ignoring length - @laurenceisla
|
||||||
|
+ Fixes the error "value too long for type character(1)" when the char length of the parameter was bigger than one.
|
||||||
|
- #2881, Fix error when a function returns `RECORD` or `SET OF RECORD` - @laurenceisla
|
||||||
|
- #2896, Fix applying superuser settings for impersonated role - @steve-chavez
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
|
||||||
|
- #2863, Deprecate resource embedding target disambiguation - @steve-chavez
|
||||||
|
+ The `/table?select=*,other!fk(*)` must be used to disambiguate
|
||||||
|
+ The server aids in choosing the `!fk` by sending a `hint` on the error whenever an ambiguous request happens.
|
||||||
|
|
||||||
|
## [11.1.0] - 2023-06-07
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- #2786, Limit idle postgresql connection lifetime - @robx
|
||||||
|
+ New option `db-pool-max-idletime` (default 30s).
|
||||||
|
+ This is equivalent to the old option `db-pool-timeout` of PostgREST 10.0.0.
|
||||||
|
+ A config alias for `db-pool-timeout` is included.
|
||||||
|
- #2703, Add pre-config function - @steve-chavez
|
||||||
|
+ New config option `db-pre-config`(empty by default)
|
||||||
|
+ Allows using the in-database configuration without SUPERUSER
|
||||||
|
- #2781, When `db-channel-enabled` is false, start automatic connection recovery on a new request when pool connections are closed with `pg_terminate_backend` - @steve-chavez
|
||||||
|
+ Mitigates the lack of LISTEN/NOTIFY for schema cache reloading on read replicas.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- #2791, Fix dropping schema cache reload notifications - @steve-chavez
|
||||||
|
- #2801, Stop retrying connection when "no password supplied" - @steve-chavez
|
||||||
|
|
||||||
|
## [11.0.1] - 2023-04-27
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- #2762, Fixes "permission denied for schema" error during schema cache load - @steve-chavez
|
||||||
|
- #2756, Fix bad error message on generated columns when using `Prefer: missing=default` - @steve-chavez
|
||||||
|
- #1139, Allow a 30 second skew for JWT validation - @steve-chavez
|
||||||
|
+ It used to be 1 second, which was too strict
|
||||||
|
|
||||||
|
## [11.0.0] - 2023-04-16
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- #1414, Add related orders - @steve-chavez
|
||||||
|
+ On a many-to-one or one-to-one relationship, you can order a parent by a child column `/projects?select=*,clients(*)&order=clients(name).desc.nullsfirst`
|
||||||
|
- #1233, #1907, #2566, Allow spreading embedded resources - @steve-chavez
|
||||||
|
+ On a many-to-one or one-to-one relationship, you can unnest a json object with `/projects?select=*,...clients(client_name:name)`
|
||||||
|
+ Allows including the join table columns when resource embedding
|
||||||
|
+ Allows disambiguating a recursive m2m embed
|
||||||
|
+ Allows disambiguating an embed that has a many-to-many relationship using two foreign keys on a junction
|
||||||
|
- #2340, Allow embedding without selecting any column - @steve-chavez
|
||||||
|
- #2563, Allow `is.null` or `not.is.null` on an embedded resource - @steve-chavez
|
||||||
|
+ Offers a more flexible replacement for `!inner`, e.g. `/projects?select=*,clients(*)&clients=not.is.null`
|
||||||
|
+ Allows doing an anti join, e.g. `/projects?select=*,clients(*)&clients=is.null`
|
||||||
|
+ Allows using or across related tables conditions
|
||||||
|
- #1100, Customizable OpenAPI title - @AnthonyFisi
|
||||||
|
- #2506, Add `server-trace-header` for tracing HTTP requests. - @steve-chavez
|
||||||
|
+ When the client sends the request header specified in the config it will be included in the response headers.
|
||||||
|
- #2694, Make `db-root-spec` stable. - @steve-chavez
|
||||||
|
+ This can be used to override the OpenAPI spec with a custom database function
|
||||||
|
- #1567, On bulk inserts, missing values can get the column DEFAULT by using the `Prefer: missing=default` header - @steve-chavez
|
||||||
|
- #2501, Allow filtering by`IS DISTINCT FROM` using the `isdistinct` operator, e.g. `/people?alias=isdistinct.foo`
|
||||||
|
- #1569, Allow `any/all` modifiers on the `eq,like,ilike,gt,gte,lt,lte,match,imatch` operators, e.g. `/tbl?id=eq(any).{1,2,3}` - @steve-chavez
|
||||||
|
- This converts the input into an array type
|
||||||
|
- #2561, Configurable role settings - @steve-chavez
|
||||||
|
- Database roles that are members of the connection role get their settings applied, e.g. doing
|
||||||
|
`ALTER ROLE anon SET statement_timeout TO '5s'` will result in that `statement_timeout` getting applied for that role.
|
||||||
|
- Works when switching roles when a JWT is sent
|
||||||
|
- Settings can be reloaded with `NOTIFY pgrst, 'reload config'`.
|
||||||
|
- #2468, Configurable transaction isolation level with `default_transaction_isolation` - @steve-chavez
|
||||||
|
- Can be set per function `create function .. set default_transaction_isolation = 'repeatable read'`
|
||||||
|
- Or per role `alter role .. set default_transaction_isolation = 'serializable'`
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- #2651, Add the missing `get` path item for RPCs to the OpenAPI output - @laurenceisla
|
||||||
|
- #2648, Fix inaccurate error codes with new ones - @laurenceisla
|
||||||
|
+ `PGRST204`: Column is not found
|
||||||
|
+ `PGRST003`: Timed out when acquiring connection to db
|
||||||
|
- #1652, Fix function call with arguments not inlining - @steve-chavez
|
||||||
|
- #2705, Fix bug when using the `Range` header on `PATCH/DELETE` - @laurenceisla
|
||||||
|
+ Fix the`"message": "syntax error at or near \"RETURNING\""` error
|
||||||
|
+ Fix doing a limited update/delete when an `order` query parameter was present
|
||||||
|
- #2742, Fix db settings and pg version queries not getting prepared - @steve-chavez
|
||||||
|
- #2618, Fix `PATCH` requests not recognizing embedded filters and using the top-level resource instead - @steve-chavez
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- #2705, The `Range` header is now only considered on `GET` requests and is ignored for any other method - @laurenceisla
|
||||||
|
+ Other methods should use the `limit/offset` query parameters for sub-ranges
|
||||||
|
+ `PUT` requests no longer return an error when this header is present (using `limit/offset` still triggers the error)
|
||||||
|
- #2733, Remove bulk RPC call with the `Prefer: params=multiple-objects` header. A function with a JSON array or object parameter should be used instead.
|
||||||
|
|
||||||
|
## [10.2.0] - 2023-04-12
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- #2663, Limit maximal postgresql connection lifetime - @robx
|
||||||
|
+ New option `db-pool-max-lifetime` (default 30m)
|
||||||
|
+ `db-pool-acquisition-timeout` is no longer optional and defaults to 10s
|
||||||
|
+ Fixes postgresql resource leak with long-lived connections (#2638)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- #2667, Fix `db-pool-acquisition-timeout` not logging to stderr when the timeout is reached - @steve-chavez
|
||||||
|
|
||||||
|
## [10.1.2] - 2023-02-01
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- #2565, Fix bad M2M embedding on RPC - @steve-chavez
|
||||||
|
- #2575, Replace misleading error message when no function is found with a hint containing functions/parameters names suggestions - @laurenceisla
|
||||||
|
- #2582, Move explanation about "single parameters" from the `message` to the `details` in the error output - @laurenceisla
|
||||||
|
- #2569, Replace misleading error message when no relationship is found with a hint containing parent/child names suggestions - @laurenceisla
|
||||||
|
- #1405, Add the required OpenAPI items object when the parameter is an array - @laurenceisla
|
||||||
|
- #2592, Add upsert headers for POST requests to the OpenAPI output - @laurenceisla
|
||||||
|
- #2623, Fix FK pointing to VIEW instead of TABLE in OpenAPI output - @laurenceisla
|
||||||
|
- #2622, Consider any PostgreSQL authentication failure as fatal and exit immediately - @michivi
|
||||||
|
- #2620, Fix `NOTIFY pgrst` not reloading the db connections catalog cache - @steve-chavez
|
||||||
|
|
||||||
|
## [10.1.1] - 2022-11-08
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- #2548, Fix regression when embedding views with partial references to multi column FKs - @wolfgangwalther
|
||||||
|
- #2558, Fix regression when requesting limit=0 and `db-max-row` is set - @laurenceisla
|
||||||
|
- #2542, Return a clear error without hitting the database when trying to update or insert an unknown column with `?columns` - @aljungberg
|
||||||
|
|
||||||
|
## [10.1.0] - 2022-10-28
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- #2348, Add `db-pool-acquisition-timeout` configuration option, time in seconds to wait to acquire a connection. - @robx
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- #2261, #2349, #2467, Reduce allocations communication with PostgreSQL, particularly for request bodies. - @robx
|
||||||
|
- #2401, #2444, Fix SIGUSR1 to fully flush connections pool. - @robx
|
||||||
|
- #2428, Fix opening an empty transaction on failed resource embedding - @steve-chavez
|
||||||
|
- #2455, Fix embedding the same table multiple times - @steve-chavez
|
||||||
|
- #2518, Fix a regression when embedding views where base tables have a different column order for FK columns - @wolfgangwalther
|
||||||
|
- #2458, Fix a regression with the location header when inserting into views with PKs from multiple tables - @wolfgangwalther
|
||||||
|
- #2356, Fix a regression in openapi output with mode follow-privileges - @wolfgangwalther
|
||||||
|
- #2283, Fix infinite recursion when loading schema cache with self-referencing view - @wolfgangwalther
|
||||||
|
- #2343, Return status code 200 for PATCH requests which don't affect any rows - @wolfgangwalther
|
||||||
|
- #2481, Treat computed relationships not marked SETOF as M2O/O2O relationship - @wolfgangwalther
|
||||||
|
- #2534, Fix embedding a computed relationship with a normal relationship - @steve-chavez
|
||||||
|
- #2362, Fix error message when [] is used inside select - @wolfgangwalther
|
||||||
|
- #2475, Disallow !inner on computed columns - @wolfgangwalther
|
||||||
|
- #2285, Ignore leading and trailing spaces in column names when parsing the query string - @wolfgangwalther
|
||||||
|
- #2545, Fix UPSERT with PostgreSQL 15 - @wolfgangwalther
|
||||||
|
- #2459, Fix embedding views with multiple references to the same base column - @wolfgangwalther
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- #2444, Removed `db-pool-timeout` option, because this was removed upstream in hasql-pool. - @robx
|
||||||
|
- #2343, PATCH requests that don't affect any rows no longer return 404 - @wolfgangwalther
|
||||||
|
- #2537, Stricter parsing of query string. Instead of silently ignoring, the parser now throws on invalid syntax like json paths for embeddings, hints for regular columns, empty casts or fts languages, etc. - @wolfgangwalther
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
|
||||||
|
- #1385, Deprecate bulk-calls when including the `Prefer: params=multiple-objects` in the request. A function with a JSON array or object parameter should be used instead for a better performance.
|
||||||
|
|
||||||
|
## [10.0.0] - 2022-08-18
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- #1933, #2109, Add a minimal health check endpoint - @steve-chavez
|
||||||
|
+ For enabling this, the `admin-server-port` config must be set explictly
|
||||||
|
+ A `<host>:<admin_server_port>/live` endpoint is available for checking if postgrest is running on its port/socket. 200 OK = alive, 503 = dead.
|
||||||
|
+ A `<host>:<admin_server_port>/ready` endpoint is available for checking a correct internal state(the database connection plus the schema cache). 200 OK = ready, 503 = not ready.
|
||||||
|
- #1988, Add the current user to the request log on stdout - @DavidLindbom, @wolfgangwalther
|
||||||
|
- #1823, Add the ability to run postgrest without any configuration. - @wolfgangwalther
|
||||||
|
+ #1991, Add the ability to run without `db-uri` using libpq's PG environment variables to connect. - @wolfgangwalther
|
||||||
|
+ #1769, Add the ability to run without `db-schemas`, defaulting to `db-schemas=public`. - @wolfgangwalther
|
||||||
|
+ #1689, Add the ability to run without `db-anon-role` disabling anonymous access. - @wolfgangwalther
|
||||||
|
- #1543, Allow access to fields of composite types in select=, order= and filters through JSON operators -> and ->>. - @wolfgangwalther
|
||||||
|
- #2075, Allow access to array items in ?select=, ?order= and filters through JSON operators -> and ->>. - @wolfgangwalther
|
||||||
|
- #2156, #2211, Allow applying `limit/offset` to UPDATE/DELETE to only affect a subset of rows - @steve-chavez
|
||||||
|
+ It requires an explicit `order` on a unique column(s)
|
||||||
|
- #1917, Add error codes with the `"PGRST"` prefix to the error response body to differentiate PostgREST errors from PostgreSQL errors - @laurenceisla
|
||||||
|
- #1917, Normalize the error response body by always having the `detail` and `hint` error fields with a `null` value if they are empty - @laurenceisla
|
||||||
|
- #2176, Errors raised with `SQLSTATE` now include the message and the code in the response body - @laurenceisla
|
||||||
|
- #2236, Support POSIX regular expression operators for row filtering - @enote-kane
|
||||||
|
- #2202, Allow returning XML from RPCs - @fjf2002
|
||||||
|
- #2268, Allow returning XML from single-column queries - @fjf2002
|
||||||
|
- #2300, RPC POST for function w/single unnamed XML param #2300 - @fjf2002
|
||||||
|
- #1564, Allow geojson output by specifying the `Accept: application/geo+json` media type - @steve-chavez
|
||||||
|
+ Requires postgis >= 3.0
|
||||||
|
+ Works for GET, RPC, POST/PATCH/DELETE with `Prefer: return=representation`.
|
||||||
|
+ Resource embedding works and the embedded rows will go into the `properties` key
|
||||||
|
+ In case of multiple geometries in the same table, you can choose which one will go into the `geometry` key with the usual `?select` query parameter.
|
||||||
|
- #1082, Add security definitions to the OpenAPI output - @laurenceisla
|
||||||
|
- #2378, Support http OPTIONS method on RPC and root path - @steve-chavez
|
||||||
|
- #2354, Allow getting the EXPLAIN plan of a request by using the `Accept: application/vnd.pgrst.plan` header - @steve-chavez
|
||||||
|
+ Only allowed if the `db-plan-enabled` config is set to true
|
||||||
|
+ Can generate the plan for different media types using the `for` parameter: `Accept: application/vnd.pgrst.plan; for="application/vnd.pgrst.object"`
|
||||||
|
+ Different options for the plan can be used with the `options` parameter: `Accept: application/vnd.pgrst.plan; options=analyze|verbose|settings|buffers|wal`
|
||||||
|
+ The plan can be obtained in text or json by using different media type suffixes: `Accept: application/vnd.pgrst.plan+text` and `Accept: application/vnd.pgrst.plan+json`.
|
||||||
|
- #2144, Support computed relationships which allow extending and overriding relationships for resource embedding - @steve-chavez, @wolfgangwalther
|
||||||
|
- #1984, Detect one-to-one relationships for resource embedding - @steve-chavez
|
||||||
|
+ Detected when there's a foreign key with a unique constraint or when a foreign key is also a primary key
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- #2058, Return 204 No Content without Content-Type for PUT - @wolfgangwalther
|
||||||
|
- #2107, Clarify error for failed schema cache load. - @steve-chavez
|
||||||
|
+ From `Database connection lost. Retrying the connection` to `Could not query the database for the schema cache. Retrying.`
|
||||||
|
- #1771, Fix silently ignoring filter on a non-existent embedded resource - @steve-chavez
|
||||||
|
- #2152, Remove functions, which are uncallable because of unnamend arguments from schema cache and OpenAPI output. - @wolfgangwalther
|
||||||
|
- #2145, Fix accessing json array fields with -> and ->> in ?select= and ?order=. - @wolfgangwalther
|
||||||
|
- #2155, Ignore `max-rows` on POST, PATCH, PUT and DELETE - @steve-chavez
|
||||||
|
- #2254, Fix inferring a foreign key column as a primary key column on views - @steve-chavez
|
||||||
|
- #2070, Restrict generated many-to-many relationships - @steve-chavez
|
||||||
|
+ Only adds many-to-many relationships when: a table has FKs to two other tables and these FK columns are part of the table's PK columns.
|
||||||
|
- #2278, Allow casting to types with underscores and numbers(e.g. `select=oid_array::_int4`) - @steve-chavez
|
||||||
|
- #2277, #2238, #1643, Prevent views from breaking one-to-many/many-to-one embeds when using column or FK as target - @steve-chavez
|
||||||
|
+ When using a column or FK as target for embedding(`/tbl?select=*,col-or-fk(*)`), only tables are now detected and views are not.
|
||||||
|
+ You can still use a column or an inferred FK on a view to embed a table(`/view?select=*,col-or-fk(*)`)
|
||||||
|
- #2317, Increase the `db-pool-timeout` to 1 hour to prevent frequent high connection latency - @steve-chavez
|
||||||
|
- #2341, The search path now correctly identifies schemas with uppercase and special characters in their names (regression) - @laurenceisla
|
||||||
|
- #2364, "404 Not Found" on nested routes and "405 Method Not Allowed" errors no longer start an empty database transaction - @steve-chavez
|
||||||
|
- #2342, Fix inaccurate result count when an inner embed was selected after a normal embed in the query string - @laurenceisla
|
||||||
|
- #2376, OPTIONS requests no longer start an empty database transaction - @steve-chavez
|
||||||
|
- #2395, Allow using columns with dollar sign($) without double quoting in filters and `select` - @steve-chavez
|
||||||
|
- #2410, Fix loop crash error on startup in Postgres 15 beta 3. Log: "UNION types \"char\" and text cannot be matched". - @yevon
|
||||||
|
- #2397, Fix race conditions managing database connection helper - @robx
|
||||||
|
- #2269, Allow `limit=0` in the request query to return an empty array - @gautam1168, @laurenceisla
|
||||||
|
- #2401, Ensure database connections can't outlive SIGUSR1 - @robx
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- #2001, Return 204 No Content without Content-Type for RPCs returning VOID - @wolfgangwalther
|
||||||
|
+ Previously, those RPCs would return "null" as a body with Content-Type: application/json.
|
||||||
|
- #2156, `limit/offset` now limits the affected rows on UPDATE/DELETE - @steve-chavez
|
||||||
|
+ Previously, `limit/offset` only limited the returned rows but not the actual updated rows
|
||||||
|
- #2155, `max-rows` is no longer applied on POST/PATCH/PUT/DELETE returned rows - @steve-chavez
|
||||||
|
+ This was misleading because the affected rows were not really affected by `max-rows`, only the returned rows were limited
|
||||||
|
- #2070, Restrict generated many-to-many relationships - @steve-chavez
|
||||||
|
+ A primary key that contains the foreign key columns is now needed for generating many-to-many relationships.
|
||||||
|
- #2277, Views now are not detected when embedding using the column or FK as target (`/view?select=*,column(*)`) - @steve-chavez
|
||||||
|
+ This embedding form was easily made ambiguous whenever a new view was added.
|
||||||
|
+ You can use computed relationships to keep this embedding form working
|
||||||
|
- #2312, Using `Prefer: return=representation` no longer returns a `Location` header - @laurenceisla
|
||||||
|
- #1984, For the cases where one to one relationships are detected, json objects will be returned instead of json arrays of length 1
|
||||||
|
+ If you wish to override this behavior, you can use computed relationships to return arrays again
|
||||||
|
|
||||||
|
## [9.0.1] - 2022-06-03
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- #2165, Fix json/jsonb columns should not have type in OpenAPI spec - @clrnd
|
||||||
|
- #2020, Execute deferred constraint triggers when using `Prefer: tx=rollback` - @wolfgangwalther
|
||||||
|
- #2077, Fix `is` not working with upper or mixed case values like `NULL, TrUe, FaLsE` - @steve-chavez
|
||||||
|
- #2024, Fix schema cache loading when views with XMLTABLE and DEFAULT are present - @wolfgangwalther
|
||||||
|
- #1724, Fix wrong CORS header Authentication -> Authorization - @wolfgangwalther
|
||||||
|
- #2120, Fix reading database configuration properly when `=` is present in value - @wolfgangwalther
|
||||||
|
- #2135, Remove trigger functions from schema cache and OpenAPI output, because they can't be called directly anyway. - @wolfgangwalther
|
||||||
|
- #2101, Remove aggregates, procedures and window functions from the schema cache and OpenAPI output. - @wolfgangwalther
|
||||||
|
- #2153, Fix --dump-schema running with a wrong PG version. - @wolfgangwalther
|
||||||
|
- #2042, Keep working when EMFILE(Too many open files) is reached. - @steve-chavez
|
||||||
|
- #2147, Ignore `Content-Type` headers for `GET` requests when calling RPCs. - @laurenceisla
|
||||||
|
+ Previously, `GET` without parameters, but with `Content-Type: text/plain` or `Content-Type: application/octet-stream` would fail with `404 Not Found`, even if a function without arguments was available.
|
||||||
|
- #2239, Fix misleading disambiguation error where the content of the `relationship` key looks like valid syntax - @laurenceisla
|
||||||
|
- #2294, Disable parallel GC for better performance on higher core CPUs - @steve-chavez
|
||||||
|
- #1076, Fix using CPU while idle - @steve-chavez
|
||||||
|
|
||||||
|
## [9.0.0] - 2021-11-25
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- #1783, Include partitioned tables into the schema cache. Allows embedding, UPSERT, INSERT with Location response, OPTIONS request and OpenAPI support for partitioned tables - @laurenceisla
|
||||||
|
- #1878, Add Retry-After hint header when in recovery mode - @gautam1168
|
||||||
|
- #1735, Allow calling function with single unnamed param through RPC POST. - @steve-chavez
|
||||||
|
+ Enables calling a function with a single json parameter without using `Prefer: params=single-object`
|
||||||
|
+ Enables uploading bytea to a function with `Content-Type: application/octet-stream`
|
||||||
|
+ Enables uploading raw text to a function with `Content-Type: text/plain`
|
||||||
|
- #1938, Allow escaping inside double quotes with a backslash, e.g. `?col=in.("Double\"Quote")`, `?col=in.("Back\\slash")` - @steve-chavez
|
||||||
|
- #1075, Allow filtering top-level resource based on embedded resources filters. This is enabled by adding `!inner` to the embedded resource, e.g. `/projects?select=*,clients!inner(*)&clients.id=eq.12`- @steve-chavez, @Iced-Sun
|
||||||
|
- #1857, Make GUC names for headers, cookies and jwt claims compatible with PostgreSQL v14 - @laurenceisla, @robertsosinski
|
||||||
|
+ Getting the value for a header GUC on PostgreSQL 14 is done using `current_setting('request.headers')::json->>'name-of-header'` and in a similar way for `request.cookies` and `request.jwt.claims`
|
||||||
|
+ PostgreSQL versions below 14 can opt in to the new JSON GUCs by setting the `db-use-legacy-gucs` config option to false (true by default)
|
||||||
|
- #1988, Allow specifying `unknown` for the `is` operator - @steve-chavez
|
||||||
|
- #2031, Improve error message for ambiguous embedding and add a relevant hint that includes unambiguous embedding suggestions - @laurenceisla
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- #1871, Fix OpenAPI missing default values for String types and identify Array types as "array" instead of "string" - @laurenceisla
|
||||||
|
- #1930, Fix RPC return type handling for `RETURNS TABLE` with a single column. Regression of #1615. - @wolfgangwalther
|
||||||
|
- #1938, Fix using single double quotes(`"`) and backslashes(`/`) as values on the "in" operator - @steve-chavez
|
||||||
|
- #1992, Fix schema cache query failing with standard_conforming_strings = off - @wolfgangwalther
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- #1949, Drop support for embedding hints used with '.'(`select=projects.client_id(*)`), '!' should be used instead(`select=projects!client_id(*)`) - @steve-chavez
|
||||||
|
- #1783, Partitions (created using `PARTITION OF`) are no longer included in the schema cache. - @laurenceisla
|
||||||
|
- #2038, Dropped support for PostgreSQL 9.5 - @wolfgangwalther
|
||||||
|
|
||||||
|
## [8.0.0] - 2021-07-25
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- #1525, Allow http status override through response.status guc - @steve-chavez
|
||||||
|
- #1512, Allow schema cache reloading with NOTIFY - @steve-chavez
|
||||||
|
- #1119, Allow config file reloading with SIGUSR2 - @steve-chavez
|
||||||
|
- #1558, Allow 'Bearer' with and without capitalization as authentication schema - @wolfgangwalther
|
||||||
|
- #1470, Allow calling RPC with variadic argument by passing repeated params - @wolfgangwalther
|
||||||
|
- #1559, No downtime when reloading the schema cache with SIGUSR1 - @steve-chavez
|
||||||
|
- #504, Add `log-level` config option. The admitted levels are: crit, error, warn and info - @steve-chavez
|
||||||
|
- #1607, Enable embedding through multiple views recursively - @wolfgangwalther
|
||||||
|
- #1598, Allow rollback of the transaction with Prefer tx=rollback - @wolfgangwalther
|
||||||
|
- #1633, Enable prepared statements for GET filters. When behind a connection pooler, you can disable preparing with `db-prepared-statements=false`
|
||||||
|
+ This increases throughput by around 30% for simple GET queries(no embedding, with filters applied).
|
||||||
|
- #1729, #1760, Get configuration parameters from the db and allow reloading config with NOTIFY - @steve-chavez
|
||||||
|
- #1824, Allow OPTIONS to generate certain HTTP methods for a DB view - @laurenceisla
|
||||||
|
- #1872, Show timestamps in startup/worker logs - @steve-chavez
|
||||||
|
- #1881, Add `openapi-mode` config option that allows ignoring roles privileges when showing the OpenAPI output - @steve-chavez
|
||||||
|
- CLI options(for debugging):
|
||||||
|
+ #1678, Add --dump-config CLI option that prints loaded config and exits - @wolfgangwalther
|
||||||
|
+ #1691, Add --example CLI option to show example config file - @wolfgangwalther
|
||||||
|
+ #1697, #1723, Add --dump-schema CLI option for debugging purposes - @monacoremo, @wolfgangwalther
|
||||||
|
- #1794, (Experimental) Add `request.spec` GUC for db-root-spec - @steve-chavez
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- #1592, Removed single column restriction to allow composite foreign keys in join tables - @goteguru
|
||||||
|
- #1530, Fix how the PostgREST version is shown in the help text when the `.git` directory is not available - @monacoremo
|
||||||
|
- #1094, Fix expired JWTs starting an empty transaction on the db - @steve-chavez
|
||||||
|
- #1162, Fix location header for POST request with select= without PK - @wolfgangwalther
|
||||||
|
- #1585, Fix error messages on connection failure for localized postgres on Windows - @wolfgangwalther
|
||||||
|
- #1636, Fix `application/octet-stream` appending `charset=utf-8` - @steve-chavez
|
||||||
|
- #1469, #1638 Fix overloading of functions with unnamed arguments - @wolfgangwalther
|
||||||
|
- #1560, Return 405 Method not Allowed for GET of volatile RPC instead of 500 - @wolfgangwalther
|
||||||
|
- #1584, Fix RPC return type handling and embedding for domains with composite base type (#1615) - @wolfgangwalther
|
||||||
|
- #1608, #1635, Fix embedding through views that have COALESCE with subselect - @wolfgangwalther
|
||||||
|
- #1572, Fix parsing of boolean config values for Docker environment variables, now it accepts double quoted truth values ("true", "false") and numbers("1", "0") - @wolfgangwalther
|
||||||
|
- #1624, Fix using `app.settings.xxx` config options in Docker, now they can be used as `PGRST_APP_SETTINGS_xxx` - @wolfgangwalther
|
||||||
|
- #1814, Fix panic when attempting to run with unix socket on non-unix host and properly close unix domain socket on exit - @monacoremo
|
||||||
|
- #1825, Disregard internal junction(in non-exposed schema) when embedding - @steve-chavez
|
||||||
|
- #1846, Fix requests for overloaded functions from html forms to no longer hang (#1848) - @laurenceisla
|
||||||
|
- #1858, Add a hint and clarification to the no relationship found error - @laurenceisla
|
||||||
|
- #1841, Show comprehensive error when an RPC is not found in a stale schema cache - @laurenceisla
|
||||||
|
- #1875, Fix Location headers in headers only representation for null PK inserts on views - @laurenceisla
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- #1522, #1528, #1535, Docker images are now built from scratch based on a the static PostgREST executable (#1494) and with Nix instead of a `Dockerfile`. This reduces the compressed image size from over 30mb to about 4mb - @monacoremo
|
||||||
|
- #1461, Location header for POST request is only included when PK is available on the table - @wolfgangwalther
|
||||||
|
- #1560, Volatile RPC called with GET now returns 405 Method not Allowed instead of 500 - @wolfgangwalther
|
||||||
|
- #1584, #1849 Functions that declare `returns composite_type` no longer return a single object array by default, only functions with `returns setof composite_type` return an array of objects - @wolfgangwalther
|
||||||
|
- #1604, Change the default logging level to `log-level=error`. Only requests with a status greater or equal than 500 will be logged. If you wish to go back to the previous behaviour and log all the requests, use `log-level=info` - @steve-chavez
|
||||||
|
+ Because currently there's no buffering for logging, defaulting to the `error` level(minimum logging) increases throughput by around 15% for simple GET queries(no embedding, with filters applied).
|
||||||
|
- #1617, Dropped support for PostgreSQL 9.4 - @wolfgangwalther
|
||||||
|
- #1679, Renamed config settings with fallback aliases. e.g. `max-rows` is now `db-max-rows`, but `max-rows` is still accepted - @wolfgangwalther
|
||||||
|
- #1656, Allow `Prefer=headers-only` on POST requests and change default to `minimal` (#1813) - @laurenceisla
|
||||||
|
- #1854, Dropped undocumented support for gzip compression (which was surprisingly slow and easily enabled by mistake). In some use-cases this makes Postgres up to 3x faster - @aljungberg
|
||||||
|
- #1872, Send startup/worker logs to stderr to differentiate from access logs on stdout - @steve-chavez
|
||||||
|
|
||||||
## [7.0.1] - 2020-05-18
|
## [7.0.1] - 2020-05-18
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
@@ -19,6 +429,10 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
- #1508, Fix `Content-Profile` not working for POST RPC - @steve-chavez
|
- #1508, Fix `Content-Profile` not working for POST RPC - @steve-chavez
|
||||||
- #1452, Fix PUT restriction for all columns - @steve-chavez
|
- #1452, Fix PUT restriction for all columns - @steve-chavez
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- From this version onwards, the release page will only include a single Linux static executable that can be run on any Linux distribution.
|
||||||
|
|
||||||
## [7.0.0] - 2020-04-03
|
## [7.0.0] - 2020-04-03
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
|
||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
We as members, contributors, and leaders pledge to make participation in our
|
||||||
|
community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||||
|
identity and expression, level of experience, education, socio-economic status,
|
||||||
|
nationality, personal appearance, race, caste, color, religion, or sexual identity
|
||||||
|
and orientation.
|
||||||
|
|
||||||
|
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||||
|
diverse, inclusive, and healthy community.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to a positive environment for our
|
||||||
|
community include:
|
||||||
|
|
||||||
|
* Demonstrating empathy and kindness toward other people
|
||||||
|
* Being respectful of differing opinions, viewpoints, and experiences
|
||||||
|
* Giving and gracefully accepting constructive feedback
|
||||||
|
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||||
|
and learning from the experience
|
||||||
|
* Focusing on what is best not just for us as individuals, but for the
|
||||||
|
overall community
|
||||||
|
|
||||||
|
Examples of unacceptable behavior include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery, and sexual attention or
|
||||||
|
advances of any kind
|
||||||
|
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or email
|
||||||
|
address, without their explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Enforcement Responsibilities
|
||||||
|
|
||||||
|
Community leaders are responsible for clarifying and enforcing our standards of
|
||||||
|
acceptable behavior and will take appropriate and fair corrective action in
|
||||||
|
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||||
|
or harmful.
|
||||||
|
|
||||||
|
Community leaders have the right and responsibility to remove, edit, or reject
|
||||||
|
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||||
|
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||||
|
decisions when appropriate.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies within all community spaces, and also applies when
|
||||||
|
an individual is officially representing the community in public spaces.
|
||||||
|
Examples of representing our community include using an official e-mail address,
|
||||||
|
posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported to the community leaders responsible for enforcement at support@postgrest.org.
|
||||||
|
All complaints will be reviewed and investigated promptly and fairly.
|
||||||
|
|
||||||
|
All community leaders are obligated to respect the privacy and security of the
|
||||||
|
reporter of any incident.
|
||||||
|
|
||||||
|
## Enforcement Guidelines
|
||||||
|
|
||||||
|
Community leaders will follow these Community Impact Guidelines in determining
|
||||||
|
the consequences for any action they deem in violation of this Code of Conduct:
|
||||||
|
|
||||||
|
### 1. Correction
|
||||||
|
|
||||||
|
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||||
|
unprofessional or unwelcome in the community.
|
||||||
|
|
||||||
|
**Consequence**: A private, written warning from community leaders, providing
|
||||||
|
clarity around the nature of the violation and an explanation of why the
|
||||||
|
behavior was inappropriate. A public apology may be requested.
|
||||||
|
|
||||||
|
### 2. Warning
|
||||||
|
|
||||||
|
**Community Impact**: A violation through a single incident or series
|
||||||
|
of actions.
|
||||||
|
|
||||||
|
**Consequence**: A warning with consequences for continued behavior. No
|
||||||
|
interaction with the people involved, including unsolicited interaction with
|
||||||
|
those enforcing the Code of Conduct, for a specified period of time. This
|
||||||
|
includes avoiding interactions in community spaces as well as external channels
|
||||||
|
like social media. Violating these terms may lead to a temporary or
|
||||||
|
permanent ban.
|
||||||
|
|
||||||
|
### 3. Temporary Ban
|
||||||
|
|
||||||
|
**Community Impact**: A serious violation of community standards, including
|
||||||
|
sustained inappropriate behavior.
|
||||||
|
|
||||||
|
**Consequence**: A temporary ban from any sort of interaction or public
|
||||||
|
communication with the community for a specified period of time. No public or
|
||||||
|
private interaction with the people involved, including unsolicited interaction
|
||||||
|
with those enforcing the Code of Conduct, is allowed during this period.
|
||||||
|
Violating these terms may lead to a permanent ban.
|
||||||
|
|
||||||
|
### 4. Permanent Ban
|
||||||
|
|
||||||
|
**Community Impact**: Demonstrating a pattern of violation of community
|
||||||
|
standards, including sustained inappropriate behavior, harassment of an
|
||||||
|
individual, or aggression toward or disparagement of classes of individuals.
|
||||||
|
|
||||||
|
**Consequence**: A permanent ban from any sort of public interaction within
|
||||||
|
the community.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||||
|
version 2.0, available at
|
||||||
|
[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
|
||||||
|
|
||||||
|
Community Impact Guidelines were inspired by
|
||||||
|
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see the FAQ at
|
||||||
|
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available
|
||||||
|
at [https://www.contributor-covenant.org/translations][translations].
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
|
||||||
|
[Mozilla CoC]: https://github.com/mozilla/diversity
|
||||||
|
[FAQ]: https://www.contributor-covenant.org/faq
|
||||||
|
[translations]: https://www.contributor-covenant.org/translations
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
.PHONY: commit-check check clean lint style test test-watch coverage circleci circleci-prof-test check-dburi prompt-clean prompt-long-process
|
|
||||||
|
|
||||||
commit-check: lint style
|
|
||||||
|
|
||||||
check: lint style test
|
|
||||||
|
|
||||||
clean: prompt-clean
|
|
||||||
stack clean --full
|
|
||||||
|
|
||||||
# For running these you'll need to install hlint and stylish-haskell first. Run:
|
|
||||||
# stack install hlint stylish-haskell
|
|
||||||
lint:
|
|
||||||
git ls-files | grep '\.l\?hs$$' | xargs stack exec -- hlint -X QuasiQuotes -X NoPatternSynonyms "$$@"
|
|
||||||
|
|
||||||
style:
|
|
||||||
git ls-files | grep '\.l\?hs$$' | xargs stack exec -- stylish-haskell -i && git diff-index --exit-code HEAD -- '*.hs' '*.lhs'
|
|
||||||
|
|
||||||
test: check-dburi
|
|
||||||
stack test
|
|
||||||
|
|
||||||
test-watch: check-dburi
|
|
||||||
stack build --file-watch --test --test-arguments '--rerun --failure-report=.TESTREPORT --rerun-all-on-success'
|
|
||||||
|
|
||||||
coverage: check-dburi clean
|
|
||||||
stack build --coverage
|
|
||||||
stack test --coverage
|
|
||||||
|
|
||||||
circleci: prompt-long-process
|
|
||||||
circleci local execute --job build-test-9.4
|
|
||||||
|
|
||||||
circleci-prof-test: prompt-long-process
|
|
||||||
circleci local execute --job build-prof-test
|
|
||||||
|
|
||||||
check-dburi:
|
|
||||||
test -n "$(POSTGREST_TEST_CONNECTION)" # Requires POSTGREST_TEST_CONNECTION environmental variable
|
|
||||||
|
|
||||||
prompt-clean:
|
|
||||||
@echo -n 'Are you sure? You will have to rebuild. [y/N] ' && read ans && [ $${ans:-N} = y ]
|
|
||||||
|
|
||||||
prompt-long-process:
|
|
||||||
@echo -n 'Are you sure? This might take a while. [y/N] ' && read ans && [ $${ans:-N} = y ]
|
|
||||||
@@ -2,13 +2,11 @@
|
|||||||
|
|
||||||
[](https://www.patreon.com/postgrest)
|
[](https://www.patreon.com/postgrest)
|
||||||
[](https://www.paypal.me/postgrest)
|
[](https://www.paypal.me/postgrest)
|
||||||
<a href="https://heroku.com/deploy?template=https://github.com/PostgREST/postgrest">
|
|
||||||
<img src="https://img.shields.io/badge/%E2%86%91_Deploy_to-Heroku-7056bf.svg" alt="Deploy">
|
|
||||||
</a>
|
|
||||||
[](https://gitter.im/begriffs/postgrest)
|
[](https://gitter.im/begriffs/postgrest)
|
||||||
[](http://postgrest.org)
|
[](http://postgrest.org)
|
||||||
[](https://hub.docker.com/r/postgrest/postgrest/)
|
[](https://hub.docker.com/r/postgrest/postgrest/)
|
||||||
[](https://circleci.com/gh/PostgREST/postgrest/tree/master)
|
[](https://github.com/PostgREST/postgrest/actions?query=branch%3Amain)
|
||||||
|
[](https://app.codecov.io/gh/PostgREST/postgrest)
|
||||||
[](http://hackage.haskell.org/package/postgrest)
|
[](http://hackage.haskell.org/package/postgrest)
|
||||||
|
|
||||||
PostgREST serves a fully RESTful API from any existing PostgreSQL
|
PostgREST serves a fully RESTful API from any existing PostgreSQL
|
||||||
@@ -21,18 +19,36 @@ API than you are likely to write from scratch.
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" valign="middle">
|
<td align="center" valign="middle">
|
||||||
<a href="https://www.cybertec-postgresql.com/en/" target="_blank">
|
<a href="https://www.cybertec-postgresql.com/en/?utm_source=postgrest.org&utm_medium=referral&utm_campaign=postgrest" target="_blank">
|
||||||
<img width="222px" src="static/cybertec.png">
|
<img width="222px" src="static/cybertec-new.png">
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" valign="middle">
|
<td align="center" valign="middle">
|
||||||
<a href="https://www.2ndquadrant.com/en/?utm_campaign=External%20Websites&utm_source=PostgREST&utm_medium=Logo" target="_blank">
|
<a href="https://www.2ndquadrant.com/en/?utm_campaign=External%20Websites&utm_source=PostgREST&utm_medium=Logo" target="_blank">
|
||||||
<img width="222px" src="static/2ndquadrant.png">
|
<img width="296px" src="static/2ndquadrant.png">
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" valign="middle">
|
<td align="center" valign="middle">
|
||||||
<a href="https://tryretool.com/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
<a href="https://tryretool.com/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||||
<img width="222px" src="static/retool.png">
|
<img width="296px" src="static/retool.png">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr></tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="middle">
|
||||||
|
<a href="https://gnuhost.eu/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||||
|
<img width="296px" src="static/gnuhost.png">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" valign="middle">
|
||||||
|
<a href="https://supabase.io?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage" target="_blank">
|
||||||
|
<img width="296px" src="static/supabase.png">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" valign="middle">
|
||||||
|
<a href="https://oblivious.ai/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||||
|
<img width="296px" src="static/oblivious.jpg">
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -93,15 +109,6 @@ the connection cannot do anything the user themselves couldn't. Other
|
|||||||
forms of authentication can be built on top of the JWT primitive. See
|
forms of authentication can be built on top of the JWT primitive. See
|
||||||
the docs for more information.
|
the docs for more information.
|
||||||
|
|
||||||
PostgreSQL 9.5 supports true [row-level
|
|
||||||
security](http://www.postgresql.org/docs/9.5/static/ddl-rowsecurity.html).
|
|
||||||
In previous versions it can be simulated with triggers and
|
|
||||||
security-barrier views. Because the possible queries to the database
|
|
||||||
are limited to certain templates using
|
|
||||||
[leakproof](http://blog.2ndquadrant.com/how-do-postgresql-security_barrier-views-work/)
|
|
||||||
functions, the trigger workaround does not compromise row-level
|
|
||||||
security.
|
|
||||||
|
|
||||||
## Versioning
|
## Versioning
|
||||||
|
|
||||||
A robust long-lived API needs the freedom to exist in multiple
|
A robust long-lived API needs the freedom to exist in multiple
|
||||||
@@ -150,8 +157,8 @@ Every donation will be spent on making PostgREST better for the whole community.
|
|||||||
|
|
||||||
The PostgREST organization is grateful to:
|
The PostgREST organization is grateful to:
|
||||||
|
|
||||||
- The project [sponsors and backers](https://github.com/PostgREST/postgrest/blob/master/BACKERS.md) who support PostgREST's development.
|
- The project [sponsors and backers](https://github.com/PostgREST/postgrest/blob/main/BACKERS.md) who support PostgREST's development.
|
||||||
- The project [contributors](https://github.com/PostgREST/postgrest/graphs/contributors) who have improved PostgREST immensely with their code
|
- The project [contributors](https://github.com/PostgREST/postgrest/graphs/contributors) who have improved PostgREST immensely with their code
|
||||||
and good judgement. See more details in the [changelog](https://github.com/PostgREST/postgrest/blob/master/CHANGELOG.md).
|
and good judgement. See more details in the [changelog](https://github.com/PostgREST/postgrest/blob/main/CHANGELOG.md).
|
||||||
|
|
||||||
The cool logo came from [Mikey Casalaina](https://github.com/casalaina).
|
The cool logo came from [Mikey Casalaina](https://github.com/casalaina).
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
|
-- This file is required by Hackage.
|
||||||
import Distribution.Simple
|
import Distribution.Simple
|
||||||
main = defaultMain
|
main = defaultMain
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "PostgREST",
|
|
||||||
"description": "RESTful API for any PostgreSQL database.",
|
|
||||||
"logo": "https://avatars2.githubusercontent.com/u/15115011",
|
|
||||||
"repository": "https://github.com/PostgREST/postgrest",
|
|
||||||
"env": {
|
|
||||||
"BUILDPACK_URL": {
|
|
||||||
"description": "Heroku buildpack for deploying Haskell applications",
|
|
||||||
"value": "https://github.com/PostgREST/postgrest-heroku"
|
|
||||||
},
|
|
||||||
"POSTGREST_VER": {
|
|
||||||
"description": "Version of PostgREST to deploy",
|
|
||||||
"value": "7.0.1"
|
|
||||||
},
|
|
||||||
"DB_URI": {
|
|
||||||
"description": "Database connection string, e.g. postgres://user:pass@xxxxxxx.rds.amazonaws.com/mydb",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
"DB_SCHEMA": {
|
|
||||||
"description": "The database schema to expose to REST clients. Tables, views and stored procedures in this schema will get API endpoints",
|
|
||||||
"required": true,
|
|
||||||
"value": "public"
|
|
||||||
},
|
|
||||||
"DB_ANON_ROLE": {
|
|
||||||
"description": "The database role to use when executing commands on behalf of unauthenticated clients",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
"DB_POOL": {
|
|
||||||
"description": "Number of connections to keep open in PostgREST’s database pool",
|
|
||||||
"required": false,
|
|
||||||
"value": "10"
|
|
||||||
},
|
|
||||||
"SERVER_PROXY_URI": {
|
|
||||||
"description": "Overrides the base URL used within the OpenAPI self-documentation hosted at the API root path",
|
|
||||||
"required": false
|
|
||||||
},
|
|
||||||
"JWT_SECRET": {
|
|
||||||
"description": "The secret used to decode JWT tokens clients provide for authentication",
|
|
||||||
"required": false
|
|
||||||
},
|
|
||||||
"SECRET_IS_BASE64": {
|
|
||||||
"description": "When this is set to true, the value derived from jwt-secret will be treated as a base64 encoded secret",
|
|
||||||
"required": false,
|
|
||||||
"value": "false"
|
|
||||||
},
|
|
||||||
"JWT_AUD": {
|
|
||||||
"description": "The audience that should be validated if the JWT token contains an aud claim",
|
|
||||||
"required": false
|
|
||||||
},
|
|
||||||
"MAX_ROWS": {
|
|
||||||
"description": "A hard limit to the number of rows PostgREST will fetch from a view, table, or stored procedure",
|
|
||||||
"required": false
|
|
||||||
},
|
|
||||||
"PRE_REQUEST": {
|
|
||||||
"description": "A schema-qualified stored procedure name to call right after switching roles for a client request",
|
|
||||||
"required": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
## AppVeyor is only used for building a Windows binary, no tests are run here.
|
|
||||||
platform: x64
|
|
||||||
image: Visual Studio 2015
|
|
||||||
|
|
||||||
cache:
|
|
||||||
- "c:\\sr"
|
|
||||||
- .stack-work
|
|
||||||
- "c:\\Users\\appveyor\\AppData\\Local\\Programs\\stack"
|
|
||||||
|
|
||||||
environment:
|
|
||||||
global:
|
|
||||||
STACK_ROOT: "c:\\sr"
|
|
||||||
GOPATH: c:\gopath
|
|
||||||
TMP: "c:\\tmp"
|
|
||||||
|
|
||||||
test: off
|
|
||||||
|
|
||||||
install:
|
|
||||||
- set PATH=C:\Program Files\PostgreSQL\9.6\bin\;%PATH%
|
|
||||||
- curl -sS -ostack.zip -L --insecure http://www.stackage.org/stack/windows-x86_64
|
|
||||||
- 7z x stack.zip stack.exe
|
|
||||||
- set PATH=%GOPATH%\bin;c:\go\bin;%PATH%
|
|
||||||
- go get -u github.com/tcnksm/ghr
|
|
||||||
|
|
||||||
build_script:
|
|
||||||
- stack setup --no-terminal > nul
|
|
||||||
# Appveyor has a timeout of 60 mins, building can take longer, limit the time and make sure this succeeds,
|
|
||||||
# previous work will get cached and finish on next commit
|
|
||||||
- bash -lc "timeout 2700 'C:\projects\postgrest\stack.exe' build -j1 --copy-bins --local-bin-path . || true"
|
|
||||||
|
|
||||||
artifacts:
|
|
||||||
- path: postgrest.exe
|
|
||||||
|
|
||||||
deploy_script:
|
|
||||||
- IF DEFINED APPVEYOR_REPO_TAG_NAME 7z a -tzip postgrest-%APPVEYOR_REPO_TAG_NAME%-windows-x64.zip postgrest.exe
|
|
||||||
- IF DEFINED APPVEYOR_REPO_TAG_NAME bash -lc " exec 0</dev/null && cd $APPVEYOR_BUILD_FOLDER && ghr -t $GITHUB_TOKEN -u $APPVEYOR_ACCOUNT_NAME -r $APPVEYOR_PROJECT_NAME -b \"$(sed -n \"1,/$(echo $APPVEYOR_REPO_TAG_NAME | cut -c2-)/d;/## \[/q;p\" CHANGELOG.md)\" --replace $APPVEYOR_REPO_TAG_NAME postgrest-$APPVEYOR_REPO_TAG_NAME-windows-x64.zip"
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
-- Settings to allow building with plain cabal. If this was
|
||||||
|
-- named just cabal.project, it would interfere with the default
|
||||||
|
-- nix build.
|
||||||
|
|
||||||
|
packages: .
|
||||||
|
|
||||||
|
-- Example of depending on a forked repository (the same dependency
|
||||||
|
-- would be mentioned in nix/overlays/haskell-packages.nix and
|
||||||
|
-- stack.yaml, and should refer to a main branch commit of the
|
||||||
|
-- repository.
|
||||||
|
--
|
||||||
|
-- source-repository-package
|
||||||
|
-- type: git
|
||||||
|
-- location: https://github.com/PostgREST/hasql-pool.git
|
||||||
|
-- tag: 4d462c4d47d762effefc7de6c85eaed55f144f1d
|
||||||
|
|
||||||
|
source-repository-package
|
||||||
|
type: git
|
||||||
|
location: https://github.com/PostgREST/postgresql-libpq.git
|
||||||
|
tag: 890a0a16cf57dd401420fdc6c7d576fb696003bc
|
||||||
+125
-53
@@ -1,9 +1,11 @@
|
|||||||
|
{ system ? builtins.currentSystem }:
|
||||||
|
|
||||||
let
|
let
|
||||||
name =
|
name =
|
||||||
"postgrest";
|
"postgrest";
|
||||||
|
|
||||||
compiler =
|
compiler =
|
||||||
"ghc883";
|
"ghc924";
|
||||||
|
|
||||||
# PostgREST source files, filtered based on the rules in the .gitignore files
|
# PostgREST source files, filtered based on the rules in the .gitignore files
|
||||||
# and file extensions. We want to include as litte as possible, as the files
|
# and file extensions. We want to include as litte as possible, as the files
|
||||||
@@ -26,40 +28,82 @@ let
|
|||||||
sha256 = nixpkgsVersion.tarballHash;
|
sha256 = nixpkgsVersion.tarballHash;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
allOverlays =
|
||||||
|
import nix/overlays;
|
||||||
|
|
||||||
overlays =
|
overlays =
|
||||||
[
|
[
|
||||||
(import nix/overlays/postgresql-legacy.nix)
|
allOverlays.build-toolbox
|
||||||
(import nix/overlays/gitignore.nix)
|
allOverlays.checked-shell-script
|
||||||
(import nix/overlays/haskell-packages)
|
allOverlays.gitignore
|
||||||
|
allOverlays.postgis
|
||||||
|
(allOverlays.postgresql-default { inherit patches; })
|
||||||
|
allOverlays.postgresql-legacy
|
||||||
|
allOverlays.postgresql-future
|
||||||
|
(allOverlays.haskell-packages { inherit compiler; })
|
||||||
|
allOverlays.slocat
|
||||||
];
|
];
|
||||||
|
|
||||||
# Evaluated expression of the Nixpkgs repository.
|
# Evaluated expression of the Nixpkgs repository.
|
||||||
pkgs =
|
pkgs =
|
||||||
import nixpkgs { inherit overlays; };
|
import nixpkgs { inherit overlays system; };
|
||||||
|
|
||||||
postgresqlVersions =
|
postgresqlVersions =
|
||||||
[
|
[
|
||||||
pkgs.postgresql_12
|
{
|
||||||
pkgs.postgresql_11
|
name = "postgresql-16";
|
||||||
pkgs.postgresql_10
|
postgresql = pkgs.postgresql_16.withPackages (p: [
|
||||||
pkgs.postgresql_9_6
|
p.postgis
|
||||||
pkgs.postgresql_9_5
|
(p.pg_safeupdate.overrideAttrs (old: {
|
||||||
pkgs.postgresql_9_4
|
installPhase = ''
|
||||||
|
mkdir -p $out/bin
|
||||||
|
cp safeupdate.dylib safeupdate.so || true
|
||||||
|
install -D safeupdate.so -t $out/lib
|
||||||
|
'';
|
||||||
|
}))
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
{ name = "postgresql-15"; postgresql = pkgs.postgresql_15.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
||||||
|
{ name = "postgresql-14"; postgresql = pkgs.postgresql_14.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
||||||
|
{ name = "postgresql-13"; postgresql = pkgs.postgresql_13.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
||||||
|
{ name = "postgresql-12"; postgresql = pkgs.postgresql_12.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
||||||
|
{ name = "postgresql-11"; postgresql = pkgs.postgresql_11.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
||||||
|
{ name = "postgresql-10"; postgresql = pkgs.postgresql_10.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
||||||
|
{ name = "postgresql-9.6"; postgresql = pkgs.postgresql_9_6.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
||||||
];
|
];
|
||||||
|
|
||||||
patches =
|
patches =
|
||||||
pkgs.callPackage nix/patches {};
|
pkgs.callPackage nix/patches { };
|
||||||
|
|
||||||
# Base dynamic derivation for the PostgREST package.
|
# Dynamic derivation for PostgREST
|
||||||
drv =
|
postgrest =
|
||||||
pkgs.haskellPackages.callCabal2nix name src {};
|
pkgs.haskell.packages."${compiler}".callCabal2nix name src { };
|
||||||
|
|
||||||
# Static derivation for the PostgREST executable.
|
# Functionality that derives a fully static Haskell package based on
|
||||||
drvStatic =
|
# nh2/static-haskell-nix
|
||||||
import nix/static { inherit nixpkgs name src compiler patches; };
|
staticHaskellPackage =
|
||||||
|
import nix/static-haskell-package.nix { inherit nixpkgs system compiler patches allOverlays; };
|
||||||
|
|
||||||
lib =
|
# Static executable.
|
||||||
pkgs.haskell.lib;
|
postgrestStatic =
|
||||||
|
lib.justStaticExecutables (lib.dontCheck (staticHaskellPackage name src).package);
|
||||||
|
|
||||||
|
packagesStatic = (staticHaskellPackage name src).survey;
|
||||||
|
|
||||||
|
# Options passed to cabal in dev tools and tests
|
||||||
|
devCabalOptions =
|
||||||
|
"-f dev --test-show-detail=direct";
|
||||||
|
|
||||||
|
profiledHaskellPackages =
|
||||||
|
pkgs.haskell.packages."${compiler}".extend (self: super:
|
||||||
|
{
|
||||||
|
mkDerivation =
|
||||||
|
args:
|
||||||
|
super.mkDerivation (args // { enableLibraryProfiling = true; });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
inherit (pkgs.haskell) lib;
|
||||||
in
|
in
|
||||||
rec {
|
rec {
|
||||||
inherit nixpkgs pkgs;
|
inherit nixpkgs pkgs;
|
||||||
@@ -67,43 +111,71 @@ rec {
|
|||||||
# Derivation for the PostgREST Haskell package, including the executable,
|
# Derivation for the PostgREST Haskell package, including the executable,
|
||||||
# libraries and documentation. We disable running the test suite on Nix
|
# libraries and documentation. We disable running the test suite on Nix
|
||||||
# builds, as they require a database to be set up.
|
# builds, as they require a database to be set up.
|
||||||
postgrestWithLib =
|
postgrestPackage =
|
||||||
lib.dontCheck drv;
|
lib.dontCheck postgrest;
|
||||||
|
|
||||||
# Derivation for just the PostgREST binary, where we strip all dynamic
|
# Profiled dynamic executable.
|
||||||
# libraries and documentation, leaving only the executable. Note that the
|
postgrestProfiled =
|
||||||
# executable is static with regards to Haskell libraries, but not system
|
lib.enableExecutableProfiling (
|
||||||
# libraries like glibc and libpq.
|
lib.dontHaddock (
|
||||||
postgrest =
|
lib.dontCheck (profiledHaskellPackages.callCabal2nix name src { })
|
||||||
lib.justStaticExecutables postgrestWithLib;
|
)
|
||||||
|
);
|
||||||
|
|
||||||
# Static executable.
|
inherit (postgrest) env;
|
||||||
postgrestStatic =
|
|
||||||
lib.justStaticExecutables (lib.dontCheck drvStatic);
|
|
||||||
|
|
||||||
# Docker image and loading script.
|
# Tooling for analyzing Haskell imports and exports.
|
||||||
docker =
|
hsie =
|
||||||
pkgs.callPackage nix/docker { postgrest = postgrestStatic; };
|
pkgs.callPackage nix/hsie {
|
||||||
|
inherit (pkgs.haskell.packages."${compiler}") ghcWithPackages;
|
||||||
# Environment in which PostgREST can be built with cabal, useful e.g. for
|
|
||||||
# defining a shell for nix-shell.
|
|
||||||
env =
|
|
||||||
drv.env;
|
|
||||||
|
|
||||||
# Utility for updating the pinned version of Nixpkgs.
|
|
||||||
nixpkgsUpgrade =
|
|
||||||
pkgs.callPackage nix/nixpkgs-upgrade.nix {};
|
|
||||||
|
|
||||||
tests =
|
|
||||||
pkgs.callPackage nix/tests.nix
|
|
||||||
{
|
|
||||||
inherit postgresqlVersions;
|
|
||||||
postgrestBuildEnv = env;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
style =
|
### Tools
|
||||||
pkgs.callPackage nix/style.nix {};
|
|
||||||
|
|
||||||
lint =
|
cabalTools =
|
||||||
pkgs.callPackage nix/lint.nix {};
|
pkgs.callPackage nix/tools/cabalTools.nix { inherit devCabalOptions postgrest; };
|
||||||
|
|
||||||
|
withTools =
|
||||||
|
pkgs.callPackage nix/tools/withTools.nix { inherit cabalTools devCabalOptions postgresqlVersions postgrest; };
|
||||||
|
|
||||||
|
# Development tools.
|
||||||
|
devTools =
|
||||||
|
pkgs.callPackage nix/tools/devTools.nix { inherit tests style devCabalOptions hsie withTools; };
|
||||||
|
|
||||||
|
# Load testing tools.
|
||||||
|
loadtest =
|
||||||
|
pkgs.callPackage nix/tools/loadtest.nix { inherit withTools; };
|
||||||
|
|
||||||
|
# Script for running memory tests.
|
||||||
|
memory =
|
||||||
|
pkgs.callPackage nix/tools/memory.nix { inherit postgrestProfiled withTools; };
|
||||||
|
|
||||||
|
# Utility for updating the pinned version of Nixpkgs.
|
||||||
|
nixpkgsTools =
|
||||||
|
pkgs.callPackage nix/tools/nixpkgsTools.nix { };
|
||||||
|
|
||||||
|
# Scripts for publishing new releases.
|
||||||
|
release =
|
||||||
|
pkgs.callPackage nix/tools/release { };
|
||||||
|
|
||||||
|
# Linting and styling tools.
|
||||||
|
style =
|
||||||
|
pkgs.callPackage nix/tools/style.nix { inherit hsie; };
|
||||||
|
|
||||||
|
# Scripts for running tests.
|
||||||
|
tests =
|
||||||
|
pkgs.callPackage nix/tools/tests.nix {
|
||||||
|
inherit postgrest devCabalOptions withTools;
|
||||||
|
ghc = pkgs.haskell.compiler."${compiler}";
|
||||||
|
inherit (pkgs.haskell.packages."${compiler}") hpc-codecov;
|
||||||
|
inherit (pkgs.haskell.packages."${compiler}") weeder;
|
||||||
|
};
|
||||||
|
} // pkgs.lib.optionalAttrs pkgs.stdenv.isLinux rec {
|
||||||
|
# Static executable.
|
||||||
|
inherit postgrestStatic;
|
||||||
|
inherit packagesStatic;
|
||||||
|
|
||||||
|
# Docker images and loading script.
|
||||||
|
docker =
|
||||||
|
pkgs.callPackage nix/tools/docker { postgrest = postgrestStatic; };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
# To build use:
|
|
||||||
# docker build --build-arg POSTGREST_VERSION=<v5.2.0 or another version> -t postgrest ./docker/
|
|
||||||
|
|
||||||
FROM debian:buster-slim
|
|
||||||
|
|
||||||
ARG POSTGREST_VERSION
|
|
||||||
|
|
||||||
# Install libpq5
|
|
||||||
RUN apt-get -qq update && \
|
|
||||||
apt-get -qq install -y --no-install-recommends libpq5 && \
|
|
||||||
apt-get -qq clean && \
|
|
||||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
|
||||||
|
|
||||||
# Install postgrest
|
|
||||||
RUN BUILD_DEPS="curl ca-certificates xz-utils" && \
|
|
||||||
apt-get -qq update && \
|
|
||||||
apt-get -qq install -y --no-install-recommends $BUILD_DEPS && \
|
|
||||||
cd /tmp && \
|
|
||||||
curl -SLO https://github.com/PostgREST/postgrest/releases/download/${POSTGREST_VERSION}/postgrest-${POSTGREST_VERSION}-ubuntu.tar.xz && \
|
|
||||||
tar -xJvf postgrest-${POSTGREST_VERSION}-ubuntu.tar.xz && \
|
|
||||||
mv postgrest /usr/local/bin/postgrest && \
|
|
||||||
cd / && \
|
|
||||||
apt-get -qq purge --auto-remove -y $BUILD_DEPS && \
|
|
||||||
apt-get -qq clean && \
|
|
||||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
|
||||||
|
|
||||||
COPY postgrest.conf /etc/postgrest.conf
|
|
||||||
|
|
||||||
|
|
||||||
ENV PGRST_DB_URI= \
|
|
||||||
PGRST_DB_SCHEMA=public \
|
|
||||||
PGRST_DB_ANON_ROLE= \
|
|
||||||
PGRST_DB_POOL=100 \
|
|
||||||
PGRST_DB_EXTRA_SEARCH_PATH=public \
|
|
||||||
PGRST_SERVER_HOST=*4 \
|
|
||||||
PGRST_SERVER_PORT=3000 \
|
|
||||||
PGRST_OPENAPI_SERVER_PROXY_URI= \
|
|
||||||
PGRST_JWT_SECRET= \
|
|
||||||
PGRST_SECRET_IS_BASE64=false \
|
|
||||||
PGRST_JWT_AUD= \
|
|
||||||
PGRST_MAX_ROWS= \
|
|
||||||
PGRST_PRE_REQUEST= \
|
|
||||||
PGRST_ROLE_CLAIM_KEY=".role" \
|
|
||||||
PGRST_ROOT_SPEC= \
|
|
||||||
PGRST_RAW_MEDIA_TYPES=
|
|
||||||
|
|
||||||
RUN groupadd -g 1000 postgrest && \
|
|
||||||
useradd -r -u 1000 -g postgrest postgrest && \
|
|
||||||
chown postgrest:postgrest /etc/postgrest.conf
|
|
||||||
|
|
||||||
USER 1000
|
|
||||||
|
|
||||||
# PostgREST reads /etc/postgrest.conf so map the configuration
|
|
||||||
# file in when you run this container
|
|
||||||
CMD exec postgrest /etc/postgrest.conf
|
|
||||||
|
|
||||||
EXPOSE 3000
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
FROM centos:centos7
|
|
||||||
|
|
||||||
RUN yum -y update
|
|
||||||
RUN yum -y install perl make automake gcc gmp-devel libffi zlib zlib-devel xz tar
|
|
||||||
RUN yum -y install yum install https://download.postgresql.org/pub/repos/yum/10/redhat/rhel-7-x86_64/pgdg-centos10-10-2.noarch.rpm
|
|
||||||
RUN yum -y install postgresql10-devel
|
|
||||||
RUN yum clean all
|
|
||||||
RUN curl -sSL https://get.haskellstack.org/ | sh
|
|
||||||
|
|
||||||
ENV PATH $PATH:/usr/pgsql-10/bin
|
|
||||||
|
|
||||||
# To disable warning when building
|
|
||||||
ENV PATH $PATH:/root/.local/bin
|
|
||||||
|
|
||||||
RUN mkdir /source
|
|
||||||
WORKDIR /source
|
|
||||||
|
|
||||||
ENTRYPOINT ["stack"]
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
FROM ubuntu:16.04
|
|
||||||
|
|
||||||
## TODO pin the stack version
|
|
||||||
#
|
|
||||||
RUN BUILD_DEPS="curl ca-certificates build-essential" && \
|
|
||||||
apt-get -qq update && \
|
|
||||||
apt-get -qqy --no-install-recommends install \
|
|
||||||
$BUILD_DEPS \
|
|
||||||
libpq-dev && \
|
|
||||||
curl -sSL https://get.haskellstack.org/ | sh && \
|
|
||||||
apt-get -qq clean && \
|
|
||||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
|
||||||
|
|
||||||
# To disable warning when building
|
|
||||||
ENV PATH $PATH:/root/.local/bin
|
|
||||||
|
|
||||||
RUN mkdir /source
|
|
||||||
WORKDIR /source
|
|
||||||
|
|
||||||
ENTRYPOINT ["stack"]
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
FROM i386/ubuntu:16.04
|
|
||||||
|
|
||||||
## TODO pin the stack version
|
|
||||||
|
|
||||||
RUN BUILD_DEPS="curl ca-certificates build-essential" && \
|
|
||||||
apt-get -qq update && \
|
|
||||||
apt-get -qqy --no-install-recommends install \
|
|
||||||
$BUILD_DEPS \
|
|
||||||
libpq-dev && \
|
|
||||||
curl -sSL https://get.haskellstack.org/ | sh && \
|
|
||||||
apt-get -qq clean && \
|
|
||||||
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
|
||||||
|
|
||||||
# To disable warning when building
|
|
||||||
ENV PATH $PATH:/root/.local/bin
|
|
||||||
|
|
||||||
RUN mkdir /source
|
|
||||||
WORKDIR /source
|
|
||||||
|
|
||||||
ENTRYPOINT ["stack"]
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
stgrest:
|
|
||||||
image: pg_local
|
|
||||||
ports:
|
|
||||||
- "3000:3000"
|
|
||||||
links:
|
|
||||||
- postgres:postgres
|
|
||||||
environment:
|
|
||||||
PGRST_DB_URI: postgres://app_user:password@postgres:5432/app_db
|
|
||||||
PGRST_DB_SCHEMA: public
|
|
||||||
PGRST_DB_ANON_ROLE: app_user
|
|
||||||
|
|
||||||
postgres:
|
|
||||||
image: postgres
|
|
||||||
ports:
|
|
||||||
- "5432:5432"
|
|
||||||
environment:
|
|
||||||
POSTGRES_DB: app_db
|
|
||||||
POSTGRES_USER: app_user
|
|
||||||
POSTGRES_PASSWORD: password
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
db-uri = "$(PGRST_DB_URI)"
|
|
||||||
db-schema = "$(PGRST_DB_SCHEMA)"
|
|
||||||
db-anon-role = "$(PGRST_DB_ANON_ROLE)"
|
|
||||||
db-pool = "$(PGRST_DB_POOL)"
|
|
||||||
db-extra-search-path = "$(PGRST_DB_EXTRA_SEARCH_PATH)"
|
|
||||||
|
|
||||||
server-host = "$(PGRST_SERVER_HOST)"
|
|
||||||
server-port = "$(PGRST_SERVER_PORT)"
|
|
||||||
|
|
||||||
openapi-server-proxy-uri = "$(PGRST_OPENAPI_SERVER_PROXY_URI)"
|
|
||||||
jwt-secret = "$(PGRST_JWT_SECRET)"
|
|
||||||
secret-is-base64 = "$(PGRST_SECRET_IS_BASE64)"
|
|
||||||
jwt-aud = "$(PGRST_JWT_AUD)"
|
|
||||||
role-claim-key = "$(PGRST_ROLE_CLAIM_KEY)"
|
|
||||||
|
|
||||||
max-rows = "$(PGRST_MAX_ROWS)"
|
|
||||||
pre-request = "$(PGRST_PRE_REQUEST)"
|
|
||||||
root-spec = "$(PGRST_ROOT_SPEC)"
|
|
||||||
raw-media-types = "$(PGRST_RAW_MEDIA_TYPES)"
|
|
||||||
+26
-314
@@ -1,330 +1,42 @@
|
|||||||
{-# LANGUAGE CPP #-}
|
{-# LANGUAGE CPP #-}
|
||||||
|
|
||||||
module Main where
|
module Main (main) where
|
||||||
|
|
||||||
import qualified Data.ByteString as BS
|
|
||||||
import qualified Data.ByteString.Base64 as B64
|
|
||||||
import qualified Hasql.Pool as P
|
|
||||||
import qualified Hasql.Transaction.Sessions as HT
|
|
||||||
|
|
||||||
import Control.AutoUpdate (defaultUpdateSettings, mkAutoUpdate,
|
|
||||||
updateAction)
|
|
||||||
import Control.Retry (RetryStatus, capDelay,
|
|
||||||
exponentialBackoff, retrying,
|
|
||||||
rsPreviousDelay)
|
|
||||||
import Data.Either.Combinators (whenLeft)
|
|
||||||
import Data.IORef (IORef, atomicWriteIORef, newIORef,
|
|
||||||
readIORef)
|
|
||||||
import Data.String (IsString (..))
|
|
||||||
import Data.Text (pack, replace, strip, stripPrefix)
|
|
||||||
import Data.Text.IO (hPutStrLn)
|
|
||||||
import Data.Time.Clock (getCurrentTime)
|
|
||||||
import Network.Wai.Handler.Warp (defaultSettings, runSettings,
|
|
||||||
setHost, setPort, setServerName)
|
|
||||||
import System.IO (BufferMode (..), hSetBuffering)
|
import System.IO (BufferMode (..), hSetBuffering)
|
||||||
|
|
||||||
import PostgREST.App (postgrest)
|
import qualified PostgREST.App as App
|
||||||
import PostgREST.Config (AppConfig (..), configPoolTimeout',
|
import qualified PostgREST.CLI as CLI
|
||||||
prettyVersion, readOptions)
|
|
||||||
import PostgREST.DbStructure (getDbStructure, getPgVersion)
|
|
||||||
import PostgREST.Error (PgError (PgError), checkIsFatal,
|
|
||||||
errorPayload)
|
|
||||||
import PostgREST.OpenAPI (isMalformedProxyUri)
|
|
||||||
import PostgREST.Types (ConnectionStatus (..), DbStructure,
|
|
||||||
PgVersion (..), Schema,
|
|
||||||
minimumPgVersion)
|
|
||||||
import Protolude hiding (hPutStrLn, head, replace, toS)
|
|
||||||
import Protolude.Conv (toS)
|
|
||||||
|
|
||||||
|
import Protolude
|
||||||
|
|
||||||
#ifndef mingw32_HOST_OS
|
#ifndef mingw32_HOST_OS
|
||||||
import System.Posix.Signals
|
import qualified PostgREST.Unix as Unix
|
||||||
import UnixSocket
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
||||||
{-|
|
|
||||||
The purpose of this worker is to fill the refDbStructure created in 'main'
|
|
||||||
with the 'DbStructure' returned from calling 'getDbStructure'. This method
|
|
||||||
is meant to be called by multiple times by the same thread, but does nothing if
|
|
||||||
the previous invocation has not terminated. In all cases this method does not
|
|
||||||
halt the calling thread, the work is preformed in a separate thread.
|
|
||||||
|
|
||||||
Note: 'atomicWriteIORef' is essentially a lazy semaphore that prevents two
|
|
||||||
threads from running 'connectionWorker' at the same time.
|
|
||||||
|
|
||||||
Background thread that does the following :
|
|
||||||
1. Tries to connect to pg server and will keep trying until success.
|
|
||||||
2. Checks if the pg version is supported and if it's not it kills the main
|
|
||||||
program.
|
|
||||||
3. Obtains the dbStructure.
|
|
||||||
4. If 2 or 3 fail to give their result it means the connection is down so it
|
|
||||||
goes back to 1, otherwise it finishes his work successfully.
|
|
||||||
-}
|
|
||||||
connectionWorker
|
|
||||||
:: ThreadId -- ^ This thread is killed if pg version is unsupported
|
|
||||||
-> P.Pool -- ^ The PostgreSQL connection pool
|
|
||||||
-> [Schema] -- ^ Schemas PostgREST is serving up
|
|
||||||
-> IORef (Maybe DbStructure) -- ^ mutable reference to 'DbStructure'
|
|
||||||
-> IORef Bool -- ^ Used as a binary Semaphore
|
|
||||||
-> IO ()
|
|
||||||
connectionWorker mainTid pool schemas refDbStructure refIsWorkerOn = do
|
|
||||||
isWorkerOn <- readIORef refIsWorkerOn
|
|
||||||
unless isWorkerOn $ do
|
|
||||||
atomicWriteIORef refIsWorkerOn True
|
|
||||||
void $ forkIO work
|
|
||||||
where
|
|
||||||
work = do
|
|
||||||
atomicWriteIORef refDbStructure Nothing
|
|
||||||
putStrLn ("Attempting to connect to the database..." :: Text)
|
|
||||||
connected <- connectionStatus pool
|
|
||||||
case connected of
|
|
||||||
FatalConnectionError reason -> hPutStrLn stderr reason
|
|
||||||
>> killThread mainTid -- Fatal error when connecting
|
|
||||||
NotConnected -> return () -- Unreachable
|
|
||||||
Connected actualPgVersion -> do -- Procede with initialization
|
|
||||||
result <- P.use pool $ do
|
|
||||||
dbStructure <- HT.transaction HT.ReadCommitted HT.Read $ getDbStructure schemas actualPgVersion
|
|
||||||
liftIO $ atomicWriteIORef refDbStructure $ Just dbStructure
|
|
||||||
case result of
|
|
||||||
Left e -> do
|
|
||||||
putStrLn ("Failed to query the database. Retrying." :: Text)
|
|
||||||
hPutStrLn stderr . toS . errorPayload $ PgError False e
|
|
||||||
work
|
|
||||||
|
|
||||||
Right _ -> do
|
|
||||||
atomicWriteIORef refIsWorkerOn False
|
|
||||||
putStrLn ("Connection successful" :: Text)
|
|
||||||
|
|
||||||
{-|
|
|
||||||
Used by 'connectionWorker' to check if the provided db-uri lets
|
|
||||||
the application access the PostgreSQL database. This method is used
|
|
||||||
the first time the connection is tested, but only to test before
|
|
||||||
calling 'getDbStructure' inside the 'connectionWorker' method.
|
|
||||||
|
|
||||||
The connection tries are capped, but if the connection times out no error is
|
|
||||||
thrown, just 'False' is returned.
|
|
||||||
-}
|
|
||||||
connectionStatus :: P.Pool -> IO ConnectionStatus
|
|
||||||
connectionStatus pool =
|
|
||||||
retrying (capDelay 32000000 $ exponentialBackoff 1000000)
|
|
||||||
shouldRetry
|
|
||||||
(const $ P.release pool >> getConnectionStatus)
|
|
||||||
where
|
|
||||||
getConnectionStatus :: IO ConnectionStatus
|
|
||||||
getConnectionStatus = do
|
|
||||||
pgVersion <- P.use pool getPgVersion
|
|
||||||
case pgVersion of
|
|
||||||
Left e -> do
|
|
||||||
let err = PgError False e
|
|
||||||
hPutStrLn stderr . toS $ errorPayload err
|
|
||||||
case checkIsFatal err of
|
|
||||||
Just reason -> return $ FatalConnectionError reason
|
|
||||||
Nothing -> return NotConnected
|
|
||||||
|
|
||||||
Right version ->
|
|
||||||
if version < minimumPgVersion
|
|
||||||
then return . FatalConnectionError $ "Cannot run in this PostgreSQL version, PostgREST needs at least " <> pgvName minimumPgVersion
|
|
||||||
else return . Connected $ version
|
|
||||||
|
|
||||||
shouldRetry :: RetryStatus -> ConnectionStatus -> IO Bool
|
|
||||||
shouldRetry rs isConnSucc = do
|
|
||||||
let delay = fromMaybe 0 (rsPreviousDelay rs) `div` 1000000
|
|
||||||
itShould = NotConnected == isConnSucc
|
|
||||||
when itShould $
|
|
||||||
putStrLn $ "Attempting to reconnect to the database in " <> (show delay::Text) <> " seconds..."
|
|
||||||
return itShould
|
|
||||||
|
|
||||||
|
|
||||||
{-|
|
|
||||||
This is where everything starts.
|
|
||||||
-}
|
|
||||||
main :: IO ()
|
main :: IO ()
|
||||||
main = do
|
main = do
|
||||||
--
|
setBuffering
|
||||||
|
opts <- CLI.readCLIShowHelp
|
||||||
|
CLI.main installSignalHandlers runAppInSocket opts
|
||||||
|
|
||||||
|
installSignalHandlers :: App.SignalHandlerInstaller
|
||||||
|
#ifndef mingw32_HOST_OS
|
||||||
|
installSignalHandlers = Unix.installSignalHandlers
|
||||||
|
#else
|
||||||
|
installSignalHandlers _ = pass
|
||||||
|
#endif
|
||||||
|
|
||||||
|
runAppInSocket :: Maybe App.SocketRunner
|
||||||
|
#ifndef mingw32_HOST_OS
|
||||||
|
runAppInSocket = Just Unix.runAppWithSocket
|
||||||
|
#else
|
||||||
|
runAppInSocket = Nothing
|
||||||
|
#endif
|
||||||
|
|
||||||
|
setBuffering :: IO ()
|
||||||
|
setBuffering = do
|
||||||
-- LineBuffering: the entire output buffer is flushed whenever a newline is
|
-- LineBuffering: the entire output buffer is flushed whenever a newline is
|
||||||
-- output, the buffer overflows, a hFlush is issued or the handle is closed
|
-- output, the buffer overflows, a hFlush is issued or the handle is closed
|
||||||
--
|
|
||||||
-- NoBuffering: output is written immediately and never stored in the buffer
|
|
||||||
hSetBuffering stdout LineBuffering
|
hSetBuffering stdout LineBuffering
|
||||||
hSetBuffering stdin LineBuffering
|
hSetBuffering stdin LineBuffering
|
||||||
hSetBuffering stderr NoBuffering
|
hSetBuffering stderr LineBuffering
|
||||||
--
|
|
||||||
-- readOptions builds the 'AppConfig' from the config file specified on the
|
|
||||||
-- command line
|
|
||||||
conf <- loadDbUriFile =<< loadSecretFile =<< readOptions
|
|
||||||
let schemas = toList $ configSchemas conf
|
|
||||||
host = configHost conf
|
|
||||||
port = configPort conf
|
|
||||||
proxy = configOpenAPIProxyUri conf
|
|
||||||
maybeSocketAddr = configSocket conf
|
|
||||||
socketFileMode = configSocketMode conf
|
|
||||||
pgSettings = toS (configDatabase conf) -- is the db-uri
|
|
||||||
roleClaimKey = configRoleClaimKey conf
|
|
||||||
appSettings =
|
|
||||||
setHost ((fromString . toS) host) -- Warp settings
|
|
||||||
. setPort port
|
|
||||||
. setServerName (toS $ "postgrest/" <> prettyVersion) $
|
|
||||||
defaultSettings
|
|
||||||
|
|
||||||
|
|
||||||
whenLeft socketFileMode panic
|
|
||||||
|
|
||||||
-- Checks that the provided proxy uri is formated correctly
|
|
||||||
when (isMalformedProxyUri $ toS <$> proxy) $
|
|
||||||
panic
|
|
||||||
"Malformed proxy uri, a correct example: https://example.com:8443/basePath"
|
|
||||||
|
|
||||||
-- Checks that the provided jspath is valid
|
|
||||||
whenLeft roleClaimKey $
|
|
||||||
panic $ show roleClaimKey
|
|
||||||
|
|
||||||
-- create connection pool with the provided settings, returns either
|
|
||||||
-- a 'Connection' or a 'ConnectionError'. Does not throw.
|
|
||||||
pool <- P.acquire (configPool conf, configPoolTimeout' conf, pgSettings)
|
|
||||||
--
|
|
||||||
-- To be filled in by connectionWorker
|
|
||||||
refDbStructure <- newIORef Nothing
|
|
||||||
--
|
|
||||||
-- Helper ref to make sure just one connectionWorker can run at a time
|
|
||||||
refIsWorkerOn <- newIORef False
|
|
||||||
--
|
|
||||||
-- This is passed to the connectionWorker method so it can kill the main
|
|
||||||
-- thread if the PostgreSQL's version is not supported.
|
|
||||||
mainTid <- myThreadId
|
|
||||||
--
|
|
||||||
-- Sets the refDbStructure
|
|
||||||
connectionWorker
|
|
||||||
mainTid
|
|
||||||
pool
|
|
||||||
schemas
|
|
||||||
refDbStructure
|
|
||||||
refIsWorkerOn
|
|
||||||
--
|
|
||||||
-- Only for systems with signals:
|
|
||||||
--
|
|
||||||
-- releases the connection pool whenever the program is terminated,
|
|
||||||
-- see issue #268
|
|
||||||
--
|
|
||||||
-- Plus the SIGHUP signal updates the internal 'DbStructure' by running
|
|
||||||
-- 'connectionWorker' exactly as before.
|
|
||||||
#ifndef mingw32_HOST_OS
|
|
||||||
forM_ [sigINT, sigTERM] $ \sig ->
|
|
||||||
void $ installHandler sig (Catch $ do
|
|
||||||
P.release pool
|
|
||||||
throwTo mainTid UserInterrupt
|
|
||||||
) Nothing
|
|
||||||
|
|
||||||
void $ installHandler sigUSR1 (
|
|
||||||
Catch $ connectionWorker
|
|
||||||
mainTid
|
|
||||||
pool
|
|
||||||
schemas
|
|
||||||
refDbStructure
|
|
||||||
refIsWorkerOn
|
|
||||||
) Nothing
|
|
||||||
#endif
|
|
||||||
|
|
||||||
|
|
||||||
-- ask for the OS time at most once per second
|
|
||||||
getTime <- mkAutoUpdate defaultUpdateSettings {updateAction = getCurrentTime}
|
|
||||||
|
|
||||||
let postgrestApplication =
|
|
||||||
postgrest
|
|
||||||
conf
|
|
||||||
refDbStructure
|
|
||||||
pool
|
|
||||||
getTime
|
|
||||||
(connectionWorker
|
|
||||||
mainTid
|
|
||||||
pool
|
|
||||||
schemas
|
|
||||||
refDbStructure
|
|
||||||
refIsWorkerOn)
|
|
||||||
|
|
||||||
-- run the postgrest application with user defined socket. Only for UNIX systems.
|
|
||||||
#ifndef mingw32_HOST_OS
|
|
||||||
whenJust maybeSocketAddr $
|
|
||||||
runAppInSocket appSettings postgrestApplication socketFileMode
|
|
||||||
#endif
|
|
||||||
|
|
||||||
-- run the postgrest application
|
|
||||||
whenNothing maybeSocketAddr $ do
|
|
||||||
putStrLn $ ("Listening on port " :: Text) <> show (configPort conf)
|
|
||||||
runSettings appSettings postgrestApplication
|
|
||||||
|
|
||||||
{-|
|
|
||||||
The purpose of this function is to load the JWT secret from a file if
|
|
||||||
configJwtSecret is actually a filepath and replaces some characters if the JWT
|
|
||||||
is base64 encoded.
|
|
||||||
|
|
||||||
The reason some characters need to be replaced is because JWT is actually
|
|
||||||
base64url encoded which must be turned into just base64 before decoding.
|
|
||||||
|
|
||||||
To check if the JWT secret is provided is in fact a file path, it must be
|
|
||||||
decoded as 'Text' to be processed.
|
|
||||||
|
|
||||||
decodeUtf8: Decode a ByteString containing UTF-8 encoded text that is known to
|
|
||||||
be valid.
|
|
||||||
-}
|
|
||||||
loadSecretFile :: AppConfig -> IO AppConfig
|
|
||||||
loadSecretFile conf = extractAndTransform mSecret
|
|
||||||
where
|
|
||||||
mSecret = decodeUtf8 <$> configJwtSecret conf
|
|
||||||
isB64 = configJwtSecretIsBase64 conf
|
|
||||||
--
|
|
||||||
-- The Text (variable name secret) here is mSecret from above which is the JWT
|
|
||||||
-- decoded as Utf8
|
|
||||||
--
|
|
||||||
-- stripPrefix: Return the suffix of the second string if its prefix matches
|
|
||||||
-- the entire first string.
|
|
||||||
--
|
|
||||||
-- The configJwtSecret is a filepath instead of the JWT secret itself if the
|
|
||||||
-- secret has @ as its prefix.
|
|
||||||
extractAndTransform :: Maybe Text -> IO AppConfig
|
|
||||||
extractAndTransform Nothing = return conf
|
|
||||||
extractAndTransform (Just secret) =
|
|
||||||
fmap setSecret $
|
|
||||||
transformString isB64 =<<
|
|
||||||
case stripPrefix "@" secret of
|
|
||||||
Nothing -> return . encodeUtf8 $ secret
|
|
||||||
Just filename -> chomp <$> BS.readFile (toS filename)
|
|
||||||
where
|
|
||||||
chomp bs = fromMaybe bs (BS.stripSuffix "\n" bs)
|
|
||||||
--
|
|
||||||
-- Turns the Base64url encoded JWT into Base64
|
|
||||||
transformString :: Bool -> ByteString -> IO ByteString
|
|
||||||
transformString False t = return t
|
|
||||||
transformString True t =
|
|
||||||
case B64.decode $ encodeUtf8 $ strip $ replaceUrlChars $ decodeUtf8 t of
|
|
||||||
Left errMsg -> panic $ pack errMsg
|
|
||||||
Right bs -> return bs
|
|
||||||
setSecret bs = conf {configJwtSecret = Just bs}
|
|
||||||
--
|
|
||||||
-- replace: Replace every occurrence of one substring with another
|
|
||||||
replaceUrlChars =
|
|
||||||
replace "_" "/" . replace "-" "+" . replace "." "="
|
|
||||||
|
|
||||||
{-
|
|
||||||
Load database uri from a separate file if `db-uri` is a filepath.
|
|
||||||
-}
|
|
||||||
loadDbUriFile :: AppConfig -> IO AppConfig
|
|
||||||
loadDbUriFile conf = extractDbUri mDbUri
|
|
||||||
where
|
|
||||||
mDbUri = configDatabase conf
|
|
||||||
extractDbUri :: Text -> IO AppConfig
|
|
||||||
extractDbUri dbUri =
|
|
||||||
fmap setDbUri $
|
|
||||||
case stripPrefix "@" dbUri of
|
|
||||||
Nothing -> return dbUri
|
|
||||||
Just filename -> strip <$> readFile (toS filename)
|
|
||||||
setDbUri dbUri = conf {configDatabase = dbUri}
|
|
||||||
|
|
||||||
-- Utilitarian functions.
|
|
||||||
whenJust :: Applicative f => Maybe a -> (a -> f ()) -> f ()
|
|
||||||
whenJust (Just x) f = f x
|
|
||||||
whenJust Nothing _ = pass
|
|
||||||
|
|
||||||
whenNothing :: Applicative f => Maybe a -> f () -> f ()
|
|
||||||
whenNothing Nothing f = f
|
|
||||||
whenNothing _ _ = pass
|
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
module UnixSocket (
|
|
||||||
runAppInSocket
|
|
||||||
)where
|
|
||||||
|
|
||||||
import Network.Socket (Family (AF_UNIX),
|
|
||||||
SockAddr (SockAddrUnix), Socket,
|
|
||||||
SocketType (Stream), bind, close,
|
|
||||||
defaultProtocol, listen,
|
|
||||||
maxListenQueue, socket)
|
|
||||||
import Network.Wai (Application)
|
|
||||||
import Network.Wai.Handler.Warp
|
|
||||||
import System.Directory (removeFile)
|
|
||||||
import System.IO.Error (isDoesNotExistError)
|
|
||||||
import System.Posix.Files (setFileMode)
|
|
||||||
import System.Posix.Types (FileMode)
|
|
||||||
|
|
||||||
import Protolude
|
|
||||||
|
|
||||||
createAndBindSocket :: FilePath -> Maybe FileMode -> IO Socket
|
|
||||||
createAndBindSocket socketFilePath maybeSocketFileMode = do
|
|
||||||
deleteSocketFileIfExist socketFilePath
|
|
||||||
sock <- socket AF_UNIX Stream defaultProtocol
|
|
||||||
bind sock $ SockAddrUnix socketFilePath
|
|
||||||
mapM_ (setFileMode socketFilePath) maybeSocketFileMode
|
|
||||||
return sock
|
|
||||||
where
|
|
||||||
deleteSocketFileIfExist path = removeFile path `catch` handleDoesNotExist
|
|
||||||
handleDoesNotExist e
|
|
||||||
| isDoesNotExistError e = return ()
|
|
||||||
| otherwise = throwIO e
|
|
||||||
|
|
||||||
-- run the postgrest application with user defined socket.
|
|
||||||
runAppInSocket :: Settings -> Application -> Either Text FileMode -> FilePath -> IO ()
|
|
||||||
runAppInSocket settings app socketFileMode sockPath = do
|
|
||||||
sock <- createAndBindSocket sockPath (rightToMaybe socketFileMode)
|
|
||||||
putStrLn $ ("Listening on unix socket " :: Text) <> show sockPath
|
|
||||||
listen sock maxListenQueue
|
|
||||||
runSettingsSocket settings sock app
|
|
||||||
-- clean socket up when done
|
|
||||||
close sock
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# This Dockerfile is only used as a development environment for
|
||||||
|
# non-nix systems, i.e. Windows.
|
||||||
|
|
||||||
|
FROM nixos/nix:latest
|
||||||
|
|
||||||
|
RUN apk --no-cache add \
|
||||||
|
wget
|
||||||
|
|
||||||
|
RUN nix-env -iA cachix -f https://cachix.org/api/v1/install \
|
||||||
|
&& cachix use postgrest
|
||||||
|
|
||||||
|
# We need an unprivileged user here, to make PG run at all.
|
||||||
|
RUN adduser --disabled-password --ingroup root nix \
|
||||||
|
&& chown -R nix:root /nix
|
||||||
|
USER nix:root
|
||||||
|
ENV USER=nix
|
||||||
|
|
||||||
|
VOLUME /nix
|
||||||
|
VOLUME /postgrest
|
||||||
|
WORKDIR /postgrest
|
||||||
|
|
||||||
|
CMD nix-shell
|
||||||
+170
-35
@@ -5,28 +5,40 @@ for developing, testing and building PostgREST.
|
|||||||
|
|
||||||
## Getting started with Nix
|
## Getting started with Nix
|
||||||
|
|
||||||
You'll need to [get Nix](https://nixos.org/download.html). The installer will
|
You'll need to [get Nix](https://nixos.org/download.html). Follow the recommended installation for your operating system from the official download website.
|
||||||
create your Nix store in the `/nix/` directory, where all build artifacts and
|
|
||||||
their dependencies will be stored. It will also link the Nix executables like
|
|
||||||
`nix-env`, `nix-build` and `nix-shell` into your PATH. Nix will manage all
|
|
||||||
other PostgREST dependencies from here on out. To clean up older build
|
|
||||||
artifacts from the `/nix/store`, you can run `nix-collect-garbage`.
|
|
||||||
|
|
||||||
## Building PostgREST
|
## Building PostgREST
|
||||||
|
|
||||||
To build PostgREST from your local checkout of the repository, run:
|
To build PostgREST from your local checkout of the repository, run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
nix-build --attr postgrest
|
$ nix-build --attr postgrestPackage
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This will create a `result` directory that contains the PostgREST binary at
|
This will create a `result` directory that contains the PostgREST binary at
|
||||||
`result/bin/postgrest`. The `--attr` parameter (or short: `-A`) tells Nix to
|
`result/bin/postgrest`. The `--attr` parameter (or short: `-A`) tells Nix to
|
||||||
build the `postgrest` attribute from the Nix expression it finds in our
|
build the `postgrestPackage` attribute from the Nix expression it finds in our
|
||||||
`default.nix` (see below for details). Nix will take care of getting the right
|
`default.nix` (see below for details). Nix will take care of getting the right
|
||||||
GHC version and all the build dependencies.
|
GHC version and all the build dependencies.
|
||||||
|
|
||||||
|
## Binary cache
|
||||||
|
|
||||||
|
We recommend that you use the PostgREST binary cache on
|
||||||
|
[cachix](https://cachix.org/):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install cachix:
|
||||||
|
$ nix-env -iA cachix -f https://cachix.org/api/v1/install
|
||||||
|
|
||||||
|
# Set cachix up to use the PostgREST binary cache:
|
||||||
|
$ cachix use postgrest
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Without cachix, your machine will have to rebuild all the dependencies that are
|
||||||
|
derived on top of `Musl` for the static builds, which can take a very long time.
|
||||||
|
|
||||||
## Developing
|
## Developing
|
||||||
|
|
||||||
A development environment for PostgREST is available with `nix-shell`. The
|
A development environment for PostgREST is available with `nix-shell`. The
|
||||||
@@ -34,7 +46,7 @@ following command will put you into a new shell that has GHC and Cabal on the
|
|||||||
PATH:
|
PATH:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
nix-shell
|
$ nix-shell
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -42,32 +54,61 @@ Within `nix-shell`, you can run Cabal commands as usual. You can also run
|
|||||||
stack with the `--nix` option, which causes stack to pick up the non-Haskell
|
stack with the `--nix` option, which causes stack to pick up the non-Haskell
|
||||||
dependencies from the same pinned Nixpkgs version that the Nix builds use.
|
dependencies from the same pinned Nixpkgs version that the Nix builds use.
|
||||||
|
|
||||||
## Aside: Working with `nix-shell` and the PostgREST utility scripts
|
## Working with `nix-shell` and the PostgREST utility scripts
|
||||||
|
|
||||||
The PostgREST utilities available in `nix-shell` all have names that begin
|
The PostgREST utilities available in `nix-shell` all have names that begin with
|
||||||
with `postgrest-`, so you can use tab completion (`postgrest-<tab>`) in
|
`postgrest-`, so you can use tab completion (typing `postgrest-` and pressing
|
||||||
`nix-shell` to see all that are available:
|
`<tab>`) in `nix-shell` to see all that are available:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
# Note: The utilities listed here might not be up to date.
|
# Note: The utilities listed here might not be up to date.
|
||||||
[nix-shell:~/Projects/postgrest]$ postgrest-
|
[nix-shell]$ postgrest-<tab>
|
||||||
postgrest-lint postgrest-test-spec-postgresql-10.12
|
postgrest-build postgrest-test-spec
|
||||||
postgrest-style postgrest-test-spec-postgresql-11.7
|
postgrest-check postgrest-watch
|
||||||
postgrest-style-check postgrest-test-spec-postgresql-12.2
|
postgrest-clean postgrest-with-all
|
||||||
postgrest-test-all postgrest-test-spec-postgresql-9.4.24
|
postgrest-coverage postgrest-with-postgresql-10
|
||||||
postgrest-test-spec postgrest-test-spec-postgresql-9.5.21
|
postgrest-lint postgrest-with-postgresql-11
|
||||||
postgrest-test-spec-all postgrest-test-spec-postgresql-9.6.17
|
postgrest-run postgrest-with-postgresql-12
|
||||||
|
postgrest-style postgrest-with-postgresql-13
|
||||||
|
postgrest-style-check postgrest-with-postgresql-9.6
|
||||||
|
postgrest-test-io
|
||||||
|
...
|
||||||
|
|
||||||
[nix-shell:~/Projects/postgrest]$
|
[nix-shell]$
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Some additional modules like `memory`, `docker` and `release`
|
||||||
|
have large dependencies that would need to be built before the shell becomes
|
||||||
|
available, which could take an especially long time if the cachix binary cache
|
||||||
|
is not used. You can activate those by passing a flag to `nix-shell` with
|
||||||
|
`nix-shell --arg <module> true`. This will make the respective utilites available:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ nix-shell --arg memory true
|
||||||
|
[nix-shell]$ postgrest-<tab>
|
||||||
|
postgrest-build postgrest-test-spec
|
||||||
|
postgrest-check postgrest-watch
|
||||||
|
postgrest-clean postgrest-with-all
|
||||||
|
postgrest-coverage postgrest-with-postgresql-10
|
||||||
|
postgrest-lint postgrest-with-postgresql-11
|
||||||
|
postgrest-run postgrest-with-postgresql-12
|
||||||
|
postgrest-style postgrest-with-postgresql-13
|
||||||
|
postgrest-style-check postgrest-with-postgresql-9.6
|
||||||
|
postgrest-test-io
|
||||||
|
postgrest-test-memory
|
||||||
|
...
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that `postgrest-test-memory` is now also available.
|
||||||
|
|
||||||
To run one-off commands, you can also use `nix-shell --run <command>`, which
|
To run one-off commands, you can also use `nix-shell --run <command>`, which
|
||||||
will lauch the Nix shell, run that one command and exit. Note that the tab
|
will lauch the Nix shell, run that one command and exit. Note that the tab
|
||||||
completion will not work with `nix-shell --run`, as Nix has yet to evaluate
|
completion will not work with `nix-shell --run`, as Nix has yet to evaluate
|
||||||
our Nix expressions to see which utilities are available.
|
our Nix expressions to see which utilities are available.
|
||||||
|
|
||||||
```
|
```bash
|
||||||
$ nix-shell --run postgrest-style
|
$ nix-shell --run postgrest-style
|
||||||
|
|
||||||
# Note that you need to quote any arguments that you would like to pass to
|
# Note that you need to quote any arguments that you would like to pass to
|
||||||
@@ -78,25 +119,33 @@ $ nix-shell --run "postgrest-foo --bar"
|
|||||||
|
|
||||||
A third option is to install utilities that you use very often locally:
|
A third option is to install utilities that you use very often locally:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
$ nix-env -f default.nix -iA style
|
$ nix-env -f default.nix -iA devTools
|
||||||
|
|
||||||
# `postgrest-style` can now be run directly:
|
# `postgrest-style` can now be run directly:
|
||||||
$ postgrest-style
|
$ postgrest-style
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that this does not yet work for all utilities (e.g. `postgrest-test-spec` currently
|
If you use `nix-shell` very often, you might like to use
|
||||||
needs to be run within the Nix shell environment).
|
https://github.com/xzfc/cached-nix-shell, which skips evaluating all our Nix
|
||||||
|
expressions if nothing changed, reducing startup time for the shell
|
||||||
|
considerably.
|
||||||
|
|
||||||
If you use `nix-shell` very often, you might like to use https://github.com/xzfc/cached-nix-shell,
|
Note: Once inside nix-shell, the utilities work from any directory inside
|
||||||
which skips evaluating all our Nix expressions if nothing changed, reducing startup time for the
|
the PostgREST repo. Paths are resolved relative to the repo root:
|
||||||
shell considerably.
|
|
||||||
|
```bash
|
||||||
|
[nix-shell]$ cd src
|
||||||
|
# Even though the current directory is ./src, the config path must still start
|
||||||
|
# from the repo root:
|
||||||
|
[nix-shell]$ postgrest-run test/io/configs/simple.conf
|
||||||
|
```
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
In nix-shell, you'll find utility scripts that make it very easy to run the
|
In nix-shell, you'll find utility scripts that make it very easy to run our
|
||||||
Haskell test suite, including setting up all required dependencies and
|
test suite, including setting up all required dependencies and
|
||||||
temporary test databases:
|
temporary test databases:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -104,14 +153,76 @@ temporary test databases:
|
|||||||
$ nix-shell --run postgrest-test-spec
|
$ nix-shell --run postgrest-test-spec
|
||||||
|
|
||||||
# Run the tests against all supported versions of PostgreSQL:
|
# Run the tests against all supported versions of PostgreSQL:
|
||||||
$ nix-shell --run postgrest-test-spec-all
|
$ nix-shell --run "postgrest-with-all postgrest-test-spec"
|
||||||
|
|
||||||
# Run the tests against a specific version of PostgreSQL (use tab-completion in
|
# Run the tests against a specific version of PostgreSQL (use tab-completion in
|
||||||
# nix-shell to see all available versions):
|
# nix-shell to see all available versions):
|
||||||
$ nix-shell --run postgrest-test-spec-postgresql-9.5.21
|
$ nix-shell --run "postgrest-with-postgresql-13 postgrest-test-spec"
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The io-test that test PostgREST as a black box with inputs and outputs can be
|
||||||
|
run with `postgrest-test-io`. The test runner under the hood is
|
||||||
|
[pytest](https://docs.pytest.org/) and you can pass it the usual options:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Filter the tests to run by name, including all that contain 'config':
|
||||||
|
[nix-shell]$ postgrest-test-io -k config
|
||||||
|
|
||||||
|
# Run tests in parallel using xdist, specifying the number of processes:
|
||||||
|
[nix-shell]$ postgrest-test-io -n auto
|
||||||
|
[nix-shell]$ postgrest-test-io -n 8
|
||||||
|
```
|
||||||
|
|
||||||
|
The memory tests check that we don't surpass a memory threshold for big request bodies.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build the dependencies needed for the memory test
|
||||||
|
$ nix-shell --arg memory true
|
||||||
|
|
||||||
|
# Run the memory test
|
||||||
|
[nix-shell]$ postgrest-test-memory
|
||||||
|
```
|
||||||
|
|
||||||
|
The loadtests ensure that performance doesn't drop on a change. Underlyingly they use
|
||||||
|
[vegeta](https://github.com/tsenart/vegeta).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run the loadtests on the latest commit(HEAD)
|
||||||
|
[nix-shell]$ postgrest-loadtest
|
||||||
|
|
||||||
|
# You can loadtest comparing to a different branch
|
||||||
|
[nix-shell]$ postgrest-loadtest-against master
|
||||||
|
|
||||||
|
# You can simulate latency client/postgrest and postgrest/database
|
||||||
|
[nix-shell]$ PGRST_DELAY=5ms PGDELAY=5ms postgrest-loadtest
|
||||||
|
|
||||||
|
# You can build postgrest directly with cabal for faster iteration
|
||||||
|
[nix-shell]$ PGRST_BUILD_CABAL=1 postgrest-loadtest
|
||||||
|
|
||||||
|
# Produce a markdown report to be used on CI
|
||||||
|
[nix-shell]$ postgrest-loadtest-report
|
||||||
|
```
|
||||||
|
|
||||||
|
doctests for some of our modules are also available:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
[nix-shell]$ postgrest-test-doctest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code coverage
|
||||||
|
|
||||||
|
Code coverage is available under the `postgrest-coverage` command. This will produce a `./coverage` directory that can be visualized with a simple http server.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Will run all the tests and produce a coverage dir
|
||||||
|
[nix-shell]$ postgrest-coverage
|
||||||
|
|
||||||
|
# Visualize the output
|
||||||
|
[nix-shell]$ cd coverage
|
||||||
|
[nix-shell]$ python -mSimpleHTTPServer 8080
|
||||||
|
```
|
||||||
|
|
||||||
## Linting and styling code
|
## Linting and styling code
|
||||||
|
|
||||||
The nix-shell also contains scripts for linting and styling the PostgREST
|
The nix-shell also contains scripts for linting and styling the PostgREST
|
||||||
@@ -126,6 +237,25 @@ $ nix-shell --run postgrest-style
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
There is also `postgrest-style-check` that exits with a non-zero exit code if
|
||||||
|
the check resulted in any uncommited changes. It's mostly useful for CI.
|
||||||
|
|
||||||
|
## General development tools
|
||||||
|
|
||||||
|
Tools like `postgrest-build`, `postgrest-run` etc. are simple wrappers around
|
||||||
|
`cabal` and should do what you expect. `postgrest-check` runs most checks that will
|
||||||
|
also run in CI, with the exception of the IO and Memory checks that need to be run
|
||||||
|
separately.
|
||||||
|
|
||||||
|
`postgrest-with-postgresql-*` take a command as an argument and will run it
|
||||||
|
with a temporary database. `postgrest-with-all` will run the command against
|
||||||
|
all supported PostgreSQL versions. Tests run without `postgrest-with-*` are
|
||||||
|
run against the latest PostgreSQL version by default.
|
||||||
|
|
||||||
|
`postgrest-watch` takes a command as an argument that it will re-run if any source
|
||||||
|
file is changed. For example, `postgrest-watch postgrest-with-all postgrest-test-spec`
|
||||||
|
will re-run the full spec test suite against all PostgreSQL versions on every change.
|
||||||
|
|
||||||
## Tour
|
## Tour
|
||||||
|
|
||||||
The following is not required for working on PostgREST with Nix, but it will
|
The following is not required for working on PostgREST with Nix, but it will
|
||||||
@@ -133,7 +263,7 @@ give you some more background and details on how it works.
|
|||||||
|
|
||||||
### `default.nix`
|
### `default.nix`
|
||||||
|
|
||||||
[`default.nix`](../default.nix) is our 'respository expression' that pulls all
|
[`default.nix`](../default.nix) is our 'repository expression' that pulls all
|
||||||
the pieces that we define with Nix together. It returns a set (like a dict in
|
the pieces that we define with Nix together. It returns a set (like a dict in
|
||||||
other programming languages), where each attribute is a derivation that Nix
|
other programming languages), where each attribute is a derivation that Nix
|
||||||
knows how to build, like the `postgrest` attribute from earlier.
|
knows how to build, like the `postgrest` attribute from earlier.
|
||||||
@@ -161,3 +291,8 @@ attribute with useful utilities that will be put on the PATH in `nix-shell`.
|
|||||||
|
|
||||||
Our overlays to the Nix package set are defined here. They allow us to tweak our
|
Our overlays to the Nix package set are defined here. They allow us to tweak our
|
||||||
`pkgs` in `default.nix` by adding new packages or overriding existing ones.
|
`pkgs` in `default.nix` by adding new packages or overriding existing ones.
|
||||||
|
|
||||||
|
## Upgrading dependencies
|
||||||
|
|
||||||
|
See the [upgrading checklist](UPGRADE.md) for how to upgrade the PostgREST
|
||||||
|
dependencies.
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# Checklist for upgrading Nix dependencies
|
||||||
|
|
||||||
|
The Nix dependencies of PostgREST should be updated regularly, in most cases it
|
||||||
|
should be a very simple operation.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Update pinned version of Nixpkgs
|
||||||
|
nix-shell --run postgrest-nixpkgs-upgrade
|
||||||
|
|
||||||
|
# Verify that everything builds
|
||||||
|
nix-build
|
||||||
|
```
|
||||||
|
|
||||||
|
The following checklist guides you through the complete process in more detail.
|
||||||
|
|
||||||
|
## Upgrade the pinned version of `nixpkgs`
|
||||||
|
|
||||||
|
The pinned version of [`nixpkgs`](https://github.com/NixOS/nixpkgs) is defined
|
||||||
|
in [`nix/nixpkgs-version.nix`](nixpkgs-version.nix). The pin refers directly to
|
||||||
|
a GitHub tarball for the given revision, which is more efficient than pulling
|
||||||
|
the complete Git repository. To upgrade it to the current `main` of
|
||||||
|
`nixpkgs`, you can use a small utility script defined in
|
||||||
|
[`nix/nixpkgs-update.nix`](nixpkgs-update.nix):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# From the root of the repository, enter nix-shell
|
||||||
|
nix-shell
|
||||||
|
|
||||||
|
# Run the utility script to pin the latest revision in main
|
||||||
|
postgrest-nixpkgs-upgrade
|
||||||
|
|
||||||
|
# Exit the nix-shell with Ctrl-d
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## Update pinned version of `static-haskell-nix`
|
||||||
|
|
||||||
|
We pin [`static-haskell-nix`](https://github.com/nh2/static-haskell-nix) in
|
||||||
|
[`nix/static-haskell-package.nix`](static-haskell-package.nix). Upgrade the
|
||||||
|
pinned revision and the tarball hash if necessary. See
|
||||||
|
[`nix/nixpkgs-upgrade.nix`](nixpkgs-upgrade.nix) for how to get the correct
|
||||||
|
tarball hash, or just change the hash to an arbitrary value of correct length,
|
||||||
|
run `nix-build` and use the expected value from the resulting error message.
|
||||||
|
|
||||||
|
## Review overlays
|
||||||
|
|
||||||
|
Check whether the individual [overlays](overlays) are still required.
|
||||||
|
|
||||||
|
## Check if patches are still required and update them as needed
|
||||||
|
|
||||||
|
We track a number of PostgREST-specific patches in [`nix/patches`](patches).
|
||||||
|
Check whether the pull-requests/issues linked in the
|
||||||
|
[`default.nix`](patches/default.nix) have progressed and remove/modify the
|
||||||
|
patches if they did. If conflicting changes occurred, you might have to rebase
|
||||||
|
the respective patches.
|
||||||
|
|
||||||
|
## Build everything
|
||||||
|
|
||||||
|
Using the PostgREST binary Nix cache is recommended. Install
|
||||||
|
[Cachix](https://cachix.org/) and run `cachix use postgrest`.
|
||||||
|
|
||||||
|
Run `nix-build` in the root directory of the project to build all PostgREST
|
||||||
|
artifacts. This might take a long time, e.g. when our static GHC version needs
|
||||||
|
to be rebuilt due to changes to some underlying package. If there are any
|
||||||
|
errors, this is probably due to one of our patches. Try to fix them and re-run
|
||||||
|
`nix-build` until everything builds.
|
||||||
|
|
||||||
|
## Update the PostgREST binary cache
|
||||||
|
|
||||||
|
If you have access to the PostgREST cachix signing key, you can push the
|
||||||
|
artifacts that you built locally to the binary cache. This will accelerate the
|
||||||
|
CI builds and tests, sometimes dramatically. This might sometimes even be
|
||||||
|
required to avoid build timeouts in CI.
|
||||||
|
|
||||||
|
You'll need to set the `CACHIX_SIGNING_KEY` before proceeding, e.g. by creating
|
||||||
|
a file containing `export CACHIX_SIGNING_KEY=...` and sourcing that file, which
|
||||||
|
avoids having the secret in you shell history.
|
||||||
|
|
||||||
|
To push all new artifacts to Cachix, run:
|
||||||
|
|
||||||
|
```
|
||||||
|
nix-store -qR --include-outputs $$(nix-instantiate) | cachix push postgrest
|
||||||
|
|
||||||
|
# Or, equivalently
|
||||||
|
nix-shell --run postgrest-push-cachix
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
The `nix-store` command will query the nix-store to list all dependencies and
|
||||||
|
build artifacts of PostgREST. The `cachix` command will efficiently push
|
||||||
|
everything that is not yet cached to the binary cache.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
nix:
|
||||||
|
container_name: postgrest-nix
|
||||||
|
build: .
|
||||||
|
volumes:
|
||||||
|
- ../:/postgrest
|
||||||
|
- nix:/nix
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
nix:
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
{ postgrest, dockerTools, writeShellScriptBin }:
|
|
||||||
let
|
|
||||||
image =
|
|
||||||
tag:
|
|
||||||
dockerTools.buildImage {
|
|
||||||
inherit tag;
|
|
||||||
|
|
||||||
name = "postgrest/postgrest";
|
|
||||||
contents = postgrest;
|
|
||||||
|
|
||||||
# Set the current time as the image creation date. This makes the build
|
|
||||||
# non-reproducible, but that should not be an issue for us.
|
|
||||||
created = "now";
|
|
||||||
|
|
||||||
extraCommands =
|
|
||||||
''
|
|
||||||
mkdir etc
|
|
||||||
cp ${./postgrest.conf} etc/postgrest.conf
|
|
||||||
'';
|
|
||||||
|
|
||||||
config = {
|
|
||||||
Cmd = [ "/bin/postgrest" "/etc/postgrest.conf" ];
|
|
||||||
Env = [
|
|
||||||
"PGRST_DB_URI=postgresql://?user=postgres"
|
|
||||||
"PGRST_DB_SCHEMA=public"
|
|
||||||
"PGRST_DB_ANON_ROLE="
|
|
||||||
"PGRST_DB_POOL=100"
|
|
||||||
"PGRST_DB_EXTRA_SEARCH_PATH=public"
|
|
||||||
"PGRST_SERVER_HOST=*4"
|
|
||||||
"PGRST_SERVER_PORT=3000"
|
|
||||||
"PGRST_OPENAPI_SERVER_PROXY_URI="
|
|
||||||
"PGRST_JWT_SECRET="
|
|
||||||
"PGRST_SECRET_IS_BASE64=false"
|
|
||||||
"PGRST_JWT_AUD="
|
|
||||||
"PGRST_MAX_ROWS="
|
|
||||||
"PGRST_PRE_REQUEST="
|
|
||||||
"PGRST_ROLE_CLAIM_KEY=.role"
|
|
||||||
"PGRST_ROOT_SPEC="
|
|
||||||
"PGRST_RAW_MEDIA_TYPES="
|
|
||||||
];
|
|
||||||
User = "1000";
|
|
||||||
ExposedPorts = {
|
|
||||||
"3000/tcp" = {};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
in
|
|
||||||
rec {
|
|
||||||
imageLatest =
|
|
||||||
image "latest";
|
|
||||||
|
|
||||||
imageWithVersion =
|
|
||||||
image "v${postgrest.version}";
|
|
||||||
|
|
||||||
load =
|
|
||||||
writeShellScriptBin "postgrest-docker-load"
|
|
||||||
''
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
docker load -i ${imageLatest}
|
|
||||||
docker load -i ${imageWithVersion}
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
db-uri = "$(PGRST_DB_URI)"
|
|
||||||
db-schema = "$(PGRST_DB_SCHEMA)"
|
|
||||||
db-anon-role = "$(PGRST_DB_ANON_ROLE)"
|
|
||||||
db-pool = "$(PGRST_DB_POOL)"
|
|
||||||
db-extra-search-path = "$(PGRST_DB_EXTRA_SEARCH_PATH)"
|
|
||||||
|
|
||||||
server-host = "$(PGRST_SERVER_HOST)"
|
|
||||||
server-port = "$(PGRST_SERVER_PORT)"
|
|
||||||
|
|
||||||
openapi-server-proxy-uri = "$(PGRST_OPENAPI_SERVER_PROXY_URI)"
|
|
||||||
jwt-secret = "$(PGRST_JWT_SECRET)"
|
|
||||||
secret-is-base64 = "$(PGRST_SECRET_IS_BASE64)"
|
|
||||||
jwt-aud = "$(PGRST_JWT_AUD)"
|
|
||||||
role-claim-key = "$(PGRST_ROLE_CLAIM_KEY)"
|
|
||||||
|
|
||||||
max-rows = "$(PGRST_MAX_ROWS)"
|
|
||||||
pre-request = "$(PGRST_PRE_REQUEST)"
|
|
||||||
root-spec = "$(PGRST_ROOT_SPEC)"
|
|
||||||
raw-media-types = "$(PGRST_RAW_MEDIA_TYPES)"
|
|
||||||
@@ -0,0 +1,361 @@
|
|||||||
|
{-# LANGUAGE DeriveAnyClass #-}
|
||||||
|
{-# LANGUAGE DeriveGeneric #-}
|
||||||
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
|
{-# LANGUAGE TupleSections #-}
|
||||||
|
{-# LANGUAGE TypeFamilies #-}
|
||||||
|
|
||||||
|
-- | Haskell Imports and Exports tool
|
||||||
|
--
|
||||||
|
-- This tool parses imports and exports from Haskell source files and provides
|
||||||
|
-- analysis on these imports. For example, you can check whether consistent
|
||||||
|
-- import aliases are used across your codebase.
|
||||||
|
|
||||||
|
module Main (main) where
|
||||||
|
|
||||||
|
import qualified Data.Aeson as JSON
|
||||||
|
import qualified Data.ByteString.Lazy.Char8 as LBS8
|
||||||
|
import qualified Data.Csv as Csv
|
||||||
|
import qualified Data.List as List
|
||||||
|
import qualified Data.Map as Map
|
||||||
|
import qualified Data.Set as Set
|
||||||
|
import qualified Data.Text as T
|
||||||
|
import qualified Data.Text.IO as T
|
||||||
|
import qualified Dot
|
||||||
|
import qualified GHC
|
||||||
|
import qualified GHC.Paths
|
||||||
|
import qualified Language.Haskell.GHC.ExactPrint.Parsers as ExactPrint
|
||||||
|
import qualified Options.Applicative as O
|
||||||
|
import qualified System.FilePath as FP
|
||||||
|
|
||||||
|
import Data.Aeson.Encode.Pretty (encodePretty)
|
||||||
|
import Data.Function ((&))
|
||||||
|
import Data.List (intercalate)
|
||||||
|
import Data.Maybe (catMaybes, mapMaybe)
|
||||||
|
import Data.Text (Text)
|
||||||
|
import GHC.Data.Bag (bagToList)
|
||||||
|
import GHC.Generics (Generic)
|
||||||
|
import GHC.Hs.Extension (GhcPs)
|
||||||
|
import GHC.Types.Name.Occurrence (occNameString)
|
||||||
|
import GHC.Types.Name.Reader (rdrNameOcc)
|
||||||
|
import GHC.Unit.Module.Name (moduleNameString)
|
||||||
|
import System.Directory.Recursive (getFilesRecursive)
|
||||||
|
import System.Exit (exitFailure)
|
||||||
|
|
||||||
|
-- TYPES
|
||||||
|
|
||||||
|
data Options =
|
||||||
|
Options
|
||||||
|
{ command :: Command
|
||||||
|
, sources :: [FilePath]
|
||||||
|
}
|
||||||
|
|
||||||
|
data Command
|
||||||
|
= Dump OutputFormat
|
||||||
|
| GraphSymbols
|
||||||
|
| GraphModules
|
||||||
|
| CheckAliases
|
||||||
|
| CheckWildcards [Text]
|
||||||
|
|
||||||
|
data OutputFormat = OutputCsv | OutputJson
|
||||||
|
|
||||||
|
data ImportedSymbol =
|
||||||
|
ImportedSymbol
|
||||||
|
{ impFromModule :: Text
|
||||||
|
, impModule :: Text
|
||||||
|
, impQualified :: ImportQualified
|
||||||
|
, impAlias :: Maybe Text
|
||||||
|
, impType :: ImportType
|
||||||
|
, impSymbol :: Maybe Text
|
||||||
|
, impInternal :: ModuleInternal
|
||||||
|
, impSource :: FilePath
|
||||||
|
, impFile :: FilePath
|
||||||
|
}
|
||||||
|
deriving (Generic, Csv.ToNamedRecord, Csv.DefaultOrdered, JSON.ToJSON)
|
||||||
|
|
||||||
|
data ImportQualified
|
||||||
|
= Qualified
|
||||||
|
| NotQualified
|
||||||
|
deriving (Eq, Generic, JSON.ToJSON)
|
||||||
|
|
||||||
|
instance Csv.ToField ImportQualified where
|
||||||
|
toField Qualified = "qualified"
|
||||||
|
toField NotQualified = "not qualified"
|
||||||
|
|
||||||
|
data ModuleInternal
|
||||||
|
= Internal
|
||||||
|
| External
|
||||||
|
deriving (Eq, Generic, JSON.ToJSON)
|
||||||
|
|
||||||
|
instance Csv.ToField ModuleInternal where
|
||||||
|
toField Internal = "internal"
|
||||||
|
toField External = "external"
|
||||||
|
|
||||||
|
data ImportType
|
||||||
|
= Wildcard
|
||||||
|
| Hiding
|
||||||
|
| Explicit
|
||||||
|
deriving (Eq, Generic, JSON.ToJSON)
|
||||||
|
|
||||||
|
instance Csv.ToField ImportType where
|
||||||
|
toField Wildcard = "wildcard"
|
||||||
|
toField Hiding = "hiding"
|
||||||
|
toField Explicit = "explicit"
|
||||||
|
|
||||||
|
-- | Mapping of modules to their aliases and to the files they are found in
|
||||||
|
type ModuleAliases = [(Text, [(Text, [FilePath])])]
|
||||||
|
|
||||||
|
-- | Mapping of modules to files
|
||||||
|
type WildcardImports = [(FilePath, [Text])]
|
||||||
|
|
||||||
|
|
||||||
|
-- MAIN
|
||||||
|
|
||||||
|
main :: IO ()
|
||||||
|
main =
|
||||||
|
run =<< O.customExecParser prefs infoOpts
|
||||||
|
where
|
||||||
|
prefs = O.prefs $ O.subparserInline <> O.showHelpOnEmpty
|
||||||
|
infoOpts =
|
||||||
|
O.info (O.helper <*> opts) $
|
||||||
|
O.fullDesc
|
||||||
|
<> O.header "hsie - Swiss army knife for HaSkell Imports and Exports"
|
||||||
|
<> O.progDesc "Parse Haskell code to analyze imports and exports"
|
||||||
|
opts =
|
||||||
|
Options <$> commandOption <*> O.some srcOption
|
||||||
|
srcOption =
|
||||||
|
O.argument O.str $
|
||||||
|
O.metavar "SRCDIR"
|
||||||
|
<> O.help "Haskell source directory"
|
||||||
|
<> O.action "directory"
|
||||||
|
commandOption =
|
||||||
|
O.subparser $
|
||||||
|
command "dump-imports" "Dump imported symbols as CSV or JSON"
|
||||||
|
(Dump <$> jsonOutputFlag)
|
||||||
|
<> command "graph-modules" "Print dot graph of module imports"
|
||||||
|
(pure GraphModules)
|
||||||
|
<> command "graph-symbols" "Print dot graph of symbol imports"
|
||||||
|
(pure GraphSymbols)
|
||||||
|
<> command "check-aliases"
|
||||||
|
"Check that aliases of imported modules are consistent"
|
||||||
|
(pure CheckAliases)
|
||||||
|
<> command "check-wildcards"
|
||||||
|
"Check that no modules are imported as unqualified wildcards"
|
||||||
|
(CheckWildcards <$> O.many okModuleOption)
|
||||||
|
command name desc options =
|
||||||
|
O.command name . O.info (O.helper <*> options) $ O.progDesc desc
|
||||||
|
jsonOutputFlag =
|
||||||
|
O.flag OutputCsv OutputJson $
|
||||||
|
O.long "json" <> O.short 'j' <> O.help "Output JSON"
|
||||||
|
okModuleOption =
|
||||||
|
O.strOption $
|
||||||
|
O.long "ok"
|
||||||
|
<> O.short 'o'
|
||||||
|
<> O.metavar "OKMODULE"
|
||||||
|
<> O.help "Module that is ok to import as unqualified wildcard"
|
||||||
|
|
||||||
|
run :: Options -> IO ()
|
||||||
|
run Options{command, sources} =
|
||||||
|
runCommand command . markInternal . concat =<< mapM sourceSymbols sources
|
||||||
|
where
|
||||||
|
runCommand :: Command -> [ImportedSymbol] -> IO ()
|
||||||
|
runCommand (Dump format) = LBS8.putStr . dump format
|
||||||
|
runCommand GraphSymbols = T.putStr . symbolsGraph
|
||||||
|
runCommand GraphModules = T.putStr . Dot.encode . modulesGraph
|
||||||
|
runCommand CheckAliases = runInconsistentAliases . inconsistentAliases
|
||||||
|
runCommand (CheckWildcards okModules) = runWildcards . wildcards okModules
|
||||||
|
|
||||||
|
runInconsistentAliases :: ModuleAliases -> IO ()
|
||||||
|
runInconsistentAliases [] = T.putStrLn "No inconsistent module aliases found."
|
||||||
|
runInconsistentAliases xs = T.putStr (formatInconsistentAliases xs) >> exitFailure
|
||||||
|
|
||||||
|
runWildcards :: WildcardImports -> IO ()
|
||||||
|
runWildcards [] = T.putStrLn "No unwanted wildcard imports found."
|
||||||
|
runWildcards xs = T.putStr (formatWildcards xs) >> exitFailure
|
||||||
|
|
||||||
|
-- | Mark imports from modules that are among the analyzed ones as internal.
|
||||||
|
markInternal :: [ImportedSymbol] -> [ImportedSymbol]
|
||||||
|
markInternal symbols =
|
||||||
|
fmap mark symbols
|
||||||
|
where
|
||||||
|
mark s = s { impInternal = if isInternal s then Internal else External }
|
||||||
|
isInternal = flip Set.member internalModules . impModule
|
||||||
|
internalModules = Set.fromList $ fmap impFromModule symbols
|
||||||
|
|
||||||
|
|
||||||
|
-- SYMBOLS
|
||||||
|
|
||||||
|
-- | Parse all imported symbols from a source of Haskell source files
|
||||||
|
sourceSymbols :: FilePath -> IO [ImportedSymbol]
|
||||||
|
sourceSymbols source = do
|
||||||
|
files <- filterExts [".hs", ".imports"] <$> getFilesRecursive source
|
||||||
|
concat <$> mapM moduleSymbols files
|
||||||
|
where
|
||||||
|
filterExts exts = filter $ flip elem exts . FP.takeExtension
|
||||||
|
moduleSymbols filepath = do
|
||||||
|
GHC.HsModule{..} <- parseModule filepath
|
||||||
|
return $ concatMap (importSymbols source filepath . GHC.unLoc) hsmodImports
|
||||||
|
|
||||||
|
-- | Parse a Haskell module
|
||||||
|
parseModule :: FilePath -> IO GHC.HsModule
|
||||||
|
parseModule filepath = do
|
||||||
|
result <- ExactPrint.parseModule GHC.Paths.libdir filepath
|
||||||
|
case result of
|
||||||
|
Right hsmod ->
|
||||||
|
return $ GHC.unLoc hsmod
|
||||||
|
Left errs ->
|
||||||
|
fail $ "Errors with " <> show filepath <> ":\n "
|
||||||
|
<> List.intercalate "\n " (show <$> bagToList errs)
|
||||||
|
|
||||||
|
-- | Symbols imported in an import declaration.
|
||||||
|
--
|
||||||
|
-- If the import is a wildcard, i.e. no symbols are selected for import, then
|
||||||
|
-- only one item is returned.
|
||||||
|
importSymbols :: FilePath -> FilePath -> GHC.ImportDecl GhcPs -> [ImportedSymbol]
|
||||||
|
importSymbols source filepath GHC.ImportDecl{..} =
|
||||||
|
case ideclHiding of
|
||||||
|
Just (hiding, syms) ->
|
||||||
|
symbol (if hiding then Hiding else Explicit) . Just . GHC.unLoc <$> GHC.unLoc syms
|
||||||
|
Nothing ->
|
||||||
|
[ symbol Wildcard Nothing ]
|
||||||
|
where
|
||||||
|
symbol hiding sym =
|
||||||
|
ImportedSymbol
|
||||||
|
{ impFile = relativePath filepath
|
||||||
|
, impSource = source
|
||||||
|
, impFromModule = T.pack $ moduleFromPath filepath
|
||||||
|
, impModule = T.pack . moduleNameString . GHC.unLoc $ ideclName
|
||||||
|
, impQualified = if ideclQualified /= GHC.NotQualified then Qualified else NotQualified
|
||||||
|
, impAlias = T.pack . moduleNameString . GHC.unLoc <$> ideclAs
|
||||||
|
, impInternal = External
|
||||||
|
, impType = hiding
|
||||||
|
, impSymbol = T.pack . occNameString . rdrNameOcc . GHC.ieName <$> sym
|
||||||
|
}
|
||||||
|
moduleFromPath =
|
||||||
|
intercalate "." . FP.splitDirectories . FP.dropExtension . relativePath
|
||||||
|
relativePath = FP.makeRelative source
|
||||||
|
|
||||||
|
|
||||||
|
-- DUMP
|
||||||
|
|
||||||
|
-- | Dump list of symbols as CSV or JSON
|
||||||
|
dump :: OutputFormat -> [ImportedSymbol] -> LBS8.ByteString
|
||||||
|
dump OutputCsv = Csv.encodeDefaultOrderedByName
|
||||||
|
dump OutputJson = encodePretty
|
||||||
|
|
||||||
|
|
||||||
|
-- ALIASES
|
||||||
|
|
||||||
|
-- | Find modules that are imported under different aliases
|
||||||
|
inconsistentAliases :: [ImportedSymbol] -> ModuleAliases
|
||||||
|
inconsistentAliases symbols =
|
||||||
|
fmap moduleAlias symbols
|
||||||
|
& foldr insertSetMapMap Map.empty
|
||||||
|
& Map.map (aliases . Map.toList)
|
||||||
|
& Map.filter ((<) 1 . length)
|
||||||
|
& Map.toList
|
||||||
|
where
|
||||||
|
moduleAlias ImportedSymbol{..} =
|
||||||
|
(impModule, impAlias, FP.joinPath [impSource, impFile])
|
||||||
|
insertSetMapMap (k1, k2, v) =
|
||||||
|
Map.insertWith (Map.unionWith Set.union) k1
|
||||||
|
(Map.singleton k2 $ Set.singleton v)
|
||||||
|
aliases :: [(Maybe Text, Set.Set FilePath)] -> [(Text, [FilePath])]
|
||||||
|
aliases = mapMaybe (\(k, v) -> fmap (, Set.toList v) k)
|
||||||
|
|
||||||
|
formatInconsistentAliases :: ModuleAliases -> Text
|
||||||
|
formatInconsistentAliases modules =
|
||||||
|
"The following imports have inconsistent aliases:\n\n"
|
||||||
|
<> T.concat (fmap formatModule modules)
|
||||||
|
where
|
||||||
|
formatModule (modName, aliases) =
|
||||||
|
"Module '"
|
||||||
|
<> modName
|
||||||
|
<> "' has the aliases:\n"
|
||||||
|
<> T.concat (fmap formatAlias aliases)
|
||||||
|
<> "\n"
|
||||||
|
formatAlias (alias, sourceFiles) =
|
||||||
|
" '"
|
||||||
|
<> alias
|
||||||
|
<> "' in file"
|
||||||
|
<> (if length sourceFiles > 2 then "s" else "")
|
||||||
|
<> ":\n"
|
||||||
|
<> T.concat (fmap formatFile sourceFiles)
|
||||||
|
formatFile sourceFile =
|
||||||
|
" " <> T.pack sourceFile <> "\n"
|
||||||
|
|
||||||
|
|
||||||
|
-- WILDCARDS
|
||||||
|
|
||||||
|
-- | Find modules that are imported as wildcards, excluding whitelisted modules.
|
||||||
|
--
|
||||||
|
-- Wildcard imports are ones that are not qualified and do not specify which
|
||||||
|
-- symbols should be imported.
|
||||||
|
wildcards :: [Text] -> [ImportedSymbol] -> WildcardImports
|
||||||
|
wildcards okModules =
|
||||||
|
groupByFile . filter isWildcard . filter (not . isOkModule)
|
||||||
|
where
|
||||||
|
isWildcard ImportedSymbol{..} =
|
||||||
|
impQualified == NotQualified && impType /= Explicit
|
||||||
|
isOkModule = flip Set.member (Set.fromList okModules) . impModule
|
||||||
|
groupByFile = Map.toList . fmap Set.toList . foldr insertMap Map.empty
|
||||||
|
insertMap ImportedSymbol{..} =
|
||||||
|
Map.insertWith Set.union impFile (Set.singleton impModule)
|
||||||
|
|
||||||
|
formatWildcards :: WildcardImports -> Text
|
||||||
|
formatWildcards files =
|
||||||
|
"Modules in the following files were imported as wildcards:\n\n"
|
||||||
|
<> T.concat (fmap formatFile files)
|
||||||
|
where
|
||||||
|
formatFile (filepath, modules) =
|
||||||
|
"In " <> T.pack filepath <> ":\n" <> T.concat (fmap formatModule modules) <> "\n"
|
||||||
|
formatModule moduleName = " " <> moduleName <> "\n"
|
||||||
|
|
||||||
|
|
||||||
|
-- GRAPHS
|
||||||
|
|
||||||
|
modulesGraph :: [ImportedSymbol] -> Dot.DotGraph
|
||||||
|
modulesGraph symbols =
|
||||||
|
Dot.DotGraph Dot.Strict Dot.Directed (Just "Modules") $ fmap edge edges
|
||||||
|
where
|
||||||
|
edge (from, to) =
|
||||||
|
Dot.StatementEdge $ Dot.EdgeStatement
|
||||||
|
(Dot.ListTwo (edgeNode from) (edgeNode to) mempty) mempty
|
||||||
|
edgeNode t = Dot.EdgeNode $ Dot.NodeId (Dot.Id t) Nothing
|
||||||
|
edges = unique . fmap edgeTuple . filter ((==) Internal . impInternal) $ symbols
|
||||||
|
edgeTuple ImportedSymbol{..} = (impFromModule, impModule)
|
||||||
|
unique = Set.toList . Set.fromList
|
||||||
|
|
||||||
|
-- Building Text directly as the Dot package currently doesn't support subgraphs.
|
||||||
|
symbolsGraph :: [ImportedSymbol] -> Text
|
||||||
|
symbolsGraph symbols =
|
||||||
|
"digraph Symbols {\n"
|
||||||
|
<> " rankdir=LR\n"
|
||||||
|
<> " ranksep=5\n"
|
||||||
|
<> T.concat (fmap edge edges)
|
||||||
|
<> T.concat (fmap cluster symbolsByModule)
|
||||||
|
<> "}\n"
|
||||||
|
where
|
||||||
|
edge (from, to, symbol) =
|
||||||
|
" "
|
||||||
|
<> quoted from
|
||||||
|
<> " -> "
|
||||||
|
<> quoted (to <> maybe "" ("." <>) symbol)
|
||||||
|
<> "\n"
|
||||||
|
cluster (moduleName, clusterSymbols) =
|
||||||
|
" subgraph "
|
||||||
|
<> quoted ("cluster_" <> moduleName)
|
||||||
|
<> " {\n"
|
||||||
|
<> " " <> quoted moduleName <> "\n"
|
||||||
|
<> T.concat (fmap (clusterNode moduleName) clusterSymbols)
|
||||||
|
<> " }\n"
|
||||||
|
clusterNode moduleName symbol =
|
||||||
|
" " <> quoted (moduleName <> "." <> symbol) <> "\n"
|
||||||
|
quoted t = "\"" <> t <> "\""
|
||||||
|
edges = unique . fmap edgeTuple . filter ((==) Internal . impInternal) $ symbols
|
||||||
|
edgeTuple ImportedSymbol{..} = (impFromModule, impModule, impSymbol)
|
||||||
|
unique = Set.toList . Set.fromList
|
||||||
|
symbolsByModule =
|
||||||
|
Map.toList . Map.map (catMaybes . Set.toList) . foldr insertMap Map.empty $ edges
|
||||||
|
insertMap (_, to, symbol) = Map.insertWith Set.union to $ Set.singleton symbol
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# hsie - Swiss army knife for HaSkell Imports and Exports
|
||||||
|
|
||||||
|
This tool parses Haskell source code to analyse the imports and exports in a
|
||||||
|
project. It's available in PostgREST's `nix-shell` by default.
|
||||||
|
|
||||||
|
## Dumping imports
|
||||||
|
|
||||||
|
Given source code in the directories `src` and `main`, for example, you can run:
|
||||||
|
|
||||||
|
```
|
||||||
|
hsie dump-imports src main
|
||||||
|
```
|
||||||
|
|
||||||
|
This dumps all imports of the modules in the given directory to a CSV file,
|
||||||
|
printed on `stdout`.
|
||||||
|
|
||||||
|
To dump to a JSON file (e.g., to further process with `jq`), add the `--json`
|
||||||
|
flag:
|
||||||
|
|
||||||
|
```
|
||||||
|
hsie dump-imports --json src main
|
||||||
|
```
|
||||||
|
|
||||||
|
## Graphing imports
|
||||||
|
|
||||||
|
The tool can generate `graphviz` graphs of module and symbol imports by printing
|
||||||
|
a file to `stdout` that can directly be rendered with `dot`:
|
||||||
|
|
||||||
|
```
|
||||||
|
hsie graph-modules src main | dot -Tpng -o modules.png
|
||||||
|
```
|
||||||
|
|
||||||
|
The command `graph-modules` prints a graph of which modules insert which other
|
||||||
|
modules. `graph-symbols` shows which symbols are imported from which modules.
|
||||||
|
|
||||||
|
## Checking imports
|
||||||
|
|
||||||
|
To check whether modules are imported under consistent aliases in your project,
|
||||||
|
run:
|
||||||
|
|
||||||
|
```
|
||||||
|
hsie check-aliases main src
|
||||||
|
```
|
||||||
|
|
||||||
|
This will exit with a non-zero exit code if any inconsistent aliases are found.
|
||||||
|
|
||||||
|
The following command checks whether any modules are imported as wildcards, i.e.
|
||||||
|
not qualified and without specifying symbols.
|
||||||
|
|
||||||
|
```
|
||||||
|
hsie check-wildcards main src
|
||||||
|
```
|
||||||
|
|
||||||
|
To whitelist certain modules to be imported as wildcards, use `--ok`:
|
||||||
|
|
||||||
|
```
|
||||||
|
hsie check-wildcards main src --ok Protolude --ok Test.Module
|
||||||
|
```
|
||||||
|
|
||||||
|
## Current limitations
|
||||||
|
|
||||||
|
This tool uses the GHC parser to parse Haskell source code. Language extensions
|
||||||
|
required to parse each file are detected based on the `{-# LANGUAGE ... #-}`
|
||||||
|
pragmas. If they are not available (e.g., as they are listed as default
|
||||||
|
extensions in the `.cabal` file), parses may fail. We can fix this by using
|
||||||
|
an extended set of non-conflicting extensions by default, as `hlint` does for
|
||||||
|
example.
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{ ghcWithPackages
|
||||||
|
, runCommand
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
name = "hsie";
|
||||||
|
src = ./Main.hs;
|
||||||
|
modules = ps: [
|
||||||
|
ps.aeson
|
||||||
|
ps.aeson-pretty
|
||||||
|
ps.cassava
|
||||||
|
ps.dir-traverse
|
||||||
|
ps.dot
|
||||||
|
ps.ghc-exactprint
|
||||||
|
ps.optparse-applicative
|
||||||
|
];
|
||||||
|
ghc = ghcWithPackages modules;
|
||||||
|
hsie =
|
||||||
|
runCommand "haskellimports" { inherit name src; }
|
||||||
|
''
|
||||||
|
cd $TMP
|
||||||
|
cp $src $TMP/Main.hs
|
||||||
|
${ghc}/bin/ghc -O -Werror -Wall -package ghc Main.hs -o Main
|
||||||
|
cp Main $out
|
||||||
|
'';
|
||||||
|
bin =
|
||||||
|
runCommand name { inherit hsie name; }
|
||||||
|
''
|
||||||
|
mkdir -p $out/bin
|
||||||
|
ln -s $hsie $out/bin/$name
|
||||||
|
'';
|
||||||
|
bash-completion =
|
||||||
|
runCommand "${name}-bash-completion" { inherit bin name; }
|
||||||
|
"$bin/bin/$name --bash-completion-script $bin/bin/$name > $out";
|
||||||
|
in
|
||||||
|
hsie // { inherit bash-completion bin; }
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{ writeShellScriptBin, git, silver-searcher, hlint }:
|
|
||||||
|
|
||||||
writeShellScriptBin "postgrest-lint"
|
|
||||||
''
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
rootdir="$(${git}/bin/git rev-parse --show-toplevel)"
|
|
||||||
|
|
||||||
# Lint Haskell files
|
|
||||||
${silver-searcher}/bin/ag -l -g '\.l?hs$' "$rootdir" \
|
|
||||||
| xargs ${hlint}/bin/hlint -X QuasiQuotes -X NoPatternSynonyms
|
|
||||||
''
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
{ curl
|
|
||||||
, jq
|
|
||||||
, writeShellScriptBin
|
|
||||||
, nix
|
|
||||||
}:
|
|
||||||
# Utility script for pinning the latest unstable version of Nixpkgs.
|
|
||||||
|
|
||||||
# Instead of pinning Nixpkgs based on the huge Git repository, we reference a
|
|
||||||
# specific tarball that only contains the source of the revision that we want
|
|
||||||
# to pin.
|
|
||||||
let
|
|
||||||
name =
|
|
||||||
"nixpkgs-upgrade";
|
|
||||||
|
|
||||||
refUrl =
|
|
||||||
https://api.github.com/repos/nixos/nixpkgs/git/ref/heads/nixpkgs-unstable;
|
|
||||||
|
|
||||||
githubV3Header =
|
|
||||||
"Accept: application/vnd.github.v3+json";
|
|
||||||
|
|
||||||
tarballUrlBase =
|
|
||||||
https://github.com/nixos/nixpkgs/archive/;
|
|
||||||
in
|
|
||||||
writeShellScriptBin name
|
|
||||||
''
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
commitHash="$(${curl}/bin/curl "${refUrl}" -H "${githubV3Header}" | ${jq}/bin/jq -r .object.sha)"
|
|
||||||
tarballUrl="${tarballUrlBase}$commitHash.tar.gz"
|
|
||||||
tarballHash="$(${nix}/bin/nix-prefetch-url --unpack "$tarballUrl")"
|
|
||||||
currentDate="$(date --iso)"
|
|
||||||
|
|
||||||
cat << EOF
|
|
||||||
# Pinned version of Nixpkgs, generated with ${name}.
|
|
||||||
{
|
|
||||||
date = "$currentDate";
|
|
||||||
rev = "$commitHash";
|
|
||||||
tarballHash = "$tarballHash";
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
''
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Pinned version of Nixpkgs, generated with nixpkgs-upgrade.
|
# Pinned version of Nixpkgs, generated with postgrest-nixpkgs-upgrade.
|
||||||
{
|
{
|
||||||
date = "2020-04-19";
|
date = "2023-03-25";
|
||||||
rev = "10100a97c8964e82b30f180fda41ade8e6f69e41";
|
rev = "dbf5322e93bcc6cfc52268367a8ad21c09d76fea";
|
||||||
tarballHash = "011f36kr3c1ria7rag7px26bh73d1b0xpqadd149bysf4hg17rln";
|
tarballHash = "0lwk4v9dkvd28xpqch0b0jrac4xl9lwm6snrnzx8k5lby72kmkng";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Creates an environment that exposes bash-completion arguments from all checkedShellScripts
|
||||||
|
{ buildEnv }:
|
||||||
|
{ name
|
||||||
|
, tools
|
||||||
|
, extra ? { }
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
bash-completion = builtins.map (tool: tool.bash-completion) tools;
|
||||||
|
|
||||||
|
env = buildEnv {
|
||||||
|
inherit name;
|
||||||
|
paths = builtins.map (tool: tool.bin) tools;
|
||||||
|
};
|
||||||
|
|
||||||
|
in
|
||||||
|
env // { inherit bash-completion; } // extra
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
self: super:
|
||||||
|
# Overlay that adds `buildToolbox`, an enhanced version of `buildEnv`
|
||||||
|
{
|
||||||
|
buildToolbox = super.callPackage ./build-toolbox.nix { };
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
# Create a bash script that is checked with shellcheck. You can either use it
|
||||||
|
# directly, or use the .bin attribute to get the script in a bin/ directory,
|
||||||
|
# to be used in a path for example.
|
||||||
|
{ argbash
|
||||||
|
, bash
|
||||||
|
, coreutils
|
||||||
|
, git
|
||||||
|
, lib
|
||||||
|
, runCommand
|
||||||
|
, shellcheck
|
||||||
|
, stdenv
|
||||||
|
, writeTextFile
|
||||||
|
}:
|
||||||
|
{ name
|
||||||
|
, docs
|
||||||
|
, args ? [ ]
|
||||||
|
, positionalCompletion ? ""
|
||||||
|
, inRootDir ? false
|
||||||
|
, redirectTixFiles ? true
|
||||||
|
, withEnv ? null
|
||||||
|
, withPath ? [ ]
|
||||||
|
, withTmpDir ? false
|
||||||
|
}: text:
|
||||||
|
let
|
||||||
|
# square brackets are a pain to escape - if even possible. just don't use them...
|
||||||
|
escape = builtins.replaceStrings [ "\n" ] [ " \\n" ];
|
||||||
|
|
||||||
|
argsTemplate =
|
||||||
|
writeTextFile {
|
||||||
|
inherit name;
|
||||||
|
destination = "/${name}.m4"; # destination is needed to have the proper basename for completion
|
||||||
|
|
||||||
|
text =
|
||||||
|
''
|
||||||
|
# BASH_ARGV0 sets $0 - which is used in parser.sh for usage information
|
||||||
|
# stripping the /nix/store/... path for nicer display
|
||||||
|
BASH_ARGV0="$(basename "$0")"
|
||||||
|
|
||||||
|
# ARG_HELP([${name}], [${escape docs}])
|
||||||
|
${lib.strings.concatMapStrings (arg: "# " + arg) args}
|
||||||
|
# ARG_POSITIONAL_DOUBLEDASH()
|
||||||
|
# ARG_DEFAULTS_POS()
|
||||||
|
# ARGBASH_GO
|
||||||
|
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
argsParser =
|
||||||
|
runCommand "${name}-parser" { }
|
||||||
|
''
|
||||||
|
${argbash}/bin/argbash ${argsTemplate}/${name}.m4 > $out
|
||||||
|
|
||||||
|
# This forces optional arguments to go *before* positional arguments,
|
||||||
|
# which allows leftovers to pass optional arguments to sub-commands.
|
||||||
|
# Example: This way `postgrest-watch -h` will return the help output for watch, while
|
||||||
|
# `postgrest-watch postgrest-test-spec -h` will return the help output for test-spec.
|
||||||
|
# Taken from: https://github.com/matejak/argbash/issues/114#issuecomment-557108274
|
||||||
|
sed '/_positionals_count + 1/a\\t\t\t\tset -- "''${@:1:1}" "--" "''${@:2}"' -i $out
|
||||||
|
'';
|
||||||
|
|
||||||
|
bash-completion =
|
||||||
|
runCommand "${name}-completion" { } (
|
||||||
|
''
|
||||||
|
${argbash}/bin/argbash --type completion --strip all ${argsTemplate}/${name}.m4 > $out
|
||||||
|
''
|
||||||
|
|
||||||
|
+ lib.optionalString (positionalCompletion != "") ''
|
||||||
|
sed 's#COMPREPLY.*compgen -o bashdefault .*$#${escape positionalCompletion}#' -i $out
|
||||||
|
''
|
||||||
|
);
|
||||||
|
|
||||||
|
bin =
|
||||||
|
writeTextFile {
|
||||||
|
inherit name;
|
||||||
|
executable = true;
|
||||||
|
destination = "/bin/${name}";
|
||||||
|
|
||||||
|
text =
|
||||||
|
''
|
||||||
|
#!${bash}/bin/bash
|
||||||
|
source ${argsParser}
|
||||||
|
set -euo pipefail
|
||||||
|
''
|
||||||
|
|
||||||
|
+ lib.optionalString redirectTixFiles ''
|
||||||
|
# storing tix files in a temporary throw away directory avoids mix/tix conflicts after changes
|
||||||
|
hpctixdir=$(${coreutils}/bin/mktemp -d)
|
||||||
|
export HPCTIXFILE="$hpctixdir"/postgrest.tix
|
||||||
|
trap 'rm -rf $hpctixdir' EXIT
|
||||||
|
''
|
||||||
|
|
||||||
|
+ lib.optionalString inRootDir ''
|
||||||
|
cd "$(${git}/bin/git rev-parse --show-toplevel)"
|
||||||
|
|
||||||
|
if test ! -f postgrest.cabal; then
|
||||||
|
>&2 echo "Couldn't find postgrest.cabal. Please make sure to" \
|
||||||
|
"run this command somewhere in the PostgREST repo."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
''
|
||||||
|
|
||||||
|
+ lib.optionalString withTmpDir ''
|
||||||
|
mkdir -p "''${TMPDIR:-/tmp}/postgrest"
|
||||||
|
tmpdir="$(${coreutils}/bin/mktemp -d --tmpdir postgrest/${name}-XXX)"
|
||||||
|
|
||||||
|
# we keep the tmpdir when an error occurs for debugging
|
||||||
|
trap 'echo Temporary directory kept at: $tmpdir' ERR
|
||||||
|
# remove the tmpdir when cancelled (postgrest-watch)
|
||||||
|
trap 'rm -rf "$tmpdir"' SIGINT SIGTERM
|
||||||
|
''
|
||||||
|
|
||||||
|
+ lib.optionalString (withEnv != null) ''
|
||||||
|
env="$(cat ${withEnv})"
|
||||||
|
export PATH="$env/bin:$PATH"
|
||||||
|
''
|
||||||
|
|
||||||
|
+ lib.optionalString (lib.length withPath > 0) ''
|
||||||
|
export PATH="${lib.concatMapStrings (p: p + "/bin:") withPath}$PATH"
|
||||||
|
''
|
||||||
|
|
||||||
|
+ "(${text})"
|
||||||
|
|
||||||
|
+ lib.optionalString withTmpDir ''
|
||||||
|
|
||||||
|
rm -rf "$tmpdir"
|
||||||
|
'';
|
||||||
|
|
||||||
|
checkPhase =
|
||||||
|
''
|
||||||
|
# check syntax
|
||||||
|
${stdenv.shell} -n $out/bin/${name}
|
||||||
|
|
||||||
|
# check for shellcheck recommendations
|
||||||
|
${shellcheck}/bin/shellcheck -x $out/bin/${name}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
script =
|
||||||
|
runCommand name { inherit bin name; } "ln -s $bin/bin/$name $out";
|
||||||
|
in
|
||||||
|
script // { inherit bin bash-completion; }
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
self: super:
|
||||||
|
# Overlay that adds `checkedShellScript`, an enhanced version of
|
||||||
|
# writeShellScript and writeShellScriptBin
|
||||||
|
{
|
||||||
|
checkedShellScript = super.callPackage ./checked-shell-script.nix { };
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
build-toolbox = import ./build-toolbox;
|
||||||
|
checked-shell-script = import ./checked-shell-script;
|
||||||
|
gitignore = import ./gitignore.nix;
|
||||||
|
haskell-packages = import ./haskell-packages.nix;
|
||||||
|
postgis = import ./postgis.nix;
|
||||||
|
postgresql-default = import ./postgresql-default.nix;
|
||||||
|
postgresql-legacy = import ./postgresql-legacy.nix;
|
||||||
|
postgresql-future = import ./postgresql-future.nix;
|
||||||
|
slocat = import ./slocat.nix;
|
||||||
|
}
|
||||||
@@ -12,8 +12,9 @@ self: super:
|
|||||||
gitignoreSrc = super.fetchFromGitHub {
|
gitignoreSrc = super.fetchFromGitHub {
|
||||||
owner = "hercules-ci";
|
owner = "hercules-ci";
|
||||||
repo = "gitignore";
|
repo = "gitignore";
|
||||||
rev = "2ced4519f865341adcb143c5d668f955a2cb997f";
|
rev = "a20de23b925fd8264fd7fad6454652e142fd7f73";
|
||||||
sha256 = "sha256:0fc5bgv9syfcblp23y05kkfnpgh3gssz6vn24frs8dzw39algk2z";
|
sha256 = "sha256-8DFJjXG8zqoONA1vXtgeKXy68KdJL5UaXR8NtVMUbx8=";
|
||||||
};
|
};
|
||||||
in (super.callPackage gitignoreSrc {}).gitignoreSource;
|
in
|
||||||
|
(super.callPackage gitignoreSrc { }).gitignoreSource;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
{ compiler, extraOverrides ? (final: prev: { }) }:
|
||||||
|
|
||||||
|
self: super:
|
||||||
|
let
|
||||||
|
inherit (self.haskell) lib;
|
||||||
|
|
||||||
|
overrides =
|
||||||
|
final: prev:
|
||||||
|
rec {
|
||||||
|
# To pin custom versions of Haskell packages:
|
||||||
|
# protolude =
|
||||||
|
# prev.callHackageDirect
|
||||||
|
# {
|
||||||
|
# pkg = "protolude";
|
||||||
|
# ver = "0.3.0";
|
||||||
|
# sha256 = "<sha256>";
|
||||||
|
# }
|
||||||
|
# { };
|
||||||
|
#
|
||||||
|
# To temporarily pin unreleased versions from GitHub:
|
||||||
|
# <name> =
|
||||||
|
# prev.callCabal2nixWithOptions "<name>" (super.fetchFromGitHub {
|
||||||
|
# owner = "<owner>";
|
||||||
|
# repo = "<repo>";
|
||||||
|
# rev = "<commit>";
|
||||||
|
# sha256 = "<sha256>";
|
||||||
|
# }) "--subpath=<subpath>" {};
|
||||||
|
#
|
||||||
|
# To fill in the sha256:
|
||||||
|
# update-nix-fetchgit nix/overlays/haskell-packages.nix
|
||||||
|
|
||||||
|
postgresql-libpq = lib.dontCheck
|
||||||
|
(prev.callCabal2nix "postgresql-libpq"
|
||||||
|
(super.fetchFromGitHub {
|
||||||
|
owner = "PostgREST";
|
||||||
|
repo = "postgresql-libpq";
|
||||||
|
rev = "890a0a16cf57dd401420fdc6c7d576fb696003bc"; # master
|
||||||
|
sha256 = "1wmyhldk0k14y8whp1p4akrkqxf5snh8qsbm7fv5f7kz95nyffd0";
|
||||||
|
})
|
||||||
|
{ });
|
||||||
|
|
||||||
|
hasql-notifications = lib.dontCheck
|
||||||
|
(prev.callHackageDirect
|
||||||
|
{
|
||||||
|
pkg = "hasql-notifications";
|
||||||
|
ver = "0.2.0.6";
|
||||||
|
sha256 = "sha256-7PyFlB2B70njudOjaX6tk1m77ol9vnF5fI0LF86kVAI=";
|
||||||
|
}
|
||||||
|
{ });
|
||||||
|
|
||||||
|
hasql-pool = lib.dontCheck
|
||||||
|
(prev.callHackageDirect
|
||||||
|
{
|
||||||
|
pkg = "hasql-pool";
|
||||||
|
ver = "0.10";
|
||||||
|
sha256 = "sha256-kHzoqtNV9BFWnn1h560JRqMooQRwxokVKgDRBexamNI=";
|
||||||
|
}
|
||||||
|
{ });
|
||||||
|
} // extraOverrides final prev;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
haskell =
|
||||||
|
super.haskell // {
|
||||||
|
packages = super.haskell.packages // {
|
||||||
|
"${compiler}" =
|
||||||
|
super.haskell.packages."${compiler}".override { inherit overrides; };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
The `.nix` files in this directory pin specific versions of Haskell packages
|
|
||||||
from Hackage. They were generated with `cabal2nix`, e.g.:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cabal2nix cabal://protolude > protolude.nix
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Those overrides might become obsolete as the pinned versions are adopted into
|
|
||||||
our pinned version of nixpkgs.
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
self: super:
|
|
||||||
|
|
||||||
{
|
|
||||||
haskellPackages =
|
|
||||||
super.haskellPackages.override {
|
|
||||||
overrides =
|
|
||||||
final: prev: rec {
|
|
||||||
protolude = prev.callPackage ./protolude.nix {};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
{ mkDerivation
|
|
||||||
, array
|
|
||||||
, async
|
|
||||||
, base
|
|
||||||
, bytestring
|
|
||||||
, containers
|
|
||||||
, deepseq
|
|
||||||
, ghc-prim
|
|
||||||
, hashable
|
|
||||||
, mtl
|
|
||||||
, mtl-compat
|
|
||||||
, stdenv
|
|
||||||
, stm
|
|
||||||
, text
|
|
||||||
, transformers
|
|
||||||
, transformers-compat
|
|
||||||
}:
|
|
||||||
mkDerivation {
|
|
||||||
pname = "protolude";
|
|
||||||
version = "0.3.0";
|
|
||||||
sha256 = "4083385a9e03fab9201f63ce198b9ced3fbc1c50d6d42574db5e36c757bedcac";
|
|
||||||
isLibrary = true;
|
|
||||||
isExecutable = true;
|
|
||||||
libraryHaskellDepends = [
|
|
||||||
array
|
|
||||||
async
|
|
||||||
base
|
|
||||||
bytestring
|
|
||||||
containers
|
|
||||||
deepseq
|
|
||||||
ghc-prim
|
|
||||||
hashable
|
|
||||||
mtl
|
|
||||||
mtl-compat
|
|
||||||
stm
|
|
||||||
text
|
|
||||||
transformers
|
|
||||||
transformers-compat
|
|
||||||
];
|
|
||||||
homepage = "https://github.com/sdiehl/protolude";
|
|
||||||
description = "A small prelude";
|
|
||||||
license = stdenv.lib.licenses.mit;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
final: prev:
|
||||||
|
let
|
||||||
|
postgis_3_2_3 = rec {
|
||||||
|
version = "3.2.3";
|
||||||
|
src = final.fetchurl {
|
||||||
|
url = "https://download.osgeo.org/postgis/source/postgis-${version}.tar.gz";
|
||||||
|
sha256 = "sha256-G02LXHVuWrpZ77wYM7Iu/k1lYneO7KVvpJf+susTZow=";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
postgresql_11 = prev.postgresql_11.override { this = final.postgresql_11; } // {
|
||||||
|
pkgs = prev.postgresql_11.pkgs // {
|
||||||
|
postgis = prev.postgresql_11.pkgs.postgis.overrideAttrs (_: postgis_3_2_3);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
postgresql_10 = prev.postgresql_10.override { this = final.postgresql_11; } // {
|
||||||
|
pkgs = prev.postgresql_10.pkgs // {
|
||||||
|
postgis = prev.postgresql_10.pkgs.postgis.overrideAttrs (_: postgis_3_2_3);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
postgresql_9_6 = prev.postgresql_9_6.override { this = final.postgresql_11; } // {
|
||||||
|
pkgs = prev.postgresql_9_6.pkgs // {
|
||||||
|
postgis = prev.postgresql_9_6.pkgs.postgis.overrideAttrs (_: postgis_3_2_3);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{ patches }: self: super:
|
||||||
|
# Overlay that sets the default version of PostgreSQL.
|
||||||
|
with patches;
|
||||||
|
{
|
||||||
|
postgresql = super.postgresql_15.overrideAttrs ({ patches ? [ ], ... }: {
|
||||||
|
patches = patches ++ [ postgresql-atexit ];
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
self: super:
|
||||||
|
# Overlay that adds future versions of PostgreSQL that are supported by
|
||||||
|
# PostgREST.
|
||||||
|
{
|
||||||
|
## Example for including a postgresql version from a specific nixpks commit:
|
||||||
|
##
|
||||||
|
postgresql_16 =
|
||||||
|
let
|
||||||
|
rev = "5148520bfab61f99fd25fb9ff7bfbb50dad3c9db";
|
||||||
|
tarballHash = "1dfjmz65h8z4lk845724vypzmf3dbgsdndjpj8ydlhx6c7rpcq3p";
|
||||||
|
|
||||||
|
pinnedPkgs =
|
||||||
|
builtins.fetchTarball {
|
||||||
|
url = "https://github.com/nixos/nixpkgs/archive/${rev}.tar.gz";
|
||||||
|
sha256 = tarballHash;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
(import pinnedPkgs { }).pkgs.postgresql_16;
|
||||||
|
}
|
||||||
@@ -2,19 +2,33 @@ self: super:
|
|||||||
# Overlay that adds legacy versions of PostgreSQL that are supported by
|
# Overlay that adds legacy versions of PostgreSQL that are supported by
|
||||||
# PostgREST.
|
# PostgREST.
|
||||||
{
|
{
|
||||||
# PostgreSQL 9.4 was removed from Nixpkgs with
|
# PostgreSQL 9.6 was removed from Nixpkgs with
|
||||||
# https://github.com/NixOS/nixpkgs/commit/8e2fc57a80d761c46702c3250e61c1bffe021e25
|
# https://github.com/NixOS/nixpkgs/commit/757dd008b2f2926fc0f7688fa8189f930ea47521
|
||||||
# We pin its parent commit 3b5b9a7 to get the last version that was available.
|
# We pin its parent commit to get the last version that was available.
|
||||||
postgresql_9_4 =
|
postgresql_9_6 =
|
||||||
let
|
let
|
||||||
rev = "3b5b9a73f59ff93d50156e250203410bdd07f4e0";
|
rev = "571cbf3d1db477058303cef8754fb85a14e90eb7";
|
||||||
tarballHash = "0kr25xqbv1ldp72jbmml21pc5hl7xcfqhclv5qxa5f860jddjznk";
|
tarballHash = "0q74wn418i1bn5sssacmw8ykpmqvzr0s93sj6pbs3rf6bf134fkz";
|
||||||
|
|
||||||
pinnedPkgs =
|
pinnedPkgs =
|
||||||
builtins.fetchTarball {
|
builtins.fetchTarball {
|
||||||
url = "https://github.com/nixos/nixpkgs/archive/${rev}.tar.gz";
|
url = "https://github.com/nixos/nixpkgs/archive/${rev}.tar.gz";
|
||||||
sha256 = tarballHash;
|
sha256 = tarballHash;
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
(import pinnedPkgs {}).pkgs.postgresql_9_4;
|
(import pinnedPkgs { }).pkgs.postgresql_9_6;
|
||||||
|
|
||||||
|
# PostgreSQL 10 was removed from Nixpkgs with
|
||||||
|
# https://github.com/NixOS/nixpkgs/commit/aa1483114bb329fee7e1266100b8d8921ed4723f
|
||||||
|
# We pin its parent commit to get the last version that was available.
|
||||||
|
postgresql_10 =
|
||||||
|
let
|
||||||
|
rev = "79661ba7e2fb96ebefbb537458a5bbae9dc5bd1a";
|
||||||
|
tarballHash = "0rn796pfn4sg90ai9fdnwmr10a2s835p1arazzgz46h6s5cxvq97";
|
||||||
|
pinnedPkgs =
|
||||||
|
builtins.fetchTarball {
|
||||||
|
url = "https://github.com/nixos/nixpkgs/archive/${rev}.tar.gz";
|
||||||
|
sha256 = tarballHash;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
(import pinnedPkgs { }).pkgs.postgresql_10;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
final: prev:
|
||||||
|
{
|
||||||
|
slocat = prev.buildGoModule {
|
||||||
|
name = "slocat";
|
||||||
|
src = prev.fetchFromGitHub {
|
||||||
|
owner = "robx";
|
||||||
|
repo = "slocat";
|
||||||
|
rev = "52e7512c6029fd00483e41ccce260a3b4b9b3b64";
|
||||||
|
sha256 = "sha256-qn6luuh5wqREu3s8RfuMCP5PKdS2WdwPrujRYTpfzQ8=";
|
||||||
|
};
|
||||||
|
vendorSha256 = "sha256-pQpattmS9VmO3ZIQUFn66az8GSmB4IvYhTTCFn6SUmo=";
|
||||||
|
};
|
||||||
|
}
|
||||||
+13
-9
@@ -1,14 +1,15 @@
|
|||||||
{ runCommand }:
|
{ runCommand }:
|
||||||
|
|
||||||
{
|
{
|
||||||
# Apply patches to a directory.
|
|
||||||
applyPatches =
|
applyPatches =
|
||||||
name: path: patches:
|
name: src: patches:
|
||||||
runCommand name { inherit patches; }
|
runCommand
|
||||||
|
name
|
||||||
|
{ inherit src patches; }
|
||||||
''
|
''
|
||||||
set -eou pipefail
|
set -eou pipefail
|
||||||
|
|
||||||
cp -r ${path} $out
|
cp -r $src $out
|
||||||
chmod -R u+w $out
|
chmod -R u+w $out
|
||||||
|
|
||||||
for patch in $patches; do
|
for patch in $patches; do
|
||||||
@@ -17,9 +18,12 @@
|
|||||||
done
|
done
|
||||||
'';
|
'';
|
||||||
|
|
||||||
static-haskell-nix-postgrest-libpq =
|
static-haskell-nix-ncurses =
|
||||||
./static-haskell-nix-postgrest-libpq.patch;
|
./static-haskell-nix-ncurses.patch;
|
||||||
|
static-haskell-nix-ghc-bignum =
|
||||||
nixpkgs-revert-ghc-bootstrap =
|
./static-haskell-nix-ghc-bignum.patch;
|
||||||
./nixpkgs-revert-ghc-bootstrap.patch;
|
static-haskell-nix-openssl =
|
||||||
|
./static-haskell-nix-openssl.patch;
|
||||||
|
postgresql-atexit =
|
||||||
|
./postgresql-atexit.patch;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,125 +0,0 @@
|
|||||||
From 93cc144b215654ca9cb0d2bcb58bb8922895ccab Mon Sep 17 00:00:00 2001
|
|
||||||
From: Remo <remo>
|
|
||||||
Date: Sat, 25 Apr 2020 17:31:55 +0200
|
|
||||||
Subject: [PATCH] Revert "ghc: 8.6.3-binary -> 8.6.5-binary"
|
|
||||||
|
|
||||||
This reverts commit 3c7ef6bcd85e3e133ac6f2ab5e238934d56d902e.
|
|
||||||
---
|
|
||||||
.../{8.6.5-binary.nix => 8.6.3-binary.nix} | 13 ++++++------
|
|
||||||
pkgs/top-level/haskell-packages.nix | 20 +++++++++----------
|
|
||||||
2 files changed, 16 insertions(+), 17 deletions(-)
|
|
||||||
rename pkgs/development/compilers/ghc/{8.6.5-binary.nix => 8.6.3-binary.nix} (95%)
|
|
||||||
|
|
||||||
diff --git a/pkgs/development/compilers/ghc/8.6.5-binary.nix b/pkgs/development/compilers/ghc/8.6.3-binary.nix
|
|
||||||
similarity index 95%
|
|
||||||
rename from pkgs/development/compilers/ghc/8.6.5-binary.nix
|
|
||||||
rename to pkgs/development/compilers/ghc/8.6.3-binary.nix
|
|
||||||
index 97793d912895..9e0fd12b96cf 100644
|
|
||||||
--- a/pkgs/development/compilers/ghc/8.6.5-binary.nix
|
|
||||||
+++ b/pkgs/development/compilers/ghc/8.6.3-binary.nix
|
|
||||||
@@ -27,19 +27,18 @@ let
|
|
||||||
in
|
|
||||||
|
|
||||||
stdenv.mkDerivation rec {
|
|
||||||
- version = "8.6.5";
|
|
||||||
+ version = "8.6.3";
|
|
||||||
|
|
||||||
name = "ghc-${version}-binary";
|
|
||||||
|
|
||||||
- # https://downloads.haskell.org/~ghc/8.6.5/
|
|
||||||
src = fetchurl ({
|
|
||||||
i686-linux = {
|
|
||||||
- url = "http://haskell.org/ghc/dist/${version}/ghc-${version}-i386-deb9-linux.tar.xz";
|
|
||||||
- sha256 = "1p2h29qghql19ajk755xa0yxkn85slbds8m9n5196ris743vkp8w";
|
|
||||||
+ url = "http://haskell.org/ghc/dist/${version}/ghc-${version}-i386-deb8-linux.tar.xz";
|
|
||||||
+ sha256 = "0bw8a7fxcbskf93rb4m542ff66vrmx5i5kj77qx37cbhijx70w5m";
|
|
||||||
};
|
|
||||||
x86_64-linux = {
|
|
||||||
- url = "http://haskell.org/ghc/dist/${version}/ghc-${version}-x86_64-deb9-linux.tar.xz";
|
|
||||||
- sha256 = "1pqlx6rdjs2110g0y1i9f8x18lmdizibjqd15f5xahcz39hgaxdw";
|
|
||||||
+ url = "http://haskell.org/ghc/dist/${version}/ghc-${version}-x86_64-deb8-linux.tar.xz";
|
|
||||||
+ sha256 = "1m9gaga2pzi2cx5gvasg0rx1dlvr68gmi20l67652kag6xjsa719";
|
|
||||||
};
|
|
||||||
aarch64-linux = {
|
|
||||||
url = "http://haskell.org/ghc/dist/${version}/ghc-${version}-aarch64-ubuntu18.04-linux.tar.xz";
|
|
||||||
@@ -47,7 +46,7 @@ stdenv.mkDerivation rec {
|
|
||||||
};
|
|
||||||
x86_64-darwin = {
|
|
||||||
url = "http://haskell.org/ghc/dist/${version}/ghc-${version}-x86_64-apple-darwin.tar.xz";
|
|
||||||
- sha256 = "0s9188vhhgf23q3rjarwhbr524z6h2qga5xaaa2pma03sfqvvhfz";
|
|
||||||
+ sha256 = "1hbzk57v45176kxcx848p5jn5p1xbp2129ramkbzsk6plyhnkl3r";
|
|
||||||
};
|
|
||||||
}.${stdenv.hostPlatform.system}
|
|
||||||
or (throw "cannot bootstrap GHC on this platform"));
|
|
||||||
diff --git a/pkgs/top-level/haskell-packages.nix b/pkgs/top-level/haskell-packages.nix
|
|
||||||
index aa7ef02a3886..712a98e147e0 100644
|
|
||||||
--- a/pkgs/top-level/haskell-packages.nix
|
|
||||||
+++ b/pkgs/top-level/haskell-packages.nix
|
|
||||||
@@ -4,7 +4,7 @@ let
|
|
||||||
# These are attributes in compiler and packages that don't support integer-simple.
|
|
||||||
integerSimpleExcludes = [
|
|
||||||
"ghc822Binary"
|
|
||||||
- "ghc865Binary"
|
|
||||||
+ "ghc863Binary"
|
|
||||||
"ghc844"
|
|
||||||
"ghcjs"
|
|
||||||
"ghcjs86"
|
|
||||||
@@ -42,7 +42,7 @@ in {
|
|
||||||
|
|
||||||
ghc822Binary = callPackage ../development/compilers/ghc/8.2.2-binary.nix { };
|
|
||||||
|
|
||||||
- ghc865Binary = callPackage ../development/compilers/ghc/8.6.5-binary.nix { };
|
|
||||||
+ ghc863Binary = callPackage ../development/compilers/ghc/8.6.3-binary.nix { };
|
|
||||||
|
|
||||||
ghc844 = callPackage ../development/compilers/ghc/8.4.4.nix {
|
|
||||||
bootPkgs = packages.ghc822Binary;
|
|
||||||
@@ -57,31 +57,31 @@ in {
|
|
||||||
llvmPackages = pkgs.llvmPackages_6;
|
|
||||||
};
|
|
||||||
ghc881 = callPackage ../development/compilers/ghc/8.8.1.nix {
|
|
||||||
- bootPkgs = packages.ghc865Binary;
|
|
||||||
+ bootPkgs = packages.ghc863Binary;
|
|
||||||
inherit (buildPackages.python3Packages) sphinx;
|
|
||||||
buildLlvmPackages = buildPackages.llvmPackages_7;
|
|
||||||
llvmPackages = pkgs.llvmPackages_7;
|
|
||||||
};
|
|
||||||
ghc882 = callPackage ../development/compilers/ghc/8.8.2.nix {
|
|
||||||
- bootPkgs = packages.ghc865Binary;
|
|
||||||
+ bootPkgs = packages.ghc863Binary;
|
|
||||||
inherit (buildPackages.python3Packages) sphinx;
|
|
||||||
buildLlvmPackages = buildPackages.llvmPackages_7;
|
|
||||||
llvmPackages = pkgs.llvmPackages_7;
|
|
||||||
};
|
|
||||||
ghc883 = callPackage ../development/compilers/ghc/8.8.3.nix {
|
|
||||||
- bootPkgs = packages.ghc865Binary;
|
|
||||||
+ bootPkgs = packages.ghc863Binary;
|
|
||||||
inherit (buildPackages.python3Packages) sphinx;
|
|
||||||
buildLlvmPackages = buildPackages.llvmPackages_7;
|
|
||||||
llvmPackages = pkgs.llvmPackages_7;
|
|
||||||
};
|
|
||||||
ghc8101 = callPackage ../development/compilers/ghc/8.10.1.nix {
|
|
||||||
- bootPkgs = packages.ghc865Binary;
|
|
||||||
+ bootPkgs = packages.ghc863Binary;
|
|
||||||
inherit (buildPackages.python3Packages) sphinx;
|
|
||||||
buildLlvmPackages = buildPackages.llvmPackages_9;
|
|
||||||
llvmPackages = pkgs.llvmPackages_9;
|
|
||||||
};
|
|
||||||
ghcHEAD = callPackage ../development/compilers/ghc/head.nix {
|
|
||||||
- bootPkgs = packages.ghc883; # no binary yet
|
|
||||||
+ bootPkgs = packages.ghc863Binary;
|
|
||||||
inherit (buildPackages.python3Packages) sphinx;
|
|
||||||
buildLlvmPackages = buildPackages.llvmPackages_9;
|
|
||||||
llvmPackages = pkgs.llvmPackages_9;
|
|
||||||
@@ -118,9 +118,9 @@ in {
|
|
||||||
compilerConfig = callPackage ../development/haskell-modules/configuration-ghc-8.2.x.nix { };
|
|
||||||
packageSetConfig = bootstrapPackageSet;
|
|
||||||
};
|
|
||||||
- ghc865Binary = callPackage ../development/haskell-modules {
|
|
||||||
- buildHaskellPackages = bh.packages.ghc865Binary;
|
|
||||||
- ghc = bh.compiler.ghc865Binary;
|
|
||||||
+ ghc863Binary = callPackage ../development/haskell-modules {
|
|
||||||
+ buildHaskellPackages = bh.packages.ghc863Binary;
|
|
||||||
+ ghc = bh.compiler.ghc863Binary;
|
|
||||||
compilerConfig = callPackage ../development/haskell-modules/configuration-ghc-8.6.x.nix { };
|
|
||||||
packageSetConfig = bootstrapPackageSet;
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
--- a/src/interfaces/libpq/Makefile
|
||||||
|
+++ b/src/interfaces/libpq/Makefile
|
||||||
|
@@ -118,7 +118,7 @@ backend_src = $(top_srcdir)/src/backend
|
||||||
|
libpq-refs-stamp: $(shlib)
|
||||||
|
ifneq ($(enable_coverage), yes)
|
||||||
|
ifeq (,$(filter aix solaris,$(PORTNAME)))
|
||||||
|
- @if nm -A -u $< 2>/dev/null | grep -v __cxa_atexit | grep exit; then \
|
||||||
|
+ @if nm -A -u $< 2>/dev/null | grep " exit"; then \
|
||||||
|
echo 'libpq must not be calling any function which invokes exit'; exit 1; \
|
||||||
|
fi
|
||||||
|
endif
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
diff --git a/survey/default.nix b/survey/default.nix
|
||||||
|
index 70afbbc..28cb0e9 100644
|
||||||
|
--- a/survey/default.nix
|
||||||
|
+++ b/survey/default.nix
|
||||||
|
@@ -81,6 +81,7 @@ let
|
||||||
|
# `.override` and the likes).
|
||||||
|
isProperHaskellPackage = val:
|
||||||
|
lib.isDerivation val && # must pass lib.isDerivation
|
||||||
|
+ val.pname != "ghc-bignum" &&
|
||||||
|
val ? env; # must have an .env key
|
||||||
|
|
||||||
|
# Function that tells us if a given Haskell package has an executable.
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
diff --git a/survey/default.nix b/survey/default.nix
|
||||||
|
index 46d8066..a47f214 100644
|
||||||
|
--- a/survey/default.nix
|
||||||
|
+++ b/survey/default.nix
|
||||||
|
@@ -1519,7 +1519,7 @@ let
|
||||||
|
[
|
||||||
|
"--enable-executable-static" # requires `useFixedCabal`
|
||||||
|
# `enableShared` seems to be required to avoid `recompile with -fPIC` errors on some packages.
|
||||||
|
- "--extra-lib-dirs=${final.ncurses.override { enableStatic = true; enableShared = true; }}/lib"
|
||||||
|
+ "--extra-lib-dirs=${final.ncurses.override { enableStatic = true; }}/lib"
|
||||||
|
]
|
||||||
|
# TODO Figure out why this and the below libffi are necessary.
|
||||||
|
# `working` and `workingStackageExecutables` don't seem to need that,
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
diff --git a/survey/default.nix b/survey/default.nix
|
||||||
|
index cf1bd31..9d34753 100644
|
||||||
|
--- a/survey/default.nix
|
||||||
|
+++ b/survey/default.nix
|
||||||
|
@@ -736,6 +736,7 @@ let
|
||||||
|
openblas = previous.openblas.override { enableStatic = true; };
|
||||||
|
|
||||||
|
openssl = previous.openssl.override { static = true; };
|
||||||
|
+ openssl_1_1 = previous.openssl_1_1.override { static = true; };
|
||||||
|
|
||||||
|
libsass = previous.libsass.overrideAttrs (old: { dontDisableStatic = true; });
|
||||||
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
From bb4c1e27e391eff01591fe60830ff68a9ada41ef Mon Sep 17 00:00:00 2001
|
|
||||||
From: Remo <remo>
|
|
||||||
Date: Wed, 22 Apr 2020 11:58:07 +0200
|
|
||||||
Subject: [PATCH] Add postgrest libpq linking fix
|
|
||||||
|
|
||||||
---
|
|
||||||
survey/default.nix | 5 +++++
|
|
||||||
1 file changed, 5 insertions(+)
|
|
||||||
|
|
||||||
diff --git a/survey/default.nix b/survey/default.nix
|
|
||||||
index b28606c..fd7cde7 100644
|
|
||||||
--- a/survey/default.nix
|
|
||||||
+++ b/survey/default.nix
|
|
||||||
@@ -1052,6 +1052,11 @@ let
|
|
||||||
super.squeal-postgresql
|
|
||||||
[ final.openssl ]
|
|
||||||
"--libs openssl";
|
|
||||||
+ postgrest =
|
|
||||||
+ addStaticLinkerFlagsWithPkgconfig
|
|
||||||
+ super.postgrest
|
|
||||||
+ [ final.openssl ]
|
|
||||||
+ "--libs openssl";
|
|
||||||
|
|
||||||
xml-to-json =
|
|
||||||
addStaticLinkerFlagsWithPkgconfig
|
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# Derive a fully static Haskell package based on musl instead of glibc.
|
||||||
|
{ nixpkgs, system, compiler, patches, allOverlays }:
|
||||||
|
|
||||||
|
name: src:
|
||||||
|
let
|
||||||
|
# The nh2/static-haskell-nix project does all the hard work for us.
|
||||||
|
static-haskell-nix =
|
||||||
|
let
|
||||||
|
rev = "bd66b86b72cff4479e1c76d5916a853c38d09837";
|
||||||
|
in
|
||||||
|
builtins.fetchTarball {
|
||||||
|
url = "https://github.com/nh2/static-haskell-nix/archive/${rev}.tar.gz";
|
||||||
|
sha256 = "0rnsxaw7v27znsg9lgqk1i4007ydqrc8gfgimrmhf24lv6galbjh";
|
||||||
|
};
|
||||||
|
|
||||||
|
patched-static-haskell-nix =
|
||||||
|
patches.applyPatches "patched-static-haskell-nix"
|
||||||
|
static-haskell-nix
|
||||||
|
[
|
||||||
|
patches.static-haskell-nix-ncurses
|
||||||
|
patches.static-haskell-nix-ghc-bignum
|
||||||
|
patches.static-haskell-nix-openssl
|
||||||
|
];
|
||||||
|
|
||||||
|
extraOverrides =
|
||||||
|
final: prev:
|
||||||
|
rec {
|
||||||
|
# We need to add our package needs to the package set that we pass to
|
||||||
|
# static-haskell-nix. Using callCabal2nix on the haskellPackages that
|
||||||
|
# it returns would result in a dynamic build based on musl, and not the
|
||||||
|
# fully static build that we want.
|
||||||
|
"${name}" = prev.callCabal2nix name src { };
|
||||||
|
};
|
||||||
|
|
||||||
|
overlays =
|
||||||
|
[
|
||||||
|
allOverlays.postgresql-future
|
||||||
|
(allOverlays.postgresql-default { inherit patches; })
|
||||||
|
(allOverlays.haskell-packages { inherit compiler extraOverrides; })
|
||||||
|
# Disable failing tests for postgresql on musl that should have no impact
|
||||||
|
# on the libpq that we need (collate.icu.utf8 and foreign regression
|
||||||
|
# tests)
|
||||||
|
(self: super:
|
||||||
|
{ postgresql = super.postgresql.overrideAttrs (_: { doCheck = false; }); }
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
# Apply our overlay to nixpkgs.
|
||||||
|
normalPkgs =
|
||||||
|
import nixpkgs { inherit overlays system; };
|
||||||
|
|
||||||
|
defaultCabalPackageVersionComingWithGhc =
|
||||||
|
{
|
||||||
|
ghc924 = "Cabal_3_6_3_0";
|
||||||
|
}."${compiler}";
|
||||||
|
|
||||||
|
# The static-haskell-nix 'survey' derives a full static set of Haskell
|
||||||
|
# packages, applying fixes where necessary.
|
||||||
|
survey =
|
||||||
|
import "${patched-static-haskell-nix}/survey" { inherit normalPkgs compiler defaultCabalPackageVersionComingWithGhc; };
|
||||||
|
in
|
||||||
|
{
|
||||||
|
inherit survey;
|
||||||
|
package = survey.haskellPackages."${name}";
|
||||||
|
}
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
{ mkDerivation
|
|
||||||
, array
|
|
||||||
, base
|
|
||||||
, base-compat
|
|
||||||
, base-orphans
|
|
||||||
, binary
|
|
||||||
, bytestring
|
|
||||||
, containers
|
|
||||||
, deepseq
|
|
||||||
, Diff
|
|
||||||
, directory
|
|
||||||
, filepath
|
|
||||||
, integer-logarithms
|
|
||||||
, mtl
|
|
||||||
, optparse-applicative
|
|
||||||
, parsec
|
|
||||||
, pretty
|
|
||||||
, process
|
|
||||||
, QuickCheck
|
|
||||||
, stdenv
|
|
||||||
, stm
|
|
||||||
, tagged
|
|
||||||
, tar
|
|
||||||
, tasty
|
|
||||||
, tasty-golden
|
|
||||||
, tasty-hunit
|
|
||||||
, tasty-quickcheck
|
|
||||||
, temporary
|
|
||||||
, text
|
|
||||||
, time
|
|
||||||
, transformers
|
|
||||||
, tree-diff
|
|
||||||
, unix
|
|
||||||
}:
|
|
||||||
mkDerivation {
|
|
||||||
pname = "Cabal";
|
|
||||||
version = "3.0.0.0";
|
|
||||||
sha256 = "5143ec26d740c1a508c93a8860e64407e7546c29b9817db20ff1595c1968d287";
|
|
||||||
setupHaskellDepends = [ mtl parsec ];
|
|
||||||
libraryHaskellDepends = [
|
|
||||||
array
|
|
||||||
base
|
|
||||||
binary
|
|
||||||
bytestring
|
|
||||||
containers
|
|
||||||
deepseq
|
|
||||||
directory
|
|
||||||
filepath
|
|
||||||
mtl
|
|
||||||
parsec
|
|
||||||
pretty
|
|
||||||
process
|
|
||||||
text
|
|
||||||
time
|
|
||||||
transformers
|
|
||||||
unix
|
|
||||||
];
|
|
||||||
testHaskellDepends = [
|
|
||||||
array
|
|
||||||
base
|
|
||||||
base-compat
|
|
||||||
base-orphans
|
|
||||||
binary
|
|
||||||
bytestring
|
|
||||||
containers
|
|
||||||
deepseq
|
|
||||||
Diff
|
|
||||||
directory
|
|
||||||
filepath
|
|
||||||
integer-logarithms
|
|
||||||
optparse-applicative
|
|
||||||
pretty
|
|
||||||
process
|
|
||||||
QuickCheck
|
|
||||||
stm
|
|
||||||
tagged
|
|
||||||
tar
|
|
||||||
tasty
|
|
||||||
tasty-golden
|
|
||||||
tasty-hunit
|
|
||||||
tasty-quickcheck
|
|
||||||
temporary
|
|
||||||
text
|
|
||||||
tree-diff
|
|
||||||
];
|
|
||||||
doCheck = false;
|
|
||||||
homepage = "http://www.haskell.org/cabal/";
|
|
||||||
description = "A framework for packaging Haskell software";
|
|
||||||
license = stdenv.lib.licenses.bsd3;
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
# Turn a Haskell source package (given its name and source) into a derivation
|
|
||||||
# for a fully static executable.
|
|
||||||
{ nixpkgs, compiler, name, src, patches }:
|
|
||||||
let
|
|
||||||
# The nh2/static-haskell-nix project does all the hard work for us for
|
|
||||||
# building static Haskell executables. We apply a patch for PostgREST below
|
|
||||||
# until the respective pull request is merged. See:
|
|
||||||
# https://github.com/nh2/static-haskell-nix/pull/91
|
|
||||||
# statix-haskell-nix builds everything based on Musl instead of glibc, so
|
|
||||||
# there will be a _lot_ to rebuild if you don't use a binary cache.
|
|
||||||
static-haskell-nix =
|
|
||||||
let
|
|
||||||
rev = "749707fc90b781c3e653e67917a7d571fe82ae7b";
|
|
||||||
in
|
|
||||||
builtins.fetchTarball {
|
|
||||||
url = "https://github.com/nh2/static-haskell-nix/archive/${rev}.tar.gz";
|
|
||||||
sha256 = "155spda2lww378bhx68w6dxwqd5y6s9kin3qbgl2m23r3vmk3m3w";
|
|
||||||
};
|
|
||||||
|
|
||||||
patched-static-haskell-nix =
|
|
||||||
patches.applyPatches "patched-static-haskell-nix" static-haskell-nix
|
|
||||||
[
|
|
||||||
patches.static-haskell-nix-postgrest-libpq
|
|
||||||
];
|
|
||||||
|
|
||||||
haskellPackagesOverrides =
|
|
||||||
lib: final: prev:
|
|
||||||
{
|
|
||||||
# Add our source package.
|
|
||||||
"${name}" = prev.callCabal2nix name src {};
|
|
||||||
|
|
||||||
# cabal2nix depends on Cabal 3.0.*, while our pinned version of Nixpkgs
|
|
||||||
# only provides 2.4 or 3.2. So we pinned 3.0.0.0 in ./Cabal.nix
|
|
||||||
cabal2nix =
|
|
||||||
prev.cabal2nix.overrideScope
|
|
||||||
(self: super: { Cabal = self.callPackage ./Cabal.nix {}; });
|
|
||||||
|
|
||||||
# Pinned version of protolude.
|
|
||||||
protolude =
|
|
||||||
prev.callPackage ../overlays/haskell-packages/protolude.nix {};
|
|
||||||
|
|
||||||
# The tests for the packages below took a long time on static
|
|
||||||
# builds, so we disable them for now - to be investigated.
|
|
||||||
happy = lib.dontCheck prev.happy;
|
|
||||||
};
|
|
||||||
|
|
||||||
# This overlay adds our source package and applies adjustments to the
|
|
||||||
# derivation of other packages that it depends on. The overlay applies to
|
|
||||||
# Haskell packages for the given compiler, which we will later use
|
|
||||||
# with the static-haskell-nix survey.
|
|
||||||
overlay =
|
|
||||||
self: super:
|
|
||||||
# Override the set of Haskell packages at
|
|
||||||
# pkgs.haskell.packages."${compiler}".
|
|
||||||
{
|
|
||||||
haskell = super.haskell // {
|
|
||||||
packages = super.haskell.packages // {
|
|
||||||
"${compiler}" =
|
|
||||||
super.haskell.packages."${compiler}".override
|
|
||||||
{ overrides = haskellPackagesOverrides self.haskell.lib; };
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
patchedNixpkgs =
|
|
||||||
patches.applyPatches "patched-nixpkgs" nixpkgs
|
|
||||||
[
|
|
||||||
patches.nixpkgs-revert-ghc-bootstrap
|
|
||||||
];
|
|
||||||
|
|
||||||
# Apply our overlay to the given pkgs.
|
|
||||||
normalPkgs =
|
|
||||||
(import patchedNixpkgs {}).appendOverlays [ overlay ];
|
|
||||||
|
|
||||||
# Each version of GHC needs a specific version of Cabal.
|
|
||||||
defaultCabalPackageVersionComingWithGhc =
|
|
||||||
{
|
|
||||||
ghc883 = "Cabal_3_2_0_0";
|
|
||||||
}."${compiler}";
|
|
||||||
|
|
||||||
# Let the static-haskell-nix project do the hard work of deriving a set of
|
|
||||||
# fully static Haskell executables, including one for the our source package
|
|
||||||
# that we added through the overlay.
|
|
||||||
survey =
|
|
||||||
import "${patched-static-haskell-nix}/survey"
|
|
||||||
{ inherit normalPkgs compiler defaultCabalPackageVersionComingWithGhc; };
|
|
||||||
in
|
|
||||||
# Return the fully static derivation of our source package.
|
|
||||||
survey.haskellPackages."${name}"
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
{ writeShellScriptBin
|
|
||||||
, git
|
|
||||||
, nixpkgs-fmt
|
|
||||||
, silver-searcher
|
|
||||||
, stylish-haskell
|
|
||||||
, buildEnv
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
style =
|
|
||||||
writeShellScriptBin "postgrest-style"
|
|
||||||
''
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
rootdir="$(${git}/bin/git rev-parse --show-toplevel)"
|
|
||||||
|
|
||||||
# Format Nix files
|
|
||||||
${nixpkgs-fmt}/bin/nixpkgs-fmt "$rootdir" > /dev/null 2> /dev/null
|
|
||||||
|
|
||||||
# Format Haskell files
|
|
||||||
${silver-searcher}/bin/ag -l -g '\.l?hs$' "$rootdir" \
|
|
||||||
| xargs ${stylish-haskell}/bin/stylish-haskell -i
|
|
||||||
'';
|
|
||||||
|
|
||||||
check =
|
|
||||||
writeShellScriptBin "postgrest-style-check"
|
|
||||||
''
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
${style}/bin/${style.name}
|
|
||||||
|
|
||||||
${git}/bin/git diff-index --exit-code HEAD -- '*.hs' '*.lhs' '*.nix'
|
|
||||||
'';
|
|
||||||
in
|
|
||||||
buildEnv {
|
|
||||||
name = "postgrest-style";
|
|
||||||
paths = [ style check ];
|
|
||||||
}
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
# Utilities for running the PostgREST test suite
|
|
||||||
|
|
||||||
{ buildEnv
|
|
||||||
, cabal-install
|
|
||||||
, git
|
|
||||||
, lib
|
|
||||||
, postgrestBuildEnv
|
|
||||||
, postgresqlVersions
|
|
||||||
, runtimeShell
|
|
||||||
, writeShellScript
|
|
||||||
, writeShellScriptBin
|
|
||||||
}:
|
|
||||||
let
|
|
||||||
# Wrap the `test/with_tmp_db` script with the required dependencies from Nix.
|
|
||||||
withTmpDb =
|
|
||||||
postgresql:
|
|
||||||
writeShellScript "postgrest-test-${postgresql.name}"
|
|
||||||
''
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
export PATH=${postgresql}/bin:${git}/bin:${runtimeShell}/bin:"$PATH"
|
|
||||||
|
|
||||||
exec ${../test/with_tmp_db} "$@"
|
|
||||||
'';
|
|
||||||
|
|
||||||
# Script to run the Haskell test suite against a specific version of
|
|
||||||
# PostgreSQL.
|
|
||||||
testSpec =
|
|
||||||
name: postgresql:
|
|
||||||
writeShellScriptBin name
|
|
||||||
''
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
cat << EOF
|
|
||||||
|
|
||||||
Running spec against ${postgresql.name}...
|
|
||||||
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# TODO: Make this work outside nix-shell when installed with nix-env.
|
|
||||||
# Probably using postgrestBuildEnv somehow?
|
|
||||||
${withTmpDb postgresql} ${cabal-install}/bin/cabal v2-test \
|
|
||||||
--test-show-detail=direct
|
|
||||||
|
|
||||||
cat << EOF
|
|
||||||
|
|
||||||
Done running spec against ${postgresql.name}.
|
|
||||||
|
|
||||||
EOF
|
|
||||||
'';
|
|
||||||
|
|
||||||
# The PostgreSQL version that we run the tests against by default.
|
|
||||||
defaultPostgresql =
|
|
||||||
builtins.head postgresqlVersions;
|
|
||||||
|
|
||||||
defaultTestSpec =
|
|
||||||
testSpec "postgrest-test-spec" defaultPostgresql;
|
|
||||||
|
|
||||||
# Create a `testSpec` for each PostgreSQL version that we want to test
|
|
||||||
# against.
|
|
||||||
testSpecVersions =
|
|
||||||
map
|
|
||||||
(postgresql: testSpec "postgrest-test-spec-${postgresql.name}" postgresql)
|
|
||||||
postgresqlVersions;
|
|
||||||
|
|
||||||
# Helper script for running the tests against all PostgreSQL versions.
|
|
||||||
testSpecAllVersions =
|
|
||||||
let
|
|
||||||
testRunners =
|
|
||||||
map (test: "${test}/bin/${test.name}") testSpecVersions;
|
|
||||||
in
|
|
||||||
writeShellScriptBin "postgrest-test-spec-all"
|
|
||||||
''
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
${lib.concatStringsSep "\n" testRunners}
|
|
||||||
'';
|
|
||||||
in
|
|
||||||
# Create an environment that contains all the utility scripts for running tests
|
|
||||||
# that we defined above.
|
|
||||||
buildEnv {
|
|
||||||
name =
|
|
||||||
"postgrest-tests";
|
|
||||||
|
|
||||||
paths =
|
|
||||||
[
|
|
||||||
defaultTestSpec
|
|
||||||
testSpecAllVersions
|
|
||||||
] ++ testSpecVersions;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
{ buildToolbox
|
||||||
|
, cabal-install
|
||||||
|
, checkedShellScript
|
||||||
|
, devCabalOptions
|
||||||
|
, postgrest
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
build =
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
name = "postgrest-build";
|
||||||
|
docs = "Build PostgREST interactively using cabal-install.";
|
||||||
|
args = [ "ARG_LEFTOVERS([Cabal arguments])" ];
|
||||||
|
inRootDir = true;
|
||||||
|
withEnv = postgrest.env;
|
||||||
|
}
|
||||||
|
''
|
||||||
|
exec ${cabal-install}/bin/cabal v2-build ${devCabalOptions} "''${_arg_leftovers[@]}"
|
||||||
|
'';
|
||||||
|
|
||||||
|
clean =
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
name = "postgrest-clean";
|
||||||
|
docs = "Clean the PostgREST project, including all cabal-install artifacts.";
|
||||||
|
inRootDir = true;
|
||||||
|
}
|
||||||
|
''
|
||||||
|
# clean old coverage data, too
|
||||||
|
rm -rf .hpc coverage
|
||||||
|
# clean old hie files
|
||||||
|
find . -name "*.hie" -type f -delete
|
||||||
|
exec ${cabal-install}/bin/cabal v2-clean
|
||||||
|
'';
|
||||||
|
|
||||||
|
run =
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
name = "postgrest-run";
|
||||||
|
docs = "Run PostgREST after building it interactively with cabal-install";
|
||||||
|
args =
|
||||||
|
[
|
||||||
|
"ARG_USE_ENV([PGRST_DB_ANON_ROLE], [postgrest_test_anonymous], [PostgREST anonymous role])"
|
||||||
|
"ARG_USE_ENV([PGRST_DB_POOL], [1], [PostgREST pool size])"
|
||||||
|
"ARG_USE_ENV([PGRST_DB_POOL_ACQUISITION_TIMEOUT], [1], [PostgREST pool size])"
|
||||||
|
"ARG_LEFTOVERS([PostgREST arguments])"
|
||||||
|
];
|
||||||
|
inRootDir = true;
|
||||||
|
withEnv = postgrest.env;
|
||||||
|
}
|
||||||
|
''
|
||||||
|
export PGRST_DB_ANON_ROLE
|
||||||
|
export PGRST_DB_POOL
|
||||||
|
export PGRST_DB_POOL_ACQUISITION_TIMEOUT
|
||||||
|
|
||||||
|
exec ${cabal-install}/bin/cabal v2-run ${devCabalOptions} --verbose=0 -- \
|
||||||
|
postgrest "''${_arg_leftovers[@]}"
|
||||||
|
'';
|
||||||
|
|
||||||
|
in
|
||||||
|
buildToolbox
|
||||||
|
{
|
||||||
|
name = "postgrest-cabal";
|
||||||
|
tools = [
|
||||||
|
build
|
||||||
|
clean
|
||||||
|
run
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,308 @@
|
|||||||
|
{ buildToolbox
|
||||||
|
, cabal-install
|
||||||
|
, cachix
|
||||||
|
, checkedShellScript
|
||||||
|
, devCabalOptions
|
||||||
|
, entr
|
||||||
|
, git
|
||||||
|
, gnugrep
|
||||||
|
, graphviz
|
||||||
|
, haskellPackages
|
||||||
|
, hsie
|
||||||
|
, nix
|
||||||
|
, silver-searcher
|
||||||
|
, style
|
||||||
|
, tests
|
||||||
|
, withTools
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
watch =
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
name = "postgrest-watch";
|
||||||
|
docs =
|
||||||
|
''
|
||||||
|
Watch the project for changes and reinvoke the given command.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
postgrest-watch postgrest-test-io
|
||||||
|
'';
|
||||||
|
args =
|
||||||
|
[
|
||||||
|
"ARG_POSITIONAL_SINGLE([command], [Command to run])"
|
||||||
|
"ARG_LEFTOVERS([command arguments])"
|
||||||
|
];
|
||||||
|
positionalCompletion = "_command";
|
||||||
|
redirectTixFiles = false; # will be done by sub-command
|
||||||
|
inRootDir = true;
|
||||||
|
}
|
||||||
|
''
|
||||||
|
while true; do
|
||||||
|
(! ${silver-searcher}/bin/ag -l . | ${entr}/bin/entr -dr "$_arg_command" "''${_arg_leftovers[@]}")
|
||||||
|
done
|
||||||
|
'';
|
||||||
|
|
||||||
|
pushCachix =
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
name = "postgrest-push-cachix";
|
||||||
|
docs = ''
|
||||||
|
Push all build artifacts to cachix.
|
||||||
|
|
||||||
|
Requires authentication with `cachix authtoken ...`.
|
||||||
|
'';
|
||||||
|
inRootDir = true;
|
||||||
|
}
|
||||||
|
''
|
||||||
|
${nix}/bin/nix-instantiate \
|
||||||
|
| while read -r drv; do
|
||||||
|
${nix}/bin/nix-store -qR --include-outputs "$drv"
|
||||||
|
done \
|
||||||
|
| ${cachix}/bin/cachix push postgrest
|
||||||
|
'';
|
||||||
|
|
||||||
|
check =
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
name = "postgrest-check";
|
||||||
|
docs =
|
||||||
|
''
|
||||||
|
Run most checks that will also run on CI, but only against the
|
||||||
|
latest PostgreSQL version.
|
||||||
|
|
||||||
|
This currently excludes the memory and spec-idempotence tests,
|
||||||
|
as those are particularly expensive.
|
||||||
|
'';
|
||||||
|
inRootDir = true;
|
||||||
|
}
|
||||||
|
''
|
||||||
|
${tests}/bin/postgrest-test-spec
|
||||||
|
${tests}/bin/postgrest-test-doctests
|
||||||
|
${tests}/bin/postgrest-test-io
|
||||||
|
${style}/bin/postgrest-lint
|
||||||
|
${style}/bin/postgrest-style-check
|
||||||
|
'';
|
||||||
|
|
||||||
|
gitHooks =
|
||||||
|
let
|
||||||
|
name = "postgrest-git-hooks";
|
||||||
|
in
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
inherit name;
|
||||||
|
docs =
|
||||||
|
''
|
||||||
|
Enable or disable git pre-commit and pre-push hooks.
|
||||||
|
|
||||||
|
Basic is faster and will only run:
|
||||||
|
- pre-commit: postgrest-style
|
||||||
|
- pre-push: postgrest-lint
|
||||||
|
|
||||||
|
Full takes a lot more time and will run:
|
||||||
|
- pre-commit: postgrest-style && postgrest-lint
|
||||||
|
- pre-push: postgrest-check
|
||||||
|
|
||||||
|
Changes made by postgrest-style will be staged automatically.
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
postgrest-git-hooks disable
|
||||||
|
postgrest-git-hooks enable basic
|
||||||
|
postgrest-git-hooks enable full
|
||||||
|
|
||||||
|
The "run" operation and "--hook" argument are only used internally.
|
||||||
|
'';
|
||||||
|
args =
|
||||||
|
[
|
||||||
|
"ARG_POSITIONAL_SINGLE([operation], [Operation])"
|
||||||
|
"ARG_TYPE_GROUP_SET([OPERATION], [OPERATION], [operation], [disable,enable,run])"
|
||||||
|
"ARG_POSITIONAL_SINGLE([mode], [Mode], [basic])"
|
||||||
|
"ARG_TYPE_GROUP_SET([MODE], [MODE], [mode], [basic,full])"
|
||||||
|
"ARG_OPTIONAL_SINGLE([hook], , [Hook], [pre-commit])"
|
||||||
|
"ARG_TYPE_GROUP_SET([HOOK], [HOOK], [hook], [pre-commit,pre-push])"
|
||||||
|
];
|
||||||
|
positionalCompletion =
|
||||||
|
''
|
||||||
|
if test "$prev" == "${name}"; then
|
||||||
|
COMPREPLY=( $(compgen -W "enable disable" -- "$cur") )
|
||||||
|
elif test "$prev" == "enable" || test "$prev" == "disable"; then
|
||||||
|
COMPREPLY=( $(compgen -W "basic full" -- "$cur") )
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
inRootDir = true;
|
||||||
|
}
|
||||||
|
''
|
||||||
|
if [ run != "$_arg_operation" ]; then
|
||||||
|
# Remove all hooks first and ignore failures because the file might be missing.
|
||||||
|
# This assumes that we're only adding lines that include "postgrest-git-hooks"
|
||||||
|
# to the hook file.
|
||||||
|
sed -i -e '/postgrest-git-hooks/d' .git/hooks/pre-{commit,push} 2> /dev/null || true
|
||||||
|
|
||||||
|
if [ disable != "$_arg_operation" ]; then
|
||||||
|
# The nix-shell && + nix-shell || pattern makes sure we can run the hook
|
||||||
|
# in a pure nix-shell, where nix-shell itself is not available, too.
|
||||||
|
|
||||||
|
# The $(nix-shell --run "command -v ...") pattern ensures we only need to enable
|
||||||
|
# the hooks once and still run the latest of our hook scripts, even when we
|
||||||
|
# update them in the repo.
|
||||||
|
|
||||||
|
echo 'command -v nix-shell > /dev/null || postgrest-git-hooks --hook=pre-commit run' "$_arg_mode" \
|
||||||
|
>> .git/hooks/pre-commit
|
||||||
|
# shellcheck disable=SC2016
|
||||||
|
echo 'command -v nix-shell > /dev/null && $(nix-shell --quiet -Q --run "command -v postgrest-git-hooks") --hook=pre-commit run' "$_arg_mode" \
|
||||||
|
>> .git/hooks/pre-commit
|
||||||
|
chmod +x .git/hooks/pre-commit
|
||||||
|
|
||||||
|
echo 'command -v nix-shell > /dev/null || postgrest-git-hooks --hook=pre-push run' "$_arg_mode" \
|
||||||
|
>> .git/hooks/pre-push
|
||||||
|
# shellcheck disable=SC2016
|
||||||
|
echo 'command -v nix-shell > /dev/null && $(nix-shell --quiet -Q --run "command -v postgrest-git-hooks") --hook=pre-push run' "$_arg_mode" \
|
||||||
|
>> .git/hooks/pre-push
|
||||||
|
chmod +x .git/hooks/pre-push
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# When run from a git hook, the GIT_ environment variables conflict with our withGit helper.
|
||||||
|
# The following unsets all GIT_ variables.
|
||||||
|
unset "''${!GIT_@}"
|
||||||
|
|
||||||
|
# shellcheck disable=SC2317
|
||||||
|
function restore () {
|
||||||
|
ref="$(git stash list --format=format:%gD --grep "$1" -n1)"
|
||||||
|
# this will avoid merge conflicts when applying the stash
|
||||||
|
${git}/bin/git restore --source="$ref" .
|
||||||
|
# restore untracked files, too. could fail with no files
|
||||||
|
if [ "$(git show --numstat --format=oneline "$ref^3" | wc -l)" -gt 1 ]; then
|
||||||
|
${git}/bin/git restore --overlay --source="$ref^3" .
|
||||||
|
fi
|
||||||
|
${git}/bin/git stash drop "$ref"
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$_arg_mode" in
|
||||||
|
basic)
|
||||||
|
case "$_arg_hook" in
|
||||||
|
pre-commit)
|
||||||
|
# To be able to automatically add only changes from postgrest-style to the staging area,
|
||||||
|
# we need to run postgrest-style twice. Otherwise we'd risk merge conflicts when popping
|
||||||
|
# the stash afterwards.
|
||||||
|
${style}/bin/postgrest-style
|
||||||
|
|
||||||
|
stash="postgrest-git-hooks-$RANDOM"
|
||||||
|
${git}/bin/git stash push --include-untracked --keep-index -m "$stash"
|
||||||
|
if [ "$(git stash list --grep $stash)" ]; then
|
||||||
|
# Only create the stash pop trap, if we actually created a stash.
|
||||||
|
# Otherwise stash pop will cause havoc.
|
||||||
|
trap 'restore "$stash"' EXIT
|
||||||
|
fi
|
||||||
|
|
||||||
|
${style}/bin/postgrest-style
|
||||||
|
${git}/bin/git add .
|
||||||
|
;;
|
||||||
|
pre-push)
|
||||||
|
# Create a clean working tree without any uncomitted changes.
|
||||||
|
${withTools.withGit} HEAD ${style}/bin/postgrest-lint
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
full)
|
||||||
|
case "$_arg_hook" in
|
||||||
|
pre-commit)
|
||||||
|
# To be able to automatically add only changes from postgrest-style to the staging area,
|
||||||
|
# we need to run postgrest-style twice. Otherwise we'd risk merge conflicts when popping
|
||||||
|
# the stash afterwards.
|
||||||
|
${style}/bin/postgrest-style
|
||||||
|
|
||||||
|
stash="postgrest-git-hooks-$RANDOM"
|
||||||
|
${git}/bin/git stash push --include-untracked --keep-index -m "$stash"
|
||||||
|
if [ "$(git stash list --grep $stash)" ]; then
|
||||||
|
# Only create the stash pop trap, if we actually created a stash.
|
||||||
|
# Otherwise stash pop will cause havoc.
|
||||||
|
trap 'restore "$stash"' EXIT
|
||||||
|
fi
|
||||||
|
|
||||||
|
${style}/bin/postgrest-style
|
||||||
|
${git}/bin/git add .
|
||||||
|
|
||||||
|
${style}/bin/postgrest-lint
|
||||||
|
;;
|
||||||
|
pre-push)
|
||||||
|
# Create a clean working tree without any uncomitted changes.
|
||||||
|
${withTools.withGit} HEAD ${check}
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
|
||||||
|
dumpMinimalImports =
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
name = "postgrest-dump-minimal-imports";
|
||||||
|
docs = "Dump minimal imports into given directory.";
|
||||||
|
args = [ "ARG_POSITIONAL_SINGLE([dumpdir], [Output directory])" ];
|
||||||
|
inRootDir = true;
|
||||||
|
withTmpDir = true;
|
||||||
|
}
|
||||||
|
''
|
||||||
|
mkdir -p "$_arg_dumpdir"
|
||||||
|
${cabal-install}/bin/cabal v2-build ${devCabalOptions} \
|
||||||
|
--builddir="$tmpdir" \
|
||||||
|
--ghc-option=-ddump-minimal-imports \
|
||||||
|
--ghc-option=-dumpdir="$_arg_dumpdir" \
|
||||||
|
1>&2
|
||||||
|
|
||||||
|
# Fix OverloadedRecordFields imports
|
||||||
|
# shellcheck disable=SC2016
|
||||||
|
sed -E 's/\$sel:.*://g' -i "$_arg_dumpdir"/*
|
||||||
|
'';
|
||||||
|
|
||||||
|
hsieMinimalImports =
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
name = "postgrest-hsie-minimal-imports";
|
||||||
|
docs = "Run hsie with a provided dump of minimal imports.";
|
||||||
|
args = [ "ARG_LEFTOVERS([hsie arguments])" ];
|
||||||
|
withTmpDir = true;
|
||||||
|
}
|
||||||
|
''
|
||||||
|
${dumpMinimalImports} "$tmpdir"
|
||||||
|
${hsie} "$tmpdir" "''${_arg_leftovers[@]}"
|
||||||
|
'';
|
||||||
|
|
||||||
|
hsieGraphModules =
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
name = "postgrest-hsie-graph-modules";
|
||||||
|
docs = "Create a PNG graph of modules imported within the codebase.";
|
||||||
|
args = [ "ARG_POSITIONAL_SINGLE([outfile], [Output filename])" ];
|
||||||
|
}
|
||||||
|
''
|
||||||
|
${hsie} graph-modules main src | ${graphviz}/bin/dot -Tpng -o "$_arg_outfile"
|
||||||
|
'';
|
||||||
|
|
||||||
|
hsieGraphSymbols =
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
name = "postgrest-hsie-graph-symbols";
|
||||||
|
docs = "Create a PNG graph of symbols imported within the codebase.";
|
||||||
|
args = [ "ARG_POSITIONAL_SINGLE([outfile], [Output filename])" ];
|
||||||
|
}
|
||||||
|
''
|
||||||
|
${hsieMinimalImports} graph-symbols | ${graphviz}/bin/dot -Tpng -o "$_arg_outfile"
|
||||||
|
'';
|
||||||
|
|
||||||
|
in
|
||||||
|
buildToolbox
|
||||||
|
{
|
||||||
|
name = "postgrest-dev";
|
||||||
|
tools = [
|
||||||
|
watch
|
||||||
|
pushCachix
|
||||||
|
check
|
||||||
|
gitHooks
|
||||||
|
dumpMinimalImports
|
||||||
|
hsieMinimalImports
|
||||||
|
hsieGraphModules
|
||||||
|
hsieGraphSymbols
|
||||||
|
];
|
||||||
|
extra = { inherit pushCachix; };
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
# Docker image built with Nix
|
||||||
|
|
||||||
|
In order to build an optimal PostgREST Docker image, we create the image from
|
||||||
|
scratch (i.e., without a parent image like `debian` or `alpine`), and only
|
||||||
|
include the file that is essential for running PostgREST: the static
|
||||||
|
PostgREST binary.
|
||||||
|
|
||||||
|
This is similar to what you would get with the following `Dockerfile`:
|
||||||
|
|
||||||
|
```Dockerfile
|
||||||
|
# `scratch` is a minimal, reserved image in Docker, see
|
||||||
|
# https://docs.docker.com/develop/develop-images/baseimages/ . It essentially
|
||||||
|
# means "don't use a parent image and start with an empty one".
|
||||||
|
FROM scratch
|
||||||
|
|
||||||
|
# The static PostgREST executable has no runtime dependencies, so it's all we
|
||||||
|
# need to include for running the application.
|
||||||
|
ADD /absolute/path/to/postgrest /bin/postgrest
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# This is the user id that Docker will run our image under by default. Note
|
||||||
|
# that we don't actually add the user to `/etc/passwd` or `/etc/shadow`. This
|
||||||
|
# means that tools like whoami would not work properly, but we don't include
|
||||||
|
# those in the image anyway. Not adding the user has the benefit that the image
|
||||||
|
# can be run under any user you specify.
|
||||||
|
USER 1000
|
||||||
|
|
||||||
|
CMD [ "/bin/postgrest" ]
|
||||||
|
```
|
||||||
|
|
||||||
|
# Building the Docker image with Nix
|
||||||
|
|
||||||
|
As we are building the static PostgREST executable with Nix and that's the main
|
||||||
|
input to the Docker file, we can also create the Docker image directly with Nix
|
||||||
|
using the [`dockerTools`
|
||||||
|
utilities](https://nixos.org/nixpkgs/manual/#sec-pkgs-dockerTools). Those
|
||||||
|
utilities don't actually use `Dockerfiles` or Docker to build Docker images,
|
||||||
|
but create them directly by putting together the required `json` and `tar`
|
||||||
|
files that make up an image. This is more efficient, does not rely on Docker or
|
||||||
|
root permissions and results in fully reproducible builds. See
|
||||||
|
[`nix/docker/default.nix`](./default.nix) for details how the image is built.
|
||||||
|
|
||||||
|
# Building and loading the image
|
||||||
|
|
||||||
|
The Nix expression provides a helper script `postgrest-docker-load` that loads
|
||||||
|
the optimized image into your local Docker instance (using `docker load -i
|
||||||
|
<image file>` under the hood). You can use it by running:
|
||||||
|
|
||||||
|
```
|
||||||
|
# Running from the root directory of the repository:
|
||||||
|
|
||||||
|
# Build the `docker` attribute from `default.nix`, the result will be symlinked
|
||||||
|
# to `result`:
|
||||||
|
nix-build -A docker
|
||||||
|
|
||||||
|
# Run the loading script:
|
||||||
|
result/bin/postgrest-docker-load
|
||||||
|
```
|
||||||
|
|
||||||
|
The Docker image built with Nix always has the name "postgrest:latest" when
|
||||||
|
loaded.
|
||||||
|
|
||||||
|
# Inspecting the optimized image
|
||||||
|
|
||||||
|
The image does not come with the usual utilities like `bash` and `ls`.
|
||||||
|
|
||||||
|
You can, however, explore the `tar` file of the image by saving it with `docker
|
||||||
|
save postgrest:latest > image.tar`.
|
||||||
|
|
||||||
|
[Dive](https://github.com/wagoodman/dive) is also useful for looking at the
|
||||||
|
contents of the image:
|
||||||
|
|
||||||
|
```
|
||||||
|
┃ ● Layers ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ Current Layer Contents ├────────────────────────────────────────────────────────────────────────────────
|
||||||
|
Cmp Size Command Permission UID:GID Size Filetree
|
||||||
|
14 MB FROM 20ee65c811575d2 dr-xr-xr-x 0:0 14 MB ├── bin
|
||||||
|
-r-xr-xr-x 0:0 14 MB │ └── postgrest
|
||||||
|
│ Layer Details ├───────────────────────────────────────────────────────────────────────────────────────── drwxr-xr-x 0:0 783 B ├── etc
|
||||||
|
-r--r--r-- 0:0 783 B │ └── postgrest.conf
|
||||||
|
Tags: (unavailable) dr-xr-xr-x 0:0 23 kB └── nix
|
||||||
|
Id: 20ee65c811575d206eb673e1887e7f7e6b7ccde902a63ccb924c5faa50b32cee dr-xr-xr-x 0:0 23 kB └── store
|
||||||
|
Digest: sha256:ece77302b83fd38fb54395dabc10c2eba06fc1d1933801d36cc2c4732d9c8f38 dr-xr-xr-x 0:0 23 kB └── s440jbrn94wmpzy7f8yfsp6jr2shllw5-openssl-1.1.1g-etc
|
||||||
|
Command: dr-xr-xr-x 0:0 23 kB └── etc
|
||||||
|
dr-xr-xr-x 0:0 23 kB └── ssl
|
||||||
|
-r--r--r-- 0:0 412 B ├── ct_log_list.cnf
|
||||||
|
│ Image Details ├───────────────────────────────────────────────────────────────────────────────────────── -r--r--r-- 0:0 412 B ├── ct_log_list.cnf.dist
|
||||||
|
dr-xr-xr-x 0:0 0 B ├── engines-1.1
|
||||||
|
-r--r--r-- 0:0 11 kB ├── openssl.cnf
|
||||||
|
Total Image size: 14 MB -r--r--r-- 0:0 11 kB └── openssl.cnf.dist
|
||||||
|
Potential wasted space: 0 B
|
||||||
|
Image efficiency score: 100 %
|
||||||
|
|
||||||
|
Count Total Space Path
|
||||||
|
```
|
||||||
|
|
||||||
|
# Deriving from the optimized image
|
||||||
|
|
||||||
|
Since the docker image is minimal, it does not contain a shell or other utilities.
|
||||||
|
To derive a non-minimal image, you can do the following:
|
||||||
|
|
||||||
|
```Dockerfile
|
||||||
|
# derive from any base image you want
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
# copy PostgREST over
|
||||||
|
COPY --from=postgrest/postgrest /bin/postgrest /bin
|
||||||
|
|
||||||
|
# add your other stuff
|
||||||
|
```
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
{ buildToolbox
|
||||||
|
, postgrest
|
||||||
|
, dockerTools
|
||||||
|
, checkedShellScript
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
image =
|
||||||
|
dockerTools.buildImage {
|
||||||
|
name = "postgrest";
|
||||||
|
tag = "latest";
|
||||||
|
copyToRoot = postgrest;
|
||||||
|
|
||||||
|
# Set the current time as the image creation date. This makes the build
|
||||||
|
# non-reproducible, but that should not be an issue for us.
|
||||||
|
created = "now";
|
||||||
|
|
||||||
|
extraCommands =
|
||||||
|
''
|
||||||
|
rmdir share
|
||||||
|
'';
|
||||||
|
|
||||||
|
config = {
|
||||||
|
Cmd = [ "/bin/postgrest" ];
|
||||||
|
User = "1000";
|
||||||
|
ExposedPorts = {
|
||||||
|
"3000/tcp" = { };
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
load =
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
name = "postgrest-docker-load";
|
||||||
|
docs = "Load the PostgREST image into Docker.";
|
||||||
|
}
|
||||||
|
''
|
||||||
|
docker load -i ${image}
|
||||||
|
'';
|
||||||
|
|
||||||
|
in
|
||||||
|
buildToolbox
|
||||||
|
{
|
||||||
|
name = "postgrest-docker";
|
||||||
|
tools = [ load ];
|
||||||
|
extra = {
|
||||||
|
inherit image;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
{ buildToolbox
|
||||||
|
, checkedShellScript
|
||||||
|
, jq
|
||||||
|
, python3Packages
|
||||||
|
, vegeta
|
||||||
|
, withTools
|
||||||
|
, writers
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
runner =
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
name = "postgrest-loadtest-runner";
|
||||||
|
docs = "Run vegeta. Assume PostgREST to be running.";
|
||||||
|
args = [
|
||||||
|
"ARG_LEFTOVERS([additional vegeta arguments])"
|
||||||
|
"ARG_USE_ENV([PGRST_SERVER_UNIX_SOCKET], [], [Unix socket to connect to running PostgREST instance])"
|
||||||
|
];
|
||||||
|
}
|
||||||
|
''
|
||||||
|
# ARG_USE_ENV only adds defaults or docs for environment variables
|
||||||
|
# We manually implement a required check here
|
||||||
|
# See also: https://github.com/matejak/argbash/issues/80
|
||||||
|
: "''${PGRST_SERVER_UNIX_SOCKET:?PGRST_SERVER_UNIX_SOCKET is required}"
|
||||||
|
|
||||||
|
${vegeta}/bin/vegeta -cpus 1 attack \
|
||||||
|
-unix-socket "$PGRST_SERVER_UNIX_SOCKET" \
|
||||||
|
-max-workers 1 \
|
||||||
|
-workers 1 \
|
||||||
|
-rate 0 \
|
||||||
|
-duration 60s \
|
||||||
|
"''${_arg_leftovers[@]}"
|
||||||
|
'';
|
||||||
|
|
||||||
|
loadtest =
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
name = "postgrest-loadtest";
|
||||||
|
docs = "Run the vegeta loadtests with PostgREST.";
|
||||||
|
args = [
|
||||||
|
"ARG_OPTIONAL_SINGLE([output], [o], [Filename to dump json output to], [./loadtest/result.bin])"
|
||||||
|
"ARG_OPTIONAL_SINGLE([testdir], [t], [Directory to load tests and fixtures from], [./test/load])"
|
||||||
|
"ARG_LEFTOVERS([additional vegeta arguments])"
|
||||||
|
];
|
||||||
|
inRootDir = true;
|
||||||
|
}
|
||||||
|
''
|
||||||
|
# previously required settings to make this work with older branches
|
||||||
|
export PGRST_DB_ANON_ROLE="postgrest_test_anonymous"
|
||||||
|
export PGRST_DB_URI="postgresql://"
|
||||||
|
export PGRST_DB_SCHEMAS="test"
|
||||||
|
|
||||||
|
export PGRST_DB_CONFIG="false"
|
||||||
|
export PGRST_DB_POOL="1"
|
||||||
|
export PGRST_DB_TX_END="rollback-allow-override"
|
||||||
|
export PGRST_LOG_LEVEL="crit"
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "$_arg_output")"
|
||||||
|
abs_output="$(realpath "$_arg_output")"
|
||||||
|
|
||||||
|
# shellcheck disable=SC2145
|
||||||
|
${withTools.withPg} --fixtures "$_arg_testdir"/fixtures.sql \
|
||||||
|
${withTools.withSlowPg} \
|
||||||
|
${withTools.withPgrst} \
|
||||||
|
${withTools.withSlowPgrst} \
|
||||||
|
sh -c "cd \"$_arg_testdir\" && ${runner} -targets targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
|
||||||
|
${vegeta}/bin/vegeta report -type=text "$_arg_output"
|
||||||
|
'';
|
||||||
|
|
||||||
|
loadtestAgainst =
|
||||||
|
let
|
||||||
|
name = "postgrest-loadtest-against";
|
||||||
|
in
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
inherit name;
|
||||||
|
docs =
|
||||||
|
''
|
||||||
|
Run the vegeta loadtest twice:
|
||||||
|
- once on the <target> branch
|
||||||
|
- once in the current worktree
|
||||||
|
'';
|
||||||
|
args = [
|
||||||
|
"ARG_POSITIONAL_SINGLE([target], [Commit-ish reference to compare with])"
|
||||||
|
"ARG_LEFTOVERS([additional vegeta arguments])"
|
||||||
|
];
|
||||||
|
positionalCompletion =
|
||||||
|
''
|
||||||
|
if test "$prev" == "${name}"; then
|
||||||
|
__gitcomp_nl "$(__git_refs)"
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
inRootDir = true;
|
||||||
|
}
|
||||||
|
''
|
||||||
|
cat << EOF
|
||||||
|
|
||||||
|
Running loadtest on "$_arg_target"...
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Runs the test files from the current working tree
|
||||||
|
# to make sure both tests are run with the same files.
|
||||||
|
# Save the results in the current working tree, too,
|
||||||
|
# otherwise they'd be lost in the temporary working tree
|
||||||
|
# created by withTools.withGit.
|
||||||
|
${withTools.withGit} "$_arg_target" ${loadtest} --output "$PWD/loadtest/$_arg_target.bin" --testdir "$PWD/test/load" "''${_arg_leftovers[@]}"
|
||||||
|
|
||||||
|
cat << EOF
|
||||||
|
|
||||||
|
Done running on "$_arg_target".
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat << EOF
|
||||||
|
|
||||||
|
Running loadtest on HEAD...
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
${loadtest} --output "$PWD/loadtest/head.bin" --testdir "$PWD/test/load" "''${_arg_leftovers[@]}"
|
||||||
|
|
||||||
|
cat << EOF
|
||||||
|
|
||||||
|
Done running on HEAD.
|
||||||
|
|
||||||
|
EOF
|
||||||
|
'';
|
||||||
|
|
||||||
|
reporter =
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
name = "postgrest-loadtest-reporter";
|
||||||
|
docs = "Create a named json report for a single result file.";
|
||||||
|
args = [
|
||||||
|
"ARG_POSITIONAL_SINGLE([file], [Filename of result to create report for])"
|
||||||
|
"ARG_LEFTOVERS([additional vegeta arguments])"
|
||||||
|
];
|
||||||
|
inRootDir = true;
|
||||||
|
}
|
||||||
|
''
|
||||||
|
${vegeta}/bin/vegeta report -type=json "$_arg_file" \
|
||||||
|
| ${jq}/bin/jq --arg branch "$(basename "$_arg_file" .bin)" '. + {branch: $branch}'
|
||||||
|
'';
|
||||||
|
|
||||||
|
toMarkdown =
|
||||||
|
writers.writePython3 "postgrest-loadtest-to-markdown"
|
||||||
|
{
|
||||||
|
libraries = [ python3Packages.pandas python3Packages.tabulate ];
|
||||||
|
}
|
||||||
|
''
|
||||||
|
import sys
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
pd.read_json(sys.stdin) \
|
||||||
|
.set_index('param') \
|
||||||
|
.drop(['branch', 'earliest', 'end', 'latest']) \
|
||||||
|
.convert_dtypes() \
|
||||||
|
.to_markdown(sys.stdout, floatfmt='.0f')
|
||||||
|
'';
|
||||||
|
|
||||||
|
|
||||||
|
report =
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
name = "postgrest-loadtest-report";
|
||||||
|
docs = "Create a report of all loadtest reports as markdown.";
|
||||||
|
inRootDir = true;
|
||||||
|
}
|
||||||
|
''
|
||||||
|
find loadtest -type f -iname '*.bin' -exec ${reporter} {} \; \
|
||||||
|
| ${jq}/bin/jq '[leaf_paths as $path | {param: $path | join("."), (.branch): getpath($path)}]' \
|
||||||
|
| ${jq}/bin/jq --slurp 'flatten | group_by(.param) | map(add)' \
|
||||||
|
| ${toMarkdown}
|
||||||
|
'';
|
||||||
|
|
||||||
|
in
|
||||||
|
buildToolbox {
|
||||||
|
name = "postgrest-loadtest";
|
||||||
|
tools = [ loadtest loadtestAgainst report ];
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# The memory tests have large dependencies (a profiled build of PostgREST)
|
||||||
|
# and are run less often than the spec tests, so we don't include them in
|
||||||
|
# the default test environment. We make them available through a separate module.
|
||||||
|
{ buildToolbox
|
||||||
|
, checkedShellScript
|
||||||
|
, curl
|
||||||
|
, postgrestProfiled
|
||||||
|
, withTools
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
test =
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
name = "postgrest-test-memory";
|
||||||
|
docs = "Run the memory tests.";
|
||||||
|
inRootDir = true;
|
||||||
|
withPath = [ postgrestProfiled curl ];
|
||||||
|
}
|
||||||
|
''
|
||||||
|
${withTools.withPg} test/memory/memory-tests.sh
|
||||||
|
'';
|
||||||
|
|
||||||
|
in
|
||||||
|
buildToolbox
|
||||||
|
{
|
||||||
|
name = "postgrest-memory";
|
||||||
|
tools = [ test ];
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
{ buildToolbox
|
||||||
|
, checkedShellScript
|
||||||
|
, coreutils
|
||||||
|
, curl
|
||||||
|
, jq
|
||||||
|
, nix
|
||||||
|
}:
|
||||||
|
# Utility script for pinning the latest unstable version of Nixpkgs.
|
||||||
|
|
||||||
|
# Instead of pinning Nixpkgs based on the huge Git repository, we reference a
|
||||||
|
# specific tarball that only contains the source of the revision that we want
|
||||||
|
# to pin.
|
||||||
|
let
|
||||||
|
name =
|
||||||
|
"postgrest-nixpkgs-upgrade";
|
||||||
|
|
||||||
|
refUrl =
|
||||||
|
"https://api.github.com/repos/nixos/nixpkgs/git/ref/heads/nixpkgs-unstable";
|
||||||
|
|
||||||
|
githubV3Header =
|
||||||
|
"Accept: application/vnd.github.v3+json";
|
||||||
|
|
||||||
|
tarballUrlBase =
|
||||||
|
"https://github.com/nixos/nixpkgs/archive/";
|
||||||
|
|
||||||
|
upgrade =
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
inherit name;
|
||||||
|
docs = "Pin the newest unstable version of Nixpkgs.";
|
||||||
|
inRootDir = true;
|
||||||
|
}
|
||||||
|
''
|
||||||
|
commitHash="$(${curl}/bin/curl "${refUrl}" -H "${githubV3Header}" | ${jq}/bin/jq -r .object.sha)"
|
||||||
|
tarballUrl="${tarballUrlBase}$commitHash.tar.gz"
|
||||||
|
tarballHash="$(${nix}/bin/nix-prefetch-url --unpack "$tarballUrl")"
|
||||||
|
currentDate="$(${coreutils}/bin/date --iso)"
|
||||||
|
|
||||||
|
cat > nix/nixpkgs-version.nix << EOF
|
||||||
|
# Pinned version of Nixpkgs, generated with ${name}.
|
||||||
|
{
|
||||||
|
date = "$currentDate";
|
||||||
|
rev = "$commitHash";
|
||||||
|
tarballHash = "$tarballHash";
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
'';
|
||||||
|
|
||||||
|
in
|
||||||
|
buildToolbox
|
||||||
|
{
|
||||||
|
name = "postgrest-nixpkgs";
|
||||||
|
tools = [ upgrade ];
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
{ buildToolbox
|
||||||
|
, checkedShellScript
|
||||||
|
, curl
|
||||||
|
, jq
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
dockerHubDescription =
|
||||||
|
let
|
||||||
|
description =
|
||||||
|
./docker-hub-description.md;
|
||||||
|
|
||||||
|
fullDescription =
|
||||||
|
./docker-hub-full-description.md;
|
||||||
|
in
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
name = "postgrest-release-dockerhub-description";
|
||||||
|
docs = "Update the repository description on Docker Hub.";
|
||||||
|
args = [
|
||||||
|
"ARG_USE_ENV([DOCKER_USER], [], [DockerHub user name])"
|
||||||
|
"ARG_USE_ENV([DOCKER_PASS], [], [DockerHub password])"
|
||||||
|
"ARG_USE_ENV([DOCKER_REPO], [], [DockerHub repository])"
|
||||||
|
];
|
||||||
|
}
|
||||||
|
''
|
||||||
|
# ARG_USE_ENV only adds defaults or docs for environment variables
|
||||||
|
# We manually implement a required check here
|
||||||
|
# See also: https://github.com/matejak/argbash/issues/80
|
||||||
|
: "''${DOCKER_USER:?DOCKER_USER is required}"
|
||||||
|
: "''${DOCKER_PASS:?DOCKER_PASS is required}"
|
||||||
|
: "''${DOCKER_REPO:?DOCKER_REPO is required}"
|
||||||
|
|
||||||
|
echo "Logging in to Docker Hub to get an auth token..."
|
||||||
|
token="$(
|
||||||
|
${curl}/bin/curl --fail -s \
|
||||||
|
--data-urlencode "username=$DOCKER_USER" \
|
||||||
|
--data-urlencode "password=$DOCKER_PASS" \
|
||||||
|
"https://hub.docker.com/v2/users/login/" \
|
||||||
|
| ${jq}/bin/jq -r .token
|
||||||
|
)"
|
||||||
|
|
||||||
|
repo_url="https://hub.docker.com/v2/repositories/$DOCKER_REPO/postgrest/"
|
||||||
|
echo "Patching both descriptions at $repo_url ..."
|
||||||
|
${curl}/bin/curl --fail -X PATCH "$repo_url" \
|
||||||
|
-H "Authorization: JWT $token" \
|
||||||
|
--data-urlencode description@${description} \
|
||||||
|
--data-urlencode full_description@${fullDescription}
|
||||||
|
'';
|
||||||
|
|
||||||
|
release =
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
name = "postgrest-release";
|
||||||
|
docs = "Patch postgrest.cabal, CHANGELOG.md, tag and push all in one go.";
|
||||||
|
args = [ "ARG_POSITIONAL_SINGLE([version], [Version to release], [pre])" ];
|
||||||
|
inRootDir = true;
|
||||||
|
}
|
||||||
|
''
|
||||||
|
trap "echo You need to be on the main branch to proceed. Exiting ..." ERR
|
||||||
|
[ "$(git rev-parse --abbrev-ref HEAD)" == "main" ]
|
||||||
|
trap "" ERR
|
||||||
|
|
||||||
|
trap "echo You have uncommitted changes in postgrest.cabal. Exiting ..." ERR
|
||||||
|
git diff --exit-code HEAD postgrest.cabal > /dev/null
|
||||||
|
trap "" ERR
|
||||||
|
|
||||||
|
current_version="$(grep -oP '^version:\s*\K.*' postgrest.cabal)"
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
IFS=. read -r major minor patch pre <<< "$current_version"
|
||||||
|
echo "Current version is $current_version"
|
||||||
|
|
||||||
|
today_date="$(date '+%Y%m%d')"
|
||||||
|
today_date_for_changelog="$(date '+%Y-%m-%d')"
|
||||||
|
bump_pre="$major.$minor.$patch.$today_date"
|
||||||
|
bump_pre_minor="$major.$((minor+1)).0.$today_date"
|
||||||
|
bump_patch="$major.$minor.$((patch+1))"
|
||||||
|
bump_minor="$major.$((minor+1)).0"
|
||||||
|
bump_major="$((major+1)).0.0"
|
||||||
|
|
||||||
|
PS3="Please select the new version: "
|
||||||
|
select new_version in "$bump_pre" "$bump_pre_minor" "$bump_patch" "$bump_minor" "$bump_major"; do
|
||||||
|
case "$REPLY" in
|
||||||
|
1|2|3|4|5)
|
||||||
|
echo "Selected $new_version"
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Invalid option $REPLY"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Updating postgrest.cabal ..."
|
||||||
|
sed -i -E "s/^(version:\s+).*$/\1$new_version/" postgrest.cabal > /dev/null
|
||||||
|
|
||||||
|
echo "Committing ..."
|
||||||
|
git add postgrest.cabal > /dev/null
|
||||||
|
|
||||||
|
if [[ "$new_version" != "$bump_pre" && "$new_version" != "$bump_pre_minor" ]]; then
|
||||||
|
echo "Updating CHANGELOG.md ..."
|
||||||
|
sed -i -E "s/Unreleased/&\n\n## [$new_version] - $today_date_for_changelog/" CHANGELOG.md > /dev/null
|
||||||
|
git add CHANGELOG.md > /dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
git commit -m "bump version to $new_version" > /dev/null
|
||||||
|
|
||||||
|
echo "Tagging ..."
|
||||||
|
git tag "v$new_version" > /dev/null
|
||||||
|
|
||||||
|
trap "echo Remote not found. Please push manually ..." ERR
|
||||||
|
remote="$(git remote -v | grep PostgREST/postgrest | grep push | cut -f1)"
|
||||||
|
trap "" ERR
|
||||||
|
|
||||||
|
push="git push --atomic $remote main v$new_version"
|
||||||
|
|
||||||
|
echo "To push both the branch and the new tag, the following will be run:"
|
||||||
|
echo
|
||||||
|
echo "$push"
|
||||||
|
echo
|
||||||
|
|
||||||
|
read -r -p 'Proceed? (y/N) ' REPLY
|
||||||
|
case "$REPLY" in
|
||||||
|
y|Y)
|
||||||
|
$push
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Aborting ..."
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
'';
|
||||||
|
|
||||||
|
in
|
||||||
|
buildToolbox
|
||||||
|
{
|
||||||
|
name = "postgrest-release";
|
||||||
|
tools = [ dockerHubDescription release ];
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
REST API for any Postgres database
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# PostgREST
|
||||||
|
|
||||||
|
[](https://gitter.im/begriffs/postgrest)
|
||||||
|
[](https://www.patreon.com/postgrest)
|
||||||
|
[](https://www.paypal.me/postgrest)
|
||||||
|
[](http://postgrest.org)
|
||||||
|
[](https://github.com/PostgREST/postgrest/actions?query=branch%3Amain)
|
||||||
|
|
||||||
|
PostgREST serves a fully RESTful API from any existing PostgreSQL database. It
|
||||||
|
provides a cleaner, more standards-compliant, faster API than you are likely to
|
||||||
|
write from scratch.
|
||||||
|
|
||||||
|
## Sponsors
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="middle">
|
||||||
|
<a href="https://www.cybertec-postgresql.com/en/?utm_source=postgrest.org&utm_medium=referral&utm_campaign=postgrest" target="_blank">
|
||||||
|
<img width="222px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/cybertec-new.png">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" valign="middle">
|
||||||
|
<a href="https://www.2ndquadrant.com/en/?utm_campaign=External%20Websites&utm_source=PostgREST&utm_medium=Logo" target="_blank">
|
||||||
|
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/2ndquadrant.png">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" valign="middle">
|
||||||
|
<a href="https://tryretool.com/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||||
|
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/retool.png">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr></tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" valign="middle">
|
||||||
|
<a href="https://gnuhost.eu/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||||
|
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/gnuhost.png">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" valign="middle">
|
||||||
|
<a href="https://supabase.io?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage" target="_blank">
|
||||||
|
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/supabase.png">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td align="center" valign="middle">
|
||||||
|
<a href="https://oblivious.ai/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||||
|
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/oblivious.jpg">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
|
||||||
|
To learn how to use this container, see the [PostgREST Docker
|
||||||
|
documentation](https://postgrest.org/en/stable/install.html#docker).
|
||||||
|
|
||||||
|
You can configure the PostgREST image by setting
|
||||||
|
[enviroment variables](https://postgrest.org/en/stable/configuration.html).
|
||||||
|
|
||||||
|
# How this image is built
|
||||||
|
|
||||||
|
The image is built from scratch using
|
||||||
|
[Nix](https://nixos.org/nixpkgs/manual/#sec-pkgs-dockerTools) instead of a
|
||||||
|
`Dockerfile`, which yields a higly secure and optimized image. This is also why
|
||||||
|
no commands are listed in the image history. See the [PostgREST
|
||||||
|
respository](https://github.com/PostgREST/postgrest/tree/main/nix/tools/docker) for
|
||||||
|
details on the build process and how to inspect the image.
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
{ actionlint
|
||||||
|
, black
|
||||||
|
, buildToolbox
|
||||||
|
, checkedShellScript
|
||||||
|
, git
|
||||||
|
, hlint
|
||||||
|
, hsie
|
||||||
|
, nixpkgs-fmt
|
||||||
|
, shellcheck
|
||||||
|
, silver-searcher
|
||||||
|
, statix
|
||||||
|
, stylish-haskell
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
style =
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
name = "postgrest-style";
|
||||||
|
docs = "Automatically format Haskell, Nix and Python files.";
|
||||||
|
inRootDir = true;
|
||||||
|
}
|
||||||
|
''
|
||||||
|
# Format Nix files
|
||||||
|
${statix}/bin/statix fix
|
||||||
|
${nixpkgs-fmt}/bin/nixpkgs-fmt . > /dev/null 2> /dev/null
|
||||||
|
|
||||||
|
# Format Haskell files
|
||||||
|
# --vimgrep fixes a bug in ag: https://github.com/ggreer/the_silver_searcher/issues/753
|
||||||
|
${silver-searcher}/bin/ag -l --vimgrep -g '\.l?hs$' . \
|
||||||
|
| xargs ${stylish-haskell}/bin/stylish-haskell -i
|
||||||
|
|
||||||
|
# Format Python files
|
||||||
|
${black}/bin/black . 2> /dev/null
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Script to check whether any uncommited changes result from postgrest-style
|
||||||
|
styleCheck =
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
name = "postgrest-style-check";
|
||||||
|
docs = "Check whether postgrest-style results in any uncommited changes.";
|
||||||
|
inRootDir = true;
|
||||||
|
}
|
||||||
|
''
|
||||||
|
${style}
|
||||||
|
|
||||||
|
trap "echo postgrest-style-check failed. Run postgrest-style to fix issues automatically." ERR
|
||||||
|
|
||||||
|
${git}/bin/git diff-index --exit-code HEAD -- '*.hs' '*.lhs' '*.nix' '*.py'
|
||||||
|
'';
|
||||||
|
|
||||||
|
lint =
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
name = "postgrest-lint";
|
||||||
|
docs = "Lint all Haskell files, bash scripts and github workflows.";
|
||||||
|
inRootDir = true;
|
||||||
|
}
|
||||||
|
''
|
||||||
|
echo "Checking consistency of import aliases in Haskell code..."
|
||||||
|
${hsie} check-aliases main src
|
||||||
|
|
||||||
|
|
||||||
|
echo "Linting Haskell files..."
|
||||||
|
# --vimgrep fixes a bug in ag: https://github.com/ggreer/the_silver_searcher/issues/753
|
||||||
|
${silver-searcher}/bin/ag -l --vimgrep -g '\.l?hs$' . \
|
||||||
|
| xargs ${hlint}/bin/hlint -X QuasiQuotes -X NoPatternSynonyms
|
||||||
|
|
||||||
|
echo "Linting bash scripts..."
|
||||||
|
${shellcheck}/bin/shellcheck \
|
||||||
|
.github/get_cirrusci_freebsd \
|
||||||
|
.github/release
|
||||||
|
|
||||||
|
echo "Linting workflows..."
|
||||||
|
${actionlint}/bin/actionlint
|
||||||
|
'';
|
||||||
|
|
||||||
|
in
|
||||||
|
buildToolbox
|
||||||
|
{
|
||||||
|
name = "postgrest-style";
|
||||||
|
tools = [ style styleCheck lint ];
|
||||||
|
}
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
{ buildToolbox
|
||||||
|
, cabal-install
|
||||||
|
, checkedShellScript
|
||||||
|
, devCabalOptions
|
||||||
|
, ghc
|
||||||
|
, glibcLocales ? null
|
||||||
|
, gnugrep
|
||||||
|
, hpc-codecov
|
||||||
|
, hostPlatform
|
||||||
|
, jq
|
||||||
|
, lib
|
||||||
|
, postgrest
|
||||||
|
, python3
|
||||||
|
, runtimeShell
|
||||||
|
, stdenv
|
||||||
|
, weeder
|
||||||
|
, withTools
|
||||||
|
, yq
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
testSpec =
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
name = "postgrest-test-spec";
|
||||||
|
docs = "Run the Haskell test suite. Use --match PATTERN for running individual specs";
|
||||||
|
args = [ "ARG_LEFTOVERS([hspec arguments])" ];
|
||||||
|
inRootDir = true;
|
||||||
|
withEnv = postgrest.env;
|
||||||
|
}
|
||||||
|
''
|
||||||
|
${withTools.withPg} ${cabal-install}/bin/cabal v2-run ${devCabalOptions} \
|
||||||
|
test:spec -- "''${_arg_leftovers[@]}"
|
||||||
|
'';
|
||||||
|
|
||||||
|
testDoctests =
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
name = "postgrest-test-doctests";
|
||||||
|
docs = "Run the Haskell doctest test suite";
|
||||||
|
inRootDir = true;
|
||||||
|
withEnv = postgrest.env;
|
||||||
|
}
|
||||||
|
''
|
||||||
|
# For unknown reasons, doctests uses the wrong GHC package database outside
|
||||||
|
# nix-shell and fails, so we set the package path explicitly
|
||||||
|
#ghcWithPackages="$(cat ${postgrest.env})"
|
||||||
|
#ghcVersion="$(ls "$ghcWithPackages/lib")"
|
||||||
|
#export GHC_PACKAGE_PATH="$ghcWithPackages/lib/$ghcVersion/package.conf.d/"
|
||||||
|
|
||||||
|
${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:doctests
|
||||||
|
'';
|
||||||
|
|
||||||
|
testSpecIdempotence =
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
name = "postgrest-test-spec-idempotence";
|
||||||
|
docs = "Check that the Haskell tests can be run multiple times against the same db.";
|
||||||
|
inRootDir = true;
|
||||||
|
withEnv = postgrest.env;
|
||||||
|
}
|
||||||
|
''
|
||||||
|
${withTools.withPg} ${runtimeShell} -c " \
|
||||||
|
${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:spec && \
|
||||||
|
${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:spec"
|
||||||
|
'';
|
||||||
|
|
||||||
|
ioTestPython =
|
||||||
|
python3.withPackages (ps: [
|
||||||
|
ps.pyjwt
|
||||||
|
ps.pytest
|
||||||
|
ps.pytest-xdist
|
||||||
|
ps.pyyaml
|
||||||
|
ps.requests
|
||||||
|
ps.requests-unixsocket
|
||||||
|
]);
|
||||||
|
|
||||||
|
testIO =
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
name = "postgrest-test-io";
|
||||||
|
docs = "Run the pytest-based IO tests. Add -k to run tests that match a given expression.";
|
||||||
|
args = [ "ARG_LEFTOVERS([pytest arguments])" ];
|
||||||
|
inRootDir = true;
|
||||||
|
withEnv = postgrest.env;
|
||||||
|
}
|
||||||
|
''
|
||||||
|
${cabal-install}/bin/cabal v2-build ${devCabalOptions}
|
||||||
|
${cabal-install}/bin/cabal v2-exec -- ${withTools.withPg} -f test/io/fixtures.sql \
|
||||||
|
${ioTestPython}/bin/pytest -v test/io "''${_arg_leftovers[@]}"
|
||||||
|
'';
|
||||||
|
|
||||||
|
dumpSchema =
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
name = "postgrest-dump-schema";
|
||||||
|
docs = "Dump the loaded schema's SchemaCache as a yaml file.";
|
||||||
|
inRootDir = true;
|
||||||
|
withEnv = postgrest.env;
|
||||||
|
withPath = [ jq ];
|
||||||
|
}
|
||||||
|
''
|
||||||
|
${withTools.withPg} \
|
||||||
|
${cabal-install}/bin/cabal v2-run ${devCabalOptions} --verbose=0 -- \
|
||||||
|
postgrest --dump-schema \
|
||||||
|
| ${yq}/bin/yq -y .
|
||||||
|
'';
|
||||||
|
|
||||||
|
coverage =
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
name = "postgrest-coverage";
|
||||||
|
docs = "Run spec and io tests while collecting hpc coverage data. First runs weeder to detect dead code.";
|
||||||
|
args = [ "ARG_LEFTOVERS([hpc report arguments])" ];
|
||||||
|
inRootDir = true;
|
||||||
|
redirectTixFiles = false;
|
||||||
|
withEnv = postgrest.env;
|
||||||
|
withTmpDir = true;
|
||||||
|
}
|
||||||
|
(
|
||||||
|
# required for `hpc markup` in CI; glibcLocales is not available e.g. on Darwin
|
||||||
|
lib.optionalString (stdenv.isLinux && hostPlatform.libc == "glibc") ''
|
||||||
|
export LOCALE_ARCHIVE="${glibcLocales}/lib/locale/locale-archive"
|
||||||
|
'' +
|
||||||
|
|
||||||
|
''
|
||||||
|
# clean up previous coverage reports
|
||||||
|
mkdir -p coverage
|
||||||
|
rm -rf coverage/*
|
||||||
|
|
||||||
|
# build once before running all the tests
|
||||||
|
${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest lib:postgrest test:spec
|
||||||
|
|
||||||
|
(
|
||||||
|
trap 'echo Found dead code: Check file list above.' ERR ;
|
||||||
|
${weeder}/bin/weeder --config=./test/weeder.dhall
|
||||||
|
)
|
||||||
|
|
||||||
|
# collect all tests
|
||||||
|
HPCTIXFILE="$tmpdir"/io.tix \
|
||||||
|
${withTools.withPg} -f test/io/fixtures.sql ${cabal-install}/bin/cabal v2-exec ${devCabalOptions} -- \
|
||||||
|
${ioTestPython}/bin/pytest -v test/io
|
||||||
|
|
||||||
|
HPCTIXFILE="$tmpdir"/spec.tix \
|
||||||
|
${withTools.withPg} ${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:spec
|
||||||
|
|
||||||
|
# Note: No coverage for doctests, as doctests leverage GHCi and GHCi does not support hpc
|
||||||
|
|
||||||
|
# collect all the tix files
|
||||||
|
${ghc}/bin/hpc sum --union --exclude=Paths_postgrest --output="$tmpdir"/tests.tix \
|
||||||
|
"$tmpdir"/io*.tix "$tmpdir"/spec.tix
|
||||||
|
|
||||||
|
# prepare the overlay
|
||||||
|
${ghc}/bin/hpc overlay --output="$tmpdir"/overlay.tix test/coverage.overlay
|
||||||
|
${ghc}/bin/hpc sum --union --output="$tmpdir"/tests-overlay.tix "$tmpdir"/tests.tix "$tmpdir"/overlay.tix
|
||||||
|
|
||||||
|
# check nothing in the overlay is actually tested
|
||||||
|
${ghc}/bin/hpc map --function=inv --output="$tmpdir"/inverted.tix "$tmpdir"/tests.tix
|
||||||
|
${ghc}/bin/hpc combine --function=sub \
|
||||||
|
--output="$tmpdir"/check.tix "$tmpdir"/overlay.tix "$tmpdir"/inverted.tix
|
||||||
|
# returns zero exit code if any count="<non-zero>" lines are found, i.e.
|
||||||
|
# something is covered by both the overlay and the tests
|
||||||
|
if ${ghc}/bin/hpc report --xml "$tmpdir"/check.tix | ${gnugrep}/bin/grep -qP 'count="[^0]'
|
||||||
|
then
|
||||||
|
${ghc}/bin/hpc markup --highlight-covered --destdir=coverage/overlay "$tmpdir"/overlay.tix || true
|
||||||
|
${ghc}/bin/hpc markup --highlight-covered --destdir=coverage/check "$tmpdir"/check.tix || true
|
||||||
|
echo "ERROR: Something is covered by both the tests and the overlay:"
|
||||||
|
echo "file://$(pwd)/coverage/check/hpc_index.html"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
# copy the result .tix file to the coverage/ dir to make it available to postgrest-coverage-draft-overlay, too
|
||||||
|
cp "$tmpdir"/tests-overlay.tix coverage/postgrest.tix
|
||||||
|
# prepare codecov json report
|
||||||
|
${hpc-codecov}/bin/hpc-codecov --mix=.hpc --out=coverage/codecov.json coverage/postgrest.tix
|
||||||
|
|
||||||
|
# create html and stdout reports
|
||||||
|
${ghc}/bin/hpc markup --destdir=coverage coverage/postgrest.tix
|
||||||
|
echo "file://$(pwd)/coverage/hpc_index.html"
|
||||||
|
${ghc}/bin/hpc report coverage/postgrest.tix "''${_arg_leftovers[@]}"
|
||||||
|
fi
|
||||||
|
''
|
||||||
|
);
|
||||||
|
|
||||||
|
coverageDraftOverlay =
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
name = "postgrest-coverage-draft-overlay";
|
||||||
|
docs = "Create a draft overlay from current coverage report.";
|
||||||
|
inRootDir = true;
|
||||||
|
}
|
||||||
|
''
|
||||||
|
${ghc}/bin/hpc draft --output=test/coverage.overlay coverage/postgrest.tix
|
||||||
|
sed -i 's|^module \(.*\):|module \1/|g' test/coverage.overlay
|
||||||
|
'';
|
||||||
|
|
||||||
|
checkStatic =
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
name = "postgrest-check-static";
|
||||||
|
docs = "Verify that the argument is a static executable.";
|
||||||
|
args = [ "ARG_POSITIONAL_SINGLE([executable], [Executable])" ];
|
||||||
|
inRootDir = true;
|
||||||
|
withEnv = postgrest.env;
|
||||||
|
}
|
||||||
|
''
|
||||||
|
exe="$_arg_executable"
|
||||||
|
ldd_output=$(ldd "$exe" 2>&1 || true)
|
||||||
|
if ! grep -q "not a dynamic executable" <<< "$ldd_output"; then
|
||||||
|
echo "not a static executable, ldd output:"
|
||||||
|
echo "$ldd_output"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
"$exe" --help
|
||||||
|
'';
|
||||||
|
|
||||||
|
in
|
||||||
|
buildToolbox
|
||||||
|
{
|
||||||
|
name = "postgrest-tests";
|
||||||
|
tools =
|
||||||
|
[
|
||||||
|
testSpec
|
||||||
|
testDoctests
|
||||||
|
testSpecIdempotence
|
||||||
|
testIO
|
||||||
|
dumpSchema
|
||||||
|
coverage
|
||||||
|
coverageDraftOverlay
|
||||||
|
checkStatic
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,387 @@
|
|||||||
|
{ bash-completion
|
||||||
|
, buildToolbox
|
||||||
|
, cabal-install
|
||||||
|
, cabalTools
|
||||||
|
, checkedShellScript
|
||||||
|
, curl
|
||||||
|
, devCabalOptions
|
||||||
|
, git
|
||||||
|
, lib
|
||||||
|
, postgresqlVersions
|
||||||
|
, postgrest
|
||||||
|
, slocat
|
||||||
|
, writeText
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
withTmpDb =
|
||||||
|
{ name, postgresql }:
|
||||||
|
let
|
||||||
|
commandName = "postgrest-with-${name}";
|
||||||
|
superuserRole = "postgres";
|
||||||
|
in
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
name = commandName;
|
||||||
|
docs = "Run the given command in a temporary database with ${name}. If you wish to mutate the database, login with the '${superuserRole}' role.";
|
||||||
|
args =
|
||||||
|
[
|
||||||
|
"ARG_OPTIONAL_SINGLE([fixtures], [f], [SQL file to load fixtures from], [test/spec/fixtures/load.sql])"
|
||||||
|
"ARG_POSITIONAL_SINGLE([command], [Command to run])"
|
||||||
|
"ARG_LEFTOVERS([command arguments])"
|
||||||
|
"ARG_USE_ENV([PGUSER], [postgrest_test_authenticator], [Authenticator PG role])"
|
||||||
|
"ARG_USE_ENV([PGDATABASE], [postgres], [PG database name])"
|
||||||
|
"ARG_USE_ENV([PGRST_DB_SCHEMAS], [test], [Schema to expose])"
|
||||||
|
"ARG_USE_ENV([PGTZ], [utc], [Timezone to use])"
|
||||||
|
"ARG_USE_ENV([PGOPTIONS], [-c search_path=public,test], [PG options to use])"
|
||||||
|
];
|
||||||
|
positionalCompletion = "_command";
|
||||||
|
inRootDir = true;
|
||||||
|
redirectTixFiles = false;
|
||||||
|
withPath = [ postgresql ];
|
||||||
|
withTmpDir = true;
|
||||||
|
}
|
||||||
|
''
|
||||||
|
# avoid starting multiple layers of withTmpDb
|
||||||
|
if test -v PGHOST; then
|
||||||
|
exec "$_arg_command" "''${_arg_leftovers[@]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
setuplog="$tmpdir/setup.log"
|
||||||
|
|
||||||
|
log () {
|
||||||
|
echo "$1" >> "$setuplog"
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdir -p "$tmpdir"/{db,socket}
|
||||||
|
# remove data dir, even if we keep tmpdir - no need to upload it to artifacts
|
||||||
|
trap 'rm -rf $tmpdir/db' EXIT
|
||||||
|
|
||||||
|
export PGDATA="$tmpdir/db"
|
||||||
|
export PGHOST="$tmpdir/socket"
|
||||||
|
export PGUSER
|
||||||
|
export PGDATABASE
|
||||||
|
export PGRST_DB_SCHEMAS
|
||||||
|
export PGTZ
|
||||||
|
export PGOPTIONS
|
||||||
|
|
||||||
|
HBA_FILE="$tmpdir/pg_hba.conf"
|
||||||
|
echo "local $PGDATABASE some_protected_user password" > "$HBA_FILE"
|
||||||
|
echo "local $PGDATABASE all trust" >> "$HBA_FILE"
|
||||||
|
|
||||||
|
log "Initializing database cluster..."
|
||||||
|
# We try to make the database cluster as independent as possible from the host
|
||||||
|
# by specifying the timezone, locale and encoding.
|
||||||
|
# initdb -U creates a superuser(man initdb)
|
||||||
|
PGTZ=UTC initdb --no-locale --encoding=UTF8 --nosync -U "${superuserRole}" --auth=trust \
|
||||||
|
>> "$setuplog"
|
||||||
|
|
||||||
|
log "Starting the database cluster..."
|
||||||
|
# Instead of listening on a local port, we will listen on a unix domain socket.
|
||||||
|
pg_ctl -l "$tmpdir/db.log" -w start -o "-F -c listen_addresses=\"\" -c hba_file=$HBA_FILE -k $PGHOST -c log_statement=\"all\" " \
|
||||||
|
>> "$setuplog"
|
||||||
|
|
||||||
|
# shellcheck disable=SC2317
|
||||||
|
stop () {
|
||||||
|
log "Stopping the database cluster..."
|
||||||
|
pg_ctl stop -m i >> "$setuplog"
|
||||||
|
rm -rf "$tmpdir/db"
|
||||||
|
}
|
||||||
|
trap stop EXIT
|
||||||
|
|
||||||
|
log "Creating a minimally privileged $PGUSER connection role..."
|
||||||
|
createuser "$PGUSER" -U "${superuserRole}" --host="$tmpdir/socket" --no-createdb --no-inherit --no-superuser --no-createrole --no-replication --login
|
||||||
|
|
||||||
|
log "Loading fixtures under the ${superuserRole} role..."
|
||||||
|
psql -U "${superuserRole}" -v PGUSER="$PGUSER" -v ON_ERROR_STOP=1 -f "$_arg_fixtures" >> "$setuplog"
|
||||||
|
|
||||||
|
log "Done. Running command..."
|
||||||
|
|
||||||
|
echo "${commandName}: You can connect with: psql 'postgres:///$PGDATABASE?host=$tmpdir/socket' -U ${superuserRole}"
|
||||||
|
echo "${commandName}: You can tail the logs with: tail -f $tmpdir/db.log"
|
||||||
|
|
||||||
|
("$_arg_command" "''${_arg_leftovers[@]}")
|
||||||
|
'';
|
||||||
|
|
||||||
|
# Helper script for running a command against all PostgreSQL versions.
|
||||||
|
withPgAll =
|
||||||
|
let
|
||||||
|
runners =
|
||||||
|
builtins.map
|
||||||
|
(version:
|
||||||
|
''
|
||||||
|
cat << EOF
|
||||||
|
|
||||||
|
Running against ${version.name}...
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
trap 'echo "Failed on ${version.name}"' exit
|
||||||
|
|
||||||
|
(${withTmpDb version} "$_arg_command" "''${_arg_leftovers[@]}")
|
||||||
|
|
||||||
|
trap "" exit
|
||||||
|
|
||||||
|
cat << EOF
|
||||||
|
|
||||||
|
Done running against ${version.name}.
|
||||||
|
|
||||||
|
EOF
|
||||||
|
'')
|
||||||
|
postgresqlVersions;
|
||||||
|
in
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
name = "postgrest-with-all";
|
||||||
|
docs = "Run command against all supported PostgreSQL versions.";
|
||||||
|
args =
|
||||||
|
[
|
||||||
|
"ARG_POSITIONAL_SINGLE([command], [Command to run])"
|
||||||
|
"ARG_LEFTOVERS([command arguments])"
|
||||||
|
];
|
||||||
|
positionalCompletion = "_command";
|
||||||
|
inRootDir = true;
|
||||||
|
}
|
||||||
|
(lib.concatStringsSep "\n\n" runners);
|
||||||
|
|
||||||
|
# Create a `postgrest-with-postgresql-` for each PostgreSQL version
|
||||||
|
withPgVersions = builtins.map withTmpDb postgresqlVersions;
|
||||||
|
|
||||||
|
withPg = builtins.head withPgVersions;
|
||||||
|
|
||||||
|
withSlowPg =
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
name = "postgrest-with-slow-pg";
|
||||||
|
docs = "Run the given command with simulated high latency postgresql";
|
||||||
|
args =
|
||||||
|
[
|
||||||
|
"ARG_POSITIONAL_SINGLE([command], [Command to run])"
|
||||||
|
"ARG_LEFTOVERS([command arguments])"
|
||||||
|
"ARG_USE_ENV([PGHOST], [], [PG host (socket name)])"
|
||||||
|
"ARG_USE_ENV([PGDELAY], [0ms], [extra PG latency (duration)])"
|
||||||
|
];
|
||||||
|
positionalCompletion = "_command";
|
||||||
|
inRootDir = true;
|
||||||
|
redirectTixFiles = false;
|
||||||
|
withTmpDir = true;
|
||||||
|
}
|
||||||
|
''
|
||||||
|
delay="''${PGDELAY:-0ms}"
|
||||||
|
echo "delaying data to/from postgres by $delay"
|
||||||
|
|
||||||
|
REALPGHOST="$PGHOST"
|
||||||
|
export PGHOST="$tmpdir/socket"
|
||||||
|
mkdir -p "$PGHOST"
|
||||||
|
|
||||||
|
${slocat}/bin/slocat -delay "$delay" -src "$PGHOST/.s.PGSQL.5432" -dst "$REALPGHOST/.s.PGSQL.5432" &
|
||||||
|
SLOCAT_PID=$!
|
||||||
|
# shellcheck disable=SC2317
|
||||||
|
stop_slocat() {
|
||||||
|
kill "$SLOCAT_PID" || true
|
||||||
|
wait "$SLOCAT_PID" || true
|
||||||
|
}
|
||||||
|
trap stop_slocat EXIT
|
||||||
|
sleep 1 # should wait for socket file to appear instead
|
||||||
|
|
||||||
|
("$_arg_command" "''${_arg_leftovers[@]}")
|
||||||
|
'';
|
||||||
|
|
||||||
|
withSlowPgrst =
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
name = "postgrest-with-slow-postgrest";
|
||||||
|
docs = "Run the given command with simulated high latency postgrest";
|
||||||
|
args =
|
||||||
|
[
|
||||||
|
"ARG_POSITIONAL_SINGLE([command], [Command to run])"
|
||||||
|
"ARG_LEFTOVERS([command arguments])"
|
||||||
|
"ARG_USE_ENV([PGRST_SERVER_UNIX_SOCKET], [], [PostgREST host (socket name)])"
|
||||||
|
"ARG_USE_ENV([PGRST_DELAY], [0ms], [extra PostgREST latency (duration)])"
|
||||||
|
];
|
||||||
|
positionalCompletion = "_command";
|
||||||
|
inRootDir = true;
|
||||||
|
redirectTixFiles = false;
|
||||||
|
withTmpDir = true;
|
||||||
|
}
|
||||||
|
''
|
||||||
|
delay="''${PGRST_DELAY:-0ms}"
|
||||||
|
echo "delaying data to/from PostgREST by $delay"
|
||||||
|
|
||||||
|
REAL_PGRST_SERVER_UNIX_SOCKET="$PGRST_SERVER_UNIX_SOCKET"
|
||||||
|
export PGRST_SERVER_UNIX_SOCKET="$tmpdir/postgrest.socket"
|
||||||
|
|
||||||
|
${slocat}/bin/slocat -delay "$delay" -src "$PGRST_SERVER_UNIX_SOCKET" -dst "$REAL_PGRST_SERVER_UNIX_SOCKET" &
|
||||||
|
SLOCAT_PID=$!
|
||||||
|
# shellcheck disable=SC2317
|
||||||
|
stop_slocat() {
|
||||||
|
kill "$SLOCAT_PID" || true
|
||||||
|
wait "$SLOCAT_PID" || true
|
||||||
|
}
|
||||||
|
trap stop_slocat EXIT
|
||||||
|
sleep 1 # should wait for socket file to appear instead
|
||||||
|
|
||||||
|
("$_arg_command" "''${_arg_leftovers[@]}")
|
||||||
|
'';
|
||||||
|
|
||||||
|
withGit =
|
||||||
|
let
|
||||||
|
name = "postgrest-with-git";
|
||||||
|
in
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
inherit name;
|
||||||
|
docs =
|
||||||
|
''
|
||||||
|
Create a new worktree of the postgrest repo in a temporary directory and
|
||||||
|
check out <commit>, then run <command> with arguments inside the temporary folder.
|
||||||
|
'';
|
||||||
|
args =
|
||||||
|
[
|
||||||
|
"ARG_POSITIONAL_SINGLE([commit], [Commit-ish reference to run command with])"
|
||||||
|
"ARG_POSITIONAL_SINGLE([command], [Command to run])"
|
||||||
|
"ARG_LEFTOVERS([command arguments])"
|
||||||
|
];
|
||||||
|
positionalCompletion =
|
||||||
|
''
|
||||||
|
if test "$prev" == "${name}"; then
|
||||||
|
__gitcomp_nl "$(__git_refs)"
|
||||||
|
else
|
||||||
|
_command_offset 2
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
inRootDir = true;
|
||||||
|
}
|
||||||
|
''
|
||||||
|
# not using withTmpDir here, because we don't want to keep the directory on error
|
||||||
|
tmpdir="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$tmpdir"' EXIT
|
||||||
|
|
||||||
|
${git}/bin/git worktree add -f "$tmpdir" "$_arg_commit" > /dev/null
|
||||||
|
|
||||||
|
cd "$tmpdir"
|
||||||
|
("$_arg_command" "''${_arg_leftovers[@]}")
|
||||||
|
|
||||||
|
${git}/bin/git worktree remove -f "$tmpdir" > /dev/null
|
||||||
|
'';
|
||||||
|
|
||||||
|
legacyConfig =
|
||||||
|
writeText "legacy.conf"
|
||||||
|
''
|
||||||
|
# Using this config file to support older postgrest versions for `postgrest-loadtest-against`
|
||||||
|
db-uri="$(PGRST_DB_URI)"
|
||||||
|
db-schema="$(PGRST_DB_SCHEMAS)"
|
||||||
|
db-anon-role="$(PGRST_DB_ANON_ROLE)"
|
||||||
|
db-pool="$(PGRST_DB_POOL)"
|
||||||
|
server-unix-socket="$(PGRST_SERVER_UNIX_SOCKET)"
|
||||||
|
log-level="$(PGRST_LOG_LEVEL)"
|
||||||
|
'';
|
||||||
|
|
||||||
|
waitForPgrstPid =
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
name = "postgrest-wait-for-pgrst-pid";
|
||||||
|
docs = "Wait for PostgREST to be running. Needs to be a separate command for timeout to work below.";
|
||||||
|
args = [
|
||||||
|
"ARG_USE_ENV([PGRST_SERVER_UNIX_SOCKET], [], [Unix socket to check for running PostgREST instance])"
|
||||||
|
];
|
||||||
|
}
|
||||||
|
''
|
||||||
|
# ARG_USE_ENV only adds defaults or docs for environment variables
|
||||||
|
# We manually implement a required check here
|
||||||
|
# See also: https://github.com/matejak/argbash/issues/80
|
||||||
|
: "''${PGRST_SERVER_UNIX_SOCKET:?PGRST_SERVER_UNIX_SOCKET is required}"
|
||||||
|
|
||||||
|
until [ -S "$PGRST_SERVER_UNIX_SOCKET" ]
|
||||||
|
do
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
|
||||||
|
# return pid of postgrest process
|
||||||
|
lsof -t -c '/^postgrest$/' "$PGRST_SERVER_UNIX_SOCKET"
|
||||||
|
'';
|
||||||
|
|
||||||
|
waitForPgrstReady =
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
name = "postgrest-wait-for-pgrst-ready";
|
||||||
|
docs = "Wait for PostgREST to be ready to serve requests. Needs to be a separate command for timeout to work below.";
|
||||||
|
args = [
|
||||||
|
"ARG_USE_ENV([PGRST_SERVER_UNIX_SOCKET], [], [Unix socket to check for running PostgREST instance])"
|
||||||
|
];
|
||||||
|
}
|
||||||
|
''
|
||||||
|
# ARG_USE_ENV only adds defaults or docs for environment variables
|
||||||
|
# We manually implement a required check here
|
||||||
|
# See also: https://github.com/matejak/argbash/issues/80
|
||||||
|
: "''${PGRST_SERVER_UNIX_SOCKET:?PGRST_SERVER_UNIX_SOCKET is required}"
|
||||||
|
|
||||||
|
function check_status () {
|
||||||
|
${curl}/bin/curl -s -o /dev/null -w "%{http_code}" --unix-socket "$PGRST_SERVER_UNIX_SOCKET" http://localhost/
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ "$(check_status)" != "200" ]];
|
||||||
|
do sleep 0.1;
|
||||||
|
done
|
||||||
|
'';
|
||||||
|
|
||||||
|
withPgrst =
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
name = "postgrest-with-pgrst";
|
||||||
|
docs = "Build and run PostgREST and run <command> with PGRST_SERVER_UNIX_SOCKET set.";
|
||||||
|
args =
|
||||||
|
[
|
||||||
|
"ARG_POSITIONAL_SINGLE([command], [Command to run])"
|
||||||
|
"ARG_LEFTOVERS([command arguments])"
|
||||||
|
];
|
||||||
|
positionalCompletion = "_command";
|
||||||
|
inRootDir = true;
|
||||||
|
withEnv = postgrest.env;
|
||||||
|
withTmpDir = true;
|
||||||
|
}
|
||||||
|
''
|
||||||
|
export PGRST_SERVER_UNIX_SOCKET="$tmpdir"/postgrest.socket
|
||||||
|
|
||||||
|
rm -f result
|
||||||
|
if [ -z "''${PGRST_BUILD_CABAL:-}" ]; then
|
||||||
|
echo -n "Building postgrest (nix)... "
|
||||||
|
nix-build -A postgrestPackage > "$tmpdir"/build.log 2>&1 || {
|
||||||
|
echo "failed, output:"
|
||||||
|
cat "$tmpdir"/build.log
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
PGRST_CMD=./result/bin/postgrest
|
||||||
|
else
|
||||||
|
echo -n "Building postgrest (cabal)... "
|
||||||
|
postgrest-build
|
||||||
|
PGRST_CMD=postgrest-run
|
||||||
|
fi
|
||||||
|
echo "done."
|
||||||
|
|
||||||
|
echo -n "Starting postgrest... "
|
||||||
|
$PGRST_CMD ${legacyConfig} > "$tmpdir"/run.log 2>&1 &
|
||||||
|
pid=$!
|
||||||
|
# shellcheck disable=SC2317
|
||||||
|
cleanup() {
|
||||||
|
kill "$pid" || true
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
timeout -s TERM 5 ${waitForPgrstReady} || {
|
||||||
|
echo "timed out, output:"
|
||||||
|
cat "$tmpdir"/run.log
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
echo "done."
|
||||||
|
|
||||||
|
("$_arg_command" "''${_arg_leftovers[@]}")
|
||||||
|
'';
|
||||||
|
|
||||||
|
in
|
||||||
|
buildToolbox
|
||||||
|
{
|
||||||
|
name = "postgrest-with";
|
||||||
|
tools = [ withPgAll withGit withPgrst withSlowPg withSlowPgrst ] ++ withPgVersions;
|
||||||
|
# make withTools available for other nix files
|
||||||
|
extra = { inherit withGit withPg withPgAll withPgrst withSlowPg withSlowPgrst; };
|
||||||
|
}
|
||||||
+198
-161
@@ -1,8 +1,8 @@
|
|||||||
name: postgrest
|
name: postgrest
|
||||||
version: 7.0.1
|
version: 11.2.2
|
||||||
synopsis: REST API for any Postgres database
|
synopsis: REST API for any Postgres database
|
||||||
description: Reads the schema of a PostgreSQL database and creates RESTful routes
|
description: Reads the schema of a PostgreSQL database and creates RESTful routes
|
||||||
for the tables and views, supporting all HTTP verbs that security
|
for tables, views, and functions, supporting all HTTP methods that security
|
||||||
permits.
|
permits.
|
||||||
license: MIT
|
license: MIT
|
||||||
license-file: LICENSE
|
license-file: LICENSE
|
||||||
@@ -19,222 +19,259 @@ source-repository head
|
|||||||
type: git
|
type: git
|
||||||
location: git://github.com/PostgREST/postgrest.git
|
location: git://github.com/PostgREST/postgrest.git
|
||||||
|
|
||||||
flag ci
|
flag dev
|
||||||
default: False
|
default: False
|
||||||
manual: True
|
manual: True
|
||||||
description: No warnings allowed in continuous integration
|
description: Development flags
|
||||||
|
|
||||||
|
flag hpc
|
||||||
|
default: True
|
||||||
|
manual: True
|
||||||
|
description: Enable HPC (dev only)
|
||||||
|
|
||||||
library
|
library
|
||||||
exposed-modules: PostgREST.ApiRequest
|
default-language: Haskell2010
|
||||||
PostgREST.App
|
default-extensions: OverloadedStrings
|
||||||
PostgREST.Auth
|
NoImplicitPrelude
|
||||||
PostgREST.Config
|
|
||||||
PostgREST.DbRequestBuilder
|
|
||||||
PostgREST.DbStructure
|
|
||||||
PostgREST.Error
|
|
||||||
PostgREST.Middleware
|
|
||||||
PostgREST.OpenAPI
|
|
||||||
PostgREST.Parsers
|
|
||||||
PostgREST.QueryBuilder
|
|
||||||
PostgREST.Statements
|
|
||||||
PostgREST.RangeQuery
|
|
||||||
PostgREST.Types
|
|
||||||
other-modules: Paths_postgrest
|
|
||||||
PostgREST.Private.Common
|
|
||||||
PostgREST.Private.QueryFragment
|
|
||||||
hs-source-dirs: src
|
hs-source-dirs: src
|
||||||
build-depends: base >= 4.9 && < 4.15
|
exposed-modules: PostgREST.Admin
|
||||||
, HTTP >= 4000.3.7 && < 4000.4
|
PostgREST.App
|
||||||
|
PostgREST.AppState
|
||||||
|
PostgREST.Auth
|
||||||
|
PostgREST.CLI
|
||||||
|
PostgREST.Config
|
||||||
|
PostgREST.Config.Database
|
||||||
|
PostgREST.Config.JSPath
|
||||||
|
PostgREST.Config.PgVersion
|
||||||
|
PostgREST.Config.Proxy
|
||||||
|
PostgREST.Cors
|
||||||
|
PostgREST.SchemaCache
|
||||||
|
PostgREST.SchemaCache.Identifiers
|
||||||
|
PostgREST.SchemaCache.Routine
|
||||||
|
PostgREST.SchemaCache.Relationship
|
||||||
|
PostgREST.SchemaCache.Representations
|
||||||
|
PostgREST.SchemaCache.Table
|
||||||
|
PostgREST.Error
|
||||||
|
PostgREST.Logger
|
||||||
|
PostgREST.MediaType
|
||||||
|
PostgREST.Query
|
||||||
|
PostgREST.Query.QueryBuilder
|
||||||
|
PostgREST.Query.SqlFragment
|
||||||
|
PostgREST.Query.Statements
|
||||||
|
PostgREST.Plan
|
||||||
|
PostgREST.Plan.CallPlan
|
||||||
|
PostgREST.Plan.MutatePlan
|
||||||
|
PostgREST.Plan.ReadPlan
|
||||||
|
PostgREST.Plan.Types
|
||||||
|
PostgREST.RangeQuery
|
||||||
|
PostgREST.ApiRequest
|
||||||
|
PostgREST.ApiRequest.Preferences
|
||||||
|
PostgREST.ApiRequest.QueryParams
|
||||||
|
PostgREST.ApiRequest.Types
|
||||||
|
PostgREST.Response
|
||||||
|
PostgREST.Response.OpenAPI
|
||||||
|
PostgREST.Response.GucHeader
|
||||||
|
PostgREST.Version
|
||||||
|
other-modules: Paths_postgrest
|
||||||
|
build-depends: base >= 4.9 && < 4.17
|
||||||
|
, HTTP >= 4000.3.7 && < 4000.5
|
||||||
, Ranged-sets >= 0.3 && < 0.5
|
, Ranged-sets >= 0.3 && < 0.5
|
||||||
, aeson >= 1.4.7 && < 1.5
|
, aeson >= 2.0.3 && < 2.2
|
||||||
, ansi-wl-pprint >= 0.6.7 && < 0.7
|
, auto-update >= 0.1.4 && < 0.2
|
||||||
, base64-bytestring >= 1 && < 1.2
|
, base64-bytestring >= 1 && < 1.3
|
||||||
, bytestring >= 0.10.8 && < 0.11
|
, bytestring >= 0.10.8 && < 0.12
|
||||||
, case-insensitive >= 1.2 && < 1.3
|
, case-insensitive >= 1.2 && < 1.3
|
||||||
, cassava >= 0.4.5 && < 0.6
|
, cassava >= 0.4.5 && < 0.6
|
||||||
, configurator-pg >= 0.2 && < 0.3
|
, configurator-pg >= 0.2 && < 0.3
|
||||||
, containers >= 0.5.7 && < 0.7
|
, containers >= 0.5.7 && < 0.7
|
||||||
, contravariant >= 1.4 && < 1.6
|
|
||||||
, contravariant-extras >= 0.3.3 && < 0.4
|
, contravariant-extras >= 0.3.3 && < 0.4
|
||||||
, cookie >= 0.4.2 && < 0.5
|
, cookie >= 0.4.2 && < 0.5
|
||||||
, either >= 4.4.1 && < 5.1
|
, either >= 4.4.1 && < 5.1
|
||||||
|
, extra >= 1.7.0 && < 2.0
|
||||||
|
, fuzzyset >= 0.2.3
|
||||||
, gitrev >= 1.2 && < 1.4
|
, gitrev >= 1.2 && < 1.4
|
||||||
, hasql >= 1.4 && < 1.5
|
, hasql >= 1.6.1.1 && < 1.7
|
||||||
, hasql-pool >= 0.5 && < 0.6
|
, hasql-dynamic-statements >= 0.3.1 && < 0.4
|
||||||
, hasql-transaction >= 0.7.2 && < 1.1
|
, hasql-notifications >= 0.2.0.6 && < 0.3
|
||||||
|
, hasql-pool >= 0.10 && < 0.11
|
||||||
|
, hasql-transaction >= 1.0.1 && < 1.1
|
||||||
, heredoc >= 0.2 && < 0.3
|
, heredoc >= 0.2 && < 0.3
|
||||||
, http-types >= 0.12.2 && < 0.13
|
, http-types >= 0.12.2 && < 0.13
|
||||||
, insert-ordered-containers >= 0.2.2 && < 0.3
|
, insert-ordered-containers >= 0.2.2 && < 0.3
|
||||||
, interpolatedstring-perl6 >= 1 && < 1.1
|
, interpolatedstring-perl6 >= 1 && < 1.1
|
||||||
, jose >= 0.8.1 && < 0.9
|
, jose >= 0.8.5.1 && < 0.11
|
||||||
, lens >= 4.14 && < 4.20
|
, lens >= 4.14 && < 5.3
|
||||||
, lens-aeson >= 1.0.1 && < 1.2
|
, lens-aeson >= 1.0.1 && < 1.3
|
||||||
|
, mtl >= 2.2.2 && < 2.3
|
||||||
|
, network >= 2.6 && < 3.2
|
||||||
, network-uri >= 2.6.1 && < 2.8
|
, network-uri >= 2.6.1 && < 2.8
|
||||||
, optparse-applicative >= 0.13 && < 0.16
|
, optparse-applicative >= 0.13 && < 0.18
|
||||||
, parsec >= 3.1.11 && < 3.2
|
, parsec >= 3.1.11 && < 3.2
|
||||||
, protolude >= 0.3 && < 0.4
|
, protolude >= 0.3.1 && < 0.4
|
||||||
, regex-tdfa >= 1.2.2 && < 1.4
|
, regex-tdfa >= 1.2.2 && < 1.4
|
||||||
|
, retry >= 0.7.4 && < 0.10
|
||||||
, scientific >= 0.3.4 && < 0.4
|
, scientific >= 0.3.4 && < 0.4
|
||||||
, swagger2 >= 2.4 && < 2.7
|
, swagger2 >= 2.4 && < 2.9
|
||||||
, text >= 1.2.2 && < 1.3
|
, text >= 1.2.2 && < 1.3
|
||||||
, time >= 1.6 && < 1.11
|
, time >= 1.6 && < 1.12
|
||||||
, unordered-containers >= 0.2.8 && < 0.3
|
, unordered-containers >= 0.2.8 && < 0.3
|
||||||
, vector >= 0.11 && < 0.13
|
, vault >= 0.3.1.5 && < 0.4
|
||||||
|
, vector >= 0.11 && < 0.14
|
||||||
, wai >= 3.2.1 && < 3.3
|
, wai >= 3.2.1 && < 3.3
|
||||||
, wai-cors >= 0.2.5 && < 0.3
|
, wai-cors >= 0.2.5 && < 0.3
|
||||||
, wai-extra >= 3.0.19 && < 3.1
|
, wai-extra >= 3.1.8 && < 3.2
|
||||||
, wai-middleware-static >= 0.8.1 && < 0.9
|
-- We already depend on wai-logger >= 2.3.7 indirectly via wai-extra,
|
||||||
default-language: Haskell2010
|
-- but we want to depend on 2.4.0 which fixes 'unknownSocket' log output
|
||||||
default-extensions: OverloadedStrings
|
-- for unix sockets; this is tested in test/io/test_io.py. See
|
||||||
QuasiQuotes
|
-- https://github.com/kazu-yamamoto/logger/commit/3a71ca70afdbb93d4ecf0083eeba1fbbbcab3fc3
|
||||||
NoImplicitPrelude
|
, wai-logger >= 2.4.0
|
||||||
ghc-options: -O2 -Werror -Wall -fwarn-identities
|
, warp >= 3.3.19 && < 3.4
|
||||||
-fno-spec-constr -optP-Wno-nonportable-include-path
|
|
||||||
-- -fno-spec-constr may help keep compile time memory use in check,
|
-- -fno-spec-constr may help keep compile time memory use in check,
|
||||||
-- see https://gitlab.haskell.org/ghc/ghc/issues/16017#note_219304
|
-- see https://gitlab.haskell.org/ghc/ghc/issues/16017#note_219304
|
||||||
-- -optP-Wno-nonportable-include-path
|
-- -optP-Wno-nonportable-include-path
|
||||||
-- prevents build failures on case-insensitive filesystems (macos),
|
-- prevents build failures on case-insensitive filesystems (macos),
|
||||||
-- see https://github.com/commercialhaskell/stack/issues/3918
|
-- see https://github.com/commercialhaskell/stack/issues/3918
|
||||||
|
ghc-options: -Werror -Wall -fwarn-identities
|
||||||
|
-fno-spec-constr -optP-Wno-nonportable-include-path
|
||||||
|
|
||||||
|
if flag(dev)
|
||||||
|
ghc-options: -O0 -fwrite-ide-info
|
||||||
|
if flag(hpc)
|
||||||
|
ghc-options: -fhpc -hpcdir .hpc
|
||||||
|
else
|
||||||
|
ghc-options: -O2
|
||||||
|
|
||||||
|
if !os(windows)
|
||||||
|
build-depends:
|
||||||
|
unix
|
||||||
|
, directory >= 1.2.6 && < 1.4
|
||||||
|
exposed-modules:
|
||||||
|
PostgREST.Unix
|
||||||
|
|
||||||
executable postgrest
|
executable postgrest
|
||||||
main-is: Main.hs
|
|
||||||
hs-source-dirs: main
|
|
||||||
build-depends: base >= 4.9 && < 4.15
|
|
||||||
, auto-update >= 0.1.4 && < 0.2
|
|
||||||
, base64-bytestring >= 1 && < 1.2
|
|
||||||
, bytestring >= 0.10.8 && < 0.11
|
|
||||||
, directory >= 1.2.6 && < 1.4
|
|
||||||
, either >= 4.4.1 && < 5.1
|
|
||||||
, hasql >= 1.4 && < 1.5
|
|
||||||
, hasql-pool >= 0.5 && < 0.6
|
|
||||||
, hasql-transaction >= 0.7.2 && < 1.1
|
|
||||||
, network < 3.2
|
|
||||||
, postgrest
|
|
||||||
, protolude >= 0.3 && < 0.4
|
|
||||||
, retry >= 0.7.4 && < 0.9
|
|
||||||
, text >= 1.2.2 && < 1.3
|
|
||||||
, time >= 1.6 && < 1.11
|
|
||||||
, wai >= 3.2.1 && < 3.3
|
|
||||||
, warp >= 3.2.12 && < 3.4
|
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
||||||
default-extensions: OverloadedStrings
|
default-extensions: OverloadedStrings
|
||||||
QuasiQuotes
|
|
||||||
NoImplicitPrelude
|
NoImplicitPrelude
|
||||||
ghc-options: -threaded -rtsopts "-with-rtsopts=-N -I2"
|
hs-source-dirs: main
|
||||||
|
main-is: Main.hs
|
||||||
|
build-depends: base >= 4.9 && < 4.17
|
||||||
|
, containers >= 0.5.7 && < 0.7
|
||||||
|
, postgrest
|
||||||
|
, protolude >= 0.3.1 && < 0.4
|
||||||
|
ghc-options: -threaded -rtsopts "-with-rtsopts=-N -I0 -qg"
|
||||||
-O2 -Werror -Wall -fwarn-identities
|
-O2 -Werror -Wall -fwarn-identities
|
||||||
-fno-spec-constr -optP-Wno-nonportable-include-path
|
-fno-spec-constr -optP-Wno-nonportable-include-path
|
||||||
|
|
||||||
if !os(windows)
|
if flag(dev)
|
||||||
build-depends: unix
|
ghc-options: -O0 -fwrite-ide-info
|
||||||
other-modules: UnixSocket
|
-- https://github.com/PostgREST/postgrest/issues/387
|
||||||
|
-with-rtsopts=-K1K
|
||||||
|
if flag(hpc)
|
||||||
|
ghc-options: -fhpc -hpcdir .hpc
|
||||||
|
else
|
||||||
|
ghc-options: -O2
|
||||||
|
|
||||||
test-suite spec
|
test-suite spec
|
||||||
type: exitcode-stdio-1.0
|
type: exitcode-stdio-1.0
|
||||||
main-is: Main.hs
|
|
||||||
other-modules: Feature.AndOrParamsSpec
|
|
||||||
Feature.AsymmetricJwtSpec
|
|
||||||
Feature.AudienceJwtSecretSpec
|
|
||||||
Feature.AuthSpec
|
|
||||||
Feature.BinaryJwtSecretSpec
|
|
||||||
Feature.ConcurrentSpec
|
|
||||||
Feature.CorsSpec
|
|
||||||
Feature.DeleteSpec
|
|
||||||
Feature.EmbedDisambiguationSpec
|
|
||||||
Feature.ExtraSearchPathSpec
|
|
||||||
Feature.InsertSpec
|
|
||||||
Feature.JsonOperatorSpec
|
|
||||||
Feature.NoJwtSpec
|
|
||||||
Feature.NonexistentSchemaSpec
|
|
||||||
Feature.PgVersion95Spec
|
|
||||||
Feature.PgVersion96Spec
|
|
||||||
Feature.ProxySpec
|
|
||||||
Feature.QueryLimitedSpec
|
|
||||||
Feature.QuerySpec
|
|
||||||
Feature.RangeSpec
|
|
||||||
Feature.RootSpec
|
|
||||||
Feature.RpcSpec
|
|
||||||
Feature.SingularSpec
|
|
||||||
Feature.StructureSpec
|
|
||||||
Feature.UnicodeSpec
|
|
||||||
Feature.UpsertSpec
|
|
||||||
Feature.RawOutputTypesSpec
|
|
||||||
Feature.HtmlRawOutputSpec
|
|
||||||
Feature.MultipleSchemaSpec
|
|
||||||
SpecHelper
|
|
||||||
TestTypes
|
|
||||||
hs-source-dirs: test
|
|
||||||
build-depends: base >= 4.9 && < 4.15
|
|
||||||
, aeson >= 1.4.7 && < 1.5
|
|
||||||
, aeson-qq >= 0.8.1 && < 0.9
|
|
||||||
, async >= 2.1.1 && < 2.3
|
|
||||||
, auto-update >= 0.1.4 && < 0.2
|
|
||||||
, base64-bytestring >= 1 && < 1.2
|
|
||||||
, bytestring >= 0.10.8 && < 0.11
|
|
||||||
, case-insensitive >= 1.2 && < 1.3
|
|
||||||
, cassava >= 0.4.5 && < 0.6
|
|
||||||
, containers >= 0.5.7 && < 0.7
|
|
||||||
, contravariant >= 1.4 && < 1.6
|
|
||||||
, hasql >= 1.4 && < 1.5
|
|
||||||
, hasql-pool >= 0.5 && < 0.6
|
|
||||||
, hasql-transaction >= 0.7.2 && < 1.1
|
|
||||||
, heredoc >= 0.2 && < 0.3
|
|
||||||
, hspec >= 2.3 && < 2.8
|
|
||||||
, hspec-wai >= 0.10 && < 0.11
|
|
||||||
, hspec-wai-json >= 0.10 && < 0.11
|
|
||||||
, http-types >= 0.12.3 && < 0.13
|
|
||||||
, lens >= 4.14 && < 4.20
|
|
||||||
, lens-aeson >= 1.0.1 && < 1.2
|
|
||||||
, monad-control >= 1.0.1 && < 1.1
|
|
||||||
, postgrest
|
|
||||||
, process >= 1.4.2 && < 1.7
|
|
||||||
, protolude >= 0.3 && < 0.4
|
|
||||||
, regex-tdfa >= 1.2.2 && < 1.4
|
|
||||||
, text >= 1.2.2 && < 1.3
|
|
||||||
, time >= 1.6 && < 1.11
|
|
||||||
, transformers-base >= 0.4.4 && < 0.5
|
|
||||||
, wai >= 3.2.1 && < 3.3
|
|
||||||
, wai-extra >= 3.0.19 && < 3.1
|
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
||||||
default-extensions: OverloadedStrings
|
default-extensions: OverloadedStrings
|
||||||
QuasiQuotes
|
QuasiQuotes
|
||||||
NoImplicitPrelude
|
NoImplicitPrelude
|
||||||
ghc-options: -threaded -rtsopts -with-rtsopts=-N
|
hs-source-dirs: test/spec
|
||||||
|
main-is: Main.hs
|
||||||
Test-Suite spec-querycost
|
other-modules: Feature.Auth.AsymmetricJwtSpec
|
||||||
Type: exitcode-stdio-1.0
|
Feature.Auth.AudienceJwtSecretSpec
|
||||||
Default-Language: Haskell2010
|
Feature.Auth.AuthSpec
|
||||||
default-extensions: OverloadedStrings, QuasiQuotes, NoImplicitPrelude
|
Feature.Auth.BinaryJwtSecretSpec
|
||||||
Hs-Source-Dirs: test
|
Feature.Auth.NoAnonSpec
|
||||||
Main-Is: QueryCost.hs
|
Feature.Auth.NoJwtSpec
|
||||||
Other-Modules: SpecHelper
|
Feature.ConcurrentSpec
|
||||||
Build-Depends: base >= 4.9 && < 4.15
|
Feature.CorsSpec
|
||||||
, aeson >= 1.4.7 && < 1.5
|
Feature.ExtraSearchPathSpec
|
||||||
|
Feature.LegacyGucsSpec
|
||||||
|
Feature.NoSuperuserSpec
|
||||||
|
Feature.ObservabilitySpec
|
||||||
|
Feature.OpenApi.DisabledOpenApiSpec
|
||||||
|
Feature.OpenApi.IgnorePrivOpenApiSpec
|
||||||
|
Feature.OpenApi.OpenApiSpec
|
||||||
|
Feature.OpenApi.ProxySpec
|
||||||
|
Feature.OpenApi.RootSpec
|
||||||
|
Feature.OpenApi.SecurityOpenApiSpec
|
||||||
|
Feature.OptionsSpec
|
||||||
|
Feature.Query.AndOrParamsSpec
|
||||||
|
Feature.Query.ComputedRelsSpec
|
||||||
|
Feature.Query.DeleteSpec
|
||||||
|
Feature.Query.EmbedDisambiguationSpec
|
||||||
|
Feature.Query.EmbedInnerJoinSpec
|
||||||
|
Feature.Query.PlanSpec
|
||||||
|
Feature.Query.HtmlRawOutputSpec
|
||||||
|
Feature.Query.InsertSpec
|
||||||
|
Feature.Query.JsonOperatorSpec
|
||||||
|
Feature.Query.MultipleSchemaSpec
|
||||||
|
Feature.Query.ErrorSpec
|
||||||
|
Feature.Query.PgSafeUpdateSpec
|
||||||
|
Feature.Query.PostGISSpec
|
||||||
|
Feature.Query.QueryLimitedSpec
|
||||||
|
Feature.Query.QuerySpec
|
||||||
|
Feature.Query.RangeSpec
|
||||||
|
Feature.Query.RawOutputTypesSpec
|
||||||
|
Feature.Query.RelatedQueriesSpec
|
||||||
|
Feature.Query.RpcSpec
|
||||||
|
Feature.Query.SingularSpec
|
||||||
|
Feature.Query.NullsStrip
|
||||||
|
Feature.Query.SpreadQueriesSpec
|
||||||
|
Feature.Query.UnicodeSpec
|
||||||
|
Feature.Query.UpdateSpec
|
||||||
|
Feature.Query.UpsertSpec
|
||||||
|
Feature.RollbackSpec
|
||||||
|
Feature.RpcPreRequestGucsSpec
|
||||||
|
SpecHelper
|
||||||
|
build-depends: base >= 4.9 && < 4.17
|
||||||
|
, aeson >= 2.0.3 && < 2.2
|
||||||
, aeson-qq >= 0.8.1 && < 0.9
|
, aeson-qq >= 0.8.1 && < 0.9
|
||||||
, async >= 2.1.1 && < 2.3
|
, async >= 2.1.1 && < 2.3
|
||||||
, auto-update >= 0.1.4 && < 0.2
|
, auto-update >= 0.1.4 && < 0.2
|
||||||
, base64-bytestring >= 1 && < 1.2
|
, base64-bytestring >= 1 && < 1.3
|
||||||
, bytestring >= 0.10.8 && < 0.11
|
, bytestring >= 0.10.8 && < 0.12
|
||||||
, case-insensitive >= 1.2 && < 1.3
|
, case-insensitive >= 1.2 && < 1.3
|
||||||
, cassava >= 0.4.5 && < 0.6
|
|
||||||
, containers >= 0.5.7 && < 0.7
|
, containers >= 0.5.7 && < 0.7
|
||||||
, contravariant >= 1.4 && < 1.6
|
, hasql-pool >= 0.10 && < 0.11
|
||||||
, hasql >= 1.4 && < 1.5
|
, hasql-transaction >= 1.0.1 && < 1.1
|
||||||
, hasql-pool >= 0.5 && < 0.6
|
|
||||||
, hasql-transaction >= 0.7.2 && < 1.1
|
|
||||||
, heredoc >= 0.2 && < 0.3
|
, heredoc >= 0.2 && < 0.3
|
||||||
, hspec >= 2.3 && < 2.8
|
, hspec >= 2.3 && < 2.10
|
||||||
, hspec-wai >= 0.10 && < 0.11
|
, hspec-wai >= 0.10 && < 0.12
|
||||||
, hspec-wai-json >= 0.10 && < 0.11
|
, hspec-wai-json >= 0.10 && < 0.12
|
||||||
, http-types >= 0.12.3 && < 0.13
|
, http-types >= 0.12.3 && < 0.13
|
||||||
, lens >= 4.14 && < 4.20
|
, lens >= 4.14 && < 5.3
|
||||||
, lens-aeson >= 1.0.1 && < 1.2
|
, lens-aeson >= 1.0.1 && < 1.3
|
||||||
, monad-control >= 1.0.1 && < 1.1
|
, monad-control >= 1.0.1 && < 1.1
|
||||||
, postgrest
|
, postgrest
|
||||||
, process >= 1.4.2 && < 1.7
|
, process >= 1.4.2 && < 1.7
|
||||||
, protolude >= 0.3 && < 0.4
|
, protolude >= 0.3.1 && < 0.4
|
||||||
, regex-tdfa >= 1.2.2 && < 1.4
|
, regex-tdfa >= 1.2.2 && < 1.4
|
||||||
|
, scientific >= 0.3.4 && < 0.4
|
||||||
, text >= 1.2.2 && < 1.3
|
, text >= 1.2.2 && < 1.3
|
||||||
, time >= 1.6 && < 1.11
|
|
||||||
, transformers-base >= 0.4.4 && < 0.5
|
, transformers-base >= 0.4.4 && < 0.5
|
||||||
, wai >= 3.2.1 && < 3.3
|
, wai >= 3.2.1 && < 3.3
|
||||||
, wai-extra >= 3.0.19 && < 3.1
|
, wai-extra >= 3.0.19 && < 3.2
|
||||||
|
ghc-options: -threaded -O0 -Werror -Wall -fwarn-identities
|
||||||
|
-fno-spec-constr -optP-Wno-nonportable-include-path
|
||||||
|
-fno-warn-missing-signatures
|
||||||
|
-fwrite-ide-info
|
||||||
|
-- https://github.com/PostgREST/postgrest/issues/387
|
||||||
|
-with-rtsopts=-K33K
|
||||||
|
|
||||||
|
test-suite doctests
|
||||||
|
type: exitcode-stdio-1.0
|
||||||
|
default-language: Haskell2010
|
||||||
|
default-extensions: OverloadedStrings
|
||||||
|
NoImplicitPrelude
|
||||||
|
hs-source-dirs: test/doc
|
||||||
|
main-is: Main.hs
|
||||||
|
build-depends: base >= 4.9 && < 4.17
|
||||||
|
, doctest >= 0.8
|
||||||
|
, postgrest
|
||||||
|
, pretty-simple
|
||||||
|
, protolude >= 0.3.1 && < 0.4
|
||||||
|
ghc-options: -threaded -O0 -Werror -Wall -fwarn-identities
|
||||||
|
-fno-spec-constr -optP-Wno-nonportable-include-path
|
||||||
|
|||||||
@@ -1,23 +1,63 @@
|
|||||||
with (import ./default.nix);
|
# The additional modules below have large dependencies and are therefore
|
||||||
pkgs.lib.overrideDerivation env (
|
# disabled by default. You can activate them by passing arguments to nix-shell,
|
||||||
|
# e.g.:
|
||||||
|
#
|
||||||
|
# nix-shell --arg docker true
|
||||||
|
#
|
||||||
|
# We highly recommend that use the PostgREST binary cache by installing cachix
|
||||||
|
# (https://app.cachix.org/) and running `cachix use postgrest`.
|
||||||
|
{ docker ? false
|
||||||
|
, memory ? false
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
postgrest =
|
||||||
|
import ./default.nix { };
|
||||||
|
|
||||||
|
inherit (postgrest) pkgs;
|
||||||
|
|
||||||
|
inherit (pkgs) lib;
|
||||||
|
|
||||||
|
toolboxes =
|
||||||
|
[
|
||||||
|
postgrest.cabalTools
|
||||||
|
postgrest.devTools
|
||||||
|
postgrest.loadtest
|
||||||
|
postgrest.nixpkgsTools
|
||||||
|
postgrest.style
|
||||||
|
postgrest.tests
|
||||||
|
postgrest.withTools
|
||||||
|
postgrest.release
|
||||||
|
]
|
||||||
|
++ lib.optional docker postgrest.docker
|
||||||
|
++ lib.optional memory postgrest.memory;
|
||||||
|
|
||||||
|
in
|
||||||
|
lib.overrideDerivation postgrest.env (
|
||||||
base: {
|
base: {
|
||||||
buildInputs =
|
buildInputs =
|
||||||
base.buildInputs ++ [
|
base.buildInputs ++ [
|
||||||
pkgs.cabal-install
|
pkgs.cabal-install
|
||||||
pkgs.stack
|
|
||||||
pkgs.cabal2nix
|
pkgs.cabal2nix
|
||||||
|
pkgs.git
|
||||||
pkgs.postgresql
|
pkgs.postgresql
|
||||||
nixpkgsUpgrade
|
pkgs.update-nix-fetchgit
|
||||||
tests
|
postgrest.hsie.bin
|
||||||
style
|
]
|
||||||
lint
|
++ toolboxes;
|
||||||
];
|
|
||||||
|
|
||||||
shellHook =
|
shellHook =
|
||||||
''
|
''
|
||||||
# Set our pinned version of Nixpkgs in the NIX_PATH so that
|
export HISTFILE=.history
|
||||||
# `stack --nix` also uses that version.
|
|
||||||
NIX_PATH="nixpkgs=${nixpkgs}"
|
source ${pkgs.bash-completion}/etc/profile.d/bash_completion.sh
|
||||||
'';
|
source ${pkgs.git}/share/git/contrib/completion/git-completion.bash
|
||||||
|
source ${postgrest.hsie.bash-completion}
|
||||||
|
|
||||||
|
''
|
||||||
|
+ builtins.concatStringsSep "\n" (
|
||||||
|
builtins.map (bash-completion: "source ${bash-completion}") (
|
||||||
|
builtins.concatLists (builtins.map (toolbox: toolbox.bash-completion) toolboxes)
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
|
|
||||||
|
module PostgREST.Admin
|
||||||
|
( runAdmin
|
||||||
|
) where
|
||||||
|
|
||||||
|
import qualified Data.Text as T
|
||||||
|
import qualified Hasql.Session as SQL
|
||||||
|
import qualified Network.HTTP.Types.Status as HTTP
|
||||||
|
import qualified Network.Wai as Wai
|
||||||
|
import qualified Network.Wai.Handler.Warp as Warp
|
||||||
|
|
||||||
|
import Control.Monad.Extra (whenJust)
|
||||||
|
|
||||||
|
import Network.Socket
|
||||||
|
import Network.Socket.ByteString
|
||||||
|
|
||||||
|
import PostgREST.AppState (AppState)
|
||||||
|
import PostgREST.Config (AppConfig (..))
|
||||||
|
|
||||||
|
import qualified PostgREST.AppState as AppState
|
||||||
|
|
||||||
|
import Protolude
|
||||||
|
|
||||||
|
runAdmin :: AppConfig -> AppState -> Warp.Settings -> IO ()
|
||||||
|
runAdmin conf@AppConfig{configAdminServerPort} appState settings =
|
||||||
|
whenJust configAdminServerPort $ \adminPort -> do
|
||||||
|
AppState.logWithZTime appState $ "Admin server listening on port " <> show adminPort
|
||||||
|
void . forkIO $ Warp.runSettings (settings & Warp.setPort adminPort) adminApp
|
||||||
|
where
|
||||||
|
adminApp = admin appState conf
|
||||||
|
|
||||||
|
-- | PostgREST admin application
|
||||||
|
admin :: AppState.AppState -> AppConfig -> Wai.Application
|
||||||
|
admin appState appConfig req respond = do
|
||||||
|
isMainAppReachable <- any isRight <$> reachMainApp appConfig
|
||||||
|
isSchemaCacheLoaded <- isJust <$> AppState.getSchemaCache appState
|
||||||
|
isConnectionUp <-
|
||||||
|
if configDbChannelEnabled appConfig
|
||||||
|
then AppState.getIsListenerOn appState
|
||||||
|
else isRight <$> AppState.usePool appState (SQL.sql "SELECT 1")
|
||||||
|
|
||||||
|
case Wai.pathInfo req of
|
||||||
|
["ready"] ->
|
||||||
|
respond $ Wai.responseLBS (if isMainAppReachable && isConnectionUp && isSchemaCacheLoaded then HTTP.status200 else HTTP.status503) [] mempty
|
||||||
|
["live"] ->
|
||||||
|
respond $ Wai.responseLBS (if isMainAppReachable then HTTP.status200 else HTTP.status503) [] mempty
|
||||||
|
_ ->
|
||||||
|
respond $ Wai.responseLBS HTTP.status404 [] mempty
|
||||||
|
|
||||||
|
-- Try to connect to the main app socket
|
||||||
|
-- Note that it doesn't even send a valid HTTP request, we just want to check that the main app is accepting connections
|
||||||
|
-- The code for resolving the "*4", "!4", "*6", "!6", "*" special values is taken from
|
||||||
|
-- https://hackage.haskell.org/package/streaming-commons-0.2.2.4/docs/src/Data.Streaming.Network.html#bindPortGenEx
|
||||||
|
reachMainApp :: AppConfig -> IO [Either IOException ()]
|
||||||
|
reachMainApp AppConfig{..} =
|
||||||
|
case configServerUnixSocket of
|
||||||
|
Just path -> do
|
||||||
|
sock <- socket AF_UNIX Stream 0
|
||||||
|
(:[]) <$> try (do
|
||||||
|
connect sock $ SockAddrUnix path
|
||||||
|
withSocketsDo $ bracket (pure sock) close sendEmpty)
|
||||||
|
Nothing -> do
|
||||||
|
let
|
||||||
|
host | configServerHost `elem` ["*4", "!4", "*6", "!6", "*"] = Nothing
|
||||||
|
| otherwise = Just configServerHost
|
||||||
|
filterAddrs xs =
|
||||||
|
case configServerHost of
|
||||||
|
"*4" -> ipv4Addrs xs ++ ipv6Addrs xs
|
||||||
|
"!4" -> ipv4Addrs xs
|
||||||
|
"*6" -> ipv6Addrs xs ++ ipv4Addrs xs
|
||||||
|
"!6" -> ipv6Addrs xs
|
||||||
|
_ -> xs
|
||||||
|
ipv4Addrs = filter ((/=) AF_INET6 . addrFamily)
|
||||||
|
ipv6Addrs = filter ((==) AF_INET6 . addrFamily)
|
||||||
|
|
||||||
|
addrs <- getAddrInfo (Just $ defaultHints { addrSocketType = Stream }) (T.unpack <$> host) (Just . show $ configServerPort)
|
||||||
|
tryAddr `traverse` filterAddrs addrs
|
||||||
|
where
|
||||||
|
sendEmpty sock = void $ send sock mempty
|
||||||
|
tryAddr :: AddrInfo -> IO (Either IOException ())
|
||||||
|
tryAddr addr = do
|
||||||
|
sock <- socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr)
|
||||||
|
try $ do
|
||||||
|
connect sock $ addrAddress addr
|
||||||
|
withSocketsDo $ bracket (pure sock) close sendEmpty
|
||||||
+278
-245
@@ -1,72 +1,113 @@
|
|||||||
{-|
|
{-|
|
||||||
Module : PostgREST.ApiRequest
|
Module : PostgREST.Request.ApiRequest
|
||||||
Description : PostgREST functions to translate HTTP request to a domain type called ApiRequest.
|
Description : PostgREST functions to translate HTTP request to a domain type called ApiRequest.
|
||||||
-}
|
-}
|
||||||
{-# LANGUAGE LambdaCase #-}
|
{-# LANGUAGE LambdaCase #-}
|
||||||
{-# LANGUAGE MultiWayIf #-}
|
{-# LANGUAGE MultiWayIf #-}
|
||||||
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
|
|
||||||
module PostgREST.ApiRequest (
|
module PostgREST.ApiRequest
|
||||||
ApiRequest(..)
|
( ApiRequest(..)
|
||||||
, InvokeMethod(..)
|
, InvokeMethod(..)
|
||||||
, ContentType(..)
|
, Mutation(..)
|
||||||
, Action(..)
|
, MediaType(..)
|
||||||
, Target(..)
|
, Action(..)
|
||||||
, mutuallyAgreeable
|
, Target(..)
|
||||||
, userApiRequest
|
, Payload(..)
|
||||||
) where
|
, userApiRequest
|
||||||
|
) where
|
||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
import qualified Data.ByteString as BS
|
import qualified Data.Aeson.Key as K
|
||||||
import qualified Data.ByteString.Lazy as BL
|
import qualified Data.Aeson.KeyMap as KM
|
||||||
|
import qualified Data.ByteString.Char8 as BS
|
||||||
|
import qualified Data.ByteString.Lazy as LBS
|
||||||
import qualified Data.CaseInsensitive as CI
|
import qualified Data.CaseInsensitive as CI
|
||||||
import qualified Data.Csv as CSV
|
import qualified Data.Csv as CSV
|
||||||
import qualified Data.HashMap.Strict as M
|
import qualified Data.HashMap.Strict as HM
|
||||||
import qualified Data.List as L
|
import qualified Data.List as L
|
||||||
|
import qualified Data.List.NonEmpty as NonEmptyList
|
||||||
|
import qualified Data.Map.Strict as M
|
||||||
import qualified Data.Set as S
|
import qualified Data.Set as S
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text.Encoding as T
|
||||||
import qualified Data.Vector as V
|
import qualified Data.Vector as V
|
||||||
|
|
||||||
|
import Data.Either.Combinators (mapBoth)
|
||||||
|
|
||||||
import Control.Arrow ((***))
|
import Control.Arrow ((***))
|
||||||
import Data.Aeson.Types (emptyArray, emptyObject)
|
import Data.Aeson.Types (emptyArray, emptyObject)
|
||||||
import Data.List (last, lookup, partition)
|
import Data.List (lookup, union)
|
||||||
import Data.List.NonEmpty (head)
|
import Data.Ranged.Ranges (emptyRange, rangeIntersection,
|
||||||
import Data.Maybe (fromJust)
|
rangeIsEmpty)
|
||||||
import Data.Ranged.Ranges (Range (..), emptyRange,
|
import Network.HTTP.Types.Header (RequestHeaders, hCookie)
|
||||||
rangeIntersection)
|
import Network.HTTP.Types.URI (parseSimpleQuery)
|
||||||
import Network.HTTP.Base (urlEncodeVars)
|
|
||||||
import Network.HTTP.Types.Header (hAuthorization, hCookie)
|
|
||||||
import Network.HTTP.Types.URI (parseQueryReplacePlus,
|
|
||||||
parseSimpleQuery)
|
|
||||||
import Network.Wai (Request (..))
|
import Network.Wai (Request (..))
|
||||||
import Network.Wai.Parse (parseHttpAccept)
|
import Network.Wai.Parse (parseHttpAccept)
|
||||||
import Web.Cookie (parseCookiesText)
|
import Web.Cookie (parseCookies)
|
||||||
|
|
||||||
|
import PostgREST.ApiRequest.QueryParams (QueryParams (..))
|
||||||
|
import PostgREST.ApiRequest.Types (ApiRequestError (..),
|
||||||
|
RangeError (..))
|
||||||
|
import PostgREST.Config (AppConfig (..),
|
||||||
|
OpenAPIMode (..))
|
||||||
|
import PostgREST.MediaType (MTPlanFormat (..),
|
||||||
|
MediaType (..))
|
||||||
|
import PostgREST.RangeQuery (NonnegRange, allRange,
|
||||||
|
convertToLimitZeroRange,
|
||||||
|
hasLimitZero,
|
||||||
|
rangeRequested)
|
||||||
|
import PostgREST.SchemaCache.Identifiers (FieldName,
|
||||||
|
QualifiedIdentifier (..),
|
||||||
|
Schema)
|
||||||
|
|
||||||
|
import qualified PostgREST.ApiRequest.Preferences as Preferences
|
||||||
|
import qualified PostgREST.ApiRequest.QueryParams as QueryParams
|
||||||
|
import qualified PostgREST.MediaType as MediaType
|
||||||
|
|
||||||
|
import Protolude
|
||||||
|
|
||||||
|
|
||||||
import Data.Ranged.Boundaries
|
type RequestBody = LBS.ByteString
|
||||||
|
|
||||||
import PostgREST.Error (ApiRequestError (..))
|
data Payload
|
||||||
import PostgREST.RangeQuery (NonnegRange, allRange, rangeGeq,
|
= ProcessedJSON -- ^ Cached attributes of a JSON payload
|
||||||
rangeLimit, rangeOffset, rangeRequested,
|
{ payRaw :: LBS.ByteString
|
||||||
restrictRange)
|
-- ^ This is the raw ByteString that comes from the request body. We
|
||||||
import PostgREST.Types
|
-- cache this instead of an Aeson Value because it was detected that for
|
||||||
import Protolude hiding (head, toS)
|
-- large payloads the encoding had high memory usage, see
|
||||||
import Protolude.Conv (toS)
|
-- https://github.com/PostgREST/postgrest/pull/1005 for more details
|
||||||
|
, payKeys :: S.Set Text
|
||||||
type RequestBody = BL.ByteString
|
-- ^ Keys of the object or if it's an array these keys are guaranteed to
|
||||||
|
-- be the same across all its objects
|
||||||
|
}
|
||||||
|
| ProcessedUrlEncoded { payArray :: [(Text, Text)], payKeys :: S.Set Text }
|
||||||
|
| RawJSON { payRaw :: LBS.ByteString }
|
||||||
|
| RawPay { payRaw :: LBS.ByteString }
|
||||||
|
|
||||||
data InvokeMethod = InvHead | InvGet | InvPost deriving Eq
|
data InvokeMethod = InvHead | InvGet | InvPost deriving Eq
|
||||||
|
data Mutation = MutationCreate | MutationDelete | MutationSingleUpsert | MutationUpdate deriving Eq
|
||||||
|
|
||||||
-- | Types of things a user wants to do to tables/views/procs
|
-- | Types of things a user wants to do to tables/views/procs
|
||||||
data Action = ActionCreate | ActionRead{isHead :: Bool}
|
data Action
|
||||||
| ActionUpdate | ActionDelete
|
= ActionMutate Mutation
|
||||||
| ActionSingleUpsert | ActionInvoke InvokeMethod
|
| ActionRead {isHead :: Bool}
|
||||||
| ActionInfo | ActionInspect{isHead :: Bool}
|
| ActionInvoke InvokeMethod
|
||||||
|
| ActionInfo
|
||||||
|
| ActionInspect {isHead :: Bool}
|
||||||
deriving Eq
|
deriving Eq
|
||||||
|
-- | The path info that will be mapped to a target (used to handle validations and errors before defining the Target)
|
||||||
|
data PathInfo
|
||||||
|
= PathInfo
|
||||||
|
{ pathName :: Text
|
||||||
|
, pathIsProc :: Bool
|
||||||
|
, pathIsDefSpec :: Bool
|
||||||
|
, pathIsRootSpec :: Bool
|
||||||
|
}
|
||||||
-- | The target db object of a user action
|
-- | The target db object of a user action
|
||||||
data Target = TargetIdent QualifiedIdentifier
|
data Target = TargetIdent QualifiedIdentifier
|
||||||
| TargetProc{tpQi :: QualifiedIdentifier, tpIsRootSpec :: Bool}
|
| TargetProc{tProc :: QualifiedIdentifier, tpIsRootSpec :: Bool}
|
||||||
| TargetDefaultSpec{tdsSchema :: Schema} -- The default spec offered at root "/"
|
| TargetDefaultSpec{tdsSchema :: Schema} -- The default spec offered at root "/"
|
||||||
| TargetUnknown [Text]
|
|
||||||
deriving Eq
|
|
||||||
|
|
||||||
{-|
|
{-|
|
||||||
Describes what the user wants to do. This data type is a
|
Describes what the user wants to do. This data type is a
|
||||||
@@ -76,227 +117,188 @@ data Target = TargetIdent QualifiedIdentifier
|
|||||||
if it is an action we are able to perform.
|
if it is an action we are able to perform.
|
||||||
-}
|
-}
|
||||||
data ApiRequest = ApiRequest {
|
data ApiRequest = ApiRequest {
|
||||||
iAction :: Action -- ^ Similar but not identical to HTTP verb, e.g. Create/Invoke both POST
|
iAction :: Action -- ^ Similar but not identical to HTTP method, e.g. Create/Invoke both POST
|
||||||
, iRange :: M.HashMap ByteString NonnegRange -- ^ Requested range of rows within response
|
, iRange :: HM.HashMap Text NonnegRange -- ^ Requested range of rows within response
|
||||||
, iTopLevelRange :: NonnegRange -- ^ Requested range of rows from the top level
|
, iTopLevelRange :: NonnegRange -- ^ Requested range of rows from the top level
|
||||||
, iTarget :: Target -- ^ The target, be it calling a proc or accessing a table
|
, iTarget :: Target -- ^ The target, be it calling a proc or accessing a table
|
||||||
, iAccepts :: [ContentType] -- ^ Content types the client will accept, [CTAny] if no Accept header
|
, iPayload :: Maybe Payload -- ^ Data sent by client and used for mutation actions
|
||||||
, iPayload :: Maybe PayloadJSON -- ^ Data sent by client and used for mutation actions
|
, iPreferences :: Preferences.Preferences -- ^ Prefer header values
|
||||||
, iPreferRepresentation :: PreferRepresentation -- ^ If client wants created items echoed back
|
, iQueryParams :: QueryParams.QueryParams
|
||||||
, iPreferParameters :: Maybe PreferParameters -- ^ How to pass parameters to a stored procedure
|
, iColumns :: S.Set FieldName -- ^ parsed colums from &columns parameter and payload
|
||||||
, iPreferCount :: Maybe PreferCount -- ^ Whether the client wants a result count
|
, iHeaders :: [(ByteString, ByteString)] -- ^ HTTP request headers
|
||||||
, iPreferResolution :: Maybe PreferResolution -- ^ Whether the client wants to UPSERT or ignore records on PK conflict
|
, iCookies :: [(ByteString, ByteString)] -- ^ Request Cookies
|
||||||
, iFilters :: [(Text, Text)] -- ^ Filters on the result ("id", "eq.10")
|
|
||||||
, iLogic :: [(Text, Text)] -- ^ &and and &or parameters used for complex boolean logic
|
|
||||||
, iSelect :: Maybe Text -- ^ &select parameter used to shape the response
|
|
||||||
, iOnConflict :: Maybe Text -- ^ &on_conflict parameter used to upsert on specific unique keys
|
|
||||||
, iColumns :: Maybe Text -- ^ &columns parameter used to shape the payload
|
|
||||||
, iOrder :: [(Text, Text)] -- ^ &order parameters for each level
|
|
||||||
, iCanonicalQS :: ByteString -- ^ Alphabetized (canonical) request query string for response URLs
|
|
||||||
, iJWT :: Text -- ^ JSON Web Token
|
|
||||||
, iHeaders :: [(Text, Text)] -- ^ HTTP request headers
|
|
||||||
, iCookies :: [(Text, Text)] -- ^ Request Cookies
|
|
||||||
, iPath :: ByteString -- ^ Raw request path
|
, iPath :: ByteString -- ^ Raw request path
|
||||||
, iMethod :: ByteString -- ^ Raw request method
|
, iMethod :: ByteString -- ^ Raw request method
|
||||||
, iProfile :: Maybe Schema -- ^ The request profile for enabling use of multiple schemas. Follows the spec in hhttps://www.w3.org/TR/dx-prof-conneg/ttps://www.w3.org/TR/dx-prof-conneg/.
|
, iSchema :: Schema -- ^ The request schema. Can vary depending on profile headers.
|
||||||
, iSchema :: Schema -- ^ The request schema. Can vary depending on iProfile.
|
, iNegotiatedByProfile :: Bool -- ^ If schema was was chosen according to the profile spec https://www.w3.org/TR/dx-prof-conneg/
|
||||||
|
, iAcceptMediaType :: MediaType -- ^ The media type in the Accept header
|
||||||
|
, iContentMediaType :: MediaType -- ^ The media type in the Content-Type header
|
||||||
}
|
}
|
||||||
|
|
||||||
-- | Examines HTTP request and translates it into user intent.
|
-- | Examines HTTP request and translates it into user intent.
|
||||||
userApiRequest :: NonEmpty Schema -> Maybe Text -> Request -> RequestBody -> Either ApiRequestError ApiRequest
|
userApiRequest :: AppConfig -> Request -> RequestBody -> Either ApiRequestError ApiRequest
|
||||||
userApiRequest confSchemas rootSpec req reqBody
|
userApiRequest conf req reqBody = do
|
||||||
| isJust profile && fromJust profile `notElem` confSchemas = Left $ UnacceptableSchema $ toList confSchemas
|
pInfo@PathInfo{..} <- getPathInfo conf $ pathInfo req
|
||||||
| isTargetingProc && method `notElem` ["HEAD", "GET", "POST"] = Left ActionInappropriate
|
act <- getAction pInfo method
|
||||||
| topLevelRange == emptyRange = Left InvalidRange
|
qPrms <- first QueryParamError $ QueryParams.parse (pathIsProc && act `elem` [ActionInvoke InvGet, ActionInvoke InvHead]) $ rawQueryString req
|
||||||
| shouldParsePayload && isLeft payload = either (Left . InvalidBody . toS) witness payload
|
(acceptMediaType, contentMediaType) <- getMediaTypes conf hdrs act pInfo
|
||||||
| otherwise = Right ApiRequest {
|
(schema, negotiatedByProfile) <- getSchema conf hdrs method
|
||||||
iAction = action
|
(topLevelRange, ranges) <- getRanges method qPrms hdrs
|
||||||
, iTarget = target
|
(payload, columns) <- getPayload reqBody contentMediaType qPrms act pInfo
|
||||||
|
return $ ApiRequest {
|
||||||
|
iAction = act
|
||||||
|
, iTarget = if | pathIsProc -> TargetProc (QualifiedIdentifier schema pathName) pathIsRootSpec
|
||||||
|
| pathIsDefSpec -> TargetDefaultSpec schema
|
||||||
|
| otherwise -> TargetIdent $ QualifiedIdentifier schema pathName
|
||||||
, iRange = ranges
|
, iRange = ranges
|
||||||
, iTopLevelRange = topLevelRange
|
, iTopLevelRange = topLevelRange
|
||||||
, iAccepts = maybe [CTAny] (map decodeContentType . parseHttpAccept) $ lookupHeader "accept"
|
, iPayload = payload
|
||||||
, iPayload = relevantPayload
|
, iPreferences = Preferences.fromHeaders (configDbTxAllowOverride conf) hdrs
|
||||||
, iPreferRepresentation = representation
|
, iQueryParams = qPrms
|
||||||
, iPreferParameters = if | hasPrefer (show SingleObject) -> Just SingleObject
|
|
||||||
| hasPrefer (show MultipleObjects) -> Just MultipleObjects
|
|
||||||
| otherwise -> Nothing
|
|
||||||
, iPreferCount = if | hasPrefer (show ExactCount) -> Just ExactCount
|
|
||||||
| hasPrefer (show PlannedCount) -> Just PlannedCount
|
|
||||||
| hasPrefer (show EstimatedCount) -> Just EstimatedCount
|
|
||||||
| otherwise -> Nothing
|
|
||||||
, iPreferResolution = if | hasPrefer (show MergeDuplicates) -> Just MergeDuplicates
|
|
||||||
| hasPrefer (show IgnoreDuplicates) -> Just IgnoreDuplicates
|
|
||||||
| otherwise -> Nothing
|
|
||||||
, iFilters = filters
|
|
||||||
, iLogic = [(toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, endingIn ["and", "or"] k ]
|
|
||||||
, iSelect = toS <$> join (lookup "select" qParams)
|
|
||||||
, iOnConflict = toS <$> join (lookup "on_conflict" qParams)
|
|
||||||
, iColumns = columns
|
, iColumns = columns
|
||||||
, iOrder = [(toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, endingIn ["order"] k ]
|
, iHeaders = iHdrs
|
||||||
, iCanonicalQS = toS $ urlEncodeVars
|
, iCookies = iCkies
|
||||||
. L.sortOn fst
|
|
||||||
. map (join (***) toS . second (fromMaybe BS.empty))
|
|
||||||
$ qString
|
|
||||||
, iJWT = tokenStr
|
|
||||||
, iHeaders = [ (toS $ CI.foldedCase k, toS v) | (k,v) <- hdrs, k /= hCookie]
|
|
||||||
, iCookies = maybe [] parseCookiesText $ lookupHeader "Cookie"
|
|
||||||
, iPath = rawPathInfo req
|
, iPath = rawPathInfo req
|
||||||
, iMethod = method
|
, iMethod = method
|
||||||
, iProfile = profile
|
|
||||||
, iSchema = schema
|
, iSchema = schema
|
||||||
|
, iNegotiatedByProfile = negotiatedByProfile
|
||||||
|
, iAcceptMediaType = acceptMediaType
|
||||||
|
, iContentMediaType = contentMediaType
|
||||||
}
|
}
|
||||||
where
|
where
|
||||||
-- queryString with '+' converted to ' '(space)
|
method = requestMethod req
|
||||||
qString = parseQueryReplacePlus True $ rawQueryString req
|
hdrs = requestHeaders req
|
||||||
-- rpcQParams = Rpc query params e.g. /rpc/name?param1=val1, similar to filter but with no operator(eq, lt..)
|
lookupHeader = flip lookup hdrs
|
||||||
(filters, rpcQParams) =
|
iHdrs = [ (CI.foldedCase k, v) | (k,v) <- hdrs, k /= hCookie]
|
||||||
case action of
|
iCkies = maybe [] parseCookies $ lookupHeader "Cookie"
|
||||||
ActionInvoke InvGet -> partitionFlts
|
|
||||||
ActionInvoke InvHead -> partitionFlts
|
getPathInfo :: AppConfig -> [Text] -> Either ApiRequestError PathInfo
|
||||||
_ -> (flts, [])
|
getPathInfo AppConfig{configOpenApiMode, configDbRootSpec} path =
|
||||||
partitionFlts = partition (liftM2 (||) (isEmbedPath . fst) (hasOperator . snd)) flts
|
case path of
|
||||||
flts =
|
[] -> case configDbRootSpec of
|
||||||
[ (toS k, toS $ fromJust v) |
|
Just (QualifiedIdentifier _ pathName) -> Right $ PathInfo pathName True False True
|
||||||
(k,v) <- qParams, isJust v,
|
Nothing | configOpenApiMode == OADisabled -> Left NotFound
|
||||||
k `notElem` ["select", "columns"],
|
| otherwise -> Right $ PathInfo mempty False True False
|
||||||
not (endingIn ["order", "limit", "offset", "and", "or"] k) ]
|
[table] -> Right $ PathInfo table False False False
|
||||||
hasOperator val = any (`T.isPrefixOf` val) $
|
["rpc", pName] -> Right $ PathInfo pName True False False
|
||||||
((<> ".") <$> "not":M.keys operators) ++
|
_ -> Left NotFound
|
||||||
((<> "(") <$> M.keys ftsOperators)
|
|
||||||
isEmbedPath = T.isInfixOf "."
|
getAction :: PathInfo -> ByteString -> Either ApiRequestError Action
|
||||||
isTargetingProc = case target of
|
getAction PathInfo{pathIsProc, pathIsDefSpec} method =
|
||||||
TargetProc _ _ -> True
|
if pathIsProc && method `notElem` ["HEAD", "GET", "POST", "OPTIONS"]
|
||||||
_ -> False
|
then Left $ InvalidRpcMethod method
|
||||||
isTargetingDefaultSpec = case target of
|
else case method of
|
||||||
TargetDefaultSpec _ -> True
|
-- The HEAD method is identical to GET except that the server MUST NOT return a message-body in the response
|
||||||
_ -> False
|
-- From https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4
|
||||||
contentType = decodeContentType . fromMaybe "application/json" $ lookupHeader "content-type"
|
"HEAD" | pathIsDefSpec -> Right $ ActionInspect{isHead=True}
|
||||||
columns
|
| pathIsProc -> Right $ ActionInvoke InvHead
|
||||||
| action `elem` [ActionCreate, ActionUpdate, ActionInvoke InvPost] = toS <$> join (lookup "columns" qParams)
|
| otherwise -> Right $ ActionRead{isHead=True}
|
||||||
| otherwise = Nothing
|
"GET" | pathIsDefSpec -> Right $ ActionInspect{isHead=False}
|
||||||
payload =
|
| pathIsProc -> Right $ ActionInvoke InvGet
|
||||||
case (contentType, action) of
|
| otherwise -> Right $ ActionRead{isHead=False}
|
||||||
(_, ActionInvoke InvGet) -> Right rpcPrmsToJson
|
"POST" | pathIsProc -> Right $ ActionInvoke InvPost
|
||||||
(_, ActionInvoke InvHead) -> Right rpcPrmsToJson
|
| otherwise -> Right $ ActionMutate MutationCreate
|
||||||
(CTApplicationJSON, _) ->
|
"PATCH" -> Right $ ActionMutate MutationUpdate
|
||||||
|
"PUT" -> Right $ ActionMutate MutationSingleUpsert
|
||||||
|
"DELETE" -> Right $ ActionMutate MutationDelete
|
||||||
|
"OPTIONS" -> Right ActionInfo
|
||||||
|
_ -> Left $ UnsupportedMethod method
|
||||||
|
|
||||||
|
getMediaTypes :: AppConfig -> RequestHeaders -> Action -> PathInfo -> Either ApiRequestError (MediaType, MediaType)
|
||||||
|
getMediaTypes conf hdrs action path = do
|
||||||
|
acceptMediaType <- negotiateContent conf action path accepts
|
||||||
|
pure (acceptMediaType, contentMediaType)
|
||||||
|
where
|
||||||
|
accepts = maybe [MTAny] (map MediaType.decodeMediaType . parseHttpAccept) $ lookupHeader "accept"
|
||||||
|
contentMediaType = maybe MTApplicationJSON MediaType.decodeMediaType $ lookupHeader "content-type"
|
||||||
|
lookupHeader = flip lookup hdrs
|
||||||
|
|
||||||
|
getSchema :: AppConfig -> RequestHeaders -> ByteString -> Either ApiRequestError (Schema, Bool)
|
||||||
|
getSchema AppConfig{configDbSchemas} hdrs method = do
|
||||||
|
case profile of
|
||||||
|
Just p | p `notElem` configDbSchemas -> Left $ UnacceptableSchema $ toList configDbSchemas
|
||||||
|
| otherwise -> Right (p, True)
|
||||||
|
Nothing -> Right (defaultSchema, length configDbSchemas /= 1) -- if we have many schemas, assume the default schema was negotiated
|
||||||
|
where
|
||||||
|
defaultSchema = NonEmptyList.head configDbSchemas
|
||||||
|
profile = case method of
|
||||||
|
-- POST/PATCH/PUT/DELETE don't use the same header as per the spec
|
||||||
|
"DELETE" -> contentProfile
|
||||||
|
"PATCH" -> contentProfile
|
||||||
|
"POST" -> contentProfile
|
||||||
|
"PUT" -> contentProfile
|
||||||
|
_ -> acceptProfile
|
||||||
|
contentProfile = T.decodeUtf8 <$> lookupHeader "Content-Profile"
|
||||||
|
acceptProfile = T.decodeUtf8 <$> lookupHeader "Accept-Profile"
|
||||||
|
lookupHeader = flip lookup hdrs
|
||||||
|
|
||||||
|
getRanges :: ByteString -> QueryParams -> RequestHeaders -> Either ApiRequestError (NonnegRange, HM.HashMap Text NonnegRange)
|
||||||
|
getRanges method QueryParams{qsOrder,qsRanges} hdrs
|
||||||
|
| isInvalidRange = Left $ InvalidRange (if rangeIsEmpty headerRange then LowerGTUpper else NegativeLimit)
|
||||||
|
| method `elem` ["PATCH", "DELETE"] && not (null qsRanges) && null qsOrder = Left LimitNoOrderError
|
||||||
|
| method == "PUT" && topLevelRange /= allRange = Left PutLimitNotAllowedError
|
||||||
|
| otherwise = Right (topLevelRange, ranges)
|
||||||
|
where
|
||||||
|
-- According to the RFC (https://www.rfc-editor.org/rfc/rfc9110.html#name-range),
|
||||||
|
-- the Range header must be ignored for all methods other than GET
|
||||||
|
headerRange = if method == "GET" then rangeRequested hdrs else allRange
|
||||||
|
limitRange = fromMaybe allRange (HM.lookup "limit" qsRanges)
|
||||||
|
headerAndLimitRange = rangeIntersection headerRange limitRange
|
||||||
|
-- Bypass all the ranges and send only the limit zero range (0 <= x <= -1) if
|
||||||
|
-- limit=0 is present in the query params (not allowed for the Range header)
|
||||||
|
ranges = HM.insert "limit" (convertToLimitZeroRange limitRange headerAndLimitRange) qsRanges
|
||||||
|
-- The only emptyRange allowed is the limit zero range
|
||||||
|
isInvalidRange = topLevelRange == emptyRange && not (hasLimitZero limitRange)
|
||||||
|
topLevelRange = fromMaybe allRange $ HM.lookup "limit" ranges -- if no limit is specified, get all the request rows
|
||||||
|
|
||||||
|
getPayload :: RequestBody -> MediaType -> QueryParams.QueryParams -> Action -> PathInfo -> Either ApiRequestError (Maybe Payload, S.Set FieldName)
|
||||||
|
getPayload reqBody contentMediaType QueryParams{qsColumns} action PathInfo{pathIsProc}= do
|
||||||
|
checkedPayload <- if shouldParsePayload then payload else Right Nothing
|
||||||
|
let cols = case (checkedPayload, columns) of
|
||||||
|
(Just ProcessedJSON{payKeys}, _) -> payKeys
|
||||||
|
(Just ProcessedUrlEncoded{payKeys}, _) -> payKeys
|
||||||
|
(Just RawJSON{}, Just cls) -> cls
|
||||||
|
_ -> S.empty
|
||||||
|
return (checkedPayload, cols)
|
||||||
|
where
|
||||||
|
payload :: Either ApiRequestError (Maybe Payload)
|
||||||
|
payload = mapBoth InvalidBody Just $ case (contentMediaType, pathIsProc) of
|
||||||
|
(MTApplicationJSON, _) ->
|
||||||
if isJust columns
|
if isJust columns
|
||||||
then Right $ RawJSON reqBody
|
then Right $ RawJSON reqBody
|
||||||
else note "All object keys must match" . payloadAttributes reqBody
|
else note "All object keys must match" . payloadAttributes reqBody
|
||||||
=<< if BL.null reqBody && isTargetingProc
|
=<< if LBS.null reqBody && pathIsProc
|
||||||
then Right emptyObject
|
then Right emptyObject
|
||||||
else JSON.eitherDecode reqBody
|
else first BS.pack $ JSON.eitherDecode reqBody
|
||||||
(CTTextCSV, _) -> do
|
(MTTextCSV, _) -> do
|
||||||
json <- csvToJson <$> CSV.decodeByName reqBody
|
json <- csvToJson <$> first BS.pack (CSV.decodeByName reqBody)
|
||||||
note "All lines must have same number of fields" $ payloadAttributes (JSON.encode json) json
|
note "All lines must have same number of fields" $ payloadAttributes (JSON.encode json) json
|
||||||
(CTOther "application/x-www-form-urlencoded", _) ->
|
(MTUrlEncoded, isProc) -> do
|
||||||
let json = M.fromList . map (toS *** JSON.String . toS) . parseSimpleQuery $ toS reqBody
|
let params = (T.decodeUtf8 *** T.decodeUtf8) <$> parseSimpleQuery (LBS.toStrict reqBody)
|
||||||
keys = S.fromList $ M.keys json in
|
if isProc
|
||||||
Right $ ProcessedJSON (JSON.encode json) keys
|
then Right $ ProcessedUrlEncoded params (S.fromList $ fst <$> params)
|
||||||
(ct, _) ->
|
else
|
||||||
Left $ toS $ "Content-Type not acceptable: " <> toMime ct
|
let paramsMap = HM.fromList $ (identity *** JSON.String) <$> params in
|
||||||
rpcPrmsToJson = ProcessedJSON (JSON.encode $ M.fromList $ second JSON.toJSON <$> rpcQParams) (S.fromList $ fst <$> rpcQParams)
|
Right $ ProcessedJSON (JSON.encode paramsMap) $ S.fromList (HM.keys paramsMap)
|
||||||
topLevelRange = fromMaybe allRange $ M.lookup "limit" ranges -- if no limit is specified, get all the request rows
|
(MTTextPlain, True) -> Right $ RawPay reqBody
|
||||||
action =
|
(MTTextXML, True) -> Right $ RawPay reqBody
|
||||||
case method of
|
(MTOctetStream, True) -> Right $ RawPay reqBody
|
||||||
-- The HEAD method is identical to GET except that the server MUST NOT return a message-body in the response
|
(ct, _) -> Left $ "Content-Type not acceptable: " <> MediaType.toMime ct
|
||||||
-- From https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4
|
|
||||||
"HEAD" | isTargetingDefaultSpec -> ActionInspect{isHead=True}
|
|
||||||
| isTargetingProc -> ActionInvoke InvHead
|
|
||||||
| otherwise -> ActionRead{isHead=True}
|
|
||||||
"GET" | isTargetingDefaultSpec -> ActionInspect{isHead=False}
|
|
||||||
| isTargetingProc -> ActionInvoke InvGet
|
|
||||||
| otherwise -> ActionRead{isHead=False}
|
|
||||||
"POST" -> if isTargetingProc
|
|
||||||
then ActionInvoke InvPost
|
|
||||||
else ActionCreate
|
|
||||||
"PATCH" -> ActionUpdate
|
|
||||||
"PUT" -> ActionSingleUpsert
|
|
||||||
"DELETE" -> ActionDelete
|
|
||||||
"OPTIONS" -> ActionInfo
|
|
||||||
_ -> ActionInspect{isHead=False}
|
|
||||||
|
|
||||||
defaultSchema = head confSchemas
|
shouldParsePayload = case (action, contentMediaType) of
|
||||||
profile
|
(ActionMutate MutationCreate, _) -> True
|
||||||
| length confSchemas <= 1 -- only enable content negotiation by profile when there are multiple schemas specified in the config
|
(ActionInvoke InvPost, _) -> True
|
||||||
= Nothing
|
(ActionMutate MutationSingleUpsert, _) -> True
|
||||||
| action `elem` [ActionCreate, ActionUpdate, ActionSingleUpsert, ActionDelete, ActionInvoke InvPost] -- POST/PATCH/PUT/DELETE don't use the same header as per the spec
|
(ActionMutate MutationUpdate, _) -> True
|
||||||
= Just $ maybe defaultSchema toS $ lookupHeader "Content-Profile"
|
_ -> False
|
||||||
| action `elem` [ActionRead True, ActionRead False, ActionInvoke InvGet, ActionInvoke InvHead,
|
|
||||||
ActionInspect False, ActionInspect True, ActionInfo]
|
|
||||||
= Just $ maybe defaultSchema toS $ lookupHeader "Accept-Profile"
|
|
||||||
| otherwise = Nothing
|
|
||||||
schema = fromMaybe defaultSchema profile
|
|
||||||
target = case path of
|
|
||||||
[] -> case rootSpec of
|
|
||||||
Just pName -> TargetProc (QualifiedIdentifier schema pName) True
|
|
||||||
Nothing -> TargetDefaultSpec schema
|
|
||||||
[table] -> TargetIdent $ QualifiedIdentifier schema table
|
|
||||||
["rpc", proc] -> TargetProc (QualifiedIdentifier schema proc) False
|
|
||||||
other -> TargetUnknown other
|
|
||||||
|
|
||||||
shouldParsePayload =
|
columns = case action of
|
||||||
action `elem`
|
ActionMutate MutationCreate -> qsColumns
|
||||||
[ActionCreate, ActionUpdate, ActionSingleUpsert,
|
ActionMutate MutationUpdate -> qsColumns
|
||||||
ActionInvoke InvPost,
|
ActionInvoke InvPost -> qsColumns
|
||||||
-- Though ActionInvoke{isGet=True}(a GET /rpc/..) doesn't really have a payload, we use the payload variable as a way
|
_ -> Nothing
|
||||||
-- to store the query string arguments to the function.
|
|
||||||
ActionInvoke InvGet,
|
|
||||||
ActionInvoke InvHead]
|
|
||||||
relevantPayload | shouldParsePayload = rightToMaybe payload
|
|
||||||
| otherwise = Nothing
|
|
||||||
path = pathInfo req
|
|
||||||
method = requestMethod req
|
|
||||||
hdrs = requestHeaders req
|
|
||||||
qParams = [(toS k, v)|(k,v) <- qString]
|
|
||||||
lookupHeader = flip lookup hdrs
|
|
||||||
hasPrefer :: Text -> Bool
|
|
||||||
hasPrefer val = any (\(h,v) -> h == "Prefer" && val `elem` split v) hdrs
|
|
||||||
where
|
|
||||||
split :: BS.ByteString -> [Text]
|
|
||||||
split = map T.strip . T.split (==',') . toS
|
|
||||||
representation
|
|
||||||
| hasPrefer (show Full) = Full
|
|
||||||
| hasPrefer (show None) = None
|
|
||||||
| otherwise = if action == ActionCreate
|
|
||||||
then HeadersOnly -- Assume the user wants the Location header(for POST) by default
|
|
||||||
else None
|
|
||||||
auth = fromMaybe "" $ lookupHeader hAuthorization
|
|
||||||
tokenStr = case T.split (== ' ') (toS auth) of
|
|
||||||
("Bearer" : t : _) -> t
|
|
||||||
_ -> ""
|
|
||||||
endingIn:: [Text] -> Text -> Bool
|
|
||||||
endingIn xx key = lastWord `elem` xx
|
|
||||||
where lastWord = last $ T.split (=='.') key
|
|
||||||
|
|
||||||
headerRange = rangeRequested hdrs
|
type CsvData = V.Vector (M.Map Text LBS.ByteString)
|
||||||
replaceLast x s = T.intercalate "." $ L.init (T.split (=='.') s) ++ [x]
|
|
||||||
limitParams :: M.HashMap ByteString NonnegRange
|
|
||||||
limitParams = M.fromList [(toS (replaceLast "limit" k), restrictRange (readMaybe =<< (toS <$> v)) allRange) | (k,v) <- qParams, isJust v, endingIn ["limit"] k]
|
|
||||||
offsetParams :: M.HashMap ByteString NonnegRange
|
|
||||||
offsetParams = M.fromList [(toS (replaceLast "limit" k), maybe allRange rangeGeq (readMaybe =<< (toS <$> v))) | (k,v) <- qParams, isJust v, endingIn ["offset"] k]
|
|
||||||
|
|
||||||
urlRange = M.unionWith f limitParams offsetParams
|
|
||||||
where
|
|
||||||
f rl ro = Range (BoundaryBelow o) (BoundaryAbove $ o + l - 1)
|
|
||||||
where
|
|
||||||
l = fromMaybe 0 $ rangeLimit rl
|
|
||||||
o = rangeOffset ro
|
|
||||||
ranges = M.insert "limit" (rangeIntersection headerRange (fromMaybe allRange (M.lookup "limit" urlRange))) urlRange
|
|
||||||
|
|
||||||
{-|
|
|
||||||
Find the best match from a list of content types accepted by the
|
|
||||||
client in order of decreasing preference and a list of types
|
|
||||||
producible by the server. If there is no match but the client
|
|
||||||
accepts */* then return the top server pick.
|
|
||||||
-}
|
|
||||||
mutuallyAgreeable :: [ContentType] -> [ContentType] -> Maybe ContentType
|
|
||||||
mutuallyAgreeable sProduces cAccepts =
|
|
||||||
let exact = listToMaybe $ L.intersect cAccepts sProduces in
|
|
||||||
if isNothing exact && CTAny `elem` cAccepts
|
|
||||||
then listToMaybe sProduces
|
|
||||||
else exact
|
|
||||||
|
|
||||||
type CsvData = V.Vector (M.HashMap Text BL.ByteString)
|
|
||||||
|
|
||||||
{-|
|
{-|
|
||||||
Converts CSV like
|
Converts CSV like
|
||||||
@@ -314,23 +316,23 @@ csvToJson :: (CSV.Header, CsvData) -> JSON.Value
|
|||||||
csvToJson (_, vals) =
|
csvToJson (_, vals) =
|
||||||
JSON.Array $ V.map rowToJsonObj vals
|
JSON.Array $ V.map rowToJsonObj vals
|
||||||
where
|
where
|
||||||
rowToJsonObj = JSON.Object .
|
rowToJsonObj = JSON.Object . KM.fromMapText .
|
||||||
M.map (\str ->
|
M.map (\str ->
|
||||||
if str == "NULL"
|
if str == "NULL"
|
||||||
then JSON.Null
|
then JSON.Null
|
||||||
else JSON.String $ toS str
|
else JSON.String . T.decodeUtf8 $ LBS.toStrict str
|
||||||
)
|
)
|
||||||
|
|
||||||
payloadAttributes :: RequestBody -> JSON.Value -> Maybe PayloadJSON
|
payloadAttributes :: RequestBody -> JSON.Value -> Maybe Payload
|
||||||
payloadAttributes raw json =
|
payloadAttributes raw json =
|
||||||
-- Test that Array contains only Objects having the same keys
|
-- Test that Array contains only Objects having the same keys
|
||||||
case json of
|
case json of
|
||||||
JSON.Array arr ->
|
JSON.Array arr ->
|
||||||
case arr V.!? 0 of
|
case arr V.!? 0 of
|
||||||
Just (JSON.Object o) ->
|
Just (JSON.Object o) ->
|
||||||
let canonicalKeys = S.fromList $ M.keys o
|
let canonicalKeys = S.fromList $ K.toText <$> KM.keys o
|
||||||
areKeysUniform = all (\case
|
areKeysUniform = all (\case
|
||||||
JSON.Object x -> S.fromList (M.keys x) == canonicalKeys
|
JSON.Object x -> S.fromList (K.toText <$> KM.keys x) == canonicalKeys
|
||||||
_ -> False) arr in
|
_ -> False) arr in
|
||||||
if areKeysUniform
|
if areKeysUniform
|
||||||
then Just $ ProcessedJSON raw canonicalKeys
|
then Just $ ProcessedJSON raw canonicalKeys
|
||||||
@@ -338,9 +340,40 @@ payloadAttributes raw json =
|
|||||||
Just _ -> Nothing
|
Just _ -> Nothing
|
||||||
Nothing -> Just emptyPJArray
|
Nothing -> Just emptyPJArray
|
||||||
|
|
||||||
JSON.Object o -> Just $ ProcessedJSON raw (S.fromList $ M.keys o)
|
JSON.Object o -> Just $ ProcessedJSON raw (S.fromList $ K.toText <$> KM.keys o)
|
||||||
|
|
||||||
-- truncate everything else to an empty array.
|
-- truncate everything else to an empty array.
|
||||||
_ -> Just emptyPJArray
|
_ -> Just emptyPJArray
|
||||||
where
|
where
|
||||||
emptyPJArray = ProcessedJSON (JSON.encode emptyArray) S.empty
|
emptyPJArray = ProcessedJSON (JSON.encode emptyArray) S.empty
|
||||||
|
|
||||||
|
|
||||||
|
-- | Do content negotiation. i.e. choose a media type based on the intersection of accepted/produced media types.
|
||||||
|
negotiateContent :: AppConfig -> Action -> PathInfo -> [MediaType] -> Either ApiRequestError MediaType
|
||||||
|
negotiateContent conf action path accepts =
|
||||||
|
case firstAcceptedPick of
|
||||||
|
Just MTAny -> Right MTApplicationJSON -- by default(for */*) we respond with json
|
||||||
|
Just mt -> Right mt
|
||||||
|
Nothing -> Left . MediaTypeError $ map MediaType.toMime accepts
|
||||||
|
where
|
||||||
|
-- if there are multiple accepted media types, pick the first
|
||||||
|
firstAcceptedPick = listToMaybe $ L.intersect accepts $ producedMediaTypes conf action path
|
||||||
|
|
||||||
|
producedMediaTypes :: AppConfig -> Action -> PathInfo -> [MediaType]
|
||||||
|
producedMediaTypes conf action path =
|
||||||
|
case action of
|
||||||
|
ActionRead _ -> defaultMediaTypes ++ rawMediaTypes
|
||||||
|
ActionInvoke _ -> invokeMediaTypes
|
||||||
|
ActionInfo -> defaultMediaTypes
|
||||||
|
ActionMutate _ -> defaultMediaTypes
|
||||||
|
ActionInspect _ -> inspectMediaTypes
|
||||||
|
where
|
||||||
|
inspectMediaTypes = [MTOpenAPI, MTApplicationJSON, MTArrayJSONStrip, MTAny]
|
||||||
|
invokeMediaTypes =
|
||||||
|
defaultMediaTypes
|
||||||
|
++ rawMediaTypes
|
||||||
|
++ [MTOpenAPI | pathIsRootSpec path]
|
||||||
|
defaultMediaTypes =
|
||||||
|
[MTApplicationJSON, MTArrayJSONStrip, MTSingularJSON True, MTSingularJSON False, MTGeoJSON, MTTextCSV] ++
|
||||||
|
[MTPlan MTApplicationJSON PlanText mempty | configDbPlanEnabled conf] ++ [MTAny]
|
||||||
|
rawMediaTypes = configRawMediaTypes conf `union` [MTOctetStream, MTTextPlain, MTTextXML]
|
||||||
|
|||||||
@@ -0,0 +1,225 @@
|
|||||||
|
-- |
|
||||||
|
-- Module: PostgREST.ApiRequest.Preferences
|
||||||
|
-- Description: Track client preferences to be employed when processing requests
|
||||||
|
--
|
||||||
|
-- Track client prefences set in HTTP 'Prefer' headers according to RFC7240[1].
|
||||||
|
--
|
||||||
|
-- [1] https://datatracker.ietf.org/doc/html/rfc7240
|
||||||
|
--
|
||||||
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
|
module PostgREST.ApiRequest.Preferences
|
||||||
|
( Preferences(..)
|
||||||
|
, PreferCount(..)
|
||||||
|
, PreferMissing(..)
|
||||||
|
, PreferParameters(..)
|
||||||
|
, PreferRepresentation(..)
|
||||||
|
, PreferResolution(..)
|
||||||
|
, PreferTransaction(..)
|
||||||
|
, fromHeaders
|
||||||
|
, shouldCount
|
||||||
|
, prefAppliedHeader
|
||||||
|
) where
|
||||||
|
|
||||||
|
import qualified Data.ByteString.Char8 as BS
|
||||||
|
import qualified Data.Map as Map
|
||||||
|
import qualified Network.HTTP.Types.Header as HTTP
|
||||||
|
|
||||||
|
import Protolude
|
||||||
|
|
||||||
|
|
||||||
|
-- $setup
|
||||||
|
-- Setup for doctests
|
||||||
|
-- >>> import Text.Pretty.Simple (pPrint)
|
||||||
|
-- >>> deriving instance Show PreferResolution
|
||||||
|
-- >>> deriving instance Show PreferRepresentation
|
||||||
|
-- >>> deriving instance Show PreferParameters
|
||||||
|
-- >>> deriving instance Show PreferCount
|
||||||
|
-- >>> deriving instance Show PreferTransaction
|
||||||
|
-- >>> deriving instance Show PreferMissing
|
||||||
|
-- >>> deriving instance Show Preferences
|
||||||
|
|
||||||
|
-- | Preferences recognized by the application.
|
||||||
|
data Preferences
|
||||||
|
= Preferences
|
||||||
|
{ preferResolution :: Maybe PreferResolution
|
||||||
|
, preferRepresentation :: Maybe PreferRepresentation
|
||||||
|
, preferParameters :: Maybe PreferParameters
|
||||||
|
, preferCount :: Maybe PreferCount
|
||||||
|
, preferTransaction :: Maybe PreferTransaction
|
||||||
|
, preferMissing :: Maybe PreferMissing
|
||||||
|
}
|
||||||
|
|
||||||
|
-- |
|
||||||
|
-- Parse HTTP headers based on RFC7240[1] to identify preferences.
|
||||||
|
--
|
||||||
|
-- One header with comma-separated values can be used to set multiple preferences:
|
||||||
|
--
|
||||||
|
-- >>> pPrint $ fromHeaders True [("Prefer", "resolution=ignore-duplicates, count=exact")]
|
||||||
|
-- Preferences
|
||||||
|
-- { preferResolution = Just IgnoreDuplicates
|
||||||
|
-- , preferRepresentation = Nothing
|
||||||
|
-- , preferParameters = Nothing
|
||||||
|
-- , preferCount = Just ExactCount
|
||||||
|
-- , preferTransaction = Nothing
|
||||||
|
-- , preferMissing = Nothing
|
||||||
|
-- }
|
||||||
|
--
|
||||||
|
-- Multiple headers can also be used:
|
||||||
|
--
|
||||||
|
-- >>> pPrint $ fromHeaders True [("Prefer", "resolution=ignore-duplicates"), ("Prefer", "count=exact"), ("Prefer", "missing=null")]
|
||||||
|
-- Preferences
|
||||||
|
-- { preferResolution = Just IgnoreDuplicates
|
||||||
|
-- , preferRepresentation = Nothing
|
||||||
|
-- , preferParameters = Nothing
|
||||||
|
-- , preferCount = Just ExactCount
|
||||||
|
-- , preferTransaction = Nothing
|
||||||
|
-- , preferMissing = Just ApplyNulls
|
||||||
|
-- }
|
||||||
|
--
|
||||||
|
-- If a preference is set more than once, only the first is used:
|
||||||
|
--
|
||||||
|
-- >>> preferTransaction $ fromHeaders True [("Prefer", "tx=commit, tx=rollback")]
|
||||||
|
-- Just Commit
|
||||||
|
--
|
||||||
|
-- This is also the case across multiple headers:
|
||||||
|
--
|
||||||
|
-- >>> :{
|
||||||
|
-- preferResolution . fromHeaders True $
|
||||||
|
-- [ ("Prefer", "resolution=ignore-duplicates")
|
||||||
|
-- , ("Prefer", "resolution=merge-duplicates")
|
||||||
|
-- ]
|
||||||
|
-- :}
|
||||||
|
-- Just IgnoreDuplicates
|
||||||
|
--
|
||||||
|
-- Preferences not recognized by the application are ignored:
|
||||||
|
--
|
||||||
|
-- >>> preferResolution $ fromHeaders True [("Prefer", "resolution=foo")]
|
||||||
|
-- Nothing
|
||||||
|
--
|
||||||
|
-- Preferences can be separated by arbitrary amounts of space, lower-case header is also recognized:
|
||||||
|
--
|
||||||
|
-- >>> pPrint $ fromHeaders True [("prefer", "count=exact, tx=commit ,return=representation , missing=default")]
|
||||||
|
-- Preferences
|
||||||
|
-- { preferResolution = Nothing
|
||||||
|
-- , preferRepresentation = Just Full
|
||||||
|
-- , preferParameters = Nothing
|
||||||
|
-- , preferCount = Just ExactCount
|
||||||
|
-- , preferTransaction = Just Commit
|
||||||
|
-- , preferMissing = Just ApplyDefaults
|
||||||
|
-- }
|
||||||
|
--
|
||||||
|
fromHeaders :: Bool -> [HTTP.Header] -> Preferences
|
||||||
|
fromHeaders allowTxEndOverride headers =
|
||||||
|
Preferences
|
||||||
|
{ preferResolution = parsePrefs [MergeDuplicates, IgnoreDuplicates]
|
||||||
|
, preferRepresentation = parsePrefs [Full, None, HeadersOnly]
|
||||||
|
, preferParameters = parsePrefs [SingleObject]
|
||||||
|
, preferCount = parsePrefs [ExactCount, PlannedCount, EstimatedCount]
|
||||||
|
, preferTransaction = if allowTxEndOverride then parsePrefs [Commit, Rollback] else Nothing
|
||||||
|
, preferMissing = parsePrefs [ApplyDefaults, ApplyNulls]
|
||||||
|
}
|
||||||
|
where
|
||||||
|
prefHeaders = filter ((==) HTTP.hPrefer . fst) headers
|
||||||
|
prefs = fmap BS.strip . concatMap (BS.split ',' . snd) $ prefHeaders
|
||||||
|
|
||||||
|
parsePrefs :: ToHeaderValue a => [a] -> Maybe a
|
||||||
|
parsePrefs vals =
|
||||||
|
head $ mapMaybe (flip Map.lookup $ prefMap vals) prefs
|
||||||
|
|
||||||
|
prefMap :: ToHeaderValue a => [a] -> Map.Map ByteString a
|
||||||
|
prefMap = Map.fromList . fmap (\pref -> (toHeaderValue pref, pref))
|
||||||
|
|
||||||
|
prefAppliedHeader :: Preferences -> Maybe HTTP.Header
|
||||||
|
prefAppliedHeader Preferences {preferResolution, preferRepresentation, preferParameters, preferCount, preferTransaction, preferMissing } =
|
||||||
|
if null prefsVals
|
||||||
|
then Nothing
|
||||||
|
else Just (HTTP.hPreferenceApplied, combined)
|
||||||
|
where
|
||||||
|
combined = BS.intercalate ", " prefsVals
|
||||||
|
prefsVals = catMaybes [
|
||||||
|
toHeaderValue <$> preferResolution
|
||||||
|
, toHeaderValue <$> preferMissing
|
||||||
|
, toHeaderValue <$> preferRepresentation
|
||||||
|
, toHeaderValue <$> preferParameters
|
||||||
|
, toHeaderValue <$> preferCount
|
||||||
|
, toHeaderValue <$> preferTransaction
|
||||||
|
]
|
||||||
|
|
||||||
|
-- |
|
||||||
|
-- Convert a preference into the value that we look for in the 'Prefer' headers.
|
||||||
|
--
|
||||||
|
-- >>> toHeaderValue MergeDuplicates
|
||||||
|
-- "resolution=merge-duplicates"
|
||||||
|
--
|
||||||
|
class ToHeaderValue a where
|
||||||
|
toHeaderValue :: a -> ByteString
|
||||||
|
|
||||||
|
-- | How to handle duplicate values.
|
||||||
|
data PreferResolution
|
||||||
|
= MergeDuplicates
|
||||||
|
| IgnoreDuplicates
|
||||||
|
|
||||||
|
instance ToHeaderValue PreferResolution where
|
||||||
|
toHeaderValue MergeDuplicates = "resolution=merge-duplicates"
|
||||||
|
toHeaderValue IgnoreDuplicates = "resolution=ignore-duplicates"
|
||||||
|
|
||||||
|
-- |
|
||||||
|
-- How to return the mutated data.
|
||||||
|
--
|
||||||
|
-- From https://tools.ietf.org/html/rfc7240#section-4.2
|
||||||
|
data PreferRepresentation
|
||||||
|
= Full -- ^ Return the body.
|
||||||
|
| HeadersOnly -- ^ Return the Location header(in case of POST). This needs a SELECT privilege on the pk.
|
||||||
|
| None -- ^ Return nothing from the mutated data.
|
||||||
|
deriving Eq
|
||||||
|
|
||||||
|
instance ToHeaderValue PreferRepresentation where
|
||||||
|
toHeaderValue Full = "return=representation"
|
||||||
|
toHeaderValue None = "return=minimal"
|
||||||
|
toHeaderValue HeadersOnly = "return=headers-only"
|
||||||
|
|
||||||
|
-- | How to pass parameters to stored procedures.
|
||||||
|
data PreferParameters
|
||||||
|
= SingleObject -- ^ Pass all parameters as a single json object to a stored procedure.
|
||||||
|
deriving Eq
|
||||||
|
|
||||||
|
instance ToHeaderValue PreferParameters where
|
||||||
|
toHeaderValue SingleObject = "params=single-object"
|
||||||
|
|
||||||
|
-- | How to determine the count of (expected) results
|
||||||
|
data PreferCount
|
||||||
|
= ExactCount -- ^ Exact count (slower).
|
||||||
|
| PlannedCount -- ^ PostgreSQL query planner rows count guess. Done by using EXPLAIN {query}.
|
||||||
|
| EstimatedCount -- ^ Use the query planner rows if the count is superior to max-rows, otherwise get the exact count.
|
||||||
|
deriving Eq
|
||||||
|
|
||||||
|
instance ToHeaderValue PreferCount where
|
||||||
|
toHeaderValue ExactCount = "count=exact"
|
||||||
|
toHeaderValue PlannedCount = "count=planned"
|
||||||
|
toHeaderValue EstimatedCount = "count=estimated"
|
||||||
|
|
||||||
|
shouldCount :: Maybe PreferCount -> Bool
|
||||||
|
shouldCount prefCount =
|
||||||
|
prefCount == Just ExactCount || prefCount == Just EstimatedCount
|
||||||
|
|
||||||
|
-- | Whether to commit or roll back transactions.
|
||||||
|
data PreferTransaction
|
||||||
|
= Commit -- ^ Commit transaction - the default.
|
||||||
|
| Rollback -- ^ Rollback transaction after sending the response - does not persist changes, e.g. for running tests.
|
||||||
|
deriving Eq
|
||||||
|
|
||||||
|
instance ToHeaderValue PreferTransaction where
|
||||||
|
toHeaderValue Commit = "tx=commit"
|
||||||
|
toHeaderValue Rollback = "tx=rollback"
|
||||||
|
|
||||||
|
-- |
|
||||||
|
-- How to handle the insertion/update when the keys specified in ?columns are not present
|
||||||
|
-- in the json body.
|
||||||
|
data PreferMissing
|
||||||
|
= ApplyDefaults -- ^ Use the default column value for missing values.
|
||||||
|
| ApplyNulls -- ^ Use the null value for missing values.
|
||||||
|
deriving Eq
|
||||||
|
|
||||||
|
instance ToHeaderValue PreferMissing where
|
||||||
|
toHeaderValue ApplyDefaults = "missing=default"
|
||||||
|
toHeaderValue ApplyNulls = "missing=null"
|
||||||
@@ -0,0 +1,813 @@
|
|||||||
|
-- |
|
||||||
|
-- Module : PostgREST.ApiRequest.QueryParams
|
||||||
|
-- Description : Parser for PostgREST Query parameters
|
||||||
|
--
|
||||||
|
-- This module is in charge of parsing all the querystring values in an url, e.g.
|
||||||
|
-- the select, id, order in `/projects?select=id,name&id=eq.1&order=id,name.desc`.
|
||||||
|
{-# LANGUAGE LambdaCase #-}
|
||||||
|
{-# LANGUAGE TupleSections #-}
|
||||||
|
module PostgREST.ApiRequest.QueryParams
|
||||||
|
( parse
|
||||||
|
, QueryParams(..)
|
||||||
|
, pRequestRange
|
||||||
|
) where
|
||||||
|
|
||||||
|
import qualified Data.ByteString.Char8 as BS
|
||||||
|
import qualified Data.HashMap.Strict as HM
|
||||||
|
import qualified Data.List as L
|
||||||
|
import qualified Data.Set as S
|
||||||
|
import qualified Data.Text as T
|
||||||
|
import qualified Data.Text.Encoding as T
|
||||||
|
import qualified Network.HTTP.Base as HTTP
|
||||||
|
import qualified Network.HTTP.Types.URI as HTTP
|
||||||
|
import qualified Text.ParserCombinators.Parsec as P
|
||||||
|
|
||||||
|
import Control.Arrow ((***))
|
||||||
|
import Data.Either.Combinators (mapLeft)
|
||||||
|
import Data.List (init, last)
|
||||||
|
import Data.Ranged.Boundaries (Boundary (..))
|
||||||
|
import Data.Ranged.Ranges (Range (..))
|
||||||
|
import Data.Tree (Tree (..))
|
||||||
|
import Text.Parsec.Error (errorMessages,
|
||||||
|
showErrorMessages)
|
||||||
|
import Text.ParserCombinators.Parsec (GenParser, ParseError, Parser,
|
||||||
|
anyChar, between, char, digit,
|
||||||
|
eof, errorPos, letter,
|
||||||
|
lookAhead, many1, noneOf,
|
||||||
|
notFollowedBy, oneOf,
|
||||||
|
optionMaybe, sepBy, sepBy1,
|
||||||
|
string, try, (<?>))
|
||||||
|
|
||||||
|
import PostgREST.RangeQuery (NonnegRange, allRange,
|
||||||
|
rangeGeq, rangeLimit,
|
||||||
|
rangeOffset, restrictRange)
|
||||||
|
import PostgREST.SchemaCache.Identifiers (FieldName)
|
||||||
|
|
||||||
|
import PostgREST.ApiRequest.Types (EmbedParam (..), EmbedPath, Field,
|
||||||
|
Filter (..), FtsOperator (..),
|
||||||
|
Hint, JoinType (..),
|
||||||
|
JsonOperand (..),
|
||||||
|
JsonOperation (..), JsonPath,
|
||||||
|
ListVal, LogicOperator (..),
|
||||||
|
LogicTree (..), OpExpr (..),
|
||||||
|
OpQuantifier (..), Operation (..),
|
||||||
|
OrderDirection (..),
|
||||||
|
OrderNulls (..), OrderTerm (..),
|
||||||
|
QPError (..), QuantOperator (..),
|
||||||
|
SelectItem (..),
|
||||||
|
SimpleOperator (..), SingleVal,
|
||||||
|
TrileanVal (..))
|
||||||
|
|
||||||
|
import Protolude hiding (try)
|
||||||
|
|
||||||
|
data QueryParams =
|
||||||
|
QueryParams
|
||||||
|
{ qsCanonical :: ByteString
|
||||||
|
-- ^ Canonical representation of the query params, sorted alphabetically
|
||||||
|
, qsParams :: [(Text, Text)]
|
||||||
|
-- ^ Parameters for RPC calls
|
||||||
|
, qsRanges :: HM.HashMap Text (Range Integer)
|
||||||
|
-- ^ Ranges derived from &limit and &offset params
|
||||||
|
, qsOrder :: [(EmbedPath, [OrderTerm])]
|
||||||
|
-- ^ &order parameters for each level
|
||||||
|
, qsLogic :: [(EmbedPath, LogicTree)]
|
||||||
|
-- ^ &and and &or parameters used for complex boolean logic
|
||||||
|
, qsColumns :: Maybe (S.Set FieldName)
|
||||||
|
-- ^ &columns parameter and payload
|
||||||
|
, qsSelect :: [Tree SelectItem]
|
||||||
|
-- ^ &select parameter used to shape the response
|
||||||
|
, qsFilters :: [(EmbedPath, Filter)]
|
||||||
|
-- ^ Filters on the result from e.g. &id=e.10
|
||||||
|
, qsFiltersRoot :: [Filter]
|
||||||
|
-- ^ Subset of the filters that apply on the root table. These are used on UPDATE/DELETE.
|
||||||
|
, qsFiltersNotRoot :: [(EmbedPath, Filter)]
|
||||||
|
-- ^ Subset of the filters that do not apply on the root table
|
||||||
|
, qsFilterFields :: S.Set FieldName
|
||||||
|
-- ^ Set of fields that filters apply to
|
||||||
|
, qsOnConflict :: Maybe [FieldName]
|
||||||
|
-- ^ &on_conflict parameter used to upsert on specific unique keys
|
||||||
|
}
|
||||||
|
|
||||||
|
-- |
|
||||||
|
-- Parse query parameters from a query string like "id=eq.1&select=name".
|
||||||
|
--
|
||||||
|
-- The canonical representation of the query string has parameters sorted alphabetically:
|
||||||
|
--
|
||||||
|
-- >>> qsCanonical <$> parse True "a=1&c=3&b=2&d"
|
||||||
|
-- Right "a=1&b=2&c=3&d="
|
||||||
|
--
|
||||||
|
-- 'select' is a reserved parameter that selects the fields to be returned:
|
||||||
|
--
|
||||||
|
-- >>> qsSelect <$> parse False "select=name,location"
|
||||||
|
-- Right [Node {rootLabel = SelectField {selField = ("name",[]), selCast = Nothing, selAlias = Nothing}, subForest = []},Node {rootLabel = SelectField {selField = ("location",[]), selCast = Nothing, selAlias = Nothing}, subForest = []}]
|
||||||
|
--
|
||||||
|
-- Filters are parameters whose value contains an operator, separated by a '.' from its value:
|
||||||
|
--
|
||||||
|
-- >>> qsFilters <$> parse False "a.b=eq.0"
|
||||||
|
-- Right [(["a"],Filter {field = ("b",[]), opExpr = OpExpr False (OpQuant OpEqual Nothing "0")})]
|
||||||
|
--
|
||||||
|
-- If the operator specified in a filter does not exist, parsing the query string fails:
|
||||||
|
--
|
||||||
|
-- >>> qsFilters <$> parse False "a.b=noop.0"
|
||||||
|
-- Left (QPError "\"failed to parse filter (noop.0)\" (line 1, column 1)" "unexpected \"o\" expecting \"not\" or operator (eq, gt, ...)")
|
||||||
|
parse :: Bool -> ByteString -> Either QPError QueryParams
|
||||||
|
parse isRpcGet qs = do
|
||||||
|
rOrd <- pRequestOrder `traverse` order
|
||||||
|
rLogic <- pRequestLogicTree `traverse` logic
|
||||||
|
rCols <- pRequestColumns columns
|
||||||
|
rSel <- pRequestSelect select
|
||||||
|
(rFlts, params) <- L.partition hasOp <$> pRequestFilter isRpcGet `traverse` filters
|
||||||
|
(rFltsRoot, rFltsNotRoot) <- pure $ L.partition hasRootFilter rFlts
|
||||||
|
rOnConflict <- pRequestOnConflict `traverse` onConflict
|
||||||
|
|
||||||
|
let rFltsFields = S.fromList (fst <$> filters)
|
||||||
|
params' = mapMaybe (\case {(_, Filter (fld, _) (NoOpExpr v)) -> Just (fld,v); _ -> Nothing}) params
|
||||||
|
rFltsRoot' = snd <$> rFltsRoot
|
||||||
|
|
||||||
|
return $ QueryParams canonical params' ranges rOrd rLogic rCols rSel rFlts rFltsRoot' rFltsNotRoot rFltsFields rOnConflict
|
||||||
|
where
|
||||||
|
hasRootFilter, hasOp :: (EmbedPath, Filter) -> Bool
|
||||||
|
hasRootFilter ([], _) = True
|
||||||
|
hasRootFilter _ = False
|
||||||
|
hasOp (_, Filter (_, _) (NoOpExpr _)) = False
|
||||||
|
hasOp _ = True
|
||||||
|
|
||||||
|
logic = filter (endingIn ["and", "or"] . fst) nonemptyParams
|
||||||
|
select = fromMaybe "*" $ lookupParam "select"
|
||||||
|
onConflict = lookupParam "on_conflict"
|
||||||
|
columns = lookupParam "columns"
|
||||||
|
order = filter (endingIn ["order"] . fst) nonemptyParams
|
||||||
|
limits = filter (endingIn ["limit"] . fst) nonemptyParams
|
||||||
|
-- Replace .offset ending with .limit to be able to match those params later in a map
|
||||||
|
offsets = first (replaceLast "limit") <$> filter (endingIn ["offset"] . fst) nonemptyParams
|
||||||
|
lookupParam :: Text -> Maybe Text
|
||||||
|
lookupParam needle = toS <$> join (L.lookup needle qParams)
|
||||||
|
nonemptyParams = mapMaybe (\(k, v) -> (k,) <$> v) qParams
|
||||||
|
|
||||||
|
qString = HTTP.parseQueryReplacePlus True qs
|
||||||
|
|
||||||
|
qParams = [(T.decodeUtf8 k, T.decodeUtf8 <$> v)|(k,v) <- qString]
|
||||||
|
|
||||||
|
canonical =
|
||||||
|
BS.pack $ HTTP.urlEncodeVars
|
||||||
|
. L.sortOn fst
|
||||||
|
. map (join (***) BS.unpack . second (fromMaybe mempty))
|
||||||
|
$ qString
|
||||||
|
|
||||||
|
endingIn:: [Text] -> Text -> Bool
|
||||||
|
endingIn xx key = lastWord `elem` xx
|
||||||
|
where lastWord = L.last $ T.split (== '.') key
|
||||||
|
|
||||||
|
filters = filter (isFilter . fst) nonemptyParams
|
||||||
|
isFilter k = not (endingIn reservedEmbeddable k) && notElem k reserved
|
||||||
|
reserved = ["select", "columns", "on_conflict"]
|
||||||
|
reservedEmbeddable = ["order", "limit", "offset", "and", "or"]
|
||||||
|
|
||||||
|
replaceLast x s = T.intercalate "." $ L.init (T.split (=='.') s) <> [x]
|
||||||
|
|
||||||
|
ranges :: HM.HashMap Text (Range Integer)
|
||||||
|
ranges = HM.unionWith f limitParams offsetParams
|
||||||
|
where
|
||||||
|
f rl ro = Range (BoundaryBelow o) (BoundaryAbove $ o + l - 1)
|
||||||
|
where
|
||||||
|
l = fromMaybe 0 $ rangeLimit rl
|
||||||
|
o = rangeOffset ro
|
||||||
|
|
||||||
|
limitParams =
|
||||||
|
HM.fromList [(k, restrictRange (readMaybe v) allRange) | (k,v) <- limits]
|
||||||
|
|
||||||
|
offsetParams =
|
||||||
|
HM.fromList [(k, maybe allRange rangeGeq (readMaybe v)) | (k,v) <- offsets]
|
||||||
|
|
||||||
|
simpleOperator :: Parser SimpleOperator
|
||||||
|
simpleOperator =
|
||||||
|
try (string "neq" $> OpNotEqual) <|>
|
||||||
|
try (string "cs" $> OpContains) <|>
|
||||||
|
try (string "cd" $> OpContained) <|>
|
||||||
|
try (string "ov" $> OpOverlap) <|>
|
||||||
|
try (string "sl" $> OpStrictlyLeft) <|>
|
||||||
|
try (string "sr" $> OpStrictlyRight) <|>
|
||||||
|
try (string "nxr" $> OpNotExtendsRight) <|>
|
||||||
|
try (string "nxl" $> OpNotExtendsLeft) <|>
|
||||||
|
try (string "adj" $> OpAdjacent) <?>
|
||||||
|
"unknown single value operator"
|
||||||
|
|
||||||
|
quantOperator :: Parser QuantOperator
|
||||||
|
quantOperator =
|
||||||
|
try (string "eq" $> OpEqual) <|>
|
||||||
|
try (string "gte" $> OpGreaterThanEqual) <|>
|
||||||
|
try (string "gt" $> OpGreaterThan) <|>
|
||||||
|
try (string "lte" $> OpLessThanEqual) <|>
|
||||||
|
try (string "lt" $> OpLessThan) <|>
|
||||||
|
try (string "like" $> OpLike) <|>
|
||||||
|
try (string "ilike" $> OpILike) <|>
|
||||||
|
try (string "match" $> OpMatch) <|>
|
||||||
|
try (string "imatch" $> OpIMatch) <?>
|
||||||
|
"unknown single value operator"
|
||||||
|
|
||||||
|
pRequestSelect :: Text -> Either QPError [Tree SelectItem]
|
||||||
|
pRequestSelect selStr =
|
||||||
|
mapError $ P.parse pFieldForest ("failed to parse select parameter (" <> toS selStr <> ")") (toS selStr)
|
||||||
|
|
||||||
|
pRequestOnConflict :: Text -> Either QPError [FieldName]
|
||||||
|
pRequestOnConflict oncStr =
|
||||||
|
mapError $ P.parse pColumns ("failed to parse on_conflict parameter (" <> toS oncStr <> ")") (toS oncStr)
|
||||||
|
|
||||||
|
-- |
|
||||||
|
-- Parse `id=eq.1`(id, eq.1) into (EmbedPath, Filter)
|
||||||
|
--
|
||||||
|
-- >>> pRequestFilter False ("id", "eq.1")
|
||||||
|
-- Right ([],Filter {field = ("id",[]), opExpr = OpExpr False (OpQuant OpEqual Nothing "1")})
|
||||||
|
--
|
||||||
|
-- >>> pRequestFilter False ("id", "val")
|
||||||
|
-- Left (QPError "\"failed to parse filter (val)\" (line 1, column 1)" "unexpected \"v\" expecting \"not\" or operator (eq, gt, ...)")
|
||||||
|
--
|
||||||
|
-- >>> pRequestFilter True ("id", "val")
|
||||||
|
-- Right ([],Filter {field = ("id",[]), opExpr = NoOpExpr "val"})
|
||||||
|
pRequestFilter :: Bool -> (Text, Text) -> Either QPError (EmbedPath, Filter)
|
||||||
|
pRequestFilter isRpcGet (k, v) = mapError $ (,) <$> path <*> (Filter <$> fld <*> oper)
|
||||||
|
where
|
||||||
|
treePath = P.parse pTreePath ("failed to parse tree path (" ++ toS k ++ ")") $ toS k
|
||||||
|
oper = P.parse parseFlt ("failed to parse filter (" ++ toS v ++ ")") $ toS v
|
||||||
|
parseFlt = if isRpcGet
|
||||||
|
then pOpExpr pSingleVal <|> pure (NoOpExpr v)
|
||||||
|
else pOpExpr pSingleVal
|
||||||
|
path = fst <$> treePath
|
||||||
|
fld = snd <$> treePath
|
||||||
|
|
||||||
|
pRequestOrder :: (Text, Text) -> Either QPError (EmbedPath, [OrderTerm])
|
||||||
|
pRequestOrder (k, v) = mapError $ (,) <$> path <*> ord'
|
||||||
|
where
|
||||||
|
treePath = P.parse pTreePath ("failed to parse tree path (" ++ toS k ++ ")") $ toS k
|
||||||
|
path = fst <$> treePath
|
||||||
|
ord' = P.parse pOrder ("failed to parse order (" ++ toS v ++ ")") $ toS v
|
||||||
|
|
||||||
|
pRequestRange :: (Text, NonnegRange) -> Either QPError (EmbedPath, NonnegRange)
|
||||||
|
pRequestRange (k, v) = mapError $ (,) <$> path <*> pure v
|
||||||
|
where
|
||||||
|
treePath = P.parse pTreePath ("failed to parse tree path (" ++ toS k ++ ")") $ toS k
|
||||||
|
path = fst <$> treePath
|
||||||
|
|
||||||
|
pRequestLogicTree :: (Text, Text) -> Either QPError (EmbedPath, LogicTree)
|
||||||
|
pRequestLogicTree (k, v) = mapError $ (,) <$> embedPath <*> logicTree
|
||||||
|
where
|
||||||
|
path = P.parse pLogicPath ("failed to parse logic path (" ++ toS k ++ ")") $ toS k
|
||||||
|
embedPath = fst <$> path
|
||||||
|
logicTree = do
|
||||||
|
op <- snd <$> path
|
||||||
|
-- Concat op and v to make pLogicTree argument regular,
|
||||||
|
-- in the form of "?and=and(.. , ..)" instead of "?and=(.. , ..)"
|
||||||
|
P.parse pLogicTree ("failed to parse logic tree (" ++ toS v ++ ")") $ toS (op <> v)
|
||||||
|
|
||||||
|
pRequestColumns :: Maybe Text -> Either QPError (Maybe (S.Set FieldName))
|
||||||
|
pRequestColumns colStr =
|
||||||
|
case colStr of
|
||||||
|
Just str ->
|
||||||
|
mapError $ Just . S.fromList <$> P.parse pColumns ("failed to parse columns parameter (" <> toS str <> ")") (toS str)
|
||||||
|
_ -> Right Nothing
|
||||||
|
|
||||||
|
ws :: Parser Text
|
||||||
|
ws = toS <$> many (oneOf " \t")
|
||||||
|
|
||||||
|
lexeme :: Parser a -> Parser a
|
||||||
|
lexeme p = ws *> p <* ws
|
||||||
|
|
||||||
|
pTreePath :: Parser (EmbedPath, Field)
|
||||||
|
pTreePath = do
|
||||||
|
p <- pFieldName `sepBy1` pDelimiter
|
||||||
|
jp <- P.option [] pJsonPath
|
||||||
|
return (init p, (last p, jp))
|
||||||
|
|
||||||
|
-- |
|
||||||
|
-- Parse select= into a Forest of SelectItems
|
||||||
|
--
|
||||||
|
-- >>> P.parse pFieldForest "" "id"
|
||||||
|
-- Right [Node {rootLabel = SelectField {selField = ("id",[]), selCast = Nothing, selAlias = Nothing}, subForest = []}]
|
||||||
|
--
|
||||||
|
-- >>> P.parse pFieldForest "" "client(id)"
|
||||||
|
-- Right [Node {rootLabel = SelectRelation {selRelation = "client", selAlias = Nothing, selHint = Nothing, selJoinType = Nothing}, subForest = [Node {rootLabel = SelectField {selField = ("id",[]), selCast = Nothing, selAlias = Nothing}, subForest = []}]}]
|
||||||
|
--
|
||||||
|
-- >>> P.parse pFieldForest "" "*,client(*,nested(*))"
|
||||||
|
-- Right [Node {rootLabel = SelectField {selField = ("*",[]), selCast = Nothing, selAlias = Nothing}, subForest = []},Node {rootLabel = SelectRelation {selRelation = "client", selAlias = Nothing, selHint = Nothing, selJoinType = Nothing}, subForest = [Node {rootLabel = SelectField {selField = ("*",[]), selCast = Nothing, selAlias = Nothing}, subForest = []},Node {rootLabel = SelectRelation {selRelation = "nested", selAlias = Nothing, selHint = Nothing, selJoinType = Nothing}, subForest = [Node {rootLabel = SelectField {selField = ("*",[]), selCast = Nothing, selAlias = Nothing}, subForest = []}]}]}]
|
||||||
|
--
|
||||||
|
-- >>> P.parse pFieldForest "" "*,...client(*),other(*)"
|
||||||
|
-- Right [Node {rootLabel = SelectField {selField = ("*",[]), selCast = Nothing, selAlias = Nothing}, subForest = []},Node {rootLabel = SpreadRelation {selRelation = "client", selHint = Nothing, selJoinType = Nothing}, subForest = [Node {rootLabel = SelectField {selField = ("*",[]), selCast = Nothing, selAlias = Nothing}, subForest = []}]},Node {rootLabel = SelectRelation {selRelation = "other", selAlias = Nothing, selHint = Nothing, selJoinType = Nothing}, subForest = [Node {rootLabel = SelectField {selField = ("*",[]), selCast = Nothing, selAlias = Nothing}, subForest = []}]}]
|
||||||
|
--
|
||||||
|
-- >>> P.parse pFieldForest "" ""
|
||||||
|
-- Right []
|
||||||
|
--
|
||||||
|
-- >>> P.parse pFieldForest "" "id,clients(name[])"
|
||||||
|
-- Left (line 1, column 16):
|
||||||
|
-- unexpected '['
|
||||||
|
-- expecting letter, digit, "-", "->>", "->", "::", ")", "," or end of input
|
||||||
|
--
|
||||||
|
-- >>> P.parse pFieldForest "" "data->>-78xy"
|
||||||
|
-- Left (line 1, column 11):
|
||||||
|
-- unexpected 'x'
|
||||||
|
-- expecting digit, "->", "::", ".", "," or end of input
|
||||||
|
pFieldForest :: Parser [Tree SelectItem]
|
||||||
|
pFieldForest = pFieldTree `sepBy` lexeme (char ',')
|
||||||
|
where
|
||||||
|
pFieldTree = Node <$> try pSpreadRelationSelect <*> between (char '(') (char ')') pFieldForest <|>
|
||||||
|
Node <$> try pRelationSelect <*> between (char '(') (char ')') pFieldForest <|>
|
||||||
|
Node <$> pFieldSelect <*> pure []
|
||||||
|
|
||||||
|
-- |
|
||||||
|
-- Parse field names
|
||||||
|
--
|
||||||
|
-- >>> P.parse pFieldName "" "identifier"
|
||||||
|
-- Right "identifier"
|
||||||
|
--
|
||||||
|
-- >>> P.parse pFieldName "" "identifier with spaces"
|
||||||
|
-- Right "identifier with spaces"
|
||||||
|
--
|
||||||
|
-- >>> P.parse pFieldName "" "identifier-with-dashes"
|
||||||
|
-- Right "identifier-with-dashes"
|
||||||
|
--
|
||||||
|
-- >>> P.parse pFieldName "" "123"
|
||||||
|
-- Right "123"
|
||||||
|
--
|
||||||
|
-- >>> P.parse pFieldName "" "_"
|
||||||
|
-- Right "_"
|
||||||
|
--
|
||||||
|
-- >>> P.parse pFieldName "" "$"
|
||||||
|
-- Right "$"
|
||||||
|
--
|
||||||
|
-- >>> P.parse pFieldName "" ":"
|
||||||
|
-- Left (line 1, column 1):
|
||||||
|
-- unexpected ":"
|
||||||
|
-- expecting field name (* or [a..z0..9_$])
|
||||||
|
--
|
||||||
|
-- >>> P.parse pFieldName "" "\":\""
|
||||||
|
-- Right ":"
|
||||||
|
--
|
||||||
|
-- >>> P.parse pFieldName "" " no leading or trailing spaces "
|
||||||
|
-- Right "no leading or trailing spaces"
|
||||||
|
--
|
||||||
|
-- >>> P.parse pFieldName "" "\" leading and trailing spaces \""
|
||||||
|
-- Right " leading and trailing spaces "
|
||||||
|
pFieldName :: Parser Text
|
||||||
|
pFieldName =
|
||||||
|
pQuotedValue <|>
|
||||||
|
T.intercalate "-" . map toS <$> (pIdentifier `sepBy1` dash) <?>
|
||||||
|
"field name (* or [a..z0..9_$])"
|
||||||
|
where
|
||||||
|
isDash :: GenParser Char st ()
|
||||||
|
isDash = try ( char '-' >> notFollowedBy (char '>') )
|
||||||
|
dash :: Parser Char
|
||||||
|
dash = isDash $> '-'
|
||||||
|
|
||||||
|
-- |
|
||||||
|
-- Parse json operators in select, order and filters
|
||||||
|
--
|
||||||
|
-- >>> P.parse pJsonPath "" "->text"
|
||||||
|
-- Right [JArrow {jOp = JKey {jVal = "text"}}]
|
||||||
|
--
|
||||||
|
-- >>> P.parse pJsonPath "" "->1"
|
||||||
|
-- Right [JArrow {jOp = JIdx {jVal = "+1"}}]
|
||||||
|
--
|
||||||
|
-- >>> P.parse pJsonPath "" "->>text"
|
||||||
|
-- Right [J2Arrow {jOp = JKey {jVal = "text"}}]
|
||||||
|
--
|
||||||
|
-- >>> P.parse pJsonPath "" "->>1"
|
||||||
|
-- Right [J2Arrow {jOp = JIdx {jVal = "+1"}}]
|
||||||
|
--
|
||||||
|
-- >>> P.parse pJsonPath "" "->0,other"
|
||||||
|
-- Right [JArrow {jOp = JIdx {jVal = "+0"}}]
|
||||||
|
--
|
||||||
|
-- >>> P.parse pJsonPath "" "->0.desc"
|
||||||
|
-- Right [JArrow {jOp = JIdx {jVal = "+0"}}]
|
||||||
|
--
|
||||||
|
-- Fails on badly formed negatives
|
||||||
|
--
|
||||||
|
-- >>> P.parse pJsonPath "" "->>-78xy"
|
||||||
|
-- Left (line 1, column 7):
|
||||||
|
-- unexpected 'x'
|
||||||
|
-- expecting digit, "->", "::", ".", "," or end of input
|
||||||
|
--
|
||||||
|
-- >>> P.parse pJsonPath "" "->>--34"
|
||||||
|
-- Left (line 1, column 5):
|
||||||
|
-- unexpected "-"
|
||||||
|
-- expecting digit
|
||||||
|
--
|
||||||
|
-- >>> P.parse pJsonPath "" "->>-xy-4"
|
||||||
|
-- Left (line 1, column 5):
|
||||||
|
-- unexpected "x"
|
||||||
|
-- expecting digit
|
||||||
|
pJsonPath :: Parser JsonPath
|
||||||
|
pJsonPath = many pJsonOperation
|
||||||
|
where
|
||||||
|
pJsonOperation :: Parser JsonOperation
|
||||||
|
pJsonOperation = pJsonArrow <*> pJsonOperand
|
||||||
|
|
||||||
|
pJsonArrow =
|
||||||
|
try (string "->>" $> J2Arrow) <|>
|
||||||
|
try (string "->" $> JArrow)
|
||||||
|
|
||||||
|
pJsonOperand =
|
||||||
|
let pJKey = JKey . toS <$> pFieldName
|
||||||
|
pJIdx = JIdx . toS <$> ((:) <$> P.option '+' (char '-') <*> many1 digit) <* pEnd
|
||||||
|
pEnd = try (void $ lookAhead (string "->")) <|>
|
||||||
|
try (void $ lookAhead (string "::")) <|>
|
||||||
|
try (void $ lookAhead (string ".")) <|>
|
||||||
|
try (void $ lookAhead (string ",")) <|>
|
||||||
|
try eof in
|
||||||
|
try pJIdx <|> try pJKey
|
||||||
|
|
||||||
|
pField :: Parser Field
|
||||||
|
pField = lexeme $ (,) <$> pFieldName <*> P.option [] pJsonPath
|
||||||
|
|
||||||
|
aliasSeparator :: Parser ()
|
||||||
|
aliasSeparator = char ':' >> notFollowedBy (char ':')
|
||||||
|
|
||||||
|
-- |
|
||||||
|
-- Parse regular fields in select
|
||||||
|
--
|
||||||
|
-- >>> P.parse pRelationSelect "" "rel(*)"
|
||||||
|
-- Right (SelectRelation {selRelation = "rel", selAlias = Nothing, selHint = Nothing, selJoinType = Nothing})
|
||||||
|
--
|
||||||
|
-- >>> P.parse pRelationSelect "" "alias:rel(*)"
|
||||||
|
-- Right (SelectRelation {selRelation = "rel", selAlias = Just "alias", selHint = Nothing, selJoinType = Nothing})
|
||||||
|
--
|
||||||
|
-- >>> P.parse pRelationSelect "" "rel!hint(*)"
|
||||||
|
-- Right (SelectRelation {selRelation = "rel", selAlias = Nothing, selHint = Just "hint", selJoinType = Nothing})
|
||||||
|
--
|
||||||
|
-- >>> P.parse pRelationSelect "" "rel!inner(*)"
|
||||||
|
-- Right (SelectRelation {selRelation = "rel", selAlias = Nothing, selHint = Nothing, selJoinType = Just JTInner})
|
||||||
|
--
|
||||||
|
-- >>> P.parse pRelationSelect "" "rel!hint!inner(*)"
|
||||||
|
-- Right (SelectRelation {selRelation = "rel", selAlias = Nothing, selHint = Just "hint", selJoinType = Just JTInner})
|
||||||
|
--
|
||||||
|
-- >>> P.parse pRelationSelect "" "alias:rel!inner!hint(*)"
|
||||||
|
-- Right (SelectRelation {selRelation = "rel", selAlias = Just "alias", selHint = Just "hint", selJoinType = Just JTInner})
|
||||||
|
--
|
||||||
|
-- >>> P.parse pRelationSelect "" "rel->jsonpath(*)"
|
||||||
|
-- Left (line 1, column 6):
|
||||||
|
-- unexpected '>'
|
||||||
|
--
|
||||||
|
-- >>> P.parse pRelationSelect "" "rel->jsonpath!hint(*)"
|
||||||
|
-- Left (line 1, column 6):
|
||||||
|
-- unexpected '>'
|
||||||
|
pRelationSelect :: Parser SelectItem
|
||||||
|
pRelationSelect = lexeme $ do
|
||||||
|
alias <- optionMaybe ( try(pFieldName <* aliasSeparator) )
|
||||||
|
name <- pFieldName
|
||||||
|
(hint, jType) <- pEmbedParams
|
||||||
|
try (void $ lookAhead (string "("))
|
||||||
|
return $ SelectRelation name alias hint jType
|
||||||
|
|
||||||
|
-- |
|
||||||
|
-- Parse regular fields in select
|
||||||
|
--
|
||||||
|
-- >>> P.parse pFieldSelect "" "name"
|
||||||
|
-- Right (SelectField {selField = ("name",[]), selCast = Nothing, selAlias = Nothing})
|
||||||
|
--
|
||||||
|
-- >>> P.parse pFieldSelect "" "name->jsonpath"
|
||||||
|
-- Right (SelectField {selField = ("name",[JArrow {jOp = JKey {jVal = "jsonpath"}}]), selCast = Nothing, selAlias = Nothing})
|
||||||
|
--
|
||||||
|
-- >>> P.parse pFieldSelect "" "name::cast"
|
||||||
|
-- Right (SelectField {selField = ("name",[]), selCast = Just "cast", selAlias = Nothing})
|
||||||
|
--
|
||||||
|
-- >>> P.parse pFieldSelect "" "alias:name"
|
||||||
|
-- Right (SelectField {selField = ("name",[]), selCast = Nothing, selAlias = Just "alias"})
|
||||||
|
--
|
||||||
|
-- >>> P.parse pFieldSelect "" "alias:name->jsonpath::cast"
|
||||||
|
-- Right (SelectField {selField = ("name",[JArrow {jOp = JKey {jVal = "jsonpath"}}]), selCast = Just "cast", selAlias = Just "alias"})
|
||||||
|
--
|
||||||
|
-- >>> P.parse pFieldSelect "" "*"
|
||||||
|
-- Right (SelectField {selField = ("*",[]), selCast = Nothing, selAlias = Nothing})
|
||||||
|
--
|
||||||
|
-- >>> P.parse pFieldSelect "" "name!hint"
|
||||||
|
-- Left (line 1, column 5):
|
||||||
|
-- unexpected '!'
|
||||||
|
-- expecting letter, digit, "-", "->>", "->", "::", ")", "," or end of input
|
||||||
|
--
|
||||||
|
-- >>> P.parse pFieldSelect "" "*!hint"
|
||||||
|
-- Left (line 1, column 2):
|
||||||
|
-- unexpected '!'
|
||||||
|
-- expecting ")", "," or end of input
|
||||||
|
--
|
||||||
|
-- >>> P.parse pFieldSelect "" "name::"
|
||||||
|
-- Left (line 1, column 7):
|
||||||
|
-- unexpected end of input
|
||||||
|
-- expecting letter or digit
|
||||||
|
pFieldSelect :: Parser SelectItem
|
||||||
|
pFieldSelect = lexeme $ try (do
|
||||||
|
s <- pStar
|
||||||
|
pEnd
|
||||||
|
return $ SelectField (s, []) Nothing Nothing)
|
||||||
|
<|> do
|
||||||
|
alias <- optionMaybe ( try(pFieldName <* aliasSeparator) )
|
||||||
|
fld <- pField
|
||||||
|
cast' <- optionMaybe (string "::" *> pIdentifier)
|
||||||
|
pEnd
|
||||||
|
return $ SelectField fld (toS <$> cast') alias
|
||||||
|
where
|
||||||
|
pEnd = try (void $ lookAhead (string ")")) <|>
|
||||||
|
try (void $ lookAhead (string ",")) <|>
|
||||||
|
try eof
|
||||||
|
pStar = string "*" $> "*"
|
||||||
|
|
||||||
|
|
||||||
|
-- |
|
||||||
|
-- Parse spread relations in select
|
||||||
|
--
|
||||||
|
-- >>> P.parse pSpreadRelationSelect "" "...rel(*)"
|
||||||
|
-- Right (SpreadRelation {selRelation = "rel", selHint = Nothing, selJoinType = Nothing})
|
||||||
|
--
|
||||||
|
-- >>> P.parse pSpreadRelationSelect "" "...rel!hint!inner(*)"
|
||||||
|
-- Right (SpreadRelation {selRelation = "rel", selHint = Just "hint", selJoinType = Just JTInner})
|
||||||
|
--
|
||||||
|
-- >>> P.parse pSpreadRelationSelect "" "rel(*)"
|
||||||
|
-- Left (line 1, column 1):
|
||||||
|
-- unexpected "r"
|
||||||
|
-- expecting "..."
|
||||||
|
--
|
||||||
|
-- >>> P.parse pSpreadRelationSelect "" "alias:...rel(*)"
|
||||||
|
-- Left (line 1, column 1):
|
||||||
|
-- unexpected "a"
|
||||||
|
-- expecting "..."
|
||||||
|
--
|
||||||
|
-- >>> P.parse pSpreadRelationSelect "" "...rel->jsonpath(*)"
|
||||||
|
-- Left (line 1, column 9):
|
||||||
|
-- unexpected '>'
|
||||||
|
pSpreadRelationSelect :: Parser SelectItem
|
||||||
|
pSpreadRelationSelect = lexeme $ do
|
||||||
|
name <- string "..." >> pFieldName
|
||||||
|
(hint, jType) <- pEmbedParams
|
||||||
|
try (void $ lookAhead (string "("))
|
||||||
|
return $ SpreadRelation name hint jType
|
||||||
|
|
||||||
|
pEmbedParams :: Parser (Maybe Hint, Maybe JoinType)
|
||||||
|
pEmbedParams = do
|
||||||
|
prm1 <- optionMaybe pEmbedParam
|
||||||
|
prm2 <- optionMaybe pEmbedParam
|
||||||
|
return (embedParamHint prm1 <|> embedParamHint prm2, embedParamJoin prm1 <|> embedParamJoin prm2)
|
||||||
|
where
|
||||||
|
pEmbedParam :: Parser EmbedParam
|
||||||
|
pEmbedParam =
|
||||||
|
char '!' *> (
|
||||||
|
try (string "left" $> EPJoinType JTLeft) <|>
|
||||||
|
try (string "inner" $> EPJoinType JTInner) <|>
|
||||||
|
try (EPHint <$> pFieldName))
|
||||||
|
embedParamHint prm = case prm of
|
||||||
|
Just (EPHint hint) -> Just hint
|
||||||
|
_ -> Nothing
|
||||||
|
embedParamJoin prm = case prm of
|
||||||
|
Just (EPJoinType jt) -> Just jt
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
|
-- |
|
||||||
|
-- Parse operator expression used in horizontal filtering
|
||||||
|
--
|
||||||
|
-- >>> P.parse (pOpExpr pSingleVal) "" "fts().value"
|
||||||
|
-- Left (line 1, column 5):
|
||||||
|
-- unexpected ")"
|
||||||
|
-- expecting operator (eq, gt, ...)
|
||||||
|
--
|
||||||
|
-- >>> P.parse (pOpExpr pSingleVal) "" "eq(any).value"
|
||||||
|
-- Right (OpExpr False (OpQuant OpEqual (Just QuantAny) "value"))
|
||||||
|
--
|
||||||
|
-- >>> P.parse (pOpExpr pSingleVal) "" "eq(all).value"
|
||||||
|
-- Right (OpExpr False (OpQuant OpEqual (Just QuantAll) "value"))
|
||||||
|
--
|
||||||
|
-- >>> P.parse (pOpExpr pSingleVal) "" "not.eq(all).value"
|
||||||
|
-- Right (OpExpr True (OpQuant OpEqual (Just QuantAll) "value"))
|
||||||
|
--
|
||||||
|
-- >>> P.parse (pOpExpr pSingleVal) "" "eq().value"
|
||||||
|
-- Left (line 1, column 4):
|
||||||
|
-- unexpected ")"
|
||||||
|
-- expecting operator (eq, gt, ...)
|
||||||
|
--
|
||||||
|
-- >>> P.parse (pOpExpr pSingleVal) "" "is().value"
|
||||||
|
-- Left (line 1, column 3):
|
||||||
|
-- unexpected "("
|
||||||
|
-- expecting operator (eq, gt, ...)
|
||||||
|
--
|
||||||
|
-- >>> P.parse (pOpExpr pSingleVal) "" "in().value"
|
||||||
|
-- Left (line 1, column 3):
|
||||||
|
-- unexpected "("
|
||||||
|
-- expecting operator (eq, gt, ...)
|
||||||
|
pOpExpr :: Parser SingleVal -> Parser OpExpr
|
||||||
|
pOpExpr pSVal = do
|
||||||
|
boolExpr <- try (string "not" *> pDelimiter $> True) <|> pure False
|
||||||
|
OpExpr boolExpr <$> pOperation
|
||||||
|
where
|
||||||
|
pOperation :: Parser Operation
|
||||||
|
pOperation = pIn <|> pIs <|> pIsDist <|> try pFts <|> try pSimpleOp <|> try pQuantOp <?> "operator (eq, gt, ...)"
|
||||||
|
|
||||||
|
pIn = In <$> (try (string "in" *> pDelimiter) *> pListVal)
|
||||||
|
pIs = Is <$> (try (string "is" *> pDelimiter) *> pTriVal)
|
||||||
|
|
||||||
|
pIsDist = IsDistinctFrom <$> (try (string "isdistinct" *> pDelimiter) *> pSVal)
|
||||||
|
|
||||||
|
pSimpleOp = do
|
||||||
|
op <- simpleOperator
|
||||||
|
pDelimiter *> (Op op <$> pSVal)
|
||||||
|
|
||||||
|
pQuantOp = do
|
||||||
|
op <- quantOperator
|
||||||
|
quant <- optionMaybe $ try (between (char '(') (char ')') (try (string "any" $> QuantAny) <|> string "all" $> QuantAll))
|
||||||
|
pDelimiter *> (OpQuant op quant <$> pSVal)
|
||||||
|
|
||||||
|
pTriVal = try (ciString "null" $> TriNull)
|
||||||
|
<|> try (ciString "unknown" $> TriUnknown)
|
||||||
|
<|> try (ciString "true" $> TriTrue)
|
||||||
|
<|> try (ciString "false" $> TriFalse)
|
||||||
|
<?> "null or trilean value (unknown, true, false)"
|
||||||
|
|
||||||
|
pFts = do
|
||||||
|
op <- try (string "fts" $> FilterFts)
|
||||||
|
<|> try (string "plfts" $> FilterFtsPlain)
|
||||||
|
<|> try (string "phfts" $> FilterFtsPhrase)
|
||||||
|
<|> try (string "wfts" $> FilterFtsWebsearch)
|
||||||
|
|
||||||
|
lang <- optionMaybe $ try (between (char '(') (char ')') pIdentifier)
|
||||||
|
pDelimiter >> Fts op (toS <$> lang) <$> pSVal
|
||||||
|
|
||||||
|
-- case insensitive char and string
|
||||||
|
ciChar :: Char -> GenParser Char state Char
|
||||||
|
ciChar c = char c <|> char (toUpper c)
|
||||||
|
ciString :: [Char] -> GenParser Char state [Char]
|
||||||
|
ciString = traverse ciChar
|
||||||
|
|
||||||
|
pSingleVal :: Parser SingleVal
|
||||||
|
pSingleVal = toS <$> many anyChar
|
||||||
|
|
||||||
|
pListVal :: Parser ListVal
|
||||||
|
pListVal = lexeme (char '(') *> pListElement `sepBy1` char ',' <* lexeme (char ')')
|
||||||
|
|
||||||
|
pListElement :: Parser Text
|
||||||
|
pListElement = try (pQuotedValue <* notFollowedBy (noneOf ",)")) <|> (toS <$> many (noneOf ",)"))
|
||||||
|
|
||||||
|
pQuotedValue :: Parser Text
|
||||||
|
pQuotedValue = toS <$> (char '"' *> many pCharsOrSlashed <* char '"')
|
||||||
|
where
|
||||||
|
pCharsOrSlashed = noneOf "\\\"" <|> (char '\\' *> anyChar)
|
||||||
|
|
||||||
|
pDelimiter :: Parser Char
|
||||||
|
pDelimiter = char '.' <?> "delimiter (.)"
|
||||||
|
|
||||||
|
-- |
|
||||||
|
-- Parses the elements in the order query parameter
|
||||||
|
--
|
||||||
|
-- >>> P.parse pOrder "" "name.desc.nullsfirst"
|
||||||
|
-- Right [OrderTerm {otTerm = ("name",[]), otDirection = Just OrderDesc, otNullOrder = Just OrderNullsFirst}]
|
||||||
|
--
|
||||||
|
-- >>> P.parse pOrder "" "json_col->key.asc.nullslast"
|
||||||
|
-- Right [OrderTerm {otTerm = ("json_col",[JArrow {jOp = JKey {jVal = "key"}}]), otDirection = Just OrderAsc, otNullOrder = Just OrderNullsLast}]
|
||||||
|
--
|
||||||
|
-- >>> P.parse pOrder "" "clients(json_col->key).desc.nullsfirst"
|
||||||
|
-- Right [OrderRelationTerm {otRelation = "clients", otRelTerm = ("json_col",[JArrow {jOp = JKey {jVal = "key"}}]), otDirection = Just OrderDesc, otNullOrder = Just OrderNullsFirst}]
|
||||||
|
--
|
||||||
|
-- >>> P.parse pOrder "" "clients(name,id)"
|
||||||
|
-- Left (line 1, column 8):
|
||||||
|
-- unexpected '('
|
||||||
|
-- expecting letter, digit, "-", "->>", "->", delimiter (.), "," or end of input
|
||||||
|
--
|
||||||
|
-- >>> P.parse pOrder "" "name,clients(name),id"
|
||||||
|
-- Right [OrderTerm {otTerm = ("name",[]), otDirection = Nothing, otNullOrder = Nothing},OrderRelationTerm {otRelation = "clients", otRelTerm = ("name",[]), otDirection = Nothing, otNullOrder = Nothing},OrderTerm {otTerm = ("id",[]), otDirection = Nothing, otNullOrder = Nothing}]
|
||||||
|
--
|
||||||
|
-- >>> P.parse pOrder "" "id.ac"
|
||||||
|
-- Left (line 1, column 4):
|
||||||
|
-- unexpected "c"
|
||||||
|
-- expecting "asc", "desc", "nullsfirst" or "nullslast"
|
||||||
|
--
|
||||||
|
-- >>> P.parse pOrder "" "id.descc"
|
||||||
|
-- Left (line 1, column 8):
|
||||||
|
-- unexpected 'c'
|
||||||
|
-- expecting delimiter (.), "," or end of input
|
||||||
|
--
|
||||||
|
-- >>> P.parse pOrder "" "id.nulsfist"
|
||||||
|
-- Left (line 1, column 4):
|
||||||
|
-- unexpected "n"
|
||||||
|
-- expecting "asc", "desc", "nullsfirst" or "nullslast"
|
||||||
|
--
|
||||||
|
-- >>> P.parse pOrder "" "id.nullslasttt"
|
||||||
|
-- Left (line 1, column 13):
|
||||||
|
-- unexpected 't'
|
||||||
|
-- expecting "," or end of input
|
||||||
|
--
|
||||||
|
-- >>> P.parse pOrder "" "id.smth34"
|
||||||
|
-- Left (line 1, column 4):
|
||||||
|
-- unexpected "s"
|
||||||
|
-- expecting "asc", "desc", "nullsfirst" or "nullslast"
|
||||||
|
--
|
||||||
|
-- >>> P.parse pOrder "" "id.asc.nlsfst"
|
||||||
|
-- Left (line 1, column 8):
|
||||||
|
-- unexpected "l"
|
||||||
|
-- expecting "nullsfirst" or "nullslast"
|
||||||
|
--
|
||||||
|
-- >>> P.parse pOrder "" "id.asc.nullslasttt"
|
||||||
|
-- Left (line 1, column 17):
|
||||||
|
-- unexpected 't'
|
||||||
|
-- expecting "," or end of input
|
||||||
|
--
|
||||||
|
-- >>> P.parse pOrder "" "id.asc.smth34"
|
||||||
|
-- Left (line 1, column 8):
|
||||||
|
-- unexpected "s"
|
||||||
|
-- expecting "nullsfirst" or "nullslast"
|
||||||
|
pOrder :: Parser [OrderTerm]
|
||||||
|
pOrder = lexeme (try pOrderRelationTerm <|> pOrderTerm) `sepBy1` char ','
|
||||||
|
where
|
||||||
|
pOrderTerm = do
|
||||||
|
fld <- pField
|
||||||
|
dir <- optionMaybe pOrdDir
|
||||||
|
nls <- optionMaybe pNulls <* pEnd <|>
|
||||||
|
pEnd $> Nothing
|
||||||
|
return $ OrderTerm fld dir nls
|
||||||
|
|
||||||
|
pOrderRelationTerm = do
|
||||||
|
nam <- pFieldName
|
||||||
|
fld <- between (char '(') (char ')') pField
|
||||||
|
dir <- optionMaybe pOrdDir
|
||||||
|
nls <- optionMaybe pNulls <* pEnd <|> pEnd $> Nothing
|
||||||
|
return $ OrderRelationTerm nam fld dir nls
|
||||||
|
|
||||||
|
pNulls :: Parser OrderNulls
|
||||||
|
pNulls = try (pDelimiter *> string "nullsfirst" $> OrderNullsFirst) <|>
|
||||||
|
try (pDelimiter *> string "nullslast" $> OrderNullsLast)
|
||||||
|
|
||||||
|
pOrdDir :: Parser OrderDirection
|
||||||
|
pOrdDir = try (pDelimiter *> string "asc" $> OrderAsc) <|>
|
||||||
|
try (pDelimiter *> string "desc" $> OrderDesc)
|
||||||
|
|
||||||
|
pEnd = try (void $ lookAhead (char ',')) <|> try eof
|
||||||
|
|
||||||
|
-- |
|
||||||
|
-- Parses the elements inside or/and
|
||||||
|
--
|
||||||
|
-- >>> P.parse pLogicTree "" "or()"
|
||||||
|
-- Left (line 1, column 4):
|
||||||
|
-- unexpected ")"
|
||||||
|
-- expecting field name (* or [a..z0..9_$]), negation operator (not) or logic operator (and, or)
|
||||||
|
--
|
||||||
|
-- >>> P.parse pLogicTree "" "or(id.in.1,2,id.eq.3)"
|
||||||
|
-- Left (line 1, column 10):
|
||||||
|
-- unexpected "1"
|
||||||
|
-- expecting "("
|
||||||
|
--
|
||||||
|
-- >>> P.parse pLogicTree "" "or)("
|
||||||
|
-- Left (line 1, column 3):
|
||||||
|
-- unexpected ")"
|
||||||
|
-- expecting "("
|
||||||
|
--
|
||||||
|
-- >>> P.parse pLogicTree "" "and(ord(id.eq.1,id.eq.1),id.eq.2)"
|
||||||
|
-- Left (line 1, column 7):
|
||||||
|
-- unexpected "d"
|
||||||
|
-- expecting "("
|
||||||
|
--
|
||||||
|
-- >>> P.parse pLogicTree "" "or(id.eq.1,not.xor(id.eq.2,id.eq.3))"
|
||||||
|
-- Left (line 1, column 16):
|
||||||
|
-- unexpected "x"
|
||||||
|
-- expecting logic operator (and, or)
|
||||||
|
pLogicTree :: Parser LogicTree
|
||||||
|
pLogicTree = Stmnt <$> try pLogicFilter
|
||||||
|
<|> Expr <$> pNot <*> pLogicOp <*> (lexeme (char '(') *> pLogicTree `sepBy1` lexeme (char ',') <* lexeme (char ')'))
|
||||||
|
where
|
||||||
|
pLogicFilter :: Parser Filter
|
||||||
|
pLogicFilter = Filter <$> pField <* pDelimiter <*> pOpExpr pLogicSingleVal
|
||||||
|
pNot :: Parser Bool
|
||||||
|
pNot = try (string "not" *> pDelimiter $> True)
|
||||||
|
<|> pure False
|
||||||
|
<?> "negation operator (not)"
|
||||||
|
pLogicOp :: Parser LogicOperator
|
||||||
|
pLogicOp = try (string "and" $> And)
|
||||||
|
<|> string "or" $> Or
|
||||||
|
<?> "logic operator (and, or)"
|
||||||
|
|
||||||
|
pLogicSingleVal :: Parser SingleVal
|
||||||
|
pLogicSingleVal = try (pQuotedValue <* notFollowedBy (noneOf ",)")) <|> try pPgArray <|> (toS <$> many (noneOf ",)"))
|
||||||
|
where
|
||||||
|
pPgArray :: Parser Text
|
||||||
|
pPgArray = do
|
||||||
|
a <- string "{"
|
||||||
|
b <- many (noneOf "{}")
|
||||||
|
c <- string "}"
|
||||||
|
pure (toS $ a ++ b ++ c)
|
||||||
|
|
||||||
|
pLogicPath :: Parser (EmbedPath, Text)
|
||||||
|
pLogicPath = do
|
||||||
|
path <- pFieldName `sepBy1` pDelimiter
|
||||||
|
let op = last path
|
||||||
|
notOp = "not." <> op
|
||||||
|
return (filter (/= "not") (init path), if "not" `elem` path then notOp else op)
|
||||||
|
|
||||||
|
pColumns :: Parser [FieldName]
|
||||||
|
pColumns = pFieldName `sepBy1` lexeme (char ',')
|
||||||
|
|
||||||
|
pIdentifier :: Parser Text
|
||||||
|
pIdentifier = T.strip . toS <$> many1 pIdentifierChar
|
||||||
|
|
||||||
|
pIdentifierChar :: Parser Char
|
||||||
|
pIdentifierChar = letter <|> digit <|> oneOf "_ $"
|
||||||
|
|
||||||
|
mapError :: Either ParseError a -> Either QPError a
|
||||||
|
mapError = mapLeft translateError
|
||||||
|
where
|
||||||
|
translateError e =
|
||||||
|
QPError message details
|
||||||
|
where
|
||||||
|
message = show $ errorPos e
|
||||||
|
details = T.strip $ T.replace "\n" " " $ toS
|
||||||
|
$ showErrorMessages "or" "unknown parse error" "expecting" "unexpected" "end of input" (errorMessages e)
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
{-# LANGUAGE DuplicateRecordFields #-}
|
||||||
|
module PostgREST.ApiRequest.Types
|
||||||
|
( Alias
|
||||||
|
, Cast
|
||||||
|
, Depth
|
||||||
|
, EmbedParam(..)
|
||||||
|
, ApiRequestError(..)
|
||||||
|
, EmbedPath
|
||||||
|
, Field
|
||||||
|
, Filter(..)
|
||||||
|
, Hint
|
||||||
|
, JoinType(..)
|
||||||
|
, JsonOperand(..)
|
||||||
|
, JsonOperation(..)
|
||||||
|
, JsonPath
|
||||||
|
, ListVal
|
||||||
|
, LogicOperator(..)
|
||||||
|
, LogicTree(..)
|
||||||
|
, NodeName
|
||||||
|
, OpExpr(..)
|
||||||
|
, Operation (..)
|
||||||
|
, OpQuantifier(..)
|
||||||
|
, OrderDirection(..)
|
||||||
|
, OrderNulls(..)
|
||||||
|
, OrderTerm(..)
|
||||||
|
, QPError(..)
|
||||||
|
, RangeError(..)
|
||||||
|
, SingleVal
|
||||||
|
, TrileanVal(..)
|
||||||
|
, SimpleOperator(..)
|
||||||
|
, QuantOperator(..)
|
||||||
|
, FtsOperator(..)
|
||||||
|
, SelectItem(..)
|
||||||
|
) where
|
||||||
|
|
||||||
|
import PostgREST.MediaType (MediaType (..))
|
||||||
|
import PostgREST.SchemaCache.Identifiers (FieldName,
|
||||||
|
QualifiedIdentifier)
|
||||||
|
import PostgREST.SchemaCache.Relationship (Relationship,
|
||||||
|
RelationshipsMap)
|
||||||
|
import PostgREST.SchemaCache.Routine (Routine (..))
|
||||||
|
|
||||||
|
import Protolude
|
||||||
|
|
||||||
|
-- | The value in `/tbl?select=alias:field::cast`
|
||||||
|
data SelectItem
|
||||||
|
= SelectField
|
||||||
|
{ selField :: Field
|
||||||
|
, selCast :: Maybe Cast
|
||||||
|
, selAlias :: Maybe Alias
|
||||||
|
}
|
||||||
|
-- | The value in `/tbl?select=alias:another_tbl(*)`
|
||||||
|
| SelectRelation
|
||||||
|
{ selRelation :: FieldName
|
||||||
|
, selAlias :: Maybe Alias
|
||||||
|
, selHint :: Maybe Hint
|
||||||
|
, selJoinType :: Maybe JoinType
|
||||||
|
}
|
||||||
|
-- | The value in `/tbl?select=...another_tbl(*)`
|
||||||
|
| SpreadRelation
|
||||||
|
{ selRelation :: FieldName
|
||||||
|
, selHint :: Maybe Hint
|
||||||
|
, selJoinType :: Maybe JoinType
|
||||||
|
}
|
||||||
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
data ApiRequestError
|
||||||
|
= AmbiguousRelBetween Text Text [Relationship]
|
||||||
|
| AmbiguousRpc [Routine]
|
||||||
|
| BinaryFieldError MediaType
|
||||||
|
| MediaTypeError [ByteString]
|
||||||
|
| InvalidBody ByteString
|
||||||
|
| InvalidFilters
|
||||||
|
| InvalidRange RangeError
|
||||||
|
| InvalidRpcMethod ByteString
|
||||||
|
| LimitNoOrderError
|
||||||
|
| NotFound
|
||||||
|
| NoRelBetween Text Text (Maybe Text) Text RelationshipsMap
|
||||||
|
| NoRpc Text Text [Text] Bool MediaType Bool [QualifiedIdentifier] [Routine]
|
||||||
|
| NotEmbedded Text
|
||||||
|
| PutLimitNotAllowedError
|
||||||
|
| QueryParamError QPError
|
||||||
|
| RelatedOrderNotToOne Text Text
|
||||||
|
| SpreadNotToOne Text Text
|
||||||
|
| UnacceptableFilter Text
|
||||||
|
| UnacceptableSchema [Text]
|
||||||
|
| UnsupportedMethod ByteString
|
||||||
|
| ColumnNotFound Text Text
|
||||||
|
deriving Show
|
||||||
|
|
||||||
|
data QPError = QPError Text Text
|
||||||
|
deriving Show
|
||||||
|
data RangeError
|
||||||
|
= NegativeLimit
|
||||||
|
| LowerGTUpper
|
||||||
|
| OutOfBounds Text Text
|
||||||
|
deriving Show
|
||||||
|
|
||||||
|
type NodeName = Text
|
||||||
|
type Depth = Integer
|
||||||
|
|
||||||
|
data OrderTerm
|
||||||
|
= OrderTerm
|
||||||
|
{ otTerm :: Field
|
||||||
|
, otDirection :: Maybe OrderDirection
|
||||||
|
, otNullOrder :: Maybe OrderNulls
|
||||||
|
}
|
||||||
|
| OrderRelationTerm
|
||||||
|
{ otRelation :: FieldName
|
||||||
|
, otRelTerm :: Field
|
||||||
|
, otDirection :: Maybe OrderDirection
|
||||||
|
, otNullOrder :: Maybe OrderNulls
|
||||||
|
}
|
||||||
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
data OrderDirection
|
||||||
|
= OrderAsc
|
||||||
|
| OrderDesc
|
||||||
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
data OrderNulls
|
||||||
|
= OrderNullsFirst
|
||||||
|
| OrderNullsLast
|
||||||
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
type Field = (FieldName, JsonPath)
|
||||||
|
type Cast = Text
|
||||||
|
type Alias = Text
|
||||||
|
type Hint = Text
|
||||||
|
|
||||||
|
data EmbedParam
|
||||||
|
-- | Disambiguates an embedding operation when there's multiple relationships
|
||||||
|
-- between two tables. Can be the name of a foreign key constraint, column
|
||||||
|
-- name or the junction in an m2m relationship.
|
||||||
|
= EPHint Hint
|
||||||
|
| EPJoinType JoinType
|
||||||
|
|
||||||
|
data JoinType
|
||||||
|
= JTInner
|
||||||
|
| JTLeft
|
||||||
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
-- | Path of the embedded levels, e.g "clients.projects.name=eq.." gives Path
|
||||||
|
-- ["clients", "projects"]
|
||||||
|
type EmbedPath = [Text]
|
||||||
|
|
||||||
|
-- | Json path operations as specified in
|
||||||
|
-- https://www.postgresql.org/docs/current/static/functions-json.html
|
||||||
|
type JsonPath = [JsonOperation]
|
||||||
|
|
||||||
|
-- | Represents the single arrow `->` or double arrow `->>` operators
|
||||||
|
data JsonOperation
|
||||||
|
= JArrow { jOp :: JsonOperand }
|
||||||
|
| J2Arrow { jOp :: JsonOperand }
|
||||||
|
deriving (Eq, Show, Ord)
|
||||||
|
|
||||||
|
-- | Represents the key(`->'key'`) or index(`->'1`::int`), the index is Text
|
||||||
|
-- because we reuse our escaping functons and let pg do the casting with
|
||||||
|
-- '1'::int
|
||||||
|
data JsonOperand
|
||||||
|
= JKey { jVal :: Text }
|
||||||
|
| JIdx { jVal :: Text }
|
||||||
|
deriving (Eq, Show, Ord)
|
||||||
|
|
||||||
|
-- | Boolean logic expression tree e.g. "and(name.eq.N,or(id.eq.1,id.eq.2))" is:
|
||||||
|
--
|
||||||
|
-- And
|
||||||
|
-- / \
|
||||||
|
-- name.eq.N Or
|
||||||
|
-- / \
|
||||||
|
-- id.eq.1 id.eq.2
|
||||||
|
data LogicTree
|
||||||
|
= Expr Bool LogicOperator [LogicTree]
|
||||||
|
| Stmnt Filter
|
||||||
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
data LogicOperator
|
||||||
|
= And
|
||||||
|
| Or
|
||||||
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
data Filter
|
||||||
|
= Filter
|
||||||
|
{ field :: Field
|
||||||
|
, opExpr :: OpExpr
|
||||||
|
}
|
||||||
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
data OpExpr
|
||||||
|
= OpExpr Bool Operation
|
||||||
|
| NoOpExpr Text
|
||||||
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
data OpQuantifier = QuantAny | QuantAll
|
||||||
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
data Operation
|
||||||
|
= Op SimpleOperator SingleVal
|
||||||
|
| OpQuant QuantOperator (Maybe OpQuantifier) SingleVal
|
||||||
|
| In ListVal
|
||||||
|
| Is TrileanVal
|
||||||
|
| IsDistinctFrom SingleVal
|
||||||
|
| Fts FtsOperator (Maybe Language) SingleVal
|
||||||
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
type Language = Text
|
||||||
|
|
||||||
|
-- | Represents a single value in a filter, e.g. id=eq.singleval
|
||||||
|
type SingleVal = Text
|
||||||
|
|
||||||
|
-- | Represents a list value in a filter, e.g. id=in.(val1,val2,val3)
|
||||||
|
type ListVal = [Text]
|
||||||
|
|
||||||
|
-- | Three-valued logic values
|
||||||
|
data TrileanVal
|
||||||
|
= TriTrue
|
||||||
|
| TriFalse
|
||||||
|
| TriNull
|
||||||
|
| TriUnknown
|
||||||
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
-- Operators that are quantifiable, i.e. they can be used with the any/all modifiers
|
||||||
|
data QuantOperator
|
||||||
|
= OpEqual
|
||||||
|
| OpGreaterThanEqual
|
||||||
|
| OpGreaterThan
|
||||||
|
| OpLessThanEqual
|
||||||
|
| OpLessThan
|
||||||
|
| OpLike
|
||||||
|
| OpILike
|
||||||
|
| OpMatch
|
||||||
|
| OpIMatch
|
||||||
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
data SimpleOperator
|
||||||
|
= OpNotEqual
|
||||||
|
| OpContains
|
||||||
|
| OpContained
|
||||||
|
| OpOverlap
|
||||||
|
| OpStrictlyLeft
|
||||||
|
| OpStrictlyRight
|
||||||
|
| OpNotExtendsRight
|
||||||
|
| OpNotExtendsLeft
|
||||||
|
| OpAdjacent
|
||||||
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
--
|
||||||
|
-- | Operators for full text search operators
|
||||||
|
data FtsOperator
|
||||||
|
= FilterFts
|
||||||
|
| FilterFtsPlain
|
||||||
|
| FilterFtsPhrase
|
||||||
|
| FilterFtsWebsearch
|
||||||
|
deriving (Eq, Show)
|
||||||
+191
-385
@@ -9,411 +9,217 @@ Some of its functionality includes:
|
|||||||
- Producing HTTP Headers according to RFCs.
|
- Producing HTTP Headers according to RFCs.
|
||||||
- Content Negotiation
|
- Content Negotiation
|
||||||
-}
|
-}
|
||||||
{-# LANGUAGE FlexibleContexts #-}
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
{-# LANGUAGE MultiWayIf #-}
|
module PostgREST.App
|
||||||
{-# LANGUAGE NamedFieldPuns #-}
|
( SignalHandlerInstaller
|
||||||
{-# LANGUAGE ScopedTypeVariables #-}
|
, SocketRunner
|
||||||
|
, postgrest
|
||||||
|
, run
|
||||||
|
) where
|
||||||
|
|
||||||
module PostgREST.App (
|
|
||||||
postgrest
|
|
||||||
) where
|
|
||||||
|
|
||||||
import qualified Data.ByteString.Char8 as BS
|
import Control.Monad.Except (liftEither)
|
||||||
import qualified Data.HashMap.Strict as M
|
import Data.Either.Combinators (mapLeft)
|
||||||
import qualified Data.List as L (union)
|
import Data.Maybe (fromJust)
|
||||||
import qualified Data.Set as S
|
import Data.String (IsString (..))
|
||||||
import qualified Hasql.Pool as P
|
import Network.Wai.Handler.Warp (defaultSettings, setHost, setPort,
|
||||||
import qualified Hasql.Transaction as H
|
setServerName)
|
||||||
import qualified Hasql.Transaction as HT
|
import System.Posix.Types (FileMode)
|
||||||
import qualified Hasql.Transaction.Sessions as HT
|
|
||||||
|
|
||||||
import Data.Function (id)
|
import qualified Data.HashMap.Strict as HM
|
||||||
import Data.IORef (IORef, readIORef)
|
import qualified Data.Text.Encoding as T
|
||||||
import Data.Time.Clock (UTCTime)
|
import qualified Hasql.Transaction.Sessions as SQL
|
||||||
import Network.HTTP.Types.URI (renderSimpleQuery)
|
import qualified Network.Wai as Wai
|
||||||
import Network.Wai.Middleware.RequestLogger (logStdout)
|
import qualified Network.Wai.Handler.Warp as Warp
|
||||||
|
|
||||||
import Control.Applicative
|
import qualified PostgREST.Admin as Admin
|
||||||
import Data.Maybe
|
import qualified PostgREST.ApiRequest as ApiRequest
|
||||||
import Network.HTTP.Types.Header
|
import qualified PostgREST.ApiRequest.Types as ApiRequestTypes
|
||||||
import Network.HTTP.Types.Status
|
import qualified PostgREST.AppState as AppState
|
||||||
import Network.Wai
|
import qualified PostgREST.Auth as Auth
|
||||||
|
import qualified PostgREST.Cors as Cors
|
||||||
|
import qualified PostgREST.Error as Error
|
||||||
|
import qualified PostgREST.Logger as Logger
|
||||||
|
import qualified PostgREST.Plan as Plan
|
||||||
|
import qualified PostgREST.Query as Query
|
||||||
|
import qualified PostgREST.Response as Response
|
||||||
|
|
||||||
import PostgREST.ApiRequest (Action (..), ApiRequest (..),
|
import PostgREST.ApiRequest (Action (..), ApiRequest (..),
|
||||||
InvokeMethod (..), Target (..),
|
Mutation (..), Target (..))
|
||||||
mutuallyAgreeable, userApiRequest)
|
import PostgREST.AppState (AppState)
|
||||||
import PostgREST.Auth (containsRole, jwtClaims,
|
import PostgREST.Auth (AuthResult (..))
|
||||||
parseSecret)
|
|
||||||
import PostgREST.Config (AppConfig (..))
|
import PostgREST.Config (AppConfig (..))
|
||||||
import PostgREST.DbRequestBuilder (mutateRequest, readRequest,
|
import PostgREST.Config.PgVersion (PgVersion (..))
|
||||||
returningCols)
|
import PostgREST.Error (Error)
|
||||||
import PostgREST.DbStructure
|
import PostgREST.Query (DbHandler)
|
||||||
import PostgREST.Error (PgError (..), SimpleError (..),
|
import PostgREST.SchemaCache (SchemaCache (..))
|
||||||
errorResponseFor, singularityError)
|
import PostgREST.SchemaCache.Routine (Routine (..))
|
||||||
import PostgREST.Middleware
|
import PostgREST.Version (docsVersion, prettyVersion)
|
||||||
import PostgREST.OpenAPI
|
|
||||||
import PostgREST.Parsers (pRequestColumns)
|
|
||||||
import PostgREST.QueryBuilder (limitedQuery, mutateRequestToQuery,
|
|
||||||
readRequestToCountQuery,
|
|
||||||
readRequestToQuery,
|
|
||||||
requestToCallProcQuery)
|
|
||||||
import PostgREST.RangeQuery (allRange, contentRangeH,
|
|
||||||
rangeStatusHeader)
|
|
||||||
import PostgREST.Statements (callProcStatement,
|
|
||||||
createExplainStatement,
|
|
||||||
createReadStatement,
|
|
||||||
createWriteStatement)
|
|
||||||
import PostgREST.Types
|
|
||||||
import Protolude hiding (Proxy, intercalate, toS)
|
|
||||||
import Protolude.Conv (toS)
|
|
||||||
|
|
||||||
postgrest :: AppConfig -> IORef (Maybe DbStructure) -> P.Pool -> IO UTCTime -> IO () -> Application
|
import Protolude hiding (Handler)
|
||||||
postgrest conf refDbStructure pool getTime worker =
|
|
||||||
let middle = (if configQuiet conf then id else logStdout) . defaultMiddle
|
|
||||||
jwtSecret = parseSecret <$> configJwtSecret conf in
|
|
||||||
middle $ \ req respond -> do
|
|
||||||
time <- getTime
|
|
||||||
body <- strictRequestBody req
|
|
||||||
maybeDbStructure <- readIORef refDbStructure
|
|
||||||
case maybeDbStructure of
|
|
||||||
Nothing -> respond . errorResponseFor $ ConnectionLostError
|
|
||||||
Just dbStructure -> do
|
|
||||||
response <- do
|
|
||||||
-- Need to parse ?columns early because findProc needs it to solve overloaded functions.
|
|
||||||
-- TODO: move this logic to the app function
|
|
||||||
let apiReq = userApiRequest (configSchemas conf) (configRootSpec conf) req body
|
|
||||||
apiReqCols = (,) <$> apiReq <*> (pRequestColumns =<< iColumns <$> apiReq)
|
|
||||||
case apiReqCols of
|
|
||||||
Left err -> return . errorResponseFor $ err
|
|
||||||
Right (apiRequest, maybeCols) -> do
|
|
||||||
eClaims <- jwtClaims jwtSecret (configJwtAudience conf) (toS $ iJWT apiRequest) time (rightToMaybe $ configRoleClaimKey conf)
|
|
||||||
let authed = containsRole eClaims
|
|
||||||
cols = case (iPayload apiRequest, maybeCols) of
|
|
||||||
(Just ProcessedJSON{pjKeys}, _) -> pjKeys
|
|
||||||
(Just RawJSON{}, Just cls) -> cls
|
|
||||||
_ -> S.empty
|
|
||||||
proc = case iTarget apiRequest of
|
|
||||||
TargetProc qi _ -> findProc qi cols (iPreferParameters apiRequest == Just SingleObject) $ dbProcs dbStructure
|
|
||||||
_ -> Nothing
|
|
||||||
handleReq = runWithClaims conf eClaims (app dbStructure proc cols conf) apiRequest
|
|
||||||
txMode = transactionMode proc (iAction apiRequest)
|
|
||||||
response <- P.use pool $ HT.transaction HT.ReadCommitted txMode handleReq
|
|
||||||
return $ either (errorResponseFor . PgError authed) identity response
|
|
||||||
when (responseStatus response == status503) worker
|
|
||||||
respond response
|
|
||||||
|
|
||||||
transactionMode :: Maybe ProcDescription -> Action -> HT.Mode
|
type Handler = ExceptT Error
|
||||||
transactionMode proc action =
|
|
||||||
case action of
|
|
||||||
ActionRead _ -> HT.Read
|
|
||||||
ActionInfo -> HT.Read
|
|
||||||
ActionInspect _ -> HT.Read
|
|
||||||
ActionInvoke InvGet -> HT.Read
|
|
||||||
ActionInvoke InvHead -> HT.Read
|
|
||||||
ActionInvoke InvPost ->
|
|
||||||
let v = maybe Volatile pdVolatility proc in
|
|
||||||
if v == Stable || v == Immutable
|
|
||||||
then HT.Read
|
|
||||||
else HT.Write
|
|
||||||
_ -> HT.Write
|
|
||||||
|
|
||||||
app :: DbStructure -> Maybe ProcDescription -> S.Set FieldName -> AppConfig -> ApiRequest -> H.Transaction Response
|
type SignalHandlerInstaller = AppState -> IO()
|
||||||
app dbStructure proc cols conf apiRequest =
|
|
||||||
let rawContentTypes = (decodeContentType <$> configRawMediaTypes conf) `L.union` [ CTOctetStream, CTTextPlain ] in
|
|
||||||
case responseContentTypeOrError (iAccepts apiRequest) rawContentTypes (iAction apiRequest) (iTarget apiRequest) of
|
|
||||||
Left errorResponse -> return errorResponse
|
|
||||||
Right contentType ->
|
|
||||||
case (iAction apiRequest, iTarget apiRequest, iPayload apiRequest) of
|
|
||||||
|
|
||||||
(ActionRead headersOnly, TargetIdent (QualifiedIdentifier tSchema tName), Nothing) ->
|
type SocketRunner = Warp.Settings -> Wai.Application -> FileMode -> FilePath -> IO()
|
||||||
case readSqlParts tSchema tName of
|
|
||||||
Left errorResponse -> return errorResponse
|
run :: SignalHandlerInstaller -> Maybe SocketRunner -> AppState -> IO ()
|
||||||
Right (q, cq, bField, _) -> do
|
run installHandlers maybeRunWithSocket appState = do
|
||||||
let cQuery = if estimatedCount
|
conf@AppConfig{..} <- AppState.getConfig appState
|
||||||
then limitedQuery cq ((+ 1) <$> maxRows) -- LIMIT maxRows + 1 so we can determine below that maxRows was surpassed
|
AppState.connectionWorker appState -- Loads the initial SchemaCache
|
||||||
else cq
|
installHandlers appState
|
||||||
stm = createReadStatement q cQuery (contentType == CTSingularJSON) shouldCount
|
-- reload schema cache + config on NOTIFY
|
||||||
(contentType == CTTextCSV) bField pgVer
|
AppState.runListener conf appState
|
||||||
explStm = createExplainStatement cq
|
|
||||||
row <- H.statement () stm
|
Admin.runAdmin conf appState $ serverSettings conf
|
||||||
let (tableTotal, queryTotal, _ , body, gucHeaders) = row
|
|
||||||
case gucHeaders of
|
let app = postgrest conf appState (AppState.connectionWorker appState)
|
||||||
Left _ -> return . errorResponseFor $ GucHeadersError
|
|
||||||
Right ghdrs -> do
|
case configServerUnixSocket of
|
||||||
total <- if | plannedCount -> H.statement () explStm
|
Just socket ->
|
||||||
| estimatedCount -> if tableTotal > (fromIntegral <$> maxRows)
|
-- run the postgrest application with user defined socket. Only for UNIX systems
|
||||||
then do estTotal <- H.statement () explStm
|
case maybeRunWithSocket of
|
||||||
pure $ if estTotal > tableTotal then estTotal else tableTotal
|
Just runWithSocket -> do
|
||||||
else pure tableTotal
|
AppState.logWithZTime appState $ "Listening on unix socket " <> show socket
|
||||||
| otherwise -> pure tableTotal
|
runWithSocket (serverSettings conf) app configServerUnixSocketMode socket
|
||||||
let (status, contentRange) = rangeStatusHeader topLevelRange queryTotal total
|
Nothing ->
|
||||||
headers = addHeadersIfNotIncluded (catMaybes [
|
panic "Cannot run with unix socket on non-unix platforms."
|
||||||
Just $ toHeader contentType, Just contentRange,
|
Nothing ->
|
||||||
Just $ contentLocationH tName (iCanonicalQS apiRequest), profileH])
|
do
|
||||||
(unwrapGucHeader <$> ghdrs)
|
AppState.logWithZTime appState $ "Listening on port " <> show configServerPort
|
||||||
rBody = if headersOnly then mempty else toS body
|
Warp.runSettings (serverSettings conf) app
|
||||||
return $
|
|
||||||
if contentType == CTSingularJSON && queryTotal /= 1
|
serverSettings :: AppConfig -> Warp.Settings
|
||||||
then errorResponseFor . singularityError $ queryTotal
|
serverSettings AppConfig{..} =
|
||||||
else responseLBS status headers rBody
|
defaultSettings
|
||||||
|
& setHost (fromString $ toS configServerHost)
|
||||||
|
& setPort configServerPort
|
||||||
|
& setServerName ("postgrest/" <> prettyVersion)
|
||||||
|
|
||||||
|
-- | PostgREST application
|
||||||
|
postgrest :: AppConfig -> AppState.AppState -> IO () -> Wai.Application
|
||||||
|
postgrest conf appState connWorker =
|
||||||
|
Response.traceHeaderMiddleware conf .
|
||||||
|
Cors.middleware .
|
||||||
|
Auth.middleware appState .
|
||||||
|
Logger.middleware (configLogLevel conf) $
|
||||||
|
-- fromJust can be used, because the auth middleware will **always** add
|
||||||
|
-- some AuthResult to the vault.
|
||||||
|
\req respond -> case fromJust $ Auth.getResult req of
|
||||||
|
Left err -> respond $ Error.errorResponseFor err
|
||||||
|
Right authResult -> do
|
||||||
|
appConf <- AppState.getConfig appState -- the config must be read again because it can reload
|
||||||
|
maybeSchemaCache <- AppState.getSchemaCache appState
|
||||||
|
pgVer <- AppState.getPgVersion appState
|
||||||
|
|
||||||
(ActionCreate, TargetIdent (QualifiedIdentifier tSchema tName), Just pJson) ->
|
|
||||||
case mutateSqlParts tSchema tName of
|
|
||||||
Left errorResponse -> return errorResponse
|
|
||||||
Right (sq, mq) -> do
|
|
||||||
let pkCols = tablePKCols dbStructure tSchema tName
|
|
||||||
stm = createWriteStatement sq mq
|
|
||||||
(contentType == CTSingularJSON) True
|
|
||||||
(contentType == CTTextCSV) (iPreferRepresentation apiRequest) pkCols pgVer
|
|
||||||
row <- H.statement (toS $ pjRaw pJson) stm
|
|
||||||
let (_, queryTotal, fields, body, gucHeaders) = row
|
|
||||||
case gucHeaders of
|
|
||||||
Left _ -> return . errorResponseFor $ GucHeadersError
|
|
||||||
Right ghdrs -> do
|
|
||||||
let
|
let
|
||||||
(ctHeaders, rBody) = if iPreferRepresentation apiRequest == Full
|
eitherResponse :: IO (Either Error Wai.Response)
|
||||||
then ([Just $ toHeader contentType, profileH], toS body)
|
eitherResponse =
|
||||||
else ([], mempty)
|
runExceptT $ postgrestResponse appState appConf maybeSchemaCache pgVer authResult req
|
||||||
headers = addHeadersIfNotIncluded (catMaybes ([
|
|
||||||
if null fields
|
|
||||||
then Nothing
|
|
||||||
else Just $ locationH tName fields
|
|
||||||
, Just $ contentRangeH 1 0 $ if shouldCount then Just queryTotal else Nothing
|
|
||||||
, if null pkCols && isNothing (iOnConflict apiRequest)
|
|
||||||
then Nothing
|
|
||||||
else (\x -> ("Preference-Applied", encodeUtf8 (show x))) <$> iPreferResolution apiRequest
|
|
||||||
] ++ ctHeaders)) (unwrapGucHeader <$> ghdrs)
|
|
||||||
if contentType == CTSingularJSON && queryTotal /= 1
|
|
||||||
then do
|
|
||||||
HT.condemn
|
|
||||||
return . errorResponseFor . singularityError $ queryTotal
|
|
||||||
else
|
|
||||||
return $ responseLBS status201 headers rBody
|
|
||||||
|
|
||||||
(ActionUpdate, TargetIdent (QualifiedIdentifier tSchema tName), Just pJson) ->
|
response <- either Error.errorResponseFor identity <$> eitherResponse
|
||||||
case mutateSqlParts tSchema tName of
|
-- Launch the connWorker when the connection is down. The postgrest
|
||||||
Left errorResponse -> return errorResponse
|
-- function can respond successfully (with a stale schema cache) before
|
||||||
Right (sq, mq) -> do
|
-- the connWorker is done.
|
||||||
let stm = createWriteStatement sq mq
|
when (Response.isServiceUnavailable response) connWorker
|
||||||
(contentType == CTSingularJSON) False (contentType == CTTextCSV)
|
resp <- do
|
||||||
(iPreferRepresentation apiRequest) [] pgVer
|
delay <- AppState.getRetryNextIn appState
|
||||||
row <- H.statement (toS $ pjRaw pJson) stm
|
return $ Response.addRetryHint delay response
|
||||||
let (_, queryTotal, _, body, gucHeaders) = row
|
respond resp
|
||||||
case gucHeaders of
|
|
||||||
Left _ -> return . errorResponseFor $ GucHeadersError
|
|
||||||
Right ghdrs -> do
|
|
||||||
let
|
|
||||||
updateIsNoOp = S.null cols
|
|
||||||
status | queryTotal == 0 && not updateIsNoOp = status404
|
|
||||||
| iPreferRepresentation apiRequest == Full = status200
|
|
||||||
| otherwise = status204
|
|
||||||
contentRangeHeader = contentRangeH 0 (queryTotal - 1) $ if shouldCount then Just queryTotal else Nothing
|
|
||||||
(ctHeaders, rBody) = if iPreferRepresentation apiRequest == Full
|
|
||||||
then ([Just $ toHeader contentType, profileH], toS body)
|
|
||||||
else ([], mempty)
|
|
||||||
headers = addHeadersIfNotIncluded (catMaybes ctHeaders ++ [contentRangeHeader]) (unwrapGucHeader <$> ghdrs)
|
|
||||||
if contentType == CTSingularJSON && queryTotal /= 1
|
|
||||||
then do
|
|
||||||
HT.condemn
|
|
||||||
return . errorResponseFor . singularityError $ queryTotal
|
|
||||||
else
|
|
||||||
return $ responseLBS status headers rBody
|
|
||||||
|
|
||||||
(ActionSingleUpsert, TargetIdent (QualifiedIdentifier tSchema tName), Just pJson) ->
|
postgrestResponse
|
||||||
case mutateSqlParts tSchema tName of
|
:: AppState.AppState
|
||||||
Left errorResponse -> return errorResponse
|
-> AppConfig
|
||||||
Right (sq, mq) ->
|
-> Maybe SchemaCache
|
||||||
if topLevelRange /= allRange
|
-> PgVersion
|
||||||
then return . errorResponseFor $ PutRangeNotAllowedError
|
-> AuthResult
|
||||||
else do
|
-> Wai.Request
|
||||||
row <- H.statement (toS $ pjRaw pJson) $
|
-> Handler IO Wai.Response
|
||||||
createWriteStatement sq mq (contentType == CTSingularJSON) False
|
postgrestResponse appState conf@AppConfig{..} maybeSchemaCache pgVer authResult@AuthResult{..} req = do
|
||||||
(contentType == CTTextCSV) (iPreferRepresentation apiRequest) [] pgVer
|
sCache <-
|
||||||
let (_, queryTotal, _, body, gucHeaders) = row
|
case maybeSchemaCache of
|
||||||
case gucHeaders of
|
Just sCache ->
|
||||||
Left _ -> return . errorResponseFor $ GucHeadersError
|
return sCache
|
||||||
Right ghdrs -> do
|
Nothing ->
|
||||||
let headers = addHeadersIfNotIncluded (catMaybes [Just $ toHeader contentType, profileH]) (unwrapGucHeader <$> ghdrs)
|
throwError Error.NoSchemaCacheError
|
||||||
(status, rBody) = if iPreferRepresentation apiRequest == Full then (status200, toS body) else (status204, mempty)
|
|
||||||
-- Makes sure the querystring pk matches the payload pk
|
|
||||||
-- e.g. PUT /items?id=eq.1 { "id" : 1, .. } is accepted, PUT /items?id=eq.14 { "id" : 2, .. } is rejected
|
|
||||||
-- If this condition is not satisfied then nothing is inserted, check the WHERE for INSERT in QueryBuilder.hs to see how it's done
|
|
||||||
if queryTotal /= 1
|
|
||||||
then do
|
|
||||||
HT.condemn
|
|
||||||
return . errorResponseFor $ PutMatchingPkError
|
|
||||||
else
|
|
||||||
return $ responseLBS status headers rBody
|
|
||||||
|
|
||||||
(ActionDelete, TargetIdent (QualifiedIdentifier tSchema tName), Nothing) ->
|
body <- lift $ Wai.strictRequestBody req
|
||||||
case mutateSqlParts tSchema tName of
|
|
||||||
Left errorResponse -> return errorResponse
|
|
||||||
Right (sq, mq) -> do
|
|
||||||
let stm = createWriteStatement sq mq
|
|
||||||
(contentType == CTSingularJSON) False
|
|
||||||
(contentType == CTTextCSV)
|
|
||||||
(iPreferRepresentation apiRequest) [] pgVer
|
|
||||||
row <- H.statement mempty stm
|
|
||||||
let (_, queryTotal, _, body, gucHeaders) = row
|
|
||||||
case gucHeaders of
|
|
||||||
Left _ -> return . errorResponseFor $ GucHeadersError
|
|
||||||
Right ghdrs -> do
|
|
||||||
let
|
|
||||||
status = if iPreferRepresentation apiRequest == Full then status200 else status204
|
|
||||||
contentRangeHeader = contentRangeH 1 0 $ if shouldCount then Just queryTotal else Nothing
|
|
||||||
(ctHeaders, rBody) = if iPreferRepresentation apiRequest == Full
|
|
||||||
then ([Just $ toHeader contentType, profileH], toS body)
|
|
||||||
else ([], mempty)
|
|
||||||
headers = addHeadersIfNotIncluded (catMaybes ctHeaders ++ [contentRangeHeader]) (unwrapGucHeader <$> ghdrs)
|
|
||||||
if contentType == CTSingularJSON
|
|
||||||
&& queryTotal /= 1
|
|
||||||
then do
|
|
||||||
HT.condemn
|
|
||||||
return . errorResponseFor . singularityError $ queryTotal
|
|
||||||
else
|
|
||||||
return $ responseLBS status headers rBody
|
|
||||||
|
|
||||||
(ActionInfo, TargetIdent (QualifiedIdentifier tSchema tTable), Nothing) ->
|
apiRequest <-
|
||||||
let mTable = find (\t -> tableName t == tTable && tableSchema t == tSchema) (dbTables dbStructure) in
|
liftEither . mapLeft Error.ApiRequestError $
|
||||||
case mTable of
|
ApiRequest.userApiRequest conf req body
|
||||||
Nothing -> return notFound
|
|
||||||
Just table ->
|
|
||||||
let allowH = (hAllow, if tableInsertable table then "GET,POST,PATCH,DELETE" else "GET")
|
|
||||||
allOrigins = ("Access-Control-Allow-Origin", "*") :: Header in
|
|
||||||
return $ responseLBS status200 [allOrigins, allowH] mempty
|
|
||||||
|
|
||||||
(ActionInvoke invMethod, TargetProc qi@(QualifiedIdentifier tSchema pName) _, Just pJson) ->
|
handleRequest authResult conf appState (Just authRole /= configDbAnonRole) configDbPreparedStatements pgVer apiRequest sCache
|
||||||
let tName = fromMaybe pName $ procTableName =<< proc in
|
|
||||||
case readSqlParts tSchema tName of
|
|
||||||
Left errorResponse -> return errorResponse
|
|
||||||
Right (q, cq, bField, returning) -> do
|
|
||||||
let
|
|
||||||
preferParams = iPreferParameters apiRequest
|
|
||||||
pq = requestToCallProcQuery qi (specifiedProcArgs cols proc) returnsScalar preferParams returning
|
|
||||||
stm = callProcStatement returnsScalar pq q cq shouldCount (contentType == CTSingularJSON)
|
|
||||||
(contentType == CTTextCSV) (contentType `elem` rawContentTypes) (preferParams == Just MultipleObjects)
|
|
||||||
bField pgVer
|
|
||||||
row <- H.statement (toS $ pjRaw pJson) stm
|
|
||||||
let (tableTotal, queryTotal, body, gucHeaders) = row
|
|
||||||
case gucHeaders of
|
|
||||||
Left _ -> return . errorResponseFor $ GucHeadersError
|
|
||||||
Right ghdrs -> do
|
|
||||||
let (status, contentRange) = rangeStatusHeader topLevelRange queryTotal tableTotal
|
|
||||||
headers = addHeadersIfNotIncluded
|
|
||||||
(catMaybes [Just $ toHeader contentType, Just contentRange, profileH])
|
|
||||||
(unwrapGucHeader <$> ghdrs)
|
|
||||||
rBody = if invMethod == InvHead then mempty else toS body
|
|
||||||
if contentType == CTSingularJSON && queryTotal /= 1
|
|
||||||
then do
|
|
||||||
HT.condemn
|
|
||||||
return . errorResponseFor . singularityError $ queryTotal
|
|
||||||
else
|
|
||||||
return $ responseLBS status headers rBody
|
|
||||||
|
|
||||||
(ActionInspect headersOnly, TargetDefaultSpec tSchema, Nothing) -> do
|
runDbHandler :: AppState.AppState -> SQL.IsolationLevel -> SQL.Mode -> Bool -> Bool -> DbHandler b -> Handler IO b
|
||||||
let host = configHost conf
|
runDbHandler appState isoLvl mode authenticated prepared handler = do
|
||||||
port = toInteger $ configPort conf
|
dbResp <- lift $ do
|
||||||
proxy = pickProxy $ toS <$> configOpenAPIProxyUri conf
|
let transaction = if prepared then SQL.transaction else SQL.unpreparedTransaction
|
||||||
uri Nothing = ("http", host, port, "/")
|
AppState.usePool appState . transaction isoLvl mode $ runExceptT handler
|
||||||
uri (Just Proxy { proxyScheme = s, proxyHost = h, proxyPort = p, proxyPath = b }) = (s, h, p, b)
|
|
||||||
uri' = uri proxy
|
|
||||||
toTableInfo :: [Table] -> [(Table, [Column], [Text])]
|
|
||||||
toTableInfo = map (\t -> let (s, tn) = (tableSchema t, tableName t) in (t, tableCols dbStructure s tn, tablePKCols dbStructure s tn))
|
|
||||||
encodeApi ti sd procs = encodeOpenAPI (concat $ M.elems procs) (toTableInfo ti) uri' sd $ dbPrimaryKeys dbStructure
|
|
||||||
|
|
||||||
body <- encodeApi <$>
|
resp <-
|
||||||
H.statement tSchema accessibleTables <*>
|
liftEither . mapLeft Error.PgErr $
|
||||||
H.statement tSchema schemaDescription <*>
|
mapLeft (Error.PgError authenticated) dbResp
|
||||||
H.statement tSchema accessibleProcs
|
|
||||||
return $ responseLBS status200 (catMaybes [Just $ toHeader CTOpenAPI, profileH]) (if headersOnly then mempty else toS body)
|
|
||||||
|
|
||||||
_ -> return notFound
|
liftEither resp
|
||||||
|
|
||||||
|
handleRequest :: AuthResult -> AppConfig -> AppState.AppState -> Bool -> Bool -> PgVersion -> ApiRequest -> SchemaCache -> Handler IO Wai.Response
|
||||||
|
handleRequest AuthResult{..} conf appState authenticated prepared pgVer apiReq@ApiRequest{..} sCache =
|
||||||
|
case (iAction, iTarget) of
|
||||||
|
(ActionRead headersOnly, TargetIdent identifier) -> do
|
||||||
|
wrPlan <- liftEither $ Plan.wrappedReadPlan identifier conf sCache apiReq
|
||||||
|
resultSet <- runQuery roleIsoLvl (Plan.wrTxMode wrPlan) $ Query.readQuery wrPlan conf apiReq
|
||||||
|
return $ Response.readResponse headersOnly identifier apiReq resultSet
|
||||||
|
|
||||||
|
(ActionMutate MutationCreate, TargetIdent identifier) -> do
|
||||||
|
mrPlan <- liftEither $ Plan.mutateReadPlan MutationCreate apiReq identifier conf sCache
|
||||||
|
resultSet <- runQuery roleIsoLvl (Plan.mrTxMode mrPlan) $ Query.createQuery mrPlan apiReq conf
|
||||||
|
return $ Response.createResponse identifier mrPlan apiReq resultSet
|
||||||
|
|
||||||
|
(ActionMutate MutationUpdate, TargetIdent identifier) -> do
|
||||||
|
mrPlan <- liftEither $ Plan.mutateReadPlan MutationUpdate apiReq identifier conf sCache
|
||||||
|
resultSet <- runQuery roleIsoLvl (Plan.mrTxMode mrPlan) $ Query.updateQuery mrPlan apiReq conf
|
||||||
|
return $ Response.updateResponse apiReq resultSet
|
||||||
|
|
||||||
|
(ActionMutate MutationSingleUpsert, TargetIdent identifier) -> do
|
||||||
|
mrPlan <- liftEither $ Plan.mutateReadPlan MutationSingleUpsert apiReq identifier conf sCache
|
||||||
|
resultSet <- runQuery roleIsoLvl (Plan.mrTxMode mrPlan) $ Query.singleUpsertQuery mrPlan apiReq conf
|
||||||
|
return $ Response.singleUpsertResponse apiReq resultSet
|
||||||
|
|
||||||
|
(ActionMutate MutationDelete, TargetIdent identifier) -> do
|
||||||
|
mrPlan <- liftEither $ Plan.mutateReadPlan MutationDelete apiReq identifier conf sCache
|
||||||
|
resultSet <- runQuery roleIsoLvl (Plan.mrTxMode mrPlan) $ Query.deleteQuery mrPlan apiReq conf
|
||||||
|
return $ Response.deleteResponse apiReq resultSet
|
||||||
|
|
||||||
|
(ActionInvoke invMethod, TargetProc identifier _) -> do
|
||||||
|
cPlan <- liftEither $ Plan.callReadPlan identifier conf sCache apiReq invMethod
|
||||||
|
resultSet <- runQuery (fromMaybe roleIsoLvl $ pdIsoLvl (Plan.crProc cPlan))(Plan.crTxMode cPlan) $ Query.invokeQuery (Plan.crProc cPlan) cPlan apiReq conf pgVer
|
||||||
|
return $ Response.invokeResponse invMethod (Plan.crProc cPlan) apiReq resultSet
|
||||||
|
|
||||||
|
(ActionInspect headersOnly, TargetDefaultSpec tSchema) -> do
|
||||||
|
oaiResult <- runQuery roleIsoLvl Plan.inspectPlanTxMode $ Query.openApiQuery sCache pgVer conf tSchema
|
||||||
|
return $ Response.openApiResponse (T.decodeUtf8 prettyVersion, docsVersion) headersOnly oaiResult conf sCache iSchema iNegotiatedByProfile
|
||||||
|
|
||||||
|
(ActionInfo, TargetIdent identifier) ->
|
||||||
|
return $ Response.infoIdentResponse identifier sCache
|
||||||
|
|
||||||
|
(ActionInfo, TargetProc identifier _) -> do
|
||||||
|
cPlan <- liftEither $ Plan.callReadPlan identifier conf sCache apiReq ApiRequest.InvHead
|
||||||
|
return $ Response.infoProcResponse (Plan.crProc cPlan)
|
||||||
|
|
||||||
|
(ActionInfo, TargetDefaultSpec _) ->
|
||||||
|
return Response.infoRootResponse
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
-- This is unreachable as the ApiRequest.hs rejects it before
|
||||||
|
-- TODO Refactor the Action/Target types to remove this line
|
||||||
|
throwError $ Error.ApiRequestError ApiRequestTypes.NotFound
|
||||||
where
|
where
|
||||||
notFound = responseLBS status404 [] ""
|
roleSettings = fromMaybe mempty (HM.lookup authRole $ configRoleSettings conf)
|
||||||
maxRows = configMaxRows conf
|
roleIsoLvl = HM.findWithDefault SQL.ReadCommitted authRole $ configRoleIsoLvl conf
|
||||||
exactCount = iPreferCount apiRequest == Just ExactCount
|
runQuery isoLvl mode query =
|
||||||
estimatedCount = iPreferCount apiRequest == Just EstimatedCount
|
runDbHandler appState isoLvl mode authenticated prepared $ do
|
||||||
plannedCount = iPreferCount apiRequest == Just PlannedCount
|
Query.setPgLocals conf authClaims authRole (HM.toList roleSettings) apiReq pgVer
|
||||||
shouldCount = exactCount || estimatedCount
|
Query.runPreReq conf
|
||||||
topLevelRange = iTopLevelRange apiRequest
|
query
|
||||||
returnsScalar = maybe False procReturnsScalar proc
|
|
||||||
pgVer = pgVersion dbStructure
|
|
||||||
profileH = contentProfileH <$> iProfile apiRequest
|
|
||||||
|
|
||||||
readSqlParts s t =
|
|
||||||
let
|
|
||||||
readReq = readRequest s t maxRows (dbRelations dbStructure) apiRequest
|
|
||||||
returnings :: ReadRequest -> Either Response [FieldName]
|
|
||||||
returnings rr = Right (returningCols rr)
|
|
||||||
in
|
|
||||||
(,,,) <$>
|
|
||||||
(readRequestToQuery <$> readReq) <*>
|
|
||||||
(readRequestToCountQuery <$> readReq) <*>
|
|
||||||
(binaryField contentType rawContentTypes returnsScalar =<< readReq) <*>
|
|
||||||
(returnings =<< readReq)
|
|
||||||
|
|
||||||
mutateSqlParts s t =
|
|
||||||
let
|
|
||||||
readReq = readRequest s t maxRows (dbRelations dbStructure) apiRequest
|
|
||||||
mutReq = mutateRequest s t apiRequest cols (tablePKCols dbStructure s t) =<< readReq
|
|
||||||
in
|
|
||||||
(,) <$>
|
|
||||||
(readRequestToQuery <$> readReq) <*>
|
|
||||||
(mutateRequestToQuery <$> mutReq)
|
|
||||||
|
|
||||||
responseContentTypeOrError :: [ContentType] -> [ContentType] -> Action -> Target -> Either Response ContentType
|
|
||||||
responseContentTypeOrError accepts rawContentTypes action target = serves contentTypesForRequest accepts
|
|
||||||
where
|
|
||||||
contentTypesForRequest = case action of
|
|
||||||
ActionRead _ -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
|
|
||||||
++ rawContentTypes
|
|
||||||
ActionCreate -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
|
|
||||||
ActionUpdate -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
|
|
||||||
ActionDelete -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
|
|
||||||
ActionInvoke _ -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
|
|
||||||
++ rawContentTypes
|
|
||||||
++ [CTOpenAPI | tpIsRootSpec target]
|
|
||||||
ActionInspect _ -> [CTOpenAPI, CTApplicationJSON]
|
|
||||||
ActionInfo -> [CTTextCSV]
|
|
||||||
ActionSingleUpsert -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
|
|
||||||
serves sProduces cAccepts =
|
|
||||||
case mutuallyAgreeable sProduces cAccepts of
|
|
||||||
Nothing -> Left . errorResponseFor . ContentTypeError . map toMime $ cAccepts
|
|
||||||
Just ct -> Right ct
|
|
||||||
|
|
||||||
{-
|
|
||||||
| If raw(binary) output is requested, check that ContentType is one of the admitted rawContentTypes and that
|
|
||||||
| `?select=...` contains only one field other than `*`
|
|
||||||
-}
|
|
||||||
binaryField :: ContentType -> [ContentType] -> Bool -> ReadRequest -> Either Response (Maybe FieldName)
|
|
||||||
binaryField ct rawContentTypes isScalarProc readReq
|
|
||||||
| isScalarProc = Right Nothing
|
|
||||||
| ct `elem` rawContentTypes =
|
|
||||||
let fieldName = headMay fldNames in
|
|
||||||
if length fldNames == 1 && fieldName /= Just "*"
|
|
||||||
then Right fieldName
|
|
||||||
else Left . errorResponseFor $ BinaryFieldError ct
|
|
||||||
| otherwise = Right Nothing
|
|
||||||
where
|
|
||||||
fldNames = fstFieldNames readReq
|
|
||||||
|
|
||||||
locationH :: TableName -> [BS.ByteString] -> Header
|
|
||||||
locationH tName fields =
|
|
||||||
let
|
|
||||||
locationFields = renderSimpleQuery True $ splitKeyValue <$> fields
|
|
||||||
in
|
|
||||||
(hLocation, "/" <> toS tName <> locationFields)
|
|
||||||
where
|
|
||||||
splitKeyValue :: BS.ByteString -> (BS.ByteString, BS.ByteString)
|
|
||||||
splitKeyValue kv =
|
|
||||||
let (k, v) = BS.break (== '=') kv
|
|
||||||
in (k, BS.tail v)
|
|
||||||
|
|
||||||
contentLocationH :: TableName -> ByteString -> Header
|
|
||||||
contentLocationH tName qString =
|
|
||||||
("Content-Location", "/" <> toS tName <> if BS.null qString then mempty else "?" <> toS qString)
|
|
||||||
|
|
||||||
contentProfileH :: Schema -> Header
|
|
||||||
contentProfileH schema =
|
|
||||||
("Content-Profile", toS schema)
|
|
||||||
|
|||||||
@@ -0,0 +1,478 @@
|
|||||||
|
{-# LANGUAGE LambdaCase #-}
|
||||||
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
|
|
||||||
|
module PostgREST.AppState
|
||||||
|
( AppState
|
||||||
|
, destroy
|
||||||
|
, getConfig
|
||||||
|
, getSchemaCache
|
||||||
|
, getIsListenerOn
|
||||||
|
, getMainThreadId
|
||||||
|
, getPgVersion
|
||||||
|
, getRetryNextIn
|
||||||
|
, getTime
|
||||||
|
, init
|
||||||
|
, initWithPool
|
||||||
|
, logWithZTime
|
||||||
|
, putSchemaCache
|
||||||
|
, putPgVersion
|
||||||
|
, usePool
|
||||||
|
, loadSchemaCache
|
||||||
|
, reReadConfig
|
||||||
|
, connectionWorker
|
||||||
|
, runListener
|
||||||
|
) where
|
||||||
|
|
||||||
|
import qualified Data.ByteString.Char8 as BS
|
||||||
|
import qualified Data.ByteString.Lazy as LBS
|
||||||
|
import Data.Either.Combinators (whenLeft)
|
||||||
|
import qualified Data.Text.Encoding as T
|
||||||
|
import Hasql.Connection (acquire)
|
||||||
|
import qualified Hasql.Notifications as SQL
|
||||||
|
import qualified Hasql.Pool as SQL
|
||||||
|
import qualified Hasql.Session as SQL
|
||||||
|
import qualified Hasql.Transaction.Sessions as SQL
|
||||||
|
import qualified PostgREST.Error as Error
|
||||||
|
import PostgREST.Version (prettyVersion)
|
||||||
|
|
||||||
|
import Control.AutoUpdate (defaultUpdateSettings, mkAutoUpdate,
|
||||||
|
updateAction)
|
||||||
|
import Control.Debounce
|
||||||
|
import Control.Retry (RetryStatus, capDelay, exponentialBackoff,
|
||||||
|
retrying, rsPreviousDelay)
|
||||||
|
import Data.IORef (IORef, atomicWriteIORef, newIORef,
|
||||||
|
readIORef)
|
||||||
|
import Data.Time (ZonedTime, defaultTimeLocale, formatTime,
|
||||||
|
getZonedTime)
|
||||||
|
import Data.Time.Clock (UTCTime, getCurrentTime)
|
||||||
|
|
||||||
|
import PostgREST.Config (AppConfig (..),
|
||||||
|
addFallbackAppName,
|
||||||
|
readAppConfig)
|
||||||
|
import PostgREST.Config.Database (queryDbSettings,
|
||||||
|
queryPgVersion,
|
||||||
|
queryRoleSettings)
|
||||||
|
import PostgREST.Config.PgVersion (PgVersion (..),
|
||||||
|
minimumPgVersion)
|
||||||
|
import PostgREST.SchemaCache (SchemaCache,
|
||||||
|
querySchemaCache)
|
||||||
|
import PostgREST.SchemaCache.Identifiers (dumpQi)
|
||||||
|
|
||||||
|
import Protolude
|
||||||
|
|
||||||
|
|
||||||
|
data AppState = AppState
|
||||||
|
-- | Database connection pool
|
||||||
|
{ statePool :: SQL.Pool
|
||||||
|
-- | Database server version, will be updated by the connectionWorker
|
||||||
|
, statePgVersion :: IORef PgVersion
|
||||||
|
-- | No schema cache at the start. Will be filled in by the connectionWorker
|
||||||
|
, stateSchemaCache :: IORef (Maybe SchemaCache)
|
||||||
|
-- | starts the connection worker with a debounce
|
||||||
|
, debouncedConnectionWorker :: IO ()
|
||||||
|
-- | Binary semaphore used to sync the listener(NOTIFY reload) with the connectionWorker.
|
||||||
|
, stateListener :: MVar ()
|
||||||
|
-- | State of the LISTEN channel, used for the admin server checks
|
||||||
|
, stateIsListenerOn :: IORef Bool
|
||||||
|
-- | Config that can change at runtime
|
||||||
|
, stateConf :: IORef AppConfig
|
||||||
|
-- | Time used for verifying JWT expiration
|
||||||
|
, stateGetTime :: IO UTCTime
|
||||||
|
-- | Time with time zone used for worker logs
|
||||||
|
, stateGetZTime :: IO ZonedTime
|
||||||
|
-- | Used for killing the main thread in case a subthread fails
|
||||||
|
, stateMainThreadId :: ThreadId
|
||||||
|
-- | Keeps track of when the next retry for connecting to database is scheduled
|
||||||
|
, stateRetryNextIn :: IORef Int
|
||||||
|
-- | Logs a pool error with a debounce
|
||||||
|
, debounceLogAcquisitionTimeout :: IO ()
|
||||||
|
}
|
||||||
|
|
||||||
|
init :: AppConfig -> IO AppState
|
||||||
|
init conf = do
|
||||||
|
pool <- initPool conf
|
||||||
|
initWithPool pool conf
|
||||||
|
|
||||||
|
initWithPool :: SQL.Pool -> AppConfig -> IO AppState
|
||||||
|
initWithPool pool conf = do
|
||||||
|
appState <- AppState pool
|
||||||
|
<$> newIORef minimumPgVersion -- assume we're in a supported version when starting, this will be corrected on a later step
|
||||||
|
<*> newIORef Nothing
|
||||||
|
<*> pure (pure ())
|
||||||
|
<*> newEmptyMVar
|
||||||
|
<*> newIORef False
|
||||||
|
<*> newIORef conf
|
||||||
|
<*> mkAutoUpdate defaultUpdateSettings { updateAction = getCurrentTime }
|
||||||
|
<*> mkAutoUpdate defaultUpdateSettings { updateAction = getZonedTime }
|
||||||
|
<*> myThreadId
|
||||||
|
<*> newIORef 0
|
||||||
|
<*> pure (pure ())
|
||||||
|
|
||||||
|
|
||||||
|
debLogTimeout <-
|
||||||
|
let oneSecond = 1000000 in
|
||||||
|
mkDebounce defaultDebounceSettings
|
||||||
|
{ debounceAction = logPgrstError appState SQL.AcquisitionTimeoutUsageError
|
||||||
|
, debounceFreq = 5*oneSecond
|
||||||
|
, debounceEdge = leadingEdge -- logs at the start and the end
|
||||||
|
}
|
||||||
|
|
||||||
|
debWorker <-
|
||||||
|
let decisecond = 100000 in
|
||||||
|
mkDebounce defaultDebounceSettings
|
||||||
|
{ debounceAction = internalConnectionWorker appState
|
||||||
|
, debounceFreq = decisecond
|
||||||
|
, debounceEdge = leadingEdge -- runs the worker at the start and the end
|
||||||
|
}
|
||||||
|
|
||||||
|
return appState { debounceLogAcquisitionTimeout = debLogTimeout, debouncedConnectionWorker = debWorker }
|
||||||
|
|
||||||
|
destroy :: AppState -> IO ()
|
||||||
|
destroy = destroyPool
|
||||||
|
|
||||||
|
initPool :: AppConfig -> IO SQL.Pool
|
||||||
|
initPool AppConfig{..} =
|
||||||
|
SQL.acquire
|
||||||
|
configDbPoolSize
|
||||||
|
(fromIntegral configDbPoolAcquisitionTimeout)
|
||||||
|
(fromIntegral configDbPoolMaxLifetime)
|
||||||
|
(fromIntegral configDbPoolMaxIdletime)
|
||||||
|
(toUtf8 $ addFallbackAppName prettyVersion configDbUri)
|
||||||
|
|
||||||
|
-- | Run an action with a database connection.
|
||||||
|
usePool :: AppState -> SQL.Session a -> IO (Either SQL.UsageError a)
|
||||||
|
usePool AppState{..} x = do
|
||||||
|
res <- SQL.use statePool x
|
||||||
|
whenLeft res (\case
|
||||||
|
SQL.AcquisitionTimeoutUsageError -> debounceLogAcquisitionTimeout -- this can happen rapidly for many requests, so we debounce
|
||||||
|
_ -> pure ())
|
||||||
|
return res
|
||||||
|
|
||||||
|
-- | Flush the connection pool so that any future use of the pool will
|
||||||
|
-- use connections freshly established after this call.
|
||||||
|
flushPool :: AppState -> IO ()
|
||||||
|
flushPool AppState{..} = SQL.release statePool
|
||||||
|
|
||||||
|
-- | Destroy the pool on shutdown.
|
||||||
|
destroyPool :: AppState -> IO ()
|
||||||
|
destroyPool AppState{..} = SQL.release statePool
|
||||||
|
|
||||||
|
getPgVersion :: AppState -> IO PgVersion
|
||||||
|
getPgVersion = readIORef . statePgVersion
|
||||||
|
|
||||||
|
putPgVersion :: AppState -> PgVersion -> IO ()
|
||||||
|
putPgVersion = atomicWriteIORef . statePgVersion
|
||||||
|
|
||||||
|
getSchemaCache :: AppState -> IO (Maybe SchemaCache)
|
||||||
|
getSchemaCache = readIORef . stateSchemaCache
|
||||||
|
|
||||||
|
putSchemaCache :: AppState -> Maybe SchemaCache -> IO ()
|
||||||
|
putSchemaCache appState = atomicWriteIORef (stateSchemaCache appState)
|
||||||
|
|
||||||
|
connectionWorker :: AppState -> IO ()
|
||||||
|
connectionWorker = debouncedConnectionWorker
|
||||||
|
|
||||||
|
getRetryNextIn :: AppState -> IO Int
|
||||||
|
getRetryNextIn = readIORef . stateRetryNextIn
|
||||||
|
|
||||||
|
putRetryNextIn :: AppState -> Int -> IO ()
|
||||||
|
putRetryNextIn = atomicWriteIORef . stateRetryNextIn
|
||||||
|
|
||||||
|
getConfig :: AppState -> IO AppConfig
|
||||||
|
getConfig = readIORef . stateConf
|
||||||
|
|
||||||
|
putConfig :: AppState -> AppConfig -> IO ()
|
||||||
|
putConfig = atomicWriteIORef . stateConf
|
||||||
|
|
||||||
|
getTime :: AppState -> IO UTCTime
|
||||||
|
getTime = stateGetTime
|
||||||
|
|
||||||
|
-- | Log to stderr with local time
|
||||||
|
logWithZTime :: AppState -> Text -> IO ()
|
||||||
|
logWithZTime appState txt = do
|
||||||
|
zTime <- stateGetZTime appState
|
||||||
|
hPutStrLn stderr $ toS (formatTime defaultTimeLocale "%d/%b/%Y:%T %z: " zTime) <> txt
|
||||||
|
|
||||||
|
logPgrstError :: AppState -> SQL.UsageError -> IO ()
|
||||||
|
logPgrstError appState e = logWithZTime appState . T.decodeUtf8 . LBS.toStrict $ Error.errorPayload $ Error.PgError False e
|
||||||
|
|
||||||
|
getMainThreadId :: AppState -> ThreadId
|
||||||
|
getMainThreadId = stateMainThreadId
|
||||||
|
|
||||||
|
-- | As this IO action uses `takeMVar` internally, it will only return once
|
||||||
|
-- `stateListener` has been set using `signalListener`. This is currently used
|
||||||
|
-- to syncronize workers.
|
||||||
|
waitListener :: AppState -> IO ()
|
||||||
|
waitListener = takeMVar . stateListener
|
||||||
|
|
||||||
|
-- tryPutMVar doesn't lock the thread. It should always succeed since
|
||||||
|
-- the connectionWorker is the only mvar producer.
|
||||||
|
signalListener :: AppState -> IO ()
|
||||||
|
signalListener appState = void $ tryPutMVar (stateListener appState) ()
|
||||||
|
|
||||||
|
getIsListenerOn :: AppState -> IO Bool
|
||||||
|
getIsListenerOn = readIORef . stateIsListenerOn
|
||||||
|
|
||||||
|
putIsListenerOn :: AppState -> Bool -> IO ()
|
||||||
|
putIsListenerOn = atomicWriteIORef . stateIsListenerOn
|
||||||
|
|
||||||
|
-- | Schema cache status
|
||||||
|
data SCacheStatus
|
||||||
|
= SCLoaded
|
||||||
|
| SCOnRetry
|
||||||
|
| SCFatalFail
|
||||||
|
|
||||||
|
-- | Load the SchemaCache by using a connection from the pool.
|
||||||
|
loadSchemaCache :: AppState -> IO SCacheStatus
|
||||||
|
loadSchemaCache appState = do
|
||||||
|
conf@AppConfig{..} <- getConfig appState
|
||||||
|
result <-
|
||||||
|
let transaction = if configDbPreparedStatements then SQL.transaction else SQL.unpreparedTransaction in
|
||||||
|
usePool appState . transaction SQL.ReadCommitted SQL.Read $
|
||||||
|
querySchemaCache conf
|
||||||
|
case result of
|
||||||
|
Left e -> do
|
||||||
|
case checkIsFatal e of
|
||||||
|
Just hint -> do
|
||||||
|
logWithZTime appState "A fatal error ocurred when loading the schema cache"
|
||||||
|
logPgrstError appState e
|
||||||
|
logWithZTime appState hint
|
||||||
|
return SCFatalFail
|
||||||
|
Nothing -> do
|
||||||
|
putSchemaCache appState Nothing
|
||||||
|
logWithZTime appState "An error ocurred when loading the schema cache"
|
||||||
|
logPgrstError appState e
|
||||||
|
return SCOnRetry
|
||||||
|
|
||||||
|
Right sCache -> do
|
||||||
|
putSchemaCache appState (Just sCache)
|
||||||
|
logWithZTime appState "Schema cache loaded"
|
||||||
|
return SCLoaded
|
||||||
|
|
||||||
|
-- | Current database connection status data ConnectionStatus
|
||||||
|
data ConnectionStatus
|
||||||
|
= NotConnected
|
||||||
|
| Connected PgVersion
|
||||||
|
| FatalConnectionError Text
|
||||||
|
deriving (Eq)
|
||||||
|
|
||||||
|
-- | The purpose of this worker is to obtain a healthy connection to pg and an
|
||||||
|
-- up-to-date schema cache(SchemaCache). This method is meant to be called
|
||||||
|
-- multiple times by the same thread, but does nothing if the previous
|
||||||
|
-- invocation has not terminated. In all cases this method does not halt the
|
||||||
|
-- calling thread, the work is performed in a separate thread.
|
||||||
|
--
|
||||||
|
-- Background thread that does the following :
|
||||||
|
-- 1. Tries to connect to pg server and will keep trying until success.
|
||||||
|
-- 2. Checks if the pg version is supported and if it's not it kills the main
|
||||||
|
-- program.
|
||||||
|
-- 3. Obtains the sCache. If this fails, it goes back to 1.
|
||||||
|
internalConnectionWorker :: AppState -> IO ()
|
||||||
|
internalConnectionWorker appState = work
|
||||||
|
where
|
||||||
|
work = do
|
||||||
|
AppConfig{..} <- getConfig appState
|
||||||
|
logWithZTime appState $ "Starting PostgREST " <> T.decodeUtf8 prettyVersion <> "..."
|
||||||
|
logWithZTime appState "Attempting to connect to the database..."
|
||||||
|
connected <- establishConnection appState
|
||||||
|
case connected of
|
||||||
|
FatalConnectionError reason ->
|
||||||
|
-- Fatal error when connecting
|
||||||
|
logWithZTime appState reason >> killThread (getMainThreadId appState)
|
||||||
|
NotConnected ->
|
||||||
|
-- Unreachable because establishConnection will keep trying to connect
|
||||||
|
return ()
|
||||||
|
Connected actualPgVersion -> do
|
||||||
|
-- Procede with initialization
|
||||||
|
putPgVersion appState actualPgVersion
|
||||||
|
when configDbChannelEnabled $
|
||||||
|
signalListener appState
|
||||||
|
logWithZTime appState "Connection successful"
|
||||||
|
-- this could be fail because the connection drops, but the loadSchemaCache will pick the error and retry again
|
||||||
|
-- We cannot retry after it fails immediately, because db-pre-config could have user errors. We just log the error and continue.
|
||||||
|
when configDbConfig $ reReadConfig False appState
|
||||||
|
scStatus <- loadSchemaCache appState
|
||||||
|
case scStatus of
|
||||||
|
SCLoaded ->
|
||||||
|
-- do nothing and proceed if the load was successful
|
||||||
|
return ()
|
||||||
|
SCOnRetry ->
|
||||||
|
-- retry reloading the schema cache
|
||||||
|
work
|
||||||
|
SCFatalFail ->
|
||||||
|
-- die if our schema cache query has an error
|
||||||
|
killThread $ getMainThreadId appState
|
||||||
|
|
||||||
|
-- | Repeatedly flush the pool, and check if a connection from the
|
||||||
|
-- pool allows access to the PostgreSQL database.
|
||||||
|
--
|
||||||
|
-- Releasing the pool is key for rapid recovery. Otherwise, the pool
|
||||||
|
-- timeout would have to be reached for new healthy connections to be acquired.
|
||||||
|
-- Which might not happen if the server is busy with requests. No idle
|
||||||
|
-- connection, no pool timeout.
|
||||||
|
--
|
||||||
|
-- The connection tries are capped, but if the connection times out no error is
|
||||||
|
-- thrown, just 'False' is returned.
|
||||||
|
establishConnection :: AppState -> IO ConnectionStatus
|
||||||
|
establishConnection appState =
|
||||||
|
retrying retrySettings shouldRetry $
|
||||||
|
const $ flushPool appState >> getConnectionStatus
|
||||||
|
where
|
||||||
|
retrySettings = capDelay delayMicroseconds $ exponentialBackoff backoffMicroseconds
|
||||||
|
delayMicroseconds = 32000000 -- 32 seconds
|
||||||
|
backoffMicroseconds = 1000000 -- 1 second
|
||||||
|
|
||||||
|
getConnectionStatus :: IO ConnectionStatus
|
||||||
|
getConnectionStatus = do
|
||||||
|
pgVersion <- usePool appState $ queryPgVersion False -- No need to prepare the query here, as the connection might not be established
|
||||||
|
case pgVersion of
|
||||||
|
Left e -> do
|
||||||
|
logPgrstError appState e
|
||||||
|
case checkIsFatal e of
|
||||||
|
Just reason ->
|
||||||
|
return $ FatalConnectionError reason
|
||||||
|
Nothing ->
|
||||||
|
return NotConnected
|
||||||
|
Right version ->
|
||||||
|
if version < minimumPgVersion then
|
||||||
|
return . FatalConnectionError $
|
||||||
|
"Cannot run in this PostgreSQL version, PostgREST needs at least "
|
||||||
|
<> pgvName minimumPgVersion
|
||||||
|
else
|
||||||
|
return . Connected $ version
|
||||||
|
|
||||||
|
shouldRetry :: RetryStatus -> ConnectionStatus -> IO Bool
|
||||||
|
shouldRetry rs isConnSucc = do
|
||||||
|
let
|
||||||
|
delay = fromMaybe 0 (rsPreviousDelay rs) `div` backoffMicroseconds
|
||||||
|
itShould = NotConnected == isConnSucc
|
||||||
|
when itShould . logWithZTime appState $
|
||||||
|
"Attempting to reconnect to the database in "
|
||||||
|
<> (show delay::Text)
|
||||||
|
<> " seconds..."
|
||||||
|
when itShould $ putRetryNextIn appState delay
|
||||||
|
return itShould
|
||||||
|
|
||||||
|
-- | Re-reads the config plus config options from the db
|
||||||
|
reReadConfig :: Bool -> AppState -> IO ()
|
||||||
|
reReadConfig startingUp appState = do
|
||||||
|
AppConfig{..} <- getConfig appState
|
||||||
|
dbSettings <-
|
||||||
|
if configDbConfig then do
|
||||||
|
qDbSettings <- usePool appState $ queryDbSettings (dumpQi <$> configDbPreConfig) configDbPreparedStatements
|
||||||
|
case qDbSettings of
|
||||||
|
Left e -> do
|
||||||
|
logWithZTime appState
|
||||||
|
"An error ocurred when trying to query database settings for the config parameters"
|
||||||
|
case checkIsFatal e of
|
||||||
|
Just hint -> do
|
||||||
|
logPgrstError appState e
|
||||||
|
logWithZTime appState hint
|
||||||
|
killThread (getMainThreadId appState)
|
||||||
|
Nothing -> do
|
||||||
|
logPgrstError appState e
|
||||||
|
pure mempty
|
||||||
|
Right x -> pure x
|
||||||
|
else
|
||||||
|
pure mempty
|
||||||
|
(roleSettings, roleIsolationLvl) <-
|
||||||
|
if configDbConfig then do
|
||||||
|
rSettings <- usePool appState $ queryRoleSettings configDbPreparedStatements
|
||||||
|
case rSettings of
|
||||||
|
Left e -> do
|
||||||
|
logWithZTime appState "An error ocurred when trying to query the role settings"
|
||||||
|
logPgrstError appState e
|
||||||
|
pure (mempty, mempty)
|
||||||
|
Right x -> pure x
|
||||||
|
else
|
||||||
|
pure mempty
|
||||||
|
readAppConfig dbSettings configFilePath (Just configDbUri) roleSettings roleIsolationLvl >>= \case
|
||||||
|
Left err ->
|
||||||
|
if startingUp then
|
||||||
|
panic err -- die on invalid config if the program is starting up
|
||||||
|
else
|
||||||
|
logWithZTime appState $ "Failed reloading config: " <> err
|
||||||
|
Right newConf -> do
|
||||||
|
putConfig appState newConf
|
||||||
|
if startingUp then
|
||||||
|
pass
|
||||||
|
else
|
||||||
|
logWithZTime appState "Config reloaded"
|
||||||
|
|
||||||
|
|
||||||
|
runListener :: AppConfig -> AppState -> IO ()
|
||||||
|
runListener AppConfig{configDbChannelEnabled} appState =
|
||||||
|
when configDbChannelEnabled $ listener appState
|
||||||
|
|
||||||
|
-- | Starts a dedicated pg connection to LISTEN for notifications. When a
|
||||||
|
-- NOTIFY <db-channel> - with an empty payload - is done, it refills the schema
|
||||||
|
-- cache. It uses the connectionWorker in case the LISTEN connection dies.
|
||||||
|
listener :: AppState -> IO ()
|
||||||
|
listener appState = do
|
||||||
|
AppConfig{..} <- getConfig appState
|
||||||
|
let dbChannel = toS configDbChannel
|
||||||
|
|
||||||
|
-- The listener has to wait for a signal from the connectionWorker.
|
||||||
|
-- This is because when the connection to the db is lost, the listener also
|
||||||
|
-- tries to recover the connection, but not with the same pace as the connectionWorker.
|
||||||
|
-- Not waiting makes stderr quickly fill with connection retries messages from the listener.
|
||||||
|
waitListener appState
|
||||||
|
|
||||||
|
-- forkFinally allows to detect if the thread dies
|
||||||
|
void . flip forkFinally (handleFinally dbChannel) $ do
|
||||||
|
dbOrError <- acquire $ toUtf8 (addFallbackAppName prettyVersion configDbUri)
|
||||||
|
case dbOrError of
|
||||||
|
Right db -> do
|
||||||
|
logWithZTime appState $ "Listening for notifications on the " <> dbChannel <> " channel"
|
||||||
|
putIsListenerOn appState True
|
||||||
|
SQL.listen db $ SQL.toPgIdentifier dbChannel
|
||||||
|
SQL.waitForNotifications handleNotification db
|
||||||
|
_ ->
|
||||||
|
die $ "Could not listen for notifications on the " <> dbChannel <> " channel"
|
||||||
|
where
|
||||||
|
handleFinally dbChannel _ = do
|
||||||
|
-- if the thread dies, we try to recover
|
||||||
|
logWithZTime appState $ "Retrying listening for notifications on the " <> dbChannel <> " channel.."
|
||||||
|
putIsListenerOn appState False
|
||||||
|
-- assume the pool connection was also lost, call the connection worker
|
||||||
|
connectionWorker appState
|
||||||
|
-- retry the listener
|
||||||
|
listener appState
|
||||||
|
|
||||||
|
handleNotification _ msg
|
||||||
|
| BS.null msg = cacheReloader
|
||||||
|
| msg == "reload schema" = cacheReloader
|
||||||
|
| msg == "reload config" = reReadConfig False appState
|
||||||
|
| otherwise = pure () -- Do nothing if anything else than an empty message is sent
|
||||||
|
|
||||||
|
cacheReloader =
|
||||||
|
-- reloads the schema cache + restarts pool connections
|
||||||
|
-- it's necessary to restart the pg connections because they cache the pg catalog(see #2620)
|
||||||
|
connectionWorker appState
|
||||||
|
|
||||||
|
checkIsFatal :: SQL.UsageError -> Maybe Text
|
||||||
|
checkIsFatal (SQL.ConnectionUsageError e)
|
||||||
|
| isAuthFailureMessage = Just $ toS failureMessage
|
||||||
|
| otherwise = Nothing
|
||||||
|
where isAuthFailureMessage =
|
||||||
|
("FATAL: password authentication failed" `isInfixOf` failureMessage) ||
|
||||||
|
("no password supplied" `isInfixOf` failureMessage)
|
||||||
|
failureMessage = BS.unpack $ fromMaybe mempty e
|
||||||
|
checkIsFatal(SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ResultError serverError)))
|
||||||
|
= case serverError of
|
||||||
|
-- Check for a syntax error (42601 is the pg code). This would mean the error is on our part somehow, so we treat it as fatal.
|
||||||
|
SQL.ServerError "42601" _ _ _ _
|
||||||
|
-> Just "Hint: This is probably a bug in PostgREST, please report it at https://github.com/PostgREST/postgrest/issues"
|
||||||
|
-- Check for a "prepared statement <name> already exists" error (Code 42P05: duplicate_prepared_statement).
|
||||||
|
-- This would mean that a connection pooler in transaction mode is being used
|
||||||
|
-- while prepared statements are enabled in the PostgREST configuration,
|
||||||
|
-- both of which are incompatible with each other.
|
||||||
|
SQL.ServerError "42P05" _ _ _ _
|
||||||
|
-> Just "Hint: If you are using connection poolers in transaction mode, try setting db-prepared-statements to false."
|
||||||
|
-- Check for a "transaction blocks not allowed in statement pooling mode" error (Code 08P01: protocol_violation).
|
||||||
|
-- This would mean that a connection pooler in statement mode is being used which is not supported in PostgREST.
|
||||||
|
SQL.ServerError "08P01" "transaction blocks not allowed in statement pooling mode" _ _ _
|
||||||
|
-> Just "Hint: Connection poolers in statement mode are not supported."
|
||||||
|
_ -> Nothing
|
||||||
|
checkIsFatal _ = Nothing
|
||||||
+95
-88
@@ -1,5 +1,3 @@
|
|||||||
{-# LANGUAGE FlexibleContexts #-}
|
|
||||||
{-# LANGUAGE LambdaCase #-}
|
|
||||||
{-|
|
{-|
|
||||||
Module : PostgREST.Auth
|
Module : PostgREST.Auth
|
||||||
Description : PostgREST authorization functions.
|
Description : PostgREST authorization functions.
|
||||||
@@ -12,103 +10,112 @@ Authentication should always be implemented in an external service.
|
|||||||
In the test suite there is an example of simple login function that can be used for a
|
In the test suite there is an example of simple login function that can be used for a
|
||||||
very simple authentication system inside the PostgreSQL database.
|
very simple authentication system inside the PostgreSQL database.
|
||||||
-}
|
-}
|
||||||
module PostgREST.Auth (
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
containsRole
|
module PostgREST.Auth
|
||||||
, jwtClaims
|
( AuthResult (..)
|
||||||
, JWTAttempt(..)
|
, getResult
|
||||||
, parseSecret
|
, getRole
|
||||||
|
, middleware
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Crypto.JOSE.Types as JOSE.Types
|
import qualified Crypto.JWT as JWT
|
||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
import qualified Data.HashMap.Strict as M
|
import qualified Data.Aeson.Key as K
|
||||||
import Data.Vector as V
|
import qualified Data.Aeson.KeyMap as KM
|
||||||
|
import qualified Data.Aeson.Types as JSON
|
||||||
|
import qualified Data.ByteString as BS
|
||||||
|
import qualified Data.ByteString.Lazy.Char8 as LBS
|
||||||
|
import qualified Data.Vault.Lazy as Vault
|
||||||
|
import qualified Data.Vector as V
|
||||||
|
import qualified Network.HTTP.Types.Header as HTTP
|
||||||
|
import qualified Network.Wai as Wai
|
||||||
|
import qualified Network.Wai.Middleware.HttpAuth as Wai
|
||||||
|
|
||||||
import Control.Lens (set)
|
import Control.Lens (set)
|
||||||
|
import Control.Monad.Except (liftEither)
|
||||||
|
import Data.Either.Combinators (mapLeft)
|
||||||
|
import Data.List (lookup)
|
||||||
import Data.Time.Clock (UTCTime)
|
import Data.Time.Clock (UTCTime)
|
||||||
|
import System.IO.Unsafe (unsafePerformIO)
|
||||||
|
|
||||||
import Control.Lens.Operators
|
import PostgREST.AppState (AppState, getConfig, getTime)
|
||||||
import Crypto.JWT
|
import PostgREST.Config (AppConfig (..), JSPath, JSPathExp (..))
|
||||||
|
import PostgREST.Error (Error (..))
|
||||||
|
|
||||||
import PostgREST.Types
|
import Protolude
|
||||||
import Protolude hiding (toS)
|
|
||||||
import Protolude.Conv (toS)
|
|
||||||
|
|
||||||
{-|
|
|
||||||
Possible situations encountered with client JWTs
|
|
||||||
-}
|
|
||||||
data JWTAttempt = JWTInvalid JWTError
|
|
||||||
| JWTMissingSecret
|
|
||||||
| JWTClaims (M.HashMap Text JSON.Value)
|
|
||||||
deriving (Eq, Show)
|
|
||||||
|
|
||||||
{-|
|
data AuthResult = AuthResult
|
||||||
Receives the JWT secret and audience (from config) and a JWT and returns a map
|
{ authClaims :: KM.KeyMap JSON.Value
|
||||||
of JWT claims.
|
, authRole :: BS.ByteString
|
||||||
-}
|
}
|
||||||
jwtClaims :: Maybe JWKSet -> Maybe StringOrURI -> LByteString -> UTCTime -> Maybe JSPath -> IO JWTAttempt
|
|
||||||
jwtClaims _ _ "" _ _ = return $ JWTClaims M.empty
|
|
||||||
jwtClaims secret audience payload time jspath =
|
|
||||||
case secret of
|
|
||||||
Nothing -> return JWTMissingSecret
|
|
||||||
Just s -> do
|
|
||||||
let validation = set allowedSkew 1 $ defaultJWTValidationSettings (maybe (const True) (==) audience)
|
|
||||||
eJwt <- runExceptT $ do
|
|
||||||
jwt <- decodeCompact payload
|
|
||||||
verifyClaimsAt validation s time jwt
|
|
||||||
return $ case eJwt of
|
|
||||||
Left e -> JWTInvalid e
|
|
||||||
Right jwt -> JWTClaims $ claims2map jwt jspath
|
|
||||||
|
|
||||||
{-|
|
-- | Receives the JWT secret and audience (from config) and a JWT and returns a
|
||||||
Turn JWT ClaimSet into something easier to work with,
|
-- JSON object of JWT claims.
|
||||||
also here the jspath is applied to put the "role" in the map
|
parseToken :: Monad m =>
|
||||||
-}
|
AppConfig -> LByteString -> UTCTime -> ExceptT Error m JSON.Value
|
||||||
claims2map :: ClaimsSet -> Maybe JSPath -> M.HashMap Text JSON.Value
|
parseToken _ "" _ = return JSON.emptyObject
|
||||||
claims2map claims jspath = (\case
|
parseToken AppConfig{..} token time = do
|
||||||
val@(JSON.Object o) ->
|
secret <- liftEither . maybeToRight JwtTokenMissing $ configJWKS
|
||||||
let role = maybe M.empty (M.singleton "role") $
|
eitherClaims <-
|
||||||
walkJSPath (Just val) =<< jspath in
|
lift . runExceptT $
|
||||||
M.delete "role" o `M.union` role -- mutating the map
|
JWT.verifyClaimsAt validation secret time =<< JWT.decodeCompact token
|
||||||
_ -> M.empty
|
liftEither . mapLeft jwtClaimsError $ JSON.toJSON <$> eitherClaims
|
||||||
) $ JSON.toJSON claims
|
|
||||||
|
|
||||||
walkJSPath :: Maybe JSON.Value -> JSPath -> Maybe JSON.Value
|
|
||||||
walkJSPath x [] = x
|
|
||||||
walkJSPath (Just (JSON.Object o)) (JSPKey key:rest) = walkJSPath (M.lookup key o) rest
|
|
||||||
walkJSPath (Just (JSON.Array ar)) (JSPIdx idx:rest) = walkJSPath (ar V.!? idx) rest
|
|
||||||
walkJSPath _ _ = Nothing
|
|
||||||
|
|
||||||
{-|
|
|
||||||
Whether a response from jwtClaims contains a role claim
|
|
||||||
-}
|
|
||||||
containsRole :: JWTAttempt -> Bool
|
|
||||||
containsRole (JWTClaims claims) = M.member "role" claims
|
|
||||||
containsRole _ = False
|
|
||||||
|
|
||||||
{-|
|
|
||||||
Parse `jwt-secret` configuration option and turn into a JWKSet.
|
|
||||||
|
|
||||||
There are three ways to specify `jwt-secret`: text secret, JSON Web Key
|
|
||||||
(JWK), or JSON Web Key Set (JWKS). The first two are converted into a JWKSet
|
|
||||||
with one key and the last is converted as is.
|
|
||||||
-}
|
|
||||||
parseSecret :: ByteString -> JWKSet
|
|
||||||
parseSecret str =
|
|
||||||
fromMaybe (maybe secret (\jwk' -> JWKSet [jwk']) maybeJWK)
|
|
||||||
maybeJWKSet
|
|
||||||
where
|
where
|
||||||
maybeJWKSet = JSON.decode (toS str) :: Maybe JWKSet
|
validation =
|
||||||
maybeJWK = JSON.decode (toS str) :: Maybe JWK
|
JWT.defaultJWTValidationSettings audienceCheck & set JWT.allowedSkew 30
|
||||||
secret = JWKSet [jwkFromSecret str]
|
|
||||||
|
|
||||||
{-|
|
audienceCheck :: JWT.StringOrURI -> Bool
|
||||||
Internal helper to generate a symmetric HMAC-SHA256 JWK from a text secret.
|
audienceCheck = maybe (const True) (==) configJwtAudience
|
||||||
-}
|
|
||||||
jwkFromSecret :: ByteString -> JWK
|
jwtClaimsError :: JWT.JWTError -> Error
|
||||||
jwkFromSecret key =
|
jwtClaimsError JWT.JWTExpired = JwtTokenInvalid "JWT expired"
|
||||||
fromKeyMaterial km
|
jwtClaimsError e = JwtTokenInvalid $ show e
|
||||||
& jwkUse ?~ Sig
|
|
||||||
& jwkAlg ?~ JWSAlg HS256
|
parseClaims :: Monad m =>
|
||||||
|
AppConfig -> JSON.Value -> ExceptT Error m AuthResult
|
||||||
|
parseClaims AppConfig{..} jclaims@(JSON.Object mclaims) = do
|
||||||
|
-- role defaults to anon if not specified in jwt
|
||||||
|
role <- liftEither . maybeToRight JwtTokenRequired $
|
||||||
|
unquoted <$> walkJSPath (Just jclaims) configJwtRoleClaimKey <|> configDbAnonRole
|
||||||
|
return AuthResult
|
||||||
|
{ authClaims = mclaims & KM.insert "role" (JSON.toJSON $ decodeUtf8 role)
|
||||||
|
, authRole = role
|
||||||
|
}
|
||||||
where
|
where
|
||||||
km = OctKeyMaterial (OctKeyParameters (JOSE.Types.Base64Octets key))
|
walkJSPath :: Maybe JSON.Value -> JSPath -> Maybe JSON.Value
|
||||||
|
walkJSPath x [] = x
|
||||||
|
walkJSPath (Just (JSON.Object o)) (JSPKey key:rest) = walkJSPath (KM.lookup (K.fromText key) o) rest
|
||||||
|
walkJSPath (Just (JSON.Array ar)) (JSPIdx idx:rest) = walkJSPath (ar V.!? idx) rest
|
||||||
|
walkJSPath _ _ = Nothing
|
||||||
|
|
||||||
|
unquoted :: JSON.Value -> BS.ByteString
|
||||||
|
unquoted (JSON.String t) = encodeUtf8 t
|
||||||
|
unquoted v = LBS.toStrict $ JSON.encode v
|
||||||
|
-- impossible case - just added to please -Wincomplete-patterns
|
||||||
|
parseClaims _ _ = return AuthResult { authClaims = KM.empty, authRole = mempty }
|
||||||
|
|
||||||
|
-- | Validate authorization header.
|
||||||
|
-- Parse and store JWT claims for future use in the request.
|
||||||
|
middleware :: AppState -> Wai.Middleware
|
||||||
|
middleware appState app req respond = do
|
||||||
|
conf <- getConfig appState
|
||||||
|
time <- getTime appState
|
||||||
|
|
||||||
|
let token = fromMaybe "" $ Wai.extractBearerAuth =<< lookup HTTP.hAuthorization (Wai.requestHeaders req)
|
||||||
|
authResult <- runExceptT $
|
||||||
|
parseToken conf (LBS.fromStrict token) time >>=
|
||||||
|
parseClaims conf
|
||||||
|
|
||||||
|
let req' = req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult }
|
||||||
|
app req' respond
|
||||||
|
|
||||||
|
authResultKey :: Vault.Key (Either Error AuthResult)
|
||||||
|
authResultKey = unsafePerformIO Vault.newKey
|
||||||
|
{-# NOINLINE authResultKey #-}
|
||||||
|
|
||||||
|
getResult :: Wai.Request -> Maybe (Either Error AuthResult)
|
||||||
|
getResult = Vault.lookup authResultKey . Wai.vault
|
||||||
|
|
||||||
|
getRole :: Wai.Request -> Maybe BS.ByteString
|
||||||
|
getRole req = authRole <$> (rightToMaybe =<< getResult req)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user