Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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:
|
||||
image: freebsd-12-1-release-amd64
|
||||
image_family: freebsd-13-1
|
||||
|
||||
build_task:
|
||||
env:
|
||||
ASSUME_ALWAYS_YES: YES
|
||||
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
|
||||
name: Build FreeBSD (Stack)
|
||||
install_script: pkg install -y postgresql13-client hs-stack git
|
||||
|
||||
#publish_task:
|
||||
#skip: $CIRRUS_TAG == ""
|
||||
#env:
|
||||
#TOKEN: ENCRYPTED[c99babbf04e8a33962ffbbc75bf20d3abe408f9bede5ed5d8956cd24da6fdb34266c920984cc43f8106ef3423fba0e98]
|
||||
#depends_on:
|
||||
#- build
|
||||
#script:
|
||||
## https://cirrus-ci.org/guide/writing-tasks/#artifacts-instruction
|
||||
#- 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}"
|
||||
stack_cache:
|
||||
folders: /.stack
|
||||
fingerprint_script: cat postgrest.cabal stack.yaml.lock
|
||||
reupload_on_changes: false
|
||||
|
||||
build_script: stack build -j 1 --local-bin-path . --copy-bins
|
||||
bin_artifacts:
|
||||
path: postgrest
|
||||
|
||||
+13
-15
@@ -15,14 +15,14 @@ your contributions.
|
||||
|
||||
## Issues
|
||||
|
||||
For questions on how to use PostgREST, please use
|
||||
[GitHub discussions](https://github.com/PostgREST/postgrest/discussions).
|
||||
|
||||
### Reporting an Issue
|
||||
|
||||
* Make sure you test against the latest released version. 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.
|
||||
* Make sure you test against the latest [stable release](https://github.com/PostgREST/postgrest/releases/latest)
|
||||
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.
|
||||
|
||||
* Provide steps to reproduce the issue, including your OS version and
|
||||
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).
|
||||
|
||||
* 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.
|
||||
|
||||
## 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
|
||||
|
||||
* 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)
|
||||
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
|
||||
we recommend adding it to `.git/hooks/pre-commit` 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.
|
||||
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` as `nix-shell --run postgrest-check` to automatically check this before doing a commit.
|
||||
|
||||
### Running Tests
|
||||
|
||||
For instructions on running tests, see the official docs hosted here:
|
||||
|
||||
https://postgrest.com/en/stable/install.html#postgrest-test-suite
|
||||
For instructions on running tests, see the [development docs](https://github.com/PostgREST/postgrest/blob/main/nix/README.md#testing).
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<!--
|
||||
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
|
||||
|
||||
|
||||
@@ -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,21 @@
|
||||
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@v18
|
||||
- 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,6 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
Executable
+56
@@ -0,0 +1,56 @@
|
||||
#!/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_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_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 "$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_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,63 @@
|
||||
#!/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; }
|
||||
|
||||
PGRST_GITHUB_COMMIT="$1"
|
||||
SCRIPT_DIR="$2"
|
||||
|
||||
DOCKER_BUILD_DIR="$SCRIPT_DIR/docker-env"
|
||||
|
||||
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 3.6.0.0
|
||||
ghcup set cabal 3.6.0.0
|
||||
}
|
||||
|
||||
install_ghc() {
|
||||
ghcup install ghc 8.10.7
|
||||
ghcup set ghc 8.10.7
|
||||
}
|
||||
|
||||
install_packages
|
||||
|
||||
# Add ghcup to the PATH for this session
|
||||
[ -f ~/.ghcup/env ] && source ~/.ghcup/env
|
||||
|
||||
ghcup --version || install_ghcup
|
||||
cabal --version || install_cabal
|
||||
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,16 @@
|
||||
# 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
|
||||
|
||||
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,506 @@
|
||||
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@v3
|
||||
- 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@v3
|
||||
- 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.1
|
||||
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]
|
||||
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@v3
|
||||
- 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
|
||||
|
||||
- name: Run query cost tests
|
||||
if: always()
|
||||
run: postgrest-with-postgresql-${{ matrix.pgVersion }} postgrest-test-querycost
|
||||
|
||||
|
||||
Test-Memory-Nix:
|
||||
name: Test memory (Nix)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- 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@v3
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||
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
|
||||
|
||||
- name: Build and push everything to Cachix (main branch only)
|
||||
if: ${{ github.ref == 'refs/heads/main' }}
|
||||
run: |
|
||||
nix-build
|
||||
nix-env -f default.nix -iA devTools
|
||||
postgrest-push-cachix
|
||||
|
||||
|
||||
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@v3
|
||||
- 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@v3
|
||||
- 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}}
|
||||
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: ['8.10.7', '9.2.4']
|
||||
fail-fast: false
|
||||
name: Build Linux (Cabal, GHC ${{ matrix.ghc }})
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- 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:
|
||||
name: Build aarch64 (Cabal)
|
||||
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 }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- id: Remote-Dir
|
||||
name: Unique directory name for the remote build
|
||||
run: echo "::set-output name=remotepath::postgrest-build-$(uuidgen)"
|
||||
- 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
|
||||
script: bash ~/$REMOTE_DIR/build.sh "$GITHUB_COMMIT" "$REMOTE_DIR"
|
||||
- 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@v2.3.1
|
||||
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@v3
|
||||
- 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 "::set-output name=version::$cabal_version"
|
||||
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 "::set-output name=isprerelease::1"
|
||||
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@v3
|
||||
- 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:
|
||||
- Build-Cabal-Arm
|
||||
- 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@v3
|
||||
- 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
|
||||
- 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"
|
||||
# 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
|
||||
|
||||
Clean-Arm-Server:
|
||||
name: Remove copied files from server
|
||||
needs:
|
||||
- Build-Cabal-Arm
|
||||
- Release-Docker
|
||||
if: ${{ always() && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || startsWith(github.ref, 'refs/heads/rel-')) }}
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
REMOTE_DIR: ${{ needs.Build-Cabal-Arm.outputs.remotepath }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2.4.0
|
||||
- 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@v3
|
||||
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.5.0
|
||||
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
|
||||
result*
|
||||
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,70 @@
|
||||
# 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.
|
||||
|
||||
### Workers.hs
|
||||
|
||||
This spawns threads which are used to execute concurrent jobs.
|
||||
|
||||
Jobs include connection recovery, a listener for the PostgreSQL LISTEN command, and an admin server.
|
||||
|
||||
### 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.
|
||||
+28
-6
@@ -8,18 +8,36 @@ PostgREST ongoing development is only possible thanks to our Sponsors and Backer
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://www.cybertec-postgresql.com/en/" target="_blank">
|
||||
<img width="222px" src="static/cybertec.png">
|
||||
<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-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="222px" src="static/2ndquadrant.png">
|
||||
<img width="296px" src="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="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>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -28,9 +46,9 @@ PostgREST ongoing development is only possible thanks to our Sponsors and Backer
|
||||
|
||||
## Lead Backers
|
||||
|
||||
- [Daniel Babiak](https://github.com/d-babiak)
|
||||
- Evans Fernandes
|
||||
- [Jan Sommer](https://github.com/nerfpops)
|
||||
- [Franz Gusenbauer](https://www.igutech.at/)
|
||||
|
||||
## Backers
|
||||
|
||||
@@ -38,13 +56,15 @@ PostgREST ongoing development is only possible thanks to our Sponsors and Backer
|
||||
- Michel Pelletier
|
||||
- Jay Hannah
|
||||
- Robert Stolarz
|
||||
- Kofi Gumbs
|
||||
- Nicholas DiBiase
|
||||
- Christopher Reid
|
||||
- Nathan Bouscal
|
||||
- Daniel Rafaj
|
||||
- David Fenko
|
||||
- Remo Rechkemmer
|
||||
- Severin Ibarluzea
|
||||
- Tom Saleeba
|
||||
- Pawel Tyll
|
||||
|
||||
## Former Backers
|
||||
|
||||
@@ -61,3 +81,5 @@ PostgREST ongoing development is only possible thanks to our Sponsors and Backer
|
||||
</table>
|
||||
|
||||
- [Christiaan Westerbeek](https://devotis.nl)
|
||||
- [Daniel Babiak](https://github.com/dbabiak)
|
||||
- Kofi Gumbs
|
||||
|
||||
+230
@@ -5,10 +5,236 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## Unreleased
|
||||
|
||||
## [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
|
||||
|
||||
### Fixed
|
||||
@@ -19,6 +245,10 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- #1508, Fix `Content-Profile` not working for POST RPC - @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
|
||||
|
||||
### 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.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)
|
||||
[](http://postgrest.org)
|
||||
[](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)
|
||||
|
||||
PostgREST serves a fully RESTful API from any existing PostgreSQL
|
||||
@@ -21,18 +19,36 @@ API than you are likely to write from scratch.
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://www.cybertec-postgresql.com/en/" target="_blank">
|
||||
<img width="222px" src="static/cybertec.png">
|
||||
<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-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="222px" src="static/2ndquadrant.png">
|
||||
<img width="296px" src="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="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>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -57,7 +73,7 @@ Latest documentation is at [postgrest.org](http://postgrest.org). You can contri
|
||||
## Performance
|
||||
|
||||
TLDR; subsecond response times for up to 2000 requests/sec on Heroku
|
||||
free tier. If you're used to servers written in interpreted languages,
|
||||
free tier. If you're used to servers written in interpreted languages,
|
||||
prepare to be pleasantly surprised by PostgREST performance.
|
||||
|
||||
Three factors contribute to the speed. First the server is written
|
||||
@@ -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
|
||||
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
|
||||
|
||||
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 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
|
||||
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).
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
-- This file is required by Hackage.
|
||||
import Distribution.Simple
|
||||
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: 33ff97db570b5b432255f5f24a68db51453f6eb8
|
||||
+99
-47
@@ -1,9 +1,11 @@
|
||||
{ system ? builtins.currentSystem }:
|
||||
|
||||
let
|
||||
name =
|
||||
"postgrest";
|
||||
|
||||
compiler =
|
||||
"ghc883";
|
||||
"ghc924";
|
||||
|
||||
# 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
|
||||
@@ -26,40 +28,62 @@ let
|
||||
sha256 = nixpkgsVersion.tarballHash;
|
||||
};
|
||||
|
||||
allOverlays =
|
||||
import nix/overlays;
|
||||
|
||||
overlays =
|
||||
[
|
||||
(import nix/overlays/postgresql-legacy.nix)
|
||||
(import nix/overlays/gitignore.nix)
|
||||
(import nix/overlays/haskell-packages)
|
||||
allOverlays.build-toolbox
|
||||
allOverlays.checked-shell-script
|
||||
allOverlays.gitignore
|
||||
allOverlays.postgis
|
||||
allOverlays.postgresql-default
|
||||
allOverlays.postgresql-legacy
|
||||
allOverlays.postgresql-future
|
||||
(allOverlays.haskell-packages { inherit compiler; })
|
||||
];
|
||||
|
||||
# Evaluated expression of the Nixpkgs repository.
|
||||
pkgs =
|
||||
import nixpkgs { inherit overlays; };
|
||||
import nixpkgs { inherit overlays system; };
|
||||
|
||||
postgresqlVersions =
|
||||
[
|
||||
pkgs.postgresql_12
|
||||
pkgs.postgresql_11
|
||||
pkgs.postgresql_10
|
||||
pkgs.postgresql_9_6
|
||||
pkgs.postgresql_9_5
|
||||
pkgs.postgresql_9_4
|
||||
{ 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 =
|
||||
pkgs.callPackage nix/patches {};
|
||||
pkgs.callPackage nix/patches { };
|
||||
|
||||
# Base dynamic derivation for the PostgREST package.
|
||||
drv =
|
||||
pkgs.haskellPackages.callCabal2nix name src {};
|
||||
# Dynamic derivation for PostgREST
|
||||
postgrest =
|
||||
pkgs.haskell.packages."${compiler}".callCabal2nix name src { };
|
||||
|
||||
# Static derivation for the PostgREST executable.
|
||||
drvStatic =
|
||||
import nix/static { inherit nixpkgs name src compiler patches; };
|
||||
# Function that derives a fully static Haskell package based on
|
||||
# nh2/static-haskell-nix
|
||||
staticHaskellPackage =
|
||||
import nix/static-haskell-package.nix { inherit nixpkgs system compiler patches allOverlays; };
|
||||
|
||||
lib =
|
||||
pkgs.haskell.lib;
|
||||
# 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
|
||||
rec {
|
||||
inherit nixpkgs pkgs;
|
||||
@@ -67,43 +91,71 @@ rec {
|
||||
# Derivation for the PostgREST Haskell package, including the executable,
|
||||
# libraries and documentation. We disable running the test suite on Nix
|
||||
# builds, as they require a database to be set up.
|
||||
postgrestWithLib =
|
||||
lib.dontCheck drv;
|
||||
|
||||
# Derivation for just the PostgREST binary, where we strip all dynamic
|
||||
# libraries and documentation, leaving only the executable. Note that the
|
||||
# executable is static with regards to Haskell libraries, but not system
|
||||
# libraries like glibc and libpq.
|
||||
postgrest =
|
||||
lib.justStaticExecutables postgrestWithLib;
|
||||
postgrestPackage =
|
||||
lib.dontCheck postgrest;
|
||||
|
||||
# Static executable.
|
||||
postgrestStatic =
|
||||
lib.justStaticExecutables (lib.dontCheck drvStatic);
|
||||
lib.justStaticExecutables (lib.dontCheck (staticHaskellPackage name src));
|
||||
|
||||
# Docker image and loading script.
|
||||
# Profiled dynamic executable.
|
||||
postgrestProfiled =
|
||||
lib.enableExecutableProfiling (
|
||||
lib.dontHaddock (
|
||||
lib.dontCheck (profiledHaskellPackages.callCabal2nix name src { })
|
||||
)
|
||||
);
|
||||
|
||||
inherit (postgrest) env;
|
||||
|
||||
# Tooling for analyzing Haskell imports and exports.
|
||||
hsie =
|
||||
pkgs.callPackage nix/hsie {
|
||||
inherit (pkgs.haskell.packages."${compiler}") ghcWithPackages;
|
||||
};
|
||||
|
||||
### Tools
|
||||
|
||||
cabalTools =
|
||||
pkgs.callPackage nix/tools/cabalTools.nix { inherit devCabalOptions postgrest; };
|
||||
|
||||
# Development tools.
|
||||
devTools =
|
||||
pkgs.callPackage nix/tools/devTools.nix { inherit tests style devCabalOptions hsie withTools; };
|
||||
|
||||
# Docker images and loading script.
|
||||
docker =
|
||||
pkgs.callPackage nix/docker { postgrest = postgrestStatic; };
|
||||
pkgs.callPackage nix/tools/docker { postgrest = postgrestStatic; };
|
||||
|
||||
# Environment in which PostgREST can be built with cabal, useful e.g. for
|
||||
# defining a shell for nix-shell.
|
||||
env =
|
||||
drv.env;
|
||||
# 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.
|
||||
nixpkgsUpgrade =
|
||||
pkgs.callPackage nix/nixpkgs-upgrade.nix {};
|
||||
nixpkgsTools =
|
||||
pkgs.callPackage nix/tools/nixpkgsTools.nix { };
|
||||
|
||||
tests =
|
||||
pkgs.callPackage nix/tests.nix
|
||||
{
|
||||
inherit postgresqlVersions;
|
||||
postgrestBuildEnv = env;
|
||||
};
|
||||
# Scripts for publishing new releases.
|
||||
release =
|
||||
pkgs.callPackage nix/tools/release { };
|
||||
|
||||
# Linting and styling tools.
|
||||
style =
|
||||
pkgs.callPackage nix/style.nix {};
|
||||
pkgs.callPackage nix/tools/style.nix { inherit hsie; };
|
||||
|
||||
lint =
|
||||
pkgs.callPackage nix/lint.nix {};
|
||||
# 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;
|
||||
};
|
||||
|
||||
withTools =
|
||||
pkgs.callPackage nix/tools/withTools.nix { inherit devCabalOptions postgresqlVersions postgrest; };
|
||||
}
|
||||
|
||||
@@ -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)"
|
||||
+27
-315
@@ -1,330 +1,42 @@
|
||||
{-# 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 System.IO (BufferMode (..), hSetBuffering)
|
||||
|
||||
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 PostgREST.App (postgrest)
|
||||
import PostgREST.Config (AppConfig (..), configPoolTimeout',
|
||||
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 qualified PostgREST.App as App
|
||||
import qualified PostgREST.CLI as CLI
|
||||
|
||||
import Protolude
|
||||
|
||||
#ifndef mingw32_HOST_OS
|
||||
import System.Posix.Signals
|
||||
import UnixSocket
|
||||
import qualified PostgREST.Unix as Unix
|
||||
#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 = 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
|
||||
-- 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 stdin LineBuffering
|
||||
hSetBuffering stderr NoBuffering
|
||||
--
|
||||
-- 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
|
||||
hSetBuffering stderr LineBuffering
|
||||
|
||||
@@ -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
|
||||
+173
-28
@@ -12,21 +12,43 @@ their dependencies will be stored. It will also link the Nix executables like
|
||||
other PostgREST dependencies from here on out. To clean up older build
|
||||
artifacts from the `/nix/store`, you can run `nix-collect-garbage`.
|
||||
|
||||
If you are on a system that does not support nix, for example Windows, you can
|
||||
run the nix development environment in a docker container. Inside the `nix/`
|
||||
directory run `docker-compose run --rm nix` to start the docker container. This
|
||||
will set up the binary cache and launch `nix-shell` automatically.
|
||||
|
||||
## Building PostgREST
|
||||
|
||||
To build PostgREST from your local checkout of the repository, run:
|
||||
|
||||
```bash
|
||||
nix-build --attr postgrest
|
||||
nix-build --attr postgrestPackage
|
||||
|
||||
```
|
||||
|
||||
This will create a `result` directory that contains the PostgREST binary at
|
||||
`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
|
||||
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
|
||||
|
||||
A development environment for PostgREST is available with `nix-shell`. The
|
||||
@@ -42,32 +64,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
|
||||
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
|
||||
with `postgrest-`, so you can use tab completion (`postgrest-<tab>`) in
|
||||
`nix-shell` to see all that are available:
|
||||
The PostgREST utilities available in `nix-shell` all have names that begin with
|
||||
`postgrest-`, so you can use tab completion (typing `postgrest-` and pressing
|
||||
`<tab>`) in `nix-shell` to see all that are available:
|
||||
|
||||
```
|
||||
```bash
|
||||
# Note: The utilities listed here might not be up to date.
|
||||
[nix-shell:~/Projects/postgrest]$ postgrest-
|
||||
postgrest-lint postgrest-test-spec-postgresql-10.12
|
||||
postgrest-style postgrest-test-spec-postgresql-11.7
|
||||
postgrest-style-check postgrest-test-spec-postgresql-12.2
|
||||
postgrest-test-all postgrest-test-spec-postgresql-9.4.24
|
||||
postgrest-test-spec postgrest-test-spec-postgresql-9.5.21
|
||||
postgrest-test-spec-all postgrest-test-spec-postgresql-9.6.17
|
||||
[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
|
||||
...
|
||||
|
||||
[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
|
||||
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
|
||||
our Nix expressions to see which utilities are available.
|
||||
|
||||
```
|
||||
```bash
|
||||
$ nix-shell --run postgrest-style
|
||||
|
||||
# Note that you need to quote any arguments that you would like to pass to
|
||||
@@ -78,25 +129,33 @@ $ nix-shell --run "postgrest-foo --bar"
|
||||
|
||||
A third option is to install utilities that you use very often locally:
|
||||
|
||||
```
|
||||
$ nix-env -f default.nix -iA style
|
||||
```bash
|
||||
$ nix-env -f default.nix -iA devTools
|
||||
|
||||
# `postgrest-style` can now be run directly:
|
||||
$ postgrest-style
|
||||
|
||||
```
|
||||
|
||||
Note that this does not yet work for all utilities (e.g. `postgrest-test-spec` currently
|
||||
needs to be run within the Nix shell environment).
|
||||
If you use `nix-shell` very often, you might like to use
|
||||
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,
|
||||
which skips evaluating all our Nix expressions if nothing changed, reducing startup time for the
|
||||
shell considerably.
|
||||
Note: Once inside nix-shell, the utilities work from any directory inside
|
||||
the PostgREST repo. Paths are resolved relative to the repo root:
|
||||
|
||||
```bash
|
||||
$ cd src
|
||||
# Even though the current directory is ./src, the config path must still start
|
||||
# from the repo root:
|
||||
$ postgrest-run test/io/configs/simple.conf
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
In nix-shell, you'll find utility scripts that make it very easy to run the
|
||||
Haskell test suite, including setting up all required dependencies and
|
||||
In nix-shell, you'll find utility scripts that make it very easy to run our
|
||||
test suite, including setting up all required dependencies and
|
||||
temporary test databases:
|
||||
|
||||
```bash
|
||||
@@ -104,14 +163,76 @@ temporary test databases:
|
||||
$ nix-shell --run postgrest-test-spec
|
||||
|
||||
# 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
|
||||
# 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':
|
||||
postgrest-test-io -k config
|
||||
|
||||
# Run tests in parallel using xdist, specifying the number of processes:
|
||||
postgrest-test-io -n auto
|
||||
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
|
||||
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)
|
||||
postgrest-loadtest
|
||||
|
||||
# You can loadtest comparing to a different branch
|
||||
postgrest-loadtest-against master
|
||||
|
||||
# Produce a markdown report to be used on CI
|
||||
postgrest-loadtest-report
|
||||
```
|
||||
|
||||
Our query cost tests ensure that our generated queries don't surpass a threshold EXPLAIN cost.
|
||||
|
||||
```bash
|
||||
postgrest-test-querycost
|
||||
```
|
||||
|
||||
doctests for some of our modules are also available:
|
||||
|
||||
```bash
|
||||
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
|
||||
postgrest-coverage
|
||||
|
||||
# Visualize the output
|
||||
cd coverage
|
||||
python -mSimpleHTTPServer 8080
|
||||
```
|
||||
|
||||
## Linting and styling code
|
||||
|
||||
The nix-shell also contains scripts for linting and styling the PostgREST
|
||||
@@ -126,6 +247,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
|
||||
|
||||
The following is not required for working on PostgREST with Nix, but it will
|
||||
@@ -133,7 +273,7 @@ give you some more background and details on how it works.
|
||||
|
||||
### `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
|
||||
other programming languages), where each attribute is a derivation that Nix
|
||||
knows how to build, like the `postgrest` attribute from earlier.
|
||||
@@ -161,3 +301,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
|
||||
`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";
|
||||
rev = "10100a97c8964e82b30f180fda41ade8e6f69e41";
|
||||
tarballHash = "011f36kr3c1ria7rag7px26bh73d1b0xpqadd149bysf4hg17rln";
|
||||
date = "2022-10-28";
|
||||
rev = "f44ba1be526c8da9e79a5759feca2365204003f6";
|
||||
tarballHash = "0npbwsdjw88py5w2pjflwh94wgi4jmnmls0k1n7q8m6h94w1y1ps";
|
||||
}
|
||||
|
||||
@@ -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,10 @@
|
||||
{
|
||||
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;
|
||||
}
|
||||
@@ -12,8 +12,9 @@ self: super:
|
||||
gitignoreSrc = super.fetchFromGitHub {
|
||||
owner = "hercules-ci";
|
||||
repo = "gitignore";
|
||||
rev = "2ced4519f865341adcb143c5d668f955a2cb997f";
|
||||
sha256 = "sha256:0fc5bgv9syfcblp23y05kkfnpgh3gssz6vn24frs8dzw39algk2z";
|
||||
rev = "a20de23b925fd8264fd7fad6454652e142fd7f73";
|
||||
sha256 = "sha256-8DFJjXG8zqoONA1vXtgeKXy68KdJL5UaXR8NtVMUbx8=";
|
||||
};
|
||||
in (super.callPackage gitignoreSrc {}).gitignoreSource;
|
||||
in
|
||||
(super.callPackage gitignoreSrc { }).gitignoreSource;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
{ 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
|
||||
|
||||
hashtables = lib.dontCheck prev.hashtables_1_3_1;
|
||||
hasql = lib.dontCheck prev.hasql_1_6_1_4;
|
||||
hasql-dynamic-statements = lib.dontCheck prev.hasql-dynamic-statements_0_3_1_2;
|
||||
hasql-pool = lib.dontCheck prev.hasql-pool_0_8_0_4;
|
||||
hasql-transaction = lib.dontCheck prev.hasql-transaction_1_0_1_2;
|
||||
isomorphism-class = lib.unmarkBroken prev.isomorphism-class;
|
||||
lens = lib.dontCheck prev.lens_5_2;
|
||||
postgresql-binary = lib.dontCheck prev.postgresql-binary_0_13_1;
|
||||
text-builder = lib.dontCheck prev.text-builder_0_6_7;
|
||||
text-builder-dev = lib.dontCheck prev.text-builder-dev_0_3_3;
|
||||
|
||||
postgresql-libpq = lib.dontCheck
|
||||
(prev.callCabal2nix "postgresql-libpq"
|
||||
(super.fetchFromGitHub {
|
||||
owner = "PostgREST";
|
||||
repo = "postgresql-libpq";
|
||||
rev = "cef92cb4c07b56568dffdbf4b719258b82183119"; # master
|
||||
sha256 = "0r59klrz47qcnd22s47h612mlz3jbg40wwalfj3f6djwg0cdyr85";
|
||||
})
|
||||
{ });
|
||||
} // 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,5 @@
|
||||
self: super:
|
||||
# Overlay that sets the default version of PostgreSQL.
|
||||
{
|
||||
postgresql = super.postgresql_14;
|
||||
}
|
||||
@@ -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_14 =
|
||||
# let
|
||||
# rev = "76b1e16c6659ccef7187ca69b287525fea133244";
|
||||
# tarballHash = "1vsahpcx80k2bgslspb0sa6j4bmhdx77sw6la455drqcrqhdqj6a";
|
||||
#
|
||||
# pinnedPkgs =
|
||||
# builtins.fetchTarball {
|
||||
# url = "https://github.com/nixos/nixpkgs/archive/${rev}.tar.gz";
|
||||
# sha256 = tarballHash;
|
||||
# };
|
||||
# in
|
||||
# (import pinnedPkgs { }).pkgs.postgresql_14;
|
||||
}
|
||||
@@ -2,19 +2,33 @@ self: super:
|
||||
# Overlay that adds legacy versions of PostgreSQL that are supported by
|
||||
# PostgREST.
|
||||
{
|
||||
# PostgreSQL 9.4 was removed from Nixpkgs with
|
||||
# https://github.com/NixOS/nixpkgs/commit/8e2fc57a80d761c46702c3250e61c1bffe021e25
|
||||
# We pin its parent commit 3b5b9a7 to get the last version that was available.
|
||||
postgresql_9_4 =
|
||||
# PostgreSQL 9.6 was removed from Nixpkgs with
|
||||
# https://github.com/NixOS/nixpkgs/commit/757dd008b2f2926fc0f7688fa8189f930ea47521
|
||||
# We pin its parent commit to get the last version that was available.
|
||||
postgresql_9_6 =
|
||||
let
|
||||
rev = "3b5b9a73f59ff93d50156e250203410bdd07f4e0";
|
||||
tarballHash = "0kr25xqbv1ldp72jbmml21pc5hl7xcfqhclv5qxa5f860jddjznk";
|
||||
|
||||
rev = "571cbf3d1db477058303cef8754fb85a14e90eb7";
|
||||
tarballHash = "0q74wn418i1bn5sssacmw8ykpmqvzr0s93sj6pbs3rf6bf134fkz";
|
||||
pinnedPkgs =
|
||||
builtins.fetchTarball {
|
||||
url = "https://github.com/nixos/nixpkgs/archive/${rev}.tar.gz";
|
||||
sha256 = tarballHash;
|
||||
};
|
||||
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;
|
||||
}
|
||||
|
||||
+19
-17
@@ -1,25 +1,27 @@
|
||||
{ runCommand }:
|
||||
|
||||
{
|
||||
# Apply patches to a directory.
|
||||
applyPatches =
|
||||
name: path: patches:
|
||||
runCommand name { inherit patches; }
|
||||
''
|
||||
set -eou pipefail
|
||||
name: src: patches:
|
||||
runCommand
|
||||
name
|
||||
{ inherit src patches; }
|
||||
''
|
||||
set -eou pipefail
|
||||
|
||||
cp -r ${path} $out
|
||||
chmod -R u+w $out
|
||||
cp -r $src $out
|
||||
chmod -R u+w $out
|
||||
|
||||
for patch in $patches; do
|
||||
echo "Applying patch $patch"
|
||||
patch -d "$out" -p1 < "$patch"
|
||||
done
|
||||
'';
|
||||
for patch in $patches; do
|
||||
echo "Applying patch $patch"
|
||||
patch -d "$out" -p1 < "$patch"
|
||||
done
|
||||
'';
|
||||
|
||||
static-haskell-nix-postgrest-libpq =
|
||||
./static-haskell-nix-postgrest-libpq.patch;
|
||||
|
||||
nixpkgs-revert-ghc-bootstrap =
|
||||
./nixpkgs-revert-ghc-bootstrap.patch;
|
||||
static-haskell-nix-ncurses =
|
||||
./static-haskell-nix-ncurses.patch;
|
||||
static-haskell-nix-ghc-bignum =
|
||||
./static-haskell-nix-ghc-bignum.patch;
|
||||
static-haskell-nix-openssl =
|
||||
./static-haskell-nix-openssl.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,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,62 @@
|
||||
# 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
|
||||
(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
|
||||
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,59 @@
|
||||
{ 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 buidling it interactively with cabal-install";
|
||||
args = [ "ARG_LEFTOVERS([PostgREST arguments])" ];
|
||||
inRootDir = true;
|
||||
withEnv = postgrest.env;
|
||||
}
|
||||
''
|
||||
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,307 @@
|
||||
{ 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-querycost
|
||||
${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_@}"
|
||||
|
||||
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
|
||||
];
|
||||
}
|
||||
@@ -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,178 @@
|
||||
{ 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")"
|
||||
|
||||
# shellcheck disable=SC2145
|
||||
${withTools.withPg} --fixtures "$_arg_testdir"/fixtures.sql \
|
||||
${withTools.withPgrst} \
|
||||
sh -c "cd \"$_arg_testdir\" && ${runner} -targets targets.http -output \"$_arg_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,136 @@
|
||||
{ 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_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_patch" "$bump_minor" "$bump_major"; do
|
||||
case "$REPLY" in
|
||||
1|2|3|4)
|
||||
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" ]]; 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 "Couldn't find remote. 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,246 @@
|
||||
{ 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[@]}"
|
||||
'';
|
||||
|
||||
testQuerycost =
|
||||
checkedShellScript
|
||||
{
|
||||
name = "postgrest-test-querycost";
|
||||
docs = "Run the Haskell test suite for query costs";
|
||||
inRootDir = true;
|
||||
withEnv = postgrest.env;
|
||||
}
|
||||
''
|
||||
${withTools.withPg} ${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:querycost
|
||||
'';
|
||||
|
||||
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 test:querycost
|
||||
|
||||
(
|
||||
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
|
||||
|
||||
HPCTIXFILE="$tmpdir"/querycost.tix \
|
||||
${withTools.withPg} ${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:querycost
|
||||
|
||||
# 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 "$tmpdir"/querycost.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
|
||||
testQuerycost
|
||||
testDoctests
|
||||
testSpecIdempotence
|
||||
testIO
|
||||
dumpSchema
|
||||
coverage
|
||||
coverageDraftOverlay
|
||||
checkStatic
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
{ bash-completion
|
||||
, buildToolbox
|
||||
, cabal-install
|
||||
, checkedShellScript
|
||||
, curl
|
||||
, devCabalOptions
|
||||
, git
|
||||
, lib
|
||||
, postgresqlVersions
|
||||
, postgrest
|
||||
, writeText
|
||||
}:
|
||||
let
|
||||
withTmpDb =
|
||||
{ name, postgresql }:
|
||||
checkedShellScript
|
||||
{
|
||||
name = "postgrest-with-${name}";
|
||||
docs = "Run the given command in a temporary database with ${name}";
|
||||
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])"
|
||||
];
|
||||
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
|
||||
|
||||
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.
|
||||
PGTZ=UTC initdb --no-locale --encoding=UTF8 --nosync -U "$PGUSER" --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=\"\" -k $PGHOST -c log_statement=\"all\"" \
|
||||
>> "$setuplog"
|
||||
|
||||
stop () {
|
||||
log "Stopping the database cluster..."
|
||||
pg_ctl stop -m i >> "$setuplog"
|
||||
rm -rf "$tmpdir/db"
|
||||
}
|
||||
trap stop EXIT
|
||||
|
||||
log "Loading fixtures..."
|
||||
psql -v ON_ERROR_STOP=1 -f "$_arg_fixtures" >> "$setuplog"
|
||||
|
||||
log "Done. Running command..."
|
||||
("$_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;
|
||||
|
||||
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
|
||||
echo -n "Building postgrest... "
|
||||
nix-build -A postgrestPackage > "$tmpdir"/build.log 2>&1 || {
|
||||
echo "failed, output:"
|
||||
cat "$tmpdir"/build.log
|
||||
exit 1
|
||||
}
|
||||
echo "done."
|
||||
|
||||
echo -n "Starting postgrest... "
|
||||
./result/bin/postgrest ${legacyConfig} > "$tmpdir"/run.log 2>&1 &
|
||||
pid=$!
|
||||
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 ] ++ withPgVersions;
|
||||
# make withTools available for other nix files
|
||||
extra = { inherit withGit withPg withPgAll withPgrst; };
|
||||
}
|
||||
+221
-155
@@ -1,8 +1,8 @@
|
||||
name: postgrest
|
||||
version: 7.0.1
|
||||
version: 10.1.0
|
||||
synopsis: REST API for any Postgres database
|
||||
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.
|
||||
license: MIT
|
||||
license-file: LICENSE
|
||||
@@ -19,222 +19,288 @@ source-repository head
|
||||
type: git
|
||||
location: git://github.com/PostgREST/postgrest.git
|
||||
|
||||
flag ci
|
||||
flag dev
|
||||
default: False
|
||||
manual: True
|
||||
description: No warnings allowed in continuous integration
|
||||
description: Development flags
|
||||
|
||||
flag hpc
|
||||
default: True
|
||||
manual: True
|
||||
description: Enable HPC (dev only)
|
||||
|
||||
library
|
||||
exposed-modules: PostgREST.ApiRequest
|
||||
PostgREST.App
|
||||
PostgREST.Auth
|
||||
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
|
||||
default-language: Haskell2010
|
||||
default-extensions: OverloadedStrings
|
||||
NoImplicitPrelude
|
||||
hs-source-dirs: src
|
||||
build-depends: base >= 4.9 && < 4.15
|
||||
exposed-modules: 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.Proc
|
||||
PostgREST.SchemaCache.Relationship
|
||||
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.RangeQuery
|
||||
PostgREST.ApiRequest
|
||||
PostgREST.ApiRequest.Preferences
|
||||
PostgREST.ApiRequest.QueryParams
|
||||
PostgREST.ApiRequest.Types
|
||||
PostgREST.Response
|
||||
PostgREST.Response.OpenAPI
|
||||
PostgREST.Response.GucHeader
|
||||
PostgREST.Version
|
||||
PostgREST.Workers
|
||||
other-modules: Paths_postgrest
|
||||
build-depends: base >= 4.9 && < 4.17
|
||||
, HTTP >= 4000.3.7 && < 4000.4
|
||||
, Ranged-sets >= 0.3 && < 0.5
|
||||
, aeson >= 1.4.7 && < 1.5
|
||||
, ansi-wl-pprint >= 0.6.7 && < 0.7
|
||||
, base64-bytestring >= 1 && < 1.2
|
||||
, bytestring >= 0.10.8 && < 0.11
|
||||
, aeson >= 2.0.3 && < 2.2
|
||||
, auto-update >= 0.1.4 && < 0.2
|
||||
, base64-bytestring >= 1 && < 1.3
|
||||
, bytestring >= 0.10.8 && < 0.12
|
||||
, case-insensitive >= 1.2 && < 1.3
|
||||
, cassava >= 0.4.5 && < 0.6
|
||||
, configurator-pg >= 0.2 && < 0.3
|
||||
, containers >= 0.5.7 && < 0.7
|
||||
, contravariant >= 1.4 && < 1.6
|
||||
, contravariant-extras >= 0.3.3 && < 0.4
|
||||
, cookie >= 0.4.2 && < 0.5
|
||||
, either >= 4.4.1 && < 5.1
|
||||
, gitrev >= 1.2 && < 1.4
|
||||
, hasql >= 1.4 && < 1.5
|
||||
, hasql-pool >= 0.5 && < 0.6
|
||||
, hasql-transaction >= 0.7.2 && < 1.1
|
||||
, hasql >= 1.6.1.1 && < 1.7
|
||||
, hasql-dynamic-statements >= 0.3.1 && < 0.4
|
||||
, hasql-notifications >= 0.1 && < 0.3
|
||||
, hasql-pool >= 0.8.0.2 && < 0.9
|
||||
, hasql-transaction >= 1.0.1 && < 1.1
|
||||
, heredoc >= 0.2 && < 0.3
|
||||
, http-types >= 0.12.2 && < 0.13
|
||||
, insert-ordered-containers >= 0.2.2 && < 0.3
|
||||
, interpolatedstring-perl6 >= 1 && < 1.1
|
||||
, jose >= 0.8.1 && < 0.9
|
||||
, lens >= 4.14 && < 4.20
|
||||
, jose >= 0.8.5.1 && < 0.11
|
||||
, lens >= 4.14 && < 5.3
|
||||
, lens-aeson >= 1.0.1 && < 1.2
|
||||
, mtl >= 2.2.2 && < 2.3
|
||||
, network >= 2.6 && < 3.2
|
||||
, network-uri >= 2.6.1 && < 2.8
|
||||
, optparse-applicative >= 0.13 && < 0.16
|
||||
, optparse-applicative >= 0.13 && < 0.17
|
||||
, parsec >= 3.1.11 && < 3.2
|
||||
, protolude >= 0.3 && < 0.4
|
||||
, protolude >= 0.3.1 && < 0.4
|
||||
, regex-tdfa >= 1.2.2 && < 1.4
|
||||
, retry >= 0.7.4 && < 0.10
|
||||
, scientific >= 0.3.4 && < 0.4
|
||||
, swagger2 >= 2.4 && < 2.7
|
||||
, swagger2 >= 2.4 && < 2.9
|
||||
, text >= 1.2.2 && < 1.3
|
||||
, time >= 1.6 && < 1.11
|
||||
, time >= 1.6 && < 1.12
|
||||
, 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-cors >= 0.2.5 && < 0.3
|
||||
, wai-extra >= 3.0.19 && < 3.1
|
||||
, wai-middleware-static >= 0.8.1 && < 0.9
|
||||
default-language: Haskell2010
|
||||
default-extensions: OverloadedStrings
|
||||
QuasiQuotes
|
||||
NoImplicitPrelude
|
||||
ghc-options: -O2 -Werror -Wall -fwarn-identities
|
||||
, wai-extra >= 3.1.8 && < 3.2
|
||||
-- We already depend on wai-logger >= 2.3.7 indirectly via wai-extra,
|
||||
-- but we want to depend on 2.4.0 which fixes 'unknownSocket' log output
|
||||
-- for unix sockets; this is tested in test/io/test_io.py. See
|
||||
-- https://github.com/kazu-yamamoto/logger/commit/3a71ca70afdbb93d4ecf0083eeba1fbbbcab3fc3
|
||||
, wai-logger >= 2.4.0
|
||||
, warp >= 3.3.19 && < 3.4
|
||||
-- -fno-spec-constr may help keep compile time memory use in check,
|
||||
-- see https://gitlab.haskell.org/ghc/ghc/issues/16017#note_219304
|
||||
-- -optP-Wno-nonportable-include-path
|
||||
-- prevents build failures on case-insensitive filesystems (macos),
|
||||
-- see https://github.com/commercialhaskell/stack/issues/3918
|
||||
ghc-options: -Werror -Wall -fwarn-identities
|
||||
-fno-spec-constr -optP-Wno-nonportable-include-path
|
||||
-- -fno-spec-constr may help keep compile time memory use in check,
|
||||
-- see https://gitlab.haskell.org/ghc/ghc/issues/16017#note_219304
|
||||
-- -optP-Wno-nonportable-include-path
|
||||
-- prevents build failures on case-insensitive filesystems (macos),
|
||||
-- see https://github.com/commercialhaskell/stack/issues/3918
|
||||
|
||||
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
|
||||
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-extensions: OverloadedStrings
|
||||
QuasiQuotes
|
||||
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
|
||||
-fno-spec-constr -optP-Wno-nonportable-include-path
|
||||
|
||||
if !os(windows)
|
||||
build-depends: unix
|
||||
other-modules: UnixSocket
|
||||
if flag(dev)
|
||||
ghc-options: -O0 -fwrite-ide-info
|
||||
-- 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
|
||||
type: exitcode-stdio-1.0
|
||||
default-language: Haskell2010
|
||||
default-extensions: OverloadedStrings
|
||||
QuasiQuotes
|
||||
NoImplicitPrelude
|
||||
hs-source-dirs: test/spec
|
||||
main-is: Main.hs
|
||||
other-modules: Feature.AndOrParamsSpec
|
||||
Feature.AsymmetricJwtSpec
|
||||
Feature.AudienceJwtSecretSpec
|
||||
Feature.AuthSpec
|
||||
Feature.BinaryJwtSecretSpec
|
||||
other-modules: Feature.Auth.AsymmetricJwtSpec
|
||||
Feature.Auth.AudienceJwtSecretSpec
|
||||
Feature.Auth.AuthSpec
|
||||
Feature.Auth.BinaryJwtSecretSpec
|
||||
Feature.Auth.NoAnonSpec
|
||||
Feature.Auth.NoJwtSpec
|
||||
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
|
||||
Feature.LegacyGucsSpec
|
||||
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.RpcSpec
|
||||
Feature.Query.SingularSpec
|
||||
Feature.Query.UnicodeSpec
|
||||
Feature.Query.UpdateSpec
|
||||
Feature.Query.UpsertSpec
|
||||
Feature.RollbackSpec
|
||||
Feature.RpcPreRequestGucsSpec
|
||||
SpecHelper
|
||||
TestTypes
|
||||
hs-source-dirs: test
|
||||
build-depends: base >= 4.9 && < 4.15
|
||||
, aeson >= 1.4.7 && < 1.5
|
||||
build-depends: base >= 4.9 && < 4.17
|
||||
, aeson >= 2.0.3 && < 2.2
|
||||
, 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
|
||||
, base64-bytestring >= 1 && < 1.3
|
||||
, bytestring >= 0.10.8 && < 0.12
|
||||
, 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
|
||||
, hasql-pool >= 0.8.0.2 && < 0.9
|
||||
, hasql-transaction >= 1.0.1 && < 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
|
||||
, hspec >= 2.3 && < 2.9
|
||||
, hspec-wai >= 0.10 && < 0.12
|
||||
, hspec-wai-json >= 0.10 && < 0.12
|
||||
, http-types >= 0.12.3 && < 0.13
|
||||
, lens >= 4.14 && < 4.20
|
||||
, lens >= 4.14 && < 5.3
|
||||
, 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
|
||||
, protolude >= 0.3.1 && < 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
|
||||
, wai-extra >= 3.0.19 && < 3.2
|
||||
ghc-options: -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 querycost
|
||||
type: exitcode-stdio-1.0
|
||||
default-language: Haskell2010
|
||||
default-extensions: OverloadedStrings
|
||||
QuasiQuotes
|
||||
NoImplicitPrelude
|
||||
ghc-options: -threaded -rtsopts -with-rtsopts=-N
|
||||
hs-source-dirs: test/spec
|
||||
main-is: QueryCost.hs
|
||||
other-modules: SpecHelper
|
||||
build-depends: base >= 4.9 && < 4.17
|
||||
, aeson >= 2.0.3 && < 2.2
|
||||
, base64-bytestring >= 1 && < 1.3
|
||||
, bytestring >= 0.10.8 && < 0.12
|
||||
, case-insensitive >= 1.2 && < 1.3
|
||||
, containers >= 0.5.7 && < 0.7
|
||||
, contravariant >= 1.4 && < 1.6
|
||||
, hasql >= 1.6 && < 1.7
|
||||
, hasql-dynamic-statements >= 0.3.1 && < 0.4
|
||||
, hasql-pool >= 0.8.0.2 && < 0.9
|
||||
, hasql-transaction >= 1.0.1 && < 1.1
|
||||
, heredoc >= 0.2 && < 0.3
|
||||
, hspec >= 2.3 && < 2.9
|
||||
, hspec-wai >= 0.10 && < 0.12
|
||||
, hspec-wai-json >= 0.10 && < 0.12
|
||||
, http-types >= 0.12.3 && < 0.13
|
||||
, lens >= 4.14 && < 5.3
|
||||
, lens-aeson >= 1.0.1 && < 1.2
|
||||
, postgrest
|
||||
, process >= 1.4.2 && < 1.7
|
||||
, protolude >= 0.3.1 && < 0.4
|
||||
, regex-tdfa >= 1.2.2 && < 1.4
|
||||
, wai-extra >= 3.0.19 && < 3.2
|
||||
ghc-options: -O0 -Werror -Wall -fwarn-identities
|
||||
-fno-spec-constr -optP-Wno-nonportable-include-path
|
||||
-fwrite-ide-info
|
||||
-- https://github.com/PostgREST/postgrest/issues/387
|
||||
-with-rtsopts=-K1K
|
||||
|
||||
Test-Suite spec-querycost
|
||||
Type: exitcode-stdio-1.0
|
||||
Default-Language: Haskell2010
|
||||
default-extensions: OverloadedStrings, QuasiQuotes, NoImplicitPrelude
|
||||
Hs-Source-Dirs: test
|
||||
Main-Is: QueryCost.hs
|
||||
Other-Modules: SpecHelper
|
||||
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
|
||||
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);
|
||||
pkgs.lib.overrideDerivation env (
|
||||
# The additional modules below have large dependencies and are therefore
|
||||
# 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: {
|
||||
buildInputs =
|
||||
base.buildInputs ++ [
|
||||
pkgs.cabal-install
|
||||
pkgs.stack
|
||||
pkgs.cabal2nix
|
||||
pkgs.git
|
||||
pkgs.postgresql
|
||||
nixpkgsUpgrade
|
||||
tests
|
||||
style
|
||||
lint
|
||||
];
|
||||
pkgs.update-nix-fetchgit
|
||||
postgrest.hsie.bin
|
||||
]
|
||||
++ toolboxes;
|
||||
|
||||
shellHook =
|
||||
''
|
||||
# Set our pinned version of Nixpkgs in the NIX_PATH so that
|
||||
# `stack --nix` also uses that version.
|
||||
NIX_PATH="nixpkgs=${nixpkgs}"
|
||||
'';
|
||||
export HISTFILE=.history
|
||||
|
||||
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)
|
||||
)
|
||||
);
|
||||
}
|
||||
)
|
||||
|
||||
+433
-245
@@ -1,72 +1,153 @@
|
||||
{-|
|
||||
Module : PostgREST.ApiRequest
|
||||
Module : PostgREST.Request.ApiRequest
|
||||
Description : PostgREST functions to translate HTTP request to a domain type called ApiRequest.
|
||||
-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE MultiWayIf #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
|
||||
module PostgREST.ApiRequest (
|
||||
ApiRequest(..)
|
||||
, InvokeMethod(..)
|
||||
, ContentType(..)
|
||||
, Action(..)
|
||||
, Target(..)
|
||||
, mutuallyAgreeable
|
||||
, userApiRequest
|
||||
) where
|
||||
module PostgREST.ApiRequest
|
||||
( ApiRequest(..)
|
||||
, InvokeMethod(..)
|
||||
, Mutation(..)
|
||||
, MediaType(..)
|
||||
, Action(..)
|
||||
, Target(..)
|
||||
, Payload(..)
|
||||
, userApiRequest
|
||||
) where
|
||||
|
||||
import qualified Data.Aeson as JSON
|
||||
import qualified Data.ByteString as BS
|
||||
import qualified Data.ByteString.Lazy as BL
|
||||
import qualified Data.CaseInsensitive as CI
|
||||
import qualified Data.Csv as CSV
|
||||
import qualified Data.HashMap.Strict as M
|
||||
import qualified Data.List as L
|
||||
import qualified Data.Set as S
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.Vector as V
|
||||
import qualified Data.Aeson as JSON
|
||||
import qualified Data.Aeson.Key as K
|
||||
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.Csv as CSV
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
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.Text.Encoding as T
|
||||
import qualified Data.Vector as V
|
||||
|
||||
import Control.Arrow ((***))
|
||||
import Data.Aeson.Types (emptyArray, emptyObject)
|
||||
import Data.List (last, lookup, partition)
|
||||
import Data.List.NonEmpty (head)
|
||||
import Data.Maybe (fromJust)
|
||||
import Data.Ranged.Ranges (Range (..), emptyRange,
|
||||
rangeIntersection)
|
||||
import Network.HTTP.Base (urlEncodeVars)
|
||||
import Network.HTTP.Types.Header (hAuthorization, hCookie)
|
||||
import Network.HTTP.Types.URI (parseQueryReplacePlus,
|
||||
parseSimpleQuery)
|
||||
import Data.List (lookup, union)
|
||||
import Data.Ranged.Ranges (emptyRange, rangeIntersection,
|
||||
rangeIsEmpty)
|
||||
import Data.Tree (Tree (..))
|
||||
import Network.HTTP.Types.Header (RequestHeaders, hCookie)
|
||||
import Network.HTTP.Types.URI (parseSimpleQuery)
|
||||
import Network.Wai (Request (..))
|
||||
import Network.Wai.Parse (parseHttpAccept)
|
||||
import Web.Cookie (parseCookiesText)
|
||||
import Web.Cookie (parseCookies)
|
||||
|
||||
import PostgREST.ApiRequest.Preferences (PreferCount (..),
|
||||
PreferParameters (..),
|
||||
PreferRepresentation (..),
|
||||
PreferResolution (..),
|
||||
PreferTransaction (..))
|
||||
import PostgREST.ApiRequest.QueryParams (QueryParams (..))
|
||||
import PostgREST.ApiRequest.Types (ApiRequestError (..),
|
||||
RangeError (..),
|
||||
SelectItem (..))
|
||||
import PostgREST.Config (AppConfig (..),
|
||||
OpenAPIMode (..))
|
||||
import PostgREST.MediaType (MTPlanAttrs (..),
|
||||
MTPlanFormat (..),
|
||||
MediaType (..))
|
||||
import PostgREST.RangeQuery (NonnegRange, allRange,
|
||||
hasLimitZero,
|
||||
limitZeroRange,
|
||||
rangeRequested)
|
||||
import PostgREST.SchemaCache (SchemaCache (..))
|
||||
import PostgREST.SchemaCache.Identifiers (FieldName,
|
||||
QualifiedIdentifier (..),
|
||||
Schema)
|
||||
import PostgREST.SchemaCache.Proc (ProcDescription (..),
|
||||
ProcParam (..), ProcsMap,
|
||||
procReturnsScalar)
|
||||
|
||||
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 (..))
|
||||
import PostgREST.RangeQuery (NonnegRange, allRange, rangeGeq,
|
||||
rangeLimit, rangeOffset, rangeRequested,
|
||||
restrictRange)
|
||||
import PostgREST.Types
|
||||
import Protolude hiding (head, toS)
|
||||
import Protolude.Conv (toS)
|
||||
|
||||
type RequestBody = BL.ByteString
|
||||
data Payload
|
||||
= ProcessedJSON -- ^ Cached attributes of a JSON payload
|
||||
{ payRaw :: LBS.ByteString
|
||||
-- ^ This is the raw ByteString that comes from the request body. We
|
||||
-- cache this instead of an Aeson Value because it was detected that for
|
||||
-- large payloads the encoding had high memory usage, see
|
||||
-- https://github.com/PostgREST/postgrest/pull/1005 for more details
|
||||
, payKeys :: S.Set Text
|
||||
-- ^ Keys of the object or if it's an array these keys are guaranteed to
|
||||
-- be the same across all its objects
|
||||
}
|
||||
| RawJSON { payRaw :: LBS.ByteString }
|
||||
| RawPay { payRaw :: LBS.ByteString }
|
||||
|
||||
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
|
||||
data Action = ActionCreate | ActionRead{isHead :: Bool}
|
||||
| ActionUpdate | ActionDelete
|
||||
| ActionSingleUpsert | ActionInvoke InvokeMethod
|
||||
| ActionInfo | ActionInspect{isHead :: Bool}
|
||||
deriving Eq
|
||||
data Action
|
||||
= ActionMutate Mutation
|
||||
| ActionRead {isHead :: Bool}
|
||||
| ActionInvoke InvokeMethod
|
||||
| ActionInfo
|
||||
| ActionInspect {isHead :: Bool}
|
||||
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
|
||||
data Target = TargetIdent QualifiedIdentifier
|
||||
| TargetProc{tpQi :: QualifiedIdentifier, tpIsRootSpec :: Bool}
|
||||
| TargetProc{tProc :: ProcDescription, tpIsRootSpec :: Bool}
|
||||
| TargetDefaultSpec{tdsSchema :: Schema} -- The default spec offered at root "/"
|
||||
| TargetUnknown [Text]
|
||||
deriving Eq
|
||||
|
||||
-- | RPC query param value `/rpc/func?v=<value>`, used for VARIADIC functions on form-urlencoded POST and GETs
|
||||
-- | It can be fixed `?v=1` or repeated `?v=1&v=2&v=3.
|
||||
data RpcParamValue = Fixed Text | Variadic [Text]
|
||||
instance JSON.ToJSON RpcParamValue where
|
||||
toJSON (Fixed v) = JSON.toJSON v
|
||||
toJSON (Variadic v) = JSON.toJSON v
|
||||
|
||||
toRpcParamValue :: ProcDescription -> (Text, Text) -> (Text, RpcParamValue)
|
||||
toRpcParamValue proc (k, v) | prmIsVariadic k = (k, Variadic [v])
|
||||
| otherwise = (k, Fixed v)
|
||||
where
|
||||
prmIsVariadic prm = isJust $ find (\ProcParam{ppName, ppVar} -> ppName == prm && ppVar) $ pdParams proc
|
||||
|
||||
-- | Convert rpc params `/rpc/func?a=val1&b=val2` to json `{"a": "val1", "b": "val2"}
|
||||
jsonRpcParams :: ProcDescription -> [(Text, Text)] -> Payload
|
||||
jsonRpcParams proc prms =
|
||||
if not $ pdHasVariadic proc then -- if proc has no variadic param, save steps and directly convert to json
|
||||
ProcessedJSON (JSON.encode $ HM.fromList $ second JSON.toJSON <$> prms) (S.fromList $ fst <$> prms)
|
||||
else
|
||||
let paramsMap = HM.fromListWith mergeParams $ toRpcParamValue proc <$> prms in
|
||||
ProcessedJSON (JSON.encode paramsMap) (S.fromList $ HM.keys paramsMap)
|
||||
where
|
||||
mergeParams :: RpcParamValue -> RpcParamValue -> RpcParamValue
|
||||
mergeParams (Variadic a) (Variadic b) = Variadic $ b ++ a
|
||||
mergeParams v _ = v -- repeated params for non-variadic parameters are not merged
|
||||
|
||||
targetToJsonRpcParams :: Maybe Target -> [(Text, Text)] -> Maybe Payload
|
||||
targetToJsonRpcParams target params =
|
||||
case target of
|
||||
Just TargetProc{tProc} -> Just $ jsonRpcParams tProc params
|
||||
_ -> Nothing
|
||||
|
||||
{-|
|
||||
Describes what the user wants to do. This data type is a
|
||||
@@ -76,227 +157,221 @@ data Target = TargetIdent QualifiedIdentifier
|
||||
if it is an action we are able to perform.
|
||||
-}
|
||||
data ApiRequest = ApiRequest {
|
||||
iAction :: Action -- ^ Similar but not identical to HTTP verb, e.g. Create/Invoke both POST
|
||||
, iRange :: M.HashMap ByteString NonnegRange -- ^ Requested range of rows within response
|
||||
iAction :: Action -- ^ Similar but not identical to HTTP method, e.g. Create/Invoke both POST
|
||||
, iRange :: HM.HashMap Text NonnegRange -- ^ Requested range of rows within response
|
||||
, iTopLevelRange :: NonnegRange -- ^ Requested range of rows from the top level
|
||||
, 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 PayloadJSON -- ^ Data sent by client and used for mutation actions
|
||||
, iPayload :: Maybe Payload -- ^ Data sent by client and used for mutation actions
|
||||
, iPreferRepresentation :: PreferRepresentation -- ^ If client wants created items echoed back
|
||||
, iPreferParameters :: Maybe PreferParameters -- ^ How to pass parameters to a stored procedure
|
||||
, iPreferCount :: Maybe PreferCount -- ^ Whether the client wants a result count
|
||||
, iPreferResolution :: Maybe PreferResolution -- ^ Whether the client wants to UPSERT or ignore records on PK conflict
|
||||
, 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
|
||||
, iPreferTransaction :: Maybe PreferTransaction -- ^ Whether the clients wants to commit or rollback the transaction
|
||||
, iQueryParams :: QueryParams.QueryParams
|
||||
, iColumns :: S.Set FieldName -- ^ parsed colums from &columns parameter and payload
|
||||
, iHeaders :: [(ByteString, ByteString)] -- ^ HTTP request headers
|
||||
, iCookies :: [(ByteString, ByteString)] -- ^ Request Cookies
|
||||
, iPath :: ByteString -- ^ Raw request path
|
||||
, 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 iProfile.
|
||||
, iSchema :: Schema -- ^ The request schema. Can vary depending on profile headers.
|
||||
, 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
|
||||
, iBinaryField :: Maybe FieldName -- ^ field used for raw output
|
||||
}
|
||||
|
||||
-- | Examines HTTP request and translates it into user intent.
|
||||
userApiRequest :: NonEmpty Schema -> Maybe Text -> Request -> RequestBody -> Either ApiRequestError ApiRequest
|
||||
userApiRequest confSchemas rootSpec req reqBody
|
||||
| isJust profile && fromJust profile `notElem` confSchemas = Left $ UnacceptableSchema $ toList confSchemas
|
||||
| isTargetingProc && method `notElem` ["HEAD", "GET", "POST"] = Left ActionInappropriate
|
||||
| topLevelRange == emptyRange = Left InvalidRange
|
||||
| shouldParsePayload && isLeft payload = either (Left . InvalidBody . toS) witness payload
|
||||
| otherwise = Right ApiRequest {
|
||||
iAction = action
|
||||
, iTarget = target
|
||||
, iRange = ranges
|
||||
, iTopLevelRange = topLevelRange
|
||||
, iAccepts = maybe [CTAny] (map decodeContentType . parseHttpAccept) $ lookupHeader "accept"
|
||||
, iPayload = relevantPayload
|
||||
, iPreferRepresentation = representation
|
||||
, 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
|
||||
, iOrder = [(toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, endingIn ["order"] k ]
|
||||
, iCanonicalQS = toS $ urlEncodeVars
|
||||
. 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
|
||||
, iMethod = method
|
||||
, iProfile = profile
|
||||
, iSchema = schema
|
||||
}
|
||||
where
|
||||
-- queryString with '+' converted to ' '(space)
|
||||
qString = parseQueryReplacePlus True $ rawQueryString req
|
||||
-- rpcQParams = Rpc query params e.g. /rpc/name?param1=val1, similar to filter but with no operator(eq, lt..)
|
||||
(filters, rpcQParams) =
|
||||
case action of
|
||||
ActionInvoke InvGet -> partitionFlts
|
||||
ActionInvoke InvHead -> partitionFlts
|
||||
_ -> (flts, [])
|
||||
partitionFlts = partition (liftM2 (||) (isEmbedPath . fst) (hasOperator . snd)) flts
|
||||
flts =
|
||||
[ (toS k, toS $ fromJust v) |
|
||||
(k,v) <- qParams, isJust v,
|
||||
k `notElem` ["select", "columns"],
|
||||
not (endingIn ["order", "limit", "offset", "and", "or"] k) ]
|
||||
hasOperator val = any (`T.isPrefixOf` val) $
|
||||
((<> ".") <$> "not":M.keys operators) ++
|
||||
((<> "(") <$> M.keys ftsOperators)
|
||||
isEmbedPath = T.isInfixOf "."
|
||||
isTargetingProc = case target of
|
||||
TargetProc _ _ -> True
|
||||
_ -> False
|
||||
isTargetingDefaultSpec = case target of
|
||||
TargetDefaultSpec _ -> True
|
||||
_ -> False
|
||||
contentType = decodeContentType . fromMaybe "application/json" $ lookupHeader "content-type"
|
||||
columns
|
||||
| action `elem` [ActionCreate, ActionUpdate, ActionInvoke InvPost] = toS <$> join (lookup "columns" qParams)
|
||||
| otherwise = Nothing
|
||||
payload =
|
||||
case (contentType, action) of
|
||||
(_, ActionInvoke InvGet) -> Right rpcPrmsToJson
|
||||
(_, ActionInvoke InvHead) -> Right rpcPrmsToJson
|
||||
(CTApplicationJSON, _) ->
|
||||
if isJust columns
|
||||
then Right $ RawJSON reqBody
|
||||
else note "All object keys must match" . payloadAttributes reqBody
|
||||
=<< if BL.null reqBody && isTargetingProc
|
||||
then Right emptyObject
|
||||
else JSON.eitherDecode reqBody
|
||||
(CTTextCSV, _) -> do
|
||||
json <- csvToJson <$> CSV.decodeByName reqBody
|
||||
note "All lines must have same number of fields" $ payloadAttributes (JSON.encode json) json
|
||||
(CTOther "application/x-www-form-urlencoded", _) ->
|
||||
let json = M.fromList . map (toS *** JSON.String . toS) . parseSimpleQuery $ toS reqBody
|
||||
keys = S.fromList $ M.keys json in
|
||||
Right $ ProcessedJSON (JSON.encode json) keys
|
||||
(ct, _) ->
|
||||
Left $ toS $ "Content-Type not acceptable: " <> toMime ct
|
||||
rpcPrmsToJson = ProcessedJSON (JSON.encode $ M.fromList $ second JSON.toJSON <$> rpcQParams) (S.fromList $ fst <$> rpcQParams)
|
||||
topLevelRange = fromMaybe allRange $ M.lookup "limit" ranges -- if no limit is specified, get all the request rows
|
||||
action =
|
||||
case method of
|
||||
userApiRequest :: AppConfig -> SchemaCache -> Request -> RequestBody -> Either ApiRequestError ApiRequest
|
||||
userApiRequest conf sCache req reqBody = do
|
||||
qPrms <- first QueryParamError $ QueryParams.parse $ rawQueryString req
|
||||
pInfo <- getPathInfo conf $ pathInfo req
|
||||
act <- getAction pInfo $ requestMethod req
|
||||
mediaTypes <- getMediaTypes conf (requestHeaders req) act pInfo
|
||||
negotiatedSchema <- getSchema conf (requestHeaders req) (requestMethod req)
|
||||
apiRequest conf sCache req reqBody qPrms pInfo act mediaTypes negotiatedSchema
|
||||
|
||||
getPathInfo :: AppConfig -> [Text] -> Either ApiRequestError PathInfo
|
||||
getPathInfo AppConfig{configOpenApiMode, configDbRootSpec} path =
|
||||
case path of
|
||||
[] -> case configDbRootSpec of
|
||||
Just (QualifiedIdentifier _ pathName) -> Right $ PathInfo pathName True False True
|
||||
Nothing | configOpenApiMode == OADisabled -> Left NotFound
|
||||
| otherwise -> Right $ PathInfo mempty False True False
|
||||
[table] -> Right $ PathInfo table False False False
|
||||
["rpc", pName] -> Right $ PathInfo pName True False False
|
||||
_ -> Left NotFound
|
||||
|
||||
getAction :: PathInfo -> ByteString -> Either ApiRequestError Action
|
||||
getAction PathInfo{pathIsProc, pathIsDefSpec} method =
|
||||
if pathIsProc && method `notElem` ["HEAD", "GET", "POST", "OPTIONS"]
|
||||
then Left $ InvalidRpcMethod method
|
||||
else case method of
|
||||
-- The HEAD method is identical to GET except that the server MUST NOT return a message-body in the response
|
||||
-- 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}
|
||||
"HEAD" | pathIsDefSpec -> Right $ ActionInspect{isHead=True}
|
||||
| pathIsProc -> Right $ ActionInvoke InvHead
|
||||
| otherwise -> Right $ ActionRead{isHead=True}
|
||||
"GET" | pathIsDefSpec -> Right $ ActionInspect{isHead=False}
|
||||
| pathIsProc -> Right $ ActionInvoke InvGet
|
||||
| otherwise -> Right $ ActionRead{isHead=False}
|
||||
"POST" | pathIsProc -> Right $ ActionInvoke InvPost
|
||||
| otherwise -> Right $ ActionMutate MutationCreate
|
||||
"PATCH" -> Right $ ActionMutate MutationUpdate
|
||||
"PUT" -> Right $ ActionMutate MutationSingleUpsert
|
||||
"DELETE" -> Right $ ActionMutate MutationDelete
|
||||
"OPTIONS" -> Right ActionInfo
|
||||
_ -> Left $ UnsupportedMethod method
|
||||
|
||||
defaultSchema = head confSchemas
|
||||
profile
|
||||
| length confSchemas <= 1 -- only enable content negotiation by profile when there are multiple schemas specified in the config
|
||||
= Nothing
|
||||
| action `elem` [ActionCreate, ActionUpdate, ActionSingleUpsert, ActionDelete, ActionInvoke InvPost] -- POST/PATCH/PUT/DELETE don't use the same header as per the spec
|
||||
= Just $ maybe defaultSchema toS $ lookupHeader "Content-Profile"
|
||||
| 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
|
||||
getMediaTypes :: AppConfig -> RequestHeaders -> Action -> PathInfo -> Either ApiRequestError (MediaType, MediaType)
|
||||
getMediaTypes conf hdrs action path = do
|
||||
acceptMediaType <- findAcceptMediaType 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
|
||||
|
||||
shouldParsePayload =
|
||||
action `elem`
|
||||
[ActionCreate, ActionUpdate, ActionSingleUpsert,
|
||||
ActionInvoke InvPost,
|
||||
-- Though ActionInvoke{isGet=True}(a GET /rpc/..) doesn't really have a payload, we use the payload variable as a way
|
||||
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
|
||||
|
||||
apiRequest :: AppConfig -> SchemaCache -> Request -> RequestBody -> QueryParams.QueryParams -> PathInfo -> Action -> (MediaType, MediaType) -> (Schema, Bool) -> Either ApiRequestError ApiRequest
|
||||
apiRequest conf sCache req reqBody queryparams@QueryParams{..} PathInfo{pathName, pathIsProc, pathIsRootSpec, pathIsDefSpec} action (acceptMediaType, contentMediaType) (schema, negotiatedByProfile)
|
||||
| isInvalidRange = Left $ InvalidRange (if rangeIsEmpty headerRange then LowerGTUpper else NegativeLimit)
|
||||
| shouldParsePayload && isLeft payload = either (Left . InvalidBody) witness payload
|
||||
| not expectParams && not (L.null qsParams) = Left $ ParseRequestError "Unexpected param or filter missing operator" ("Failed to parse " <> show qsParams)
|
||||
| method `elem` ["PATCH", "DELETE"] && not (null qsRanges) && null qsOrder = Left LimitNoOrderError
|
||||
| method == "PUT" && topLevelRange /= allRange = Left PutRangeNotAllowedError
|
||||
| otherwise = do
|
||||
checkedTarget <- target
|
||||
bField <- binaryField conf acceptMediaType checkedTarget queryparams
|
||||
return ApiRequest {
|
||||
iAction = action
|
||||
, iTarget = checkedTarget
|
||||
, iRange = ranges
|
||||
, iTopLevelRange = topLevelRange
|
||||
, iPayload = relevantPayload
|
||||
, iPreferRepresentation = fromMaybe None preferRepresentation
|
||||
, iPreferParameters = preferParameters
|
||||
, iPreferCount = preferCount
|
||||
, iPreferResolution = preferResolution
|
||||
, iPreferTransaction = preferTransaction
|
||||
, iQueryParams = queryparams
|
||||
, iColumns = payloadColumns
|
||||
, iHeaders = [ (CI.foldedCase k, v) | (k,v) <- hdrs, k /= hCookie]
|
||||
, iCookies = maybe [] parseCookies $ lookupHeader "Cookie"
|
||||
, iPath = rawPathInfo req
|
||||
, iMethod = method
|
||||
, iSchema = schema
|
||||
, iNegotiatedByProfile = negotiatedByProfile
|
||||
, iAcceptMediaType = acceptMediaType
|
||||
, iBinaryField = bField
|
||||
}
|
||||
where
|
||||
expectParams = pathIsProc && method /= "POST"
|
||||
|
||||
columns = case action of
|
||||
ActionMutate MutationCreate -> qsColumns
|
||||
ActionMutate MutationUpdate -> qsColumns
|
||||
ActionInvoke InvPost -> qsColumns
|
||||
_ -> Nothing
|
||||
|
||||
payloadColumns =
|
||||
case (contentMediaType, action) of
|
||||
(_, ActionInvoke InvGet) -> S.fromList $ fst <$> qsParams
|
||||
(_, ActionInvoke InvHead) -> S.fromList $ fst <$> qsParams
|
||||
(MTUrlEncoded, _) -> S.fromList $ map (T.decodeUtf8 . fst) $ parseSimpleQuery $ LBS.toStrict reqBody
|
||||
_ -> case (relevantPayload, columns) of
|
||||
(Just ProcessedJSON{payKeys}, _) -> payKeys
|
||||
(Just RawJSON{}, Just cls) -> cls
|
||||
_ -> S.empty
|
||||
payload :: Either ByteString Payload
|
||||
payload = case (contentMediaType, pathIsProc) of
|
||||
(MTApplicationJSON, _) ->
|
||||
if isJust columns
|
||||
then Right $ RawJSON reqBody
|
||||
else note "All object keys must match" . payloadAttributes reqBody
|
||||
=<< if LBS.null reqBody && pathIsProc
|
||||
then Right emptyObject
|
||||
else first BS.pack $ JSON.eitherDecode reqBody
|
||||
(MTTextCSV, _) -> do
|
||||
json <- csvToJson <$> first BS.pack (CSV.decodeByName reqBody)
|
||||
note "All lines must have same number of fields" $ payloadAttributes (JSON.encode json) json
|
||||
(MTUrlEncoded, _) ->
|
||||
let paramsMap = HM.fromList $ (T.decodeUtf8 *** JSON.String . T.decodeUtf8) <$> parseSimpleQuery (LBS.toStrict reqBody) in
|
||||
Right $ ProcessedJSON (JSON.encode paramsMap) $ S.fromList (HM.keys paramsMap)
|
||||
(MTTextPlain, True) -> Right $ RawPay reqBody
|
||||
(MTTextXML, True) -> Right $ RawPay reqBody
|
||||
(MTOctetStream, True) -> Right $ RawPay reqBody
|
||||
(ct, _) -> Left $ "Content-Type not acceptable: " <> MediaType.toMime ct
|
||||
topLevelRange = fromMaybe allRange $ HM.lookup "limit" ranges -- if no limit is specified, get all the request rows
|
||||
|
||||
target
|
||||
| pathIsProc = (`TargetProc` pathIsRootSpec) <$> callFindProc schema pathName
|
||||
| pathIsDefSpec = Right $ TargetDefaultSpec schema
|
||||
| otherwise = Right $ TargetIdent $ QualifiedIdentifier schema pathName
|
||||
where
|
||||
callFindProc procSch procNam = findProc
|
||||
(QualifiedIdentifier procSch procNam) payloadColumns (preferParameters == Just SingleObject) (dbProcs sCache)
|
||||
contentMediaType (action == ActionInvoke InvPost)
|
||||
|
||||
shouldParsePayload = case (action, contentMediaType) of
|
||||
(ActionMutate MutationCreate, _) -> True
|
||||
(ActionInvoke InvPost, MTUrlEncoded) -> False
|
||||
(ActionInvoke InvPost, _) -> True
|
||||
(ActionMutate MutationSingleUpsert, _) -> True
|
||||
(ActionMutate MutationUpdate, _) -> True
|
||||
_ -> False
|
||||
relevantPayload = case (contentMediaType, action) of
|
||||
-- Though ActionInvoke GET/HEAD doesn't really have a payload, we use the payload variable as a way
|
||||
-- to store the query string arguments to the function.
|
||||
ActionInvoke InvGet,
|
||||
ActionInvoke InvHead]
|
||||
relevantPayload | shouldParsePayload = rightToMaybe payload
|
||||
| otherwise = Nothing
|
||||
path = pathInfo req
|
||||
(_, ActionInvoke InvGet) -> targetToJsonRpcParams (rightToMaybe target) qsParams
|
||||
(_, ActionInvoke InvHead) -> targetToJsonRpcParams (rightToMaybe target) qsParams
|
||||
(MTUrlEncoded, ActionInvoke InvPost) -> targetToJsonRpcParams (rightToMaybe target) $ (T.decodeUtf8 *** T.decodeUtf8) <$> parseSimpleQuery (LBS.toStrict reqBody)
|
||||
_ | shouldParsePayload -> rightToMaybe payload
|
||||
| otherwise -> Nothing
|
||||
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
|
||||
|
||||
Preferences.Preferences{..} = Preferences.fromHeaders hdrs
|
||||
headerRange = rangeRequested hdrs
|
||||
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]
|
||||
limitRange = fromMaybe allRange (HM.lookup "limit" qsRanges)
|
||||
headerAndLimitRange = rangeIntersection headerRange limitRange
|
||||
|
||||
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
|
||||
-- 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" (if hasLimitZero limitRange then limitZeroRange else headerAndLimitRange) qsRanges
|
||||
-- The only emptyRange allowed is the limit zero range
|
||||
isInvalidRange = topLevelRange == emptyRange && not (hasLimitZero limitRange)
|
||||
|
||||
{-|
|
||||
Find the best match from a list of content types accepted by the
|
||||
Find the best match from a list of media 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 :: [MediaType] -> [MediaType] -> Maybe MediaType
|
||||
mutuallyAgreeable sProduces cAccepts =
|
||||
let exact = listToMaybe $ L.intersect cAccepts sProduces in
|
||||
if isNothing exact && CTAny `elem` cAccepts
|
||||
if isNothing exact && MTAny `elem` cAccepts
|
||||
then listToMaybe sProduces
|
||||
else exact
|
||||
|
||||
type CsvData = V.Vector (M.HashMap Text BL.ByteString)
|
||||
type CsvData = V.Vector (M.Map Text LBS.ByteString)
|
||||
|
||||
{-|
|
||||
Converts CSV like
|
||||
@@ -314,23 +389,23 @@ csvToJson :: (CSV.Header, CsvData) -> JSON.Value
|
||||
csvToJson (_, vals) =
|
||||
JSON.Array $ V.map rowToJsonObj vals
|
||||
where
|
||||
rowToJsonObj = JSON.Object .
|
||||
rowToJsonObj = JSON.Object . KM.fromMapText .
|
||||
M.map (\str ->
|
||||
if str == "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 =
|
||||
-- Test that Array contains only Objects having the same keys
|
||||
case json of
|
||||
JSON.Array arr ->
|
||||
case arr V.!? 0 of
|
||||
Just (JSON.Object o) ->
|
||||
let canonicalKeys = S.fromList $ M.keys o
|
||||
let canonicalKeys = S.fromList $ K.toText <$> KM.keys o
|
||||
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
|
||||
if areKeysUniform
|
||||
then Just $ ProcessedJSON raw canonicalKeys
|
||||
@@ -338,9 +413,122 @@ payloadAttributes raw json =
|
||||
Just _ -> Nothing
|
||||
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.
|
||||
_ -> Just emptyPJArray
|
||||
where
|
||||
emptyPJArray = ProcessedJSON (JSON.encode emptyArray) S.empty
|
||||
|
||||
findAcceptMediaType :: AppConfig -> Action -> PathInfo -> [MediaType] -> Either ApiRequestError MediaType
|
||||
findAcceptMediaType conf action path accepts =
|
||||
case mutuallyAgreeable (requestMediaTypes conf action path) accepts of
|
||||
Just ct ->
|
||||
Right ct
|
||||
Nothing ->
|
||||
Left . MediaTypeError $ map MediaType.toMime accepts
|
||||
|
||||
requestMediaTypes :: AppConfig -> Action -> PathInfo -> [MediaType]
|
||||
requestMediaTypes conf action path =
|
||||
case action of
|
||||
ActionRead _ -> defaultMediaTypes ++ rawMediaTypes
|
||||
ActionInvoke _ -> invokeMediaTypes
|
||||
ActionInspect _ -> [MTOpenAPI, MTApplicationJSON]
|
||||
ActionInfo -> [MTTextCSV]
|
||||
_ -> defaultMediaTypes
|
||||
where
|
||||
invokeMediaTypes =
|
||||
defaultMediaTypes
|
||||
++ rawMediaTypes
|
||||
++ [MTOpenAPI | pathIsRootSpec path]
|
||||
defaultMediaTypes =
|
||||
[MTApplicationJSON, MTSingularJSON, MTGeoJSON, MTTextCSV] ++
|
||||
[MTPlan $ MTPlanAttrs Nothing PlanJSON mempty | configDbPlanEnabled conf]
|
||||
rawMediaTypes = configRawMediaTypes conf `union` [MTOctetStream, MTTextPlain, MTTextXML]
|
||||
|
||||
{-|
|
||||
Search a pg proc by matching name and arguments keys to parameters. Since a function can be overloaded,
|
||||
the name is not enough to find it. An overloaded function can have a different volatility or even a different return type.
|
||||
-}
|
||||
findProc :: QualifiedIdentifier -> S.Set Text -> Bool -> ProcsMap -> MediaType -> Bool -> Either ApiRequestError ProcDescription
|
||||
findProc qi argumentsKeys paramsAsSingleObject allProcs contentMediaType isInvPost =
|
||||
case matchProc of
|
||||
([], []) -> Left $ NoRpc (qiSchema qi) (qiName qi) (S.toList argumentsKeys) paramsAsSingleObject contentMediaType isInvPost
|
||||
-- If there are no functions with named arguments, fallback to the single unnamed argument function
|
||||
([], [proc]) -> Right proc
|
||||
([], procs) -> Left $ AmbiguousRpc (toList procs)
|
||||
-- Matches the functions with named arguments
|
||||
([proc], _) -> Right proc
|
||||
(procs, _) -> Left $ AmbiguousRpc (toList procs)
|
||||
where
|
||||
matchProc = overloadedProcPartition $ HM.lookupDefault mempty qi allProcs -- first find the proc by name
|
||||
-- The partition obtained has the form (overloadedProcs,fallbackProcs)
|
||||
-- where fallbackProcs are functions with a single unnamed parameter
|
||||
overloadedProcPartition = foldr select ([],[])
|
||||
select proc ~(ts,fs)
|
||||
| matchesParams proc = (proc:ts,fs)
|
||||
| hasSingleUnnamedParam proc = (ts,proc:fs)
|
||||
| otherwise = (ts,fs)
|
||||
-- If the function is called with post and has a single unnamed parameter
|
||||
-- it can be called depending on content type and the parameter type
|
||||
hasSingleUnnamedParam ProcDescription{pdParams=[ProcParam{ppType}]} = isInvPost && case (contentMediaType, ppType) of
|
||||
(MTApplicationJSON, "json") -> True
|
||||
(MTApplicationJSON, "jsonb") -> True
|
||||
(MTTextPlain, "text") -> True
|
||||
(MTTextXML, "xml") -> True
|
||||
(MTOctetStream, "bytea") -> True
|
||||
_ -> False
|
||||
hasSingleUnnamedParam _ = False
|
||||
matchesParams proc =
|
||||
let
|
||||
params = pdParams proc
|
||||
firstType = (ppType <$> headMay params)
|
||||
in
|
||||
-- exceptional case for Prefer: params=single-object
|
||||
if paramsAsSingleObject
|
||||
then length params == 1 && (firstType == Just "json" || firstType == Just "jsonb")
|
||||
-- If the function has no parameters, the arguments keys must be empty as well
|
||||
else if null params
|
||||
then null argumentsKeys && not (isInvPost && contentMediaType `elem` [MTOctetStream, MTTextPlain, MTTextXML])
|
||||
-- A function has optional and required parameters. Optional parameters have a default value and
|
||||
-- don't require arguments for the function to be executed, required parameters must have an argument present.
|
||||
else case L.partition ppReq params of
|
||||
-- If the function only has required parameters, the arguments keys must match those parameters
|
||||
(reqParams, []) -> argumentsKeys == S.fromList (ppName <$> reqParams)
|
||||
-- If the function only has optional parameters, the arguments keys can match none or any of them(a subset)
|
||||
([], optParams) -> argumentsKeys `S.isSubsetOf` S.fromList (ppName <$> optParams)
|
||||
-- If the function has required and optional parameters, the arguments keys have to match the required parameters
|
||||
-- and can match any or none of the default parameters.
|
||||
(reqParams, optParams) -> argumentsKeys `S.difference` S.fromList (ppName <$> optParams) == S.fromList (ppName <$> reqParams)
|
||||
|
||||
-- | If raw(binary) output is requested, check that MediaType is one of the
|
||||
-- admitted rawMediaTypes and that`?select=...` contains only one field other
|
||||
-- than `*`
|
||||
binaryField :: AppConfig -> MediaType -> Target -> QueryParams -> Either ApiRequestError (Maybe FieldName)
|
||||
binaryField AppConfig{configRawMediaTypes} acceptMediaType target QueryParams{qsSelect}
|
||||
| returnsScalar target && isRawMediaType =
|
||||
Right $ Just "pgrst_scalar"
|
||||
| isRawMediaType =
|
||||
let
|
||||
fieldName = fstFieldName qsSelect
|
||||
in
|
||||
case fieldName of
|
||||
Just fld -> Right $ Just fld
|
||||
Nothing -> Left $ BinaryFieldError acceptMediaType
|
||||
| otherwise =
|
||||
Right Nothing
|
||||
where
|
||||
isRawMediaType = acceptMediaType `elem` configRawMediaTypes `union` [MTOctetStream, MTTextPlain, MTTextXML] || isRawPlan acceptMediaType
|
||||
isRawPlan mt = case mt of
|
||||
MTPlan (MTPlanAttrs (Just MTOctetStream) _ _) -> True
|
||||
MTPlan (MTPlanAttrs (Just MTTextPlain) _ _) -> True
|
||||
MTPlan (MTPlanAttrs (Just MTTextXML) _ _) -> True
|
||||
_ -> False
|
||||
returnsScalar :: Target -> Bool
|
||||
returnsScalar (TargetProc proc _) = procReturnsScalar proc
|
||||
returnsScalar _ = False
|
||||
|
||||
fstFieldName :: [Tree SelectItem] -> Maybe FieldName
|
||||
fstFieldName [Node SelectField{selField=("*", _)} []] = Nothing
|
||||
fstFieldName [Node SelectField{selField=(fld, _)} []] = Just fld
|
||||
fstFieldName _ = Nothing
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
-- |
|
||||
-- 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
|
||||
--
|
||||
module PostgREST.ApiRequest.Preferences
|
||||
( Preferences(..)
|
||||
, PreferCount(..)
|
||||
, PreferParameters(..)
|
||||
, PreferRepresentation(..)
|
||||
, PreferResolution(..)
|
||||
, PreferTransaction(..)
|
||||
, fromHeaders
|
||||
, ToAppliedHeader(..)
|
||||
, shouldCount
|
||||
) 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 Preferences
|
||||
|
||||
-- | Preferences recognized by the application.
|
||||
data Preferences
|
||||
= Preferences
|
||||
{ preferResolution :: Maybe PreferResolution
|
||||
, preferRepresentation :: Maybe PreferRepresentation
|
||||
, preferParameters :: Maybe PreferParameters
|
||||
, preferCount :: Maybe PreferCount
|
||||
, preferTransaction :: Maybe PreferTransaction
|
||||
}
|
||||
|
||||
-- |
|
||||
-- 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 [("Prefer", "resolution=ignore-duplicates, count=exact")]
|
||||
-- Preferences
|
||||
-- { preferResolution = Just IgnoreDuplicates
|
||||
-- , preferRepresentation = Nothing
|
||||
-- , preferParameters = Nothing
|
||||
-- , preferCount = Just ExactCount
|
||||
-- , preferTransaction = Nothing
|
||||
-- }
|
||||
--
|
||||
-- Multiple headers can also be used:
|
||||
--
|
||||
-- >>> pPrint $ fromHeaders [("Prefer", "resolution=ignore-duplicates"), ("Prefer", "count=exact")]
|
||||
-- Preferences
|
||||
-- { preferResolution = Just IgnoreDuplicates
|
||||
-- , preferRepresentation = Nothing
|
||||
-- , preferParameters = Nothing
|
||||
-- , preferCount = Just ExactCount
|
||||
-- , preferTransaction = Nothing
|
||||
-- }
|
||||
--
|
||||
-- If a preference is set more than once, only the first is used:
|
||||
--
|
||||
-- >>> preferTransaction $ fromHeaders [("Prefer", "tx=commit, tx=rollback")]
|
||||
-- Just Commit
|
||||
--
|
||||
-- This is also the case across multiple headers:
|
||||
--
|
||||
-- >>> :{
|
||||
-- preferResolution . fromHeaders $
|
||||
-- [ ("Prefer", "resolution=ignore-duplicates")
|
||||
-- , ("Prefer", "resolution=merge-duplicates")
|
||||
-- ]
|
||||
-- :}
|
||||
-- Just IgnoreDuplicates
|
||||
--
|
||||
-- Preferences not recognized by the application are ignored:
|
||||
--
|
||||
-- >>> preferResolution $ fromHeaders [("Prefer", "resolution=foo")]
|
||||
-- Nothing
|
||||
--
|
||||
-- Preferences can be separated by arbitrary amounts of space, lower-case header is also recognized:
|
||||
--
|
||||
-- >>> pPrint $ fromHeaders [("prefer", "count=exact, tx=commit ,return=minimal")]
|
||||
-- Preferences
|
||||
-- { preferResolution = Nothing
|
||||
-- , preferRepresentation = Just None
|
||||
-- , preferParameters = Nothing
|
||||
-- , preferCount = Just ExactCount
|
||||
-- , preferTransaction = Just Commit
|
||||
-- }
|
||||
--
|
||||
fromHeaders :: [HTTP.Header] -> Preferences
|
||||
fromHeaders headers =
|
||||
Preferences
|
||||
{ preferResolution = parsePrefs [MergeDuplicates, IgnoreDuplicates]
|
||||
, preferRepresentation = parsePrefs [Full, None, HeadersOnly]
|
||||
, preferParameters = parsePrefs [SingleObject, MultipleObjects]
|
||||
, preferCount = parsePrefs [ExactCount, PlannedCount, EstimatedCount]
|
||||
, preferTransaction = parsePrefs [Commit, Rollback]
|
||||
}
|
||||
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))
|
||||
|
||||
-- |
|
||||
-- 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
|
||||
|
||||
-- |
|
||||
-- Header to indicate that a preference has been applied.
|
||||
--
|
||||
-- >>> toAppliedHeader MergeDuplicates
|
||||
-- ("Preference-Applied","resolution=merge-duplicates")
|
||||
--
|
||||
class ToHeaderValue a => ToAppliedHeader a where
|
||||
toAppliedHeader :: a -> HTTP.Header
|
||||
toAppliedHeader x = (HTTP.hPreferenceApplied, toHeaderValue x)
|
||||
|
||||
-- | How to handle duplicate values.
|
||||
data PreferResolution
|
||||
= MergeDuplicates
|
||||
| IgnoreDuplicates
|
||||
|
||||
instance ToHeaderValue PreferResolution where
|
||||
toHeaderValue MergeDuplicates = "resolution=merge-duplicates"
|
||||
toHeaderValue IgnoreDuplicates = "resolution=ignore-duplicates"
|
||||
|
||||
instance ToAppliedHeader PreferResolution
|
||||
|
||||
-- |
|
||||
-- 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.
|
||||
| MultipleObjects -- ^ Pass an array of json objects as params to a stored procedure.
|
||||
deriving Eq
|
||||
|
||||
-- TODO: Deprecate params=multiple-objects in next major version
|
||||
instance ToHeaderValue PreferParameters where
|
||||
toHeaderValue SingleObject = "params=single-object"
|
||||
toHeaderValue MultipleObjects = "params=multiple-objects"
|
||||
|
||||
-- | 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"
|
||||
|
||||
instance ToAppliedHeader PreferTransaction
|
||||
@@ -0,0 +1,653 @@
|
||||
-- |
|
||||
-- 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.Parsec.Prim (parserFail)
|
||||
import Text.ParserCombinators.Parsec (GenParser, ParseError, Parser,
|
||||
anyChar, between, char, digit,
|
||||
eof, errorPos, letter,
|
||||
lookAhead, many1, noneOf,
|
||||
notFollowedBy, oneOf,
|
||||
optionMaybe, 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 (..),
|
||||
JoinType (..), JsonOperand (..),
|
||||
JsonOperation (..), JsonPath,
|
||||
ListVal, LogicOperator (..),
|
||||
LogicTree (..), OpExpr (..),
|
||||
Operation (..),
|
||||
OrderDirection (..),
|
||||
OrderNulls (..), OrderTerm (..),
|
||||
QPError (..), SelectItem (..),
|
||||
SimpleOperator (..), SingleVal,
|
||||
TrileanVal (..))
|
||||
|
||||
import Protolude hiding (try)
|
||||
|
||||
|
||||
-- $setup
|
||||
-- Setup for doctests
|
||||
-- >>> import Text.Pretty.Simple (pPrint)
|
||||
-- >>> deriving instance Show QPError
|
||||
-- >>> deriving instance Show TrileanVal
|
||||
-- >>> deriving instance Show FtsOperator
|
||||
-- >>> deriving instance Show SimpleOperator
|
||||
-- >>> deriving instance Show Operation
|
||||
-- >>> deriving instance Show OpExpr
|
||||
-- >>> deriving instance Show JsonOperand
|
||||
-- >>> deriving instance Show JsonOperation
|
||||
-- >>> deriving instance Show Filter
|
||||
-- >>> deriving instance Show JoinType
|
||||
-- >>> deriving instance Show SelectItem
|
||||
|
||||
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 "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 "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 "a.b=eq.0"
|
||||
-- Right [(["a"],Filter {field = ("b",[]), opExpr = OpExpr False (Op OpEqual "0")})]
|
||||
--
|
||||
-- If the operator specified in a filter does not exist, parsing the query string fails:
|
||||
--
|
||||
-- >>> qsFilters <$> parse "a.b=noop.0"
|
||||
-- Left (QPError "\"failed to parse filter (noop.0)\" (line 1, column 6)" "unknown single value operator noop")
|
||||
parse :: ByteString -> Either QPError QueryParams
|
||||
parse qs =
|
||||
QueryParams
|
||||
canonical
|
||||
params
|
||||
ranges
|
||||
<$> pRequestOrder `traverse` order
|
||||
<*> pRequestLogicTree `traverse` logic
|
||||
<*> pRequestColumns columns
|
||||
<*> pRequestSelect select
|
||||
<*> pRequestFilter `traverse` filters
|
||||
<*> (fmap snd <$> (pRequestFilter `traverse` filtersRoot))
|
||||
<*> pRequestFilter `traverse` filtersNotRoot
|
||||
<*> pure (S.fromList (fst <$> filters))
|
||||
<*> sequenceA (pRequestOnConflict <$> onConflict)
|
||||
where
|
||||
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, params) = L.partition isParam filtersAndParams
|
||||
isParam (k, v) = isEmbedPath k || hasOperator v || hasFtsOperator v
|
||||
|
||||
filtersAndParams = filter (isFilterOrParam . fst) nonemptyParams
|
||||
isFilterOrParam k = not (endingIn reservedEmbeddable k) && notElem k reserved
|
||||
reserved = ["select", "columns", "on_conflict"]
|
||||
reservedEmbeddable = ["order", "limit", "offset", "and", "or"]
|
||||
|
||||
(filtersNotRoot, filtersRoot) = L.partition isNotRoot filters
|
||||
isNotRoot = flip T.isInfixOf "." . fst
|
||||
|
||||
-- TODO: These checks are redundant to the parsers, should use parsers to differentiate params
|
||||
hasOperator val =
|
||||
case T.splitOn "." val of
|
||||
"not" : _ : _ -> True
|
||||
"is" : _ -> True
|
||||
"in" : _ -> True
|
||||
x : _ -> isJust (operator x) || isJust (ftsOperator x)
|
||||
_ -> False
|
||||
|
||||
hasFtsOperator val =
|
||||
case T.splitOn "(" val of
|
||||
x : _ : _ -> isJust $ ftsOperator x
|
||||
_ -> False
|
||||
|
||||
isEmbedPath = T.isInfixOf "."
|
||||
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]
|
||||
|
||||
operator :: Text -> Maybe SimpleOperator
|
||||
operator = \case
|
||||
"eq" -> Just OpEqual
|
||||
"gte" -> Just OpGreaterThanEqual
|
||||
"gt" -> Just OpGreaterThan
|
||||
"lte" -> Just OpLessThanEqual
|
||||
"lt" -> Just OpLessThan
|
||||
"neq" -> Just OpNotEqual
|
||||
"like" -> Just OpLike
|
||||
"ilike" -> Just OpILike
|
||||
"cs" -> Just OpContains
|
||||
"cd" -> Just OpContained
|
||||
"ov" -> Just OpOverlap
|
||||
"sl" -> Just OpStrictlyLeft
|
||||
"sr" -> Just OpStrictlyRight
|
||||
"nxr" -> Just OpNotExtendsRight
|
||||
"nxl" -> Just OpNotExtendsLeft
|
||||
"adj" -> Just OpAdjacent
|
||||
"match" -> Just OpMatch
|
||||
"imatch" -> Just OpIMatch
|
||||
_ -> Nothing
|
||||
|
||||
ftsOperator :: Text -> Maybe FtsOperator
|
||||
ftsOperator = \case
|
||||
"fts" -> Just FilterFts
|
||||
"plfts" -> Just FilterFtsPlain
|
||||
"phfts" -> Just FilterFtsPhrase
|
||||
"wfts" -> Just FilterFtsWebsearch
|
||||
_ -> Nothing
|
||||
|
||||
|
||||
-- PARSERS
|
||||
|
||||
|
||||
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)
|
||||
|
||||
pRequestFilter :: (Text, Text) -> Either QPError (EmbedPath, Filter)
|
||||
pRequestFilter (k, v) = mapError $ (,) <$> path <*> (Filter <$> fld <*> oper)
|
||||
where
|
||||
treePath = P.parse pTreePath ("failed to parse tree path (" ++ toS k ++ ")") $ toS k
|
||||
oper = P.parse (pOpExpr pSingleVal) ("failed to parse filter (" ++ toS v ++ ")") $ toS v
|
||||
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 "" "id,clients(name[])"
|
||||
-- Left (line 1, column 16):
|
||||
-- unexpected '['
|
||||
-- expecting letter, digit, "-", "!", "(", "->>", "->", "::", ")", "," or end of input
|
||||
pFieldForest :: Parser [Tree SelectItem]
|
||||
pFieldForest = pFieldTree `sepBy1` lexeme (char ',')
|
||||
where
|
||||
pFieldTree :: Parser (Tree SelectItem)
|
||||
pFieldTree = try (Node <$> pRelationSelect <*> between (char '(') (char ')') pFieldForest) <|>
|
||||
Node <$> pFieldSelect <*> pure []
|
||||
|
||||
pStar :: Parser Text
|
||||
pStar = string "*" $> "*"
|
||||
|
||||
-- |
|
||||
-- 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"}}]
|
||||
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 $ try ( do
|
||||
alias <- optionMaybe ( try(pFieldName <* aliasSeparator) )
|
||||
name <- pFieldName
|
||||
prm1 <- optionMaybe pEmbedParam
|
||||
prm2 <- optionMaybe pEmbedParam
|
||||
try (void $ lookAhead (string "("))
|
||||
return $ SelectRelation name alias (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 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
|
||||
alias <- optionMaybe ( try(pFieldName <* aliasSeparator) )
|
||||
fld <- pField
|
||||
cast' <- optionMaybe (string "::" *> pIdentifier)
|
||||
pEnd
|
||||
return $ SelectField fld (toS <$> cast') alias
|
||||
)
|
||||
<|> do
|
||||
s <- pStar
|
||||
pEnd
|
||||
return $ SelectField (s, []) Nothing Nothing
|
||||
where
|
||||
pEnd = try (void $ lookAhead (string ")")) <|>
|
||||
try (void $ lookAhead (string ",")) <|>
|
||||
try eof
|
||||
|
||||
-- |
|
||||
-- Parse operator expression used in horizontal filtering
|
||||
--
|
||||
-- >>> P.parse (pOpExpr pSingleVal) "" "fts().value"
|
||||
-- Left (line 1, column 7):
|
||||
-- unknown single value operator fts()
|
||||
pOpExpr :: Parser SingleVal -> Parser OpExpr
|
||||
pOpExpr pSVal = try ( string "not" *> pDelimiter *> (OpExpr True <$> pOperation)) <|> OpExpr False <$> pOperation
|
||||
where
|
||||
pOperation :: Parser Operation
|
||||
pOperation = pIn <|> pIs <|> try pFts <|> pOp <?> "operator (eq, gt, ...)"
|
||||
|
||||
pIn = In <$> (try (string "in" *> pDelimiter) *> pListVal)
|
||||
pIs = Is <$> (try (string "is" *> pDelimiter) *> pTriVal)
|
||||
|
||||
pOp = do
|
||||
opStr <- try (P.manyTill anyChar (try pDelimiter))
|
||||
op <- parseMaybe ("unknown single value operator " <> opStr) . operator $ toS opStr
|
||||
Op op <$> 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
|
||||
opStr <- try (P.many (noneOf ".("))
|
||||
op <- parseMaybe ("unknown fts operator " <> opStr) . ftsOperator $ toS opStr
|
||||
lang <- optionMaybe $ try (between (char '(') (char ')') pIdentifier)
|
||||
pDelimiter >> Fts op (toS <$> lang) <$> pSVal
|
||||
|
||||
parseMaybe :: [Char] -> Maybe a -> Parser a
|
||||
parseMaybe err Nothing = parserFail err
|
||||
parseMaybe _ (Just x) = pure x
|
||||
|
||||
-- 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 (.)"
|
||||
|
||||
pOrder :: Parser [OrderTerm]
|
||||
pOrder = lexeme pOrderTerm `sepBy1` char ','
|
||||
|
||||
pOrderTerm :: Parser OrderTerm
|
||||
pOrderTerm = do
|
||||
fld <- pField
|
||||
dir <- optionMaybe $
|
||||
try (pDelimiter *> string "asc" $> OrderAsc) <|>
|
||||
try (pDelimiter *> string "desc" $> OrderDesc)
|
||||
nls <- optionMaybe pNulls <* pEnd <|>
|
||||
pEnd $> Nothing
|
||||
return $ OrderTerm fld dir nls
|
||||
where
|
||||
pNulls = try (pDelimiter *> string "nullsfirst" $> OrderNullsFirst) <|>
|
||||
try (pDelimiter *> string "nullslast" $> OrderNullsLast)
|
||||
pEnd = try (void $ lookAhead (char ',')) <|>
|
||||
try eof
|
||||
|
||||
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,218 @@
|
||||
{-# 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 (..)
|
||||
, OrderDirection(..)
|
||||
, OrderNulls(..)
|
||||
, OrderTerm(..)
|
||||
, QPError(..)
|
||||
, RangeError(..)
|
||||
, SingleVal
|
||||
, TrileanVal(..)
|
||||
, SimpleOperator(..)
|
||||
, FtsOperator(..)
|
||||
, SelectItem(..)
|
||||
) where
|
||||
|
||||
import PostgREST.MediaType (MediaType (..))
|
||||
import PostgREST.SchemaCache.Identifiers (FieldName)
|
||||
import PostgREST.SchemaCache.Proc (ProcDescription (..))
|
||||
import PostgREST.SchemaCache.Relationship (Relationship)
|
||||
|
||||
import Protolude
|
||||
|
||||
-- | The select value in `/tbl?select=alias:field::cast`
|
||||
data SelectItem
|
||||
= SelectField
|
||||
{ selField :: Field
|
||||
, selCast :: Maybe Cast
|
||||
, selAlias :: Maybe Alias
|
||||
}
|
||||
| SelectRelation
|
||||
{ selRelation :: FieldName
|
||||
, selAlias :: Maybe Alias
|
||||
, selHint :: Maybe Hint
|
||||
, selJoinType :: Maybe JoinType
|
||||
}
|
||||
deriving (Eq)
|
||||
|
||||
data ApiRequestError
|
||||
= AmbiguousRelBetween Text Text [Relationship]
|
||||
| AmbiguousRpc [ProcDescription]
|
||||
| BinaryFieldError MediaType
|
||||
| MediaTypeError [ByteString]
|
||||
| InvalidBody ByteString
|
||||
| InvalidFilters
|
||||
| InvalidRange RangeError
|
||||
| InvalidRpcMethod ByteString
|
||||
| LimitNoOrderError
|
||||
| NotFound
|
||||
| NoRelBetween Text Text Text
|
||||
| NoRpc Text Text [Text] Bool MediaType Bool
|
||||
| NotEmbedded Text
|
||||
| ParseRequestError Text Text
|
||||
| PutRangeNotAllowedError
|
||||
| QueryParamError QPError
|
||||
| UnacceptableSchema [Text]
|
||||
| UnsupportedMethod ByteString
|
||||
|
||||
data QPError = QPError Text Text
|
||||
data RangeError
|
||||
= NegativeLimit
|
||||
| LowerGTUpper
|
||||
| OutOfBounds Text Text
|
||||
|
||||
type NodeName = Text
|
||||
type Depth = Integer
|
||||
|
||||
data OrderTerm = OrderTerm
|
||||
{ otTerm :: Field
|
||||
, otDirection :: Maybe OrderDirection
|
||||
, otNullOrder :: Maybe OrderNulls
|
||||
}
|
||||
deriving (Eq)
|
||||
|
||||
data OrderDirection
|
||||
= OrderAsc
|
||||
| OrderDesc
|
||||
deriving (Eq)
|
||||
|
||||
data OrderNulls
|
||||
= OrderNullsFirst
|
||||
| OrderNullsLast
|
||||
deriving (Eq)
|
||||
|
||||
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
|
||||
|
||||
-- | 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)
|
||||
|
||||
-- | 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)
|
||||
|
||||
-- | 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)
|
||||
|
||||
data LogicOperator
|
||||
= And
|
||||
| Or
|
||||
deriving Eq
|
||||
|
||||
data Filter = Filter
|
||||
{ field :: Field
|
||||
, opExpr :: OpExpr
|
||||
}
|
||||
deriving (Eq)
|
||||
|
||||
data OpExpr =
|
||||
OpExpr Bool Operation
|
||||
deriving (Eq)
|
||||
|
||||
data Operation
|
||||
= Op SimpleOperator SingleVal
|
||||
| In ListVal
|
||||
| Is TrileanVal
|
||||
| Fts FtsOperator (Maybe Language) SingleVal
|
||||
deriving (Eq)
|
||||
|
||||
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
|
||||
|
||||
data SimpleOperator
|
||||
= OpEqual
|
||||
| OpGreaterThanEqual
|
||||
| OpGreaterThan
|
||||
| OpLessThanEqual
|
||||
| OpLessThan
|
||||
| OpNotEqual
|
||||
| OpLike
|
||||
| OpILike
|
||||
| OpContains
|
||||
| OpContained
|
||||
| OpOverlap
|
||||
| OpStrictlyLeft
|
||||
| OpStrictlyRight
|
||||
| OpNotExtendsRight
|
||||
| OpNotExtendsLeft
|
||||
| OpAdjacent
|
||||
| OpMatch
|
||||
| OpIMatch
|
||||
deriving Eq
|
||||
|
||||
-- | Operators for full text search operators
|
||||
data FtsOperator
|
||||
= FilterFts
|
||||
| FilterFtsPlain
|
||||
| FilterFtsPhrase
|
||||
| FilterFtsWebsearch
|
||||
deriving Eq
|
||||
+179
-384
@@ -9,411 +9,206 @@ Some of its functionality includes:
|
||||
- Producing HTTP Headers according to RFCs.
|
||||
- Content Negotiation
|
||||
-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE MultiWayIf #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
module PostgREST.App
|
||||
( SignalHandlerInstaller
|
||||
, SocketRunner
|
||||
, postgrest
|
||||
, run
|
||||
) where
|
||||
|
||||
module PostgREST.App (
|
||||
postgrest
|
||||
) where
|
||||
|
||||
import qualified Data.ByteString.Char8 as BS
|
||||
import qualified Data.HashMap.Strict as M
|
||||
import qualified Data.List as L (union)
|
||||
import qualified Data.Set as S
|
||||
import qualified Hasql.Pool as P
|
||||
import qualified Hasql.Transaction as H
|
||||
import qualified Hasql.Transaction as HT
|
||||
import qualified Hasql.Transaction.Sessions as HT
|
||||
import Control.Monad.Except (liftEither)
|
||||
import Data.Either.Combinators (mapLeft)
|
||||
import Data.Maybe (fromJust)
|
||||
import Data.String (IsString (..))
|
||||
import Network.Wai.Handler.Warp (defaultSettings, setHost, setPort,
|
||||
setServerName)
|
||||
import System.Posix.Types (FileMode)
|
||||
|
||||
import Data.Function (id)
|
||||
import Data.IORef (IORef, readIORef)
|
||||
import Data.Time.Clock (UTCTime)
|
||||
import Network.HTTP.Types.URI (renderSimpleQuery)
|
||||
import Network.Wai.Middleware.RequestLogger (logStdout)
|
||||
import qualified Hasql.Transaction.Sessions as SQL
|
||||
import qualified Network.Wai as Wai
|
||||
import qualified Network.Wai.Handler.Warp as Warp
|
||||
|
||||
import Control.Applicative
|
||||
import Data.Maybe
|
||||
import Network.HTTP.Types.Header
|
||||
import Network.HTTP.Types.Status
|
||||
import Network.Wai
|
||||
import qualified PostgREST.ApiRequest as ApiRequest
|
||||
import qualified PostgREST.ApiRequest.Types as ApiRequestTypes
|
||||
import qualified PostgREST.AppState as AppState
|
||||
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 qualified PostgREST.Workers as Workers
|
||||
|
||||
import PostgREST.ApiRequest (Action (..), ApiRequest (..),
|
||||
InvokeMethod (..), Target (..),
|
||||
mutuallyAgreeable, userApiRequest)
|
||||
import PostgREST.Auth (containsRole, jwtClaims,
|
||||
parseSecret)
|
||||
import PostgREST.Config (AppConfig (..))
|
||||
import PostgREST.DbRequestBuilder (mutateRequest, readRequest,
|
||||
returningCols)
|
||||
import PostgREST.DbStructure
|
||||
import PostgREST.Error (PgError (..), SimpleError (..),
|
||||
errorResponseFor, singularityError)
|
||||
import PostgREST.Middleware
|
||||
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)
|
||||
Mutation (..), Target (..))
|
||||
import PostgREST.AppState (AppState)
|
||||
import PostgREST.Auth (AuthResult (..))
|
||||
import PostgREST.Config (AppConfig (..), LogLevel (..))
|
||||
import PostgREST.Config.PgVersion (PgVersion (..))
|
||||
import PostgREST.Error (Error)
|
||||
import PostgREST.Query (DbHandler)
|
||||
import PostgREST.SchemaCache (SchemaCache (..))
|
||||
import PostgREST.Version (prettyVersion)
|
||||
|
||||
postgrest :: AppConfig -> IORef (Maybe DbStructure) -> P.Pool -> IO UTCTime -> IO () -> Application
|
||||
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
|
||||
import Protolude hiding (Handler)
|
||||
|
||||
transactionMode :: Maybe ProcDescription -> Action -> HT.Mode
|
||||
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
|
||||
type Handler = ExceptT Error
|
||||
|
||||
app :: DbStructure -> Maybe ProcDescription -> S.Set FieldName -> AppConfig -> ApiRequest -> H.Transaction Response
|
||||
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
|
||||
type SignalHandlerInstaller = AppState -> IO()
|
||||
|
||||
(ActionRead headersOnly, TargetIdent (QualifiedIdentifier tSchema tName), Nothing) ->
|
||||
case readSqlParts tSchema tName of
|
||||
Left errorResponse -> return errorResponse
|
||||
Right (q, cq, bField, _) -> do
|
||||
let cQuery = if estimatedCount
|
||||
then limitedQuery cq ((+ 1) <$> maxRows) -- LIMIT maxRows + 1 so we can determine below that maxRows was surpassed
|
||||
else cq
|
||||
stm = createReadStatement q cQuery (contentType == CTSingularJSON) shouldCount
|
||||
(contentType == CTTextCSV) bField pgVer
|
||||
explStm = createExplainStatement cq
|
||||
row <- H.statement () stm
|
||||
let (tableTotal, queryTotal, _ , body, gucHeaders) = row
|
||||
case gucHeaders of
|
||||
Left _ -> return . errorResponseFor $ GucHeadersError
|
||||
Right ghdrs -> do
|
||||
total <- if | plannedCount -> H.statement () explStm
|
||||
| estimatedCount -> if tableTotal > (fromIntegral <$> maxRows)
|
||||
then do estTotal <- H.statement () explStm
|
||||
pure $ if estTotal > tableTotal then estTotal else tableTotal
|
||||
else pure tableTotal
|
||||
| otherwise -> pure tableTotal
|
||||
let (status, contentRange) = rangeStatusHeader topLevelRange queryTotal total
|
||||
headers = addHeadersIfNotIncluded (catMaybes [
|
||||
Just $ toHeader contentType, Just contentRange,
|
||||
Just $ contentLocationH tName (iCanonicalQS apiRequest), profileH])
|
||||
(unwrapGucHeader <$> ghdrs)
|
||||
rBody = if headersOnly then mempty else toS body
|
||||
return $
|
||||
if contentType == CTSingularJSON && queryTotal /= 1
|
||||
then errorResponseFor . singularityError $ queryTotal
|
||||
else responseLBS status headers rBody
|
||||
type SocketRunner = Warp.Settings -> Wai.Application -> FileMode -> FilePath -> IO()
|
||||
|
||||
(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
|
||||
(ctHeaders, rBody) = if iPreferRepresentation apiRequest == Full
|
||||
then ([Just $ toHeader contentType, profileH], toS body)
|
||||
else ([], mempty)
|
||||
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
|
||||
run :: SignalHandlerInstaller -> Maybe SocketRunner -> AppState -> IO ()
|
||||
run installHandlers maybeRunWithSocket appState = do
|
||||
conf@AppConfig{..} <- AppState.getConfig appState
|
||||
Workers.connectionWorker appState -- Loads the initial SchemaCache
|
||||
installHandlers appState
|
||||
-- reload schema cache + config on NOTIFY
|
||||
Workers.runListener conf appState
|
||||
|
||||
(ActionUpdate, TargetIdent (QualifiedIdentifier tSchema tName), Just pJson) ->
|
||||
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 (toS $ pjRaw pJson) stm
|
||||
let (_, queryTotal, _, body, gucHeaders) = row
|
||||
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
|
||||
Workers.runAdmin conf appState $ serverSettings conf
|
||||
|
||||
(ActionSingleUpsert, TargetIdent (QualifiedIdentifier tSchema tName), Just pJson) ->
|
||||
case mutateSqlParts tSchema tName of
|
||||
Left errorResponse -> return errorResponse
|
||||
Right (sq, mq) ->
|
||||
if topLevelRange /= allRange
|
||||
then return . errorResponseFor $ PutRangeNotAllowedError
|
||||
else do
|
||||
row <- H.statement (toS $ pjRaw pJson) $
|
||||
createWriteStatement sq mq (contentType == CTSingularJSON) False
|
||||
(contentType == CTTextCSV) (iPreferRepresentation apiRequest) [] pgVer
|
||||
let (_, queryTotal, _, body, gucHeaders) = row
|
||||
case gucHeaders of
|
||||
Left _ -> return . errorResponseFor $ GucHeadersError
|
||||
Right ghdrs -> do
|
||||
let headers = addHeadersIfNotIncluded (catMaybes [Just $ toHeader contentType, profileH]) (unwrapGucHeader <$> ghdrs)
|
||||
(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
|
||||
let app = postgrest configLogLevel appState (Workers.connectionWorker appState)
|
||||
|
||||
(ActionDelete, TargetIdent (QualifiedIdentifier tSchema tName), Nothing) ->
|
||||
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
|
||||
case configServerUnixSocket of
|
||||
Just socket ->
|
||||
-- run the postgrest application with user defined socket. Only for UNIX systems
|
||||
case maybeRunWithSocket of
|
||||
Just runWithSocket -> do
|
||||
AppState.logWithZTime appState $ "Listening on unix socket " <> show socket
|
||||
runWithSocket (serverSettings conf) app configServerUnixSocketMode socket
|
||||
Nothing ->
|
||||
panic "Cannot run with unix socket on non-unix platforms."
|
||||
Nothing ->
|
||||
do
|
||||
AppState.logWithZTime appState $ "Listening on port " <> show configServerPort
|
||||
Warp.runSettings (serverSettings conf) app
|
||||
|
||||
(ActionInfo, TargetIdent (QualifiedIdentifier tSchema tTable), Nothing) ->
|
||||
let mTable = find (\t -> tableName t == tTable && tableSchema t == tSchema) (dbTables dbStructure) in
|
||||
case mTable of
|
||||
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
|
||||
serverSettings :: AppConfig -> Warp.Settings
|
||||
serverSettings AppConfig{..} =
|
||||
defaultSettings
|
||||
& setHost (fromString $ toS configServerHost)
|
||||
& setPort configServerPort
|
||||
& setServerName ("postgrest/" <> prettyVersion)
|
||||
|
||||
(ActionInvoke invMethod, TargetProc qi@(QualifiedIdentifier tSchema pName) _, Just pJson) ->
|
||||
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
|
||||
-- | PostgREST application
|
||||
postgrest :: LogLevel -> AppState.AppState -> IO () -> Wai.Application
|
||||
postgrest logLevel appState connWorker =
|
||||
Cors.middleware .
|
||||
Auth.middleware appState .
|
||||
Logger.middleware logLevel $
|
||||
-- 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
|
||||
conf <- AppState.getConfig appState
|
||||
maybeSchemaCache <- AppState.getSchemaCache appState
|
||||
pgVer <- AppState.getPgVersion appState
|
||||
jsonDbS <- AppState.getJsonDbS appState
|
||||
|
||||
(ActionInspect headersOnly, TargetDefaultSpec tSchema, Nothing) -> do
|
||||
let host = configHost conf
|
||||
port = toInteger $ configPort conf
|
||||
proxy = pickProxy $ toS <$> configOpenAPIProxyUri conf
|
||||
uri Nothing = ("http", host, port, "/")
|
||||
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
|
||||
let
|
||||
eitherResponse :: IO (Either Error Wai.Response)
|
||||
eitherResponse =
|
||||
runExceptT $ postgrestResponse appState conf maybeSchemaCache jsonDbS pgVer authResult req
|
||||
|
||||
body <- encodeApi <$>
|
||||
H.statement tSchema accessibleTables <*>
|
||||
H.statement tSchema schemaDescription <*>
|
||||
H.statement tSchema accessibleProcs
|
||||
return $ responseLBS status200 (catMaybes [Just $ toHeader CTOpenAPI, profileH]) (if headersOnly then mempty else toS body)
|
||||
response <- either Error.errorResponseFor identity <$> eitherResponse
|
||||
-- Launch the connWorker when the connection is down. The postgrest
|
||||
-- function can respond successfully (with a stale schema cache) before
|
||||
-- the connWorker is done.
|
||||
when (Response.isServiceUnavailable response) connWorker
|
||||
resp <- do
|
||||
delay <- AppState.getRetryNextIn appState
|
||||
return $ Response.addRetryHint delay response
|
||||
respond resp
|
||||
|
||||
_ -> return notFound
|
||||
postgrestResponse
|
||||
:: AppState.AppState
|
||||
-> AppConfig
|
||||
-> Maybe SchemaCache
|
||||
-> ByteString
|
||||
-> PgVersion
|
||||
-> AuthResult
|
||||
-> Wai.Request
|
||||
-> Handler IO Wai.Response
|
||||
postgrestResponse appState conf@AppConfig{..} maybeSchemaCache jsonDbS pgVer authResult@AuthResult{..} req = do
|
||||
sCache <-
|
||||
case maybeSchemaCache of
|
||||
Just sCache ->
|
||||
return sCache
|
||||
Nothing ->
|
||||
throwError Error.NoSchemaCacheError
|
||||
|
||||
where
|
||||
notFound = responseLBS status404 [] ""
|
||||
maxRows = configMaxRows conf
|
||||
exactCount = iPreferCount apiRequest == Just ExactCount
|
||||
estimatedCount = iPreferCount apiRequest == Just EstimatedCount
|
||||
plannedCount = iPreferCount apiRequest == Just PlannedCount
|
||||
shouldCount = exactCount || estimatedCount
|
||||
topLevelRange = iTopLevelRange apiRequest
|
||||
returnsScalar = maybe False procReturnsScalar proc
|
||||
pgVer = pgVersion dbStructure
|
||||
profileH = contentProfileH <$> iProfile apiRequest
|
||||
body <- lift $ Wai.strictRequestBody req
|
||||
|
||||
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)
|
||||
apiRequest <-
|
||||
liftEither . mapLeft Error.ApiRequestError $
|
||||
ApiRequest.userApiRequest conf sCache req body
|
||||
|
||||
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)
|
||||
Response.optionalRollback conf apiRequest $
|
||||
handleRequest authResult conf appState (Query.txMode apiRequest) (Just authRole /= configDbAnonRole) configDbPreparedStatements jsonDbS pgVer apiRequest sCache
|
||||
|
||||
responseContentTypeOrError :: [ContentType] -> [ContentType] -> Action -> Target -> Either Response ContentType
|
||||
responseContentTypeOrError accepts rawContentTypes action target = serves contentTypesForRequest accepts
|
||||
runDbHandler :: AppState.AppState -> SQL.Mode -> Bool -> Bool -> DbHandler b -> Handler IO b
|
||||
runDbHandler appState mode authenticated prepared handler = do
|
||||
dbResp <-
|
||||
let transaction = if prepared then SQL.transaction else SQL.unpreparedTransaction in
|
||||
lift . AppState.usePool appState . transaction SQL.ReadCommitted mode $ runExceptT handler
|
||||
|
||||
resp <-
|
||||
liftEither . mapLeft Error.PgErr $
|
||||
mapLeft (Error.PgError authenticated) dbResp
|
||||
|
||||
liftEither resp
|
||||
|
||||
handleRequest :: AuthResult -> AppConfig -> AppState.AppState -> SQL.Mode -> Bool -> Bool -> ByteString -> PgVersion -> ApiRequest -> SchemaCache -> Handler IO Wai.Response
|
||||
handleRequest AuthResult{..} conf appState mode authenticated prepared jsonDbS pgVer apiReq@ApiRequest{..} sCache =
|
||||
case (iAction, iTarget) of
|
||||
(ActionRead headersOnly, TargetIdent identifier) -> do
|
||||
rPlan <- liftEither $ Plan.readPlan identifier conf sCache apiReq
|
||||
resultSet <- runQuery $ Query.readQuery rPlan 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 $ 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 $ 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 $ 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 $ Query.deleteQuery mrPlan apiReq conf
|
||||
return $ Response.deleteResponse apiReq resultSet
|
||||
|
||||
(ActionInvoke invMethod, TargetProc proc _) -> do
|
||||
cPlan <- liftEither $ Plan.callReadPlan proc conf sCache apiReq
|
||||
resultSet <- runQuery $ Query.invokeQuery proc cPlan apiReq conf
|
||||
return $ Response.invokeResponse invMethod proc apiReq resultSet
|
||||
|
||||
(ActionInspect headersOnly, TargetDefaultSpec tSchema) -> do
|
||||
oaiResult <- runQuery $ Query.openApiQuery sCache pgVer conf tSchema
|
||||
return $ Response.openApiResponse headersOnly oaiResult conf sCache iSchema iNegotiatedByProfile
|
||||
|
||||
(ActionInfo, _) ->
|
||||
return $ Response.infoResponse iTarget sCache
|
||||
|
||||
_ ->
|
||||
-- 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
|
||||
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)
|
||||
runQuery query =
|
||||
runDbHandler appState mode authenticated prepared $ do
|
||||
Query.setPgLocals conf authClaims authRole apiReq jsonDbS pgVer
|
||||
query
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
|
||||
module PostgREST.AppState
|
||||
( AppState
|
||||
, destroy
|
||||
, flushPool
|
||||
, getConfig
|
||||
, getSchemaCache
|
||||
, getIsListenerOn
|
||||
, getJsonDbS
|
||||
, getMainThreadId
|
||||
, getPgVersion
|
||||
, getRetryNextIn
|
||||
, getTime
|
||||
, getWorkerSem
|
||||
, init
|
||||
, initWithPool
|
||||
, logWithZTime
|
||||
, putConfig
|
||||
, putSchemaCache
|
||||
, putIsListenerOn
|
||||
, putJsonDbS
|
||||
, putPgVersion
|
||||
, putRetryNextIn
|
||||
, signalListener
|
||||
, usePool
|
||||
, waitListener
|
||||
) where
|
||||
|
||||
import qualified Hasql.Pool as SQL
|
||||
import qualified Hasql.Session as SQL
|
||||
|
||||
import Control.AutoUpdate (defaultUpdateSettings, mkAutoUpdate,
|
||||
updateAction)
|
||||
import Data.IORef (IORef, atomicWriteIORef, newIORef,
|
||||
readIORef)
|
||||
import Data.Time (ZonedTime, defaultTimeLocale, formatTime,
|
||||
getZonedTime)
|
||||
import Data.Time.Clock (UTCTime, getCurrentTime)
|
||||
|
||||
import PostgREST.Config (AppConfig (..))
|
||||
import PostgREST.Config.PgVersion (PgVersion (..), minimumPgVersion)
|
||||
import PostgREST.SchemaCache (SchemaCache)
|
||||
|
||||
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)
|
||||
-- | Cached SchemaCache in json
|
||||
, stateJsonDbS :: IORef ByteString
|
||||
-- | Binary semaphore to make sure just one connectionWorker can run at a time
|
||||
, stateWorkerSem :: MVar ()
|
||||
-- | 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
|
||||
}
|
||||
|
||||
init :: AppConfig -> IO AppState
|
||||
init conf = do
|
||||
pool <- initPool conf
|
||||
initWithPool pool conf
|
||||
|
||||
initWithPool :: SQL.Pool -> AppConfig -> IO AppState
|
||||
initWithPool pool conf =
|
||||
AppState pool
|
||||
<$> newIORef minimumPgVersion -- assume we're in a supported version when starting, this will be corrected on a later step
|
||||
<*> newIORef Nothing
|
||||
<*> newIORef mempty
|
||||
<*> newEmptyMVar
|
||||
<*> newEmptyMVar
|
||||
<*> newIORef False
|
||||
<*> newIORef conf
|
||||
<*> mkAutoUpdate defaultUpdateSettings { updateAction = getCurrentTime }
|
||||
<*> mkAutoUpdate defaultUpdateSettings { updateAction = getZonedTime }
|
||||
<*> myThreadId
|
||||
<*> newIORef 0
|
||||
|
||||
destroy :: AppState -> IO ()
|
||||
destroy = destroyPool
|
||||
|
||||
initPool :: AppConfig -> IO SQL.Pool
|
||||
initPool AppConfig{..} =
|
||||
SQL.acquire configDbPoolSize timeoutMilliseconds $ toUtf8 configDbUri
|
||||
where
|
||||
timeoutMilliseconds = (* oneSecond) <$> configDbPoolAcquisitionTimeout
|
||||
oneSecond = 1000000
|
||||
|
||||
-- | Run an action with a database connection.
|
||||
usePool :: AppState -> SQL.Session a -> IO (Either SQL.UsageError a)
|
||||
usePool AppState{..} = SQL.use statePool
|
||||
|
||||
-- | 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)
|
||||
|
||||
getJsonDbS :: AppState -> IO ByteString
|
||||
getJsonDbS = readIORef . stateJsonDbS
|
||||
|
||||
putJsonDbS :: AppState -> ByteString -> IO ()
|
||||
putJsonDbS appState = atomicWriteIORef (stateJsonDbS appState)
|
||||
|
||||
getWorkerSem :: AppState -> MVar ()
|
||||
getWorkerSem = stateWorkerSem
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
+96
-89
@@ -1,5 +1,3 @@
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-|
|
||||
Module : PostgREST.Auth
|
||||
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
|
||||
very simple authentication system inside the PostgreSQL database.
|
||||
-}
|
||||
module PostgREST.Auth (
|
||||
containsRole
|
||||
, jwtClaims
|
||||
, JWTAttempt(..)
|
||||
, parseSecret
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
module PostgREST.Auth
|
||||
( AuthResult (..)
|
||||
, getResult
|
||||
, getRole
|
||||
, middleware
|
||||
) where
|
||||
|
||||
import qualified Crypto.JOSE.Types as JOSE.Types
|
||||
import qualified Data.Aeson as JSON
|
||||
import qualified Data.HashMap.Strict as M
|
||||
import Data.Vector as V
|
||||
import qualified Crypto.JWT as JWT
|
||||
import qualified Data.Aeson as JSON
|
||||
import qualified Data.Aeson.Key as K
|
||||
import qualified Data.Aeson.KeyMap as KM
|
||||
import qualified Data.Aeson.Types as JSON
|
||||
import qualified Data.ByteString.Lazy.Char8 as LBS
|
||||
import qualified Data.Text.Encoding as T
|
||||
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 Data.Time.Clock (UTCTime)
|
||||
import Control.Lens (set)
|
||||
import Control.Monad.Except (liftEither)
|
||||
import Data.Either.Combinators (mapLeft)
|
||||
import Data.List (lookup)
|
||||
import Data.Time.Clock (UTCTime)
|
||||
import System.IO.Unsafe (unsafePerformIO)
|
||||
|
||||
import Control.Lens.Operators
|
||||
import Crypto.JWT
|
||||
import PostgREST.AppState (AppState, getConfig, getTime)
|
||||
import PostgREST.Config (AppConfig (..), JSPath, JSPathExp (..))
|
||||
import PostgREST.Error (Error (..))
|
||||
|
||||
import PostgREST.Types
|
||||
import Protolude hiding (toS)
|
||||
import Protolude.Conv (toS)
|
||||
import Protolude
|
||||
|
||||
{-|
|
||||
Possible situations encountered with client JWTs
|
||||
-}
|
||||
data JWTAttempt = JWTInvalid JWTError
|
||||
| JWTMissingSecret
|
||||
| JWTClaims (M.HashMap Text JSON.Value)
|
||||
deriving (Eq, Show)
|
||||
|
||||
{-|
|
||||
Receives the JWT secret and audience (from config) and a JWT and returns a map
|
||||
of JWT claims.
|
||||
-}
|
||||
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
|
||||
data AuthResult = AuthResult
|
||||
{ authClaims :: KM.KeyMap JSON.Value
|
||||
, authRole :: Text
|
||||
}
|
||||
|
||||
{-|
|
||||
Turn JWT ClaimSet into something easier to work with,
|
||||
also here the jspath is applied to put the "role" in the map
|
||||
-}
|
||||
claims2map :: ClaimsSet -> Maybe JSPath -> M.HashMap Text JSON.Value
|
||||
claims2map claims jspath = (\case
|
||||
val@(JSON.Object o) ->
|
||||
let role = maybe M.empty (M.singleton "role") $
|
||||
walkJSPath (Just val) =<< jspath in
|
||||
M.delete "role" o `M.union` role -- mutating the map
|
||||
_ -> M.empty
|
||||
) $ JSON.toJSON claims
|
||||
-- | Receives the JWT secret and audience (from config) and a JWT and returns a
|
||||
-- JSON object of JWT claims.
|
||||
parseToken :: Monad m =>
|
||||
AppConfig -> LByteString -> UTCTime -> ExceptT Error m JSON.Value
|
||||
parseToken _ "" _ = return JSON.emptyObject
|
||||
parseToken AppConfig{..} token time = do
|
||||
secret <- liftEither . maybeToRight JwtTokenMissing $ configJWKS
|
||||
eitherClaims <-
|
||||
lift . runExceptT $
|
||||
JWT.verifyClaimsAt validation secret time =<< JWT.decodeCompact token
|
||||
liftEither . mapLeft jwtClaimsError $ JSON.toJSON <$> eitherClaims
|
||||
where
|
||||
validation =
|
||||
JWT.defaultJWTValidationSettings audienceCheck & set JWT.allowedSkew 1
|
||||
|
||||
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
|
||||
audienceCheck :: JWT.StringOrURI -> Bool
|
||||
audienceCheck = maybe (const True) (==) configJwtAudience
|
||||
|
||||
{-|
|
||||
Whether a response from jwtClaims contains a role claim
|
||||
-}
|
||||
containsRole :: JWTAttempt -> Bool
|
||||
containsRole (JWTClaims claims) = M.member "role" claims
|
||||
containsRole _ = False
|
||||
jwtClaimsError :: JWT.JWTError -> Error
|
||||
jwtClaimsError JWT.JWTExpired = JwtTokenInvalid "JWT expired"
|
||||
jwtClaimsError e = JwtTokenInvalid $ show e
|
||||
|
||||
{-|
|
||||
Parse `jwt-secret` configuration option and turn into a JWKSet.
|
||||
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 role)
|
||||
, authRole = role
|
||||
}
|
||||
where
|
||||
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
|
||||
|
||||
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
|
||||
maybeJWKSet = JSON.decode (toS str) :: Maybe JWKSet
|
||||
maybeJWK = JSON.decode (toS str) :: Maybe JWK
|
||||
secret = JWKSet [jwkFromSecret str]
|
||||
unquoted :: JSON.Value -> Text
|
||||
unquoted (JSON.String t) = t
|
||||
unquoted v = T.decodeUtf8 . LBS.toStrict $ JSON.encode v
|
||||
-- impossible case - just added to please -Wincomplete-patterns
|
||||
parseClaims _ _ = return AuthResult { authClaims = KM.empty, authRole = mempty }
|
||||
|
||||
{-|
|
||||
Internal helper to generate a symmetric HMAC-SHA256 JWK from a text secret.
|
||||
-}
|
||||
jwkFromSecret :: ByteString -> JWK
|
||||
jwkFromSecret key =
|
||||
fromKeyMaterial km
|
||||
& jwkUse ?~ Sig
|
||||
& jwkAlg ?~ JWSAlg HS256
|
||||
where
|
||||
km = OctKeyMaterial (OctKeyParameters (JOSE.Types.Base64Octets key))
|
||||
-- | 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 Text
|
||||
getRole req = authRole <$> (rightToMaybe =<< getResult req)
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
module PostgREST.CLI
|
||||
( main
|
||||
, CLI (..)
|
||||
, Command (..)
|
||||
, readCLIShowHelp
|
||||
) where
|
||||
|
||||
import qualified Data.Aeson as JSON
|
||||
import qualified Data.ByteString.Char8 as BS
|
||||
import qualified Data.ByteString.Lazy as LBS
|
||||
import qualified Hasql.Transaction.Sessions as SQL
|
||||
import qualified Options.Applicative as O
|
||||
|
||||
import Data.Text.IO (hPutStrLn)
|
||||
import Text.Heredoc (str)
|
||||
|
||||
import PostgREST.AppState (AppState)
|
||||
import PostgREST.Config (AppConfig (..))
|
||||
import PostgREST.SchemaCache (querySchemaCache)
|
||||
import PostgREST.Version (prettyVersion)
|
||||
import PostgREST.Workers (reReadConfig)
|
||||
|
||||
import qualified PostgREST.App as App
|
||||
import qualified PostgREST.AppState as AppState
|
||||
import qualified PostgREST.Config as Config
|
||||
|
||||
import Protolude hiding (hPutStrLn)
|
||||
|
||||
|
||||
main :: App.SignalHandlerInstaller -> Maybe App.SocketRunner -> CLI -> IO ()
|
||||
main installSignalHandlers runAppWithSocket CLI{cliCommand, cliPath} = do
|
||||
conf@AppConfig{..} <-
|
||||
either panic identity <$> Config.readAppConfig mempty cliPath Nothing
|
||||
|
||||
-- Per https://github.com/PostgREST/postgrest/issues/268, we want to
|
||||
-- explicitly close the connections to PostgreSQL on shutdown.
|
||||
-- 'AppState.destroy' takes care of that.
|
||||
bracket
|
||||
(AppState.init conf)
|
||||
AppState.destroy
|
||||
(\appState -> case cliCommand of
|
||||
CmdDumpConfig -> do
|
||||
when configDbConfig $ reReadConfig True appState
|
||||
putStr . Config.toText =<< AppState.getConfig appState
|
||||
CmdDumpSchema -> putStrLn =<< dumpSchema appState
|
||||
CmdRun -> App.run installSignalHandlers runAppWithSocket appState)
|
||||
|
||||
-- | Dump SchemaCache schema to JSON
|
||||
dumpSchema :: AppState -> IO LBS.ByteString
|
||||
dumpSchema appState = do
|
||||
AppConfig{..} <- AppState.getConfig appState
|
||||
result <-
|
||||
let transaction = if configDbPreparedStatements then SQL.transaction else SQL.unpreparedTransaction in
|
||||
AppState.usePool appState $
|
||||
transaction SQL.ReadCommitted SQL.Read $
|
||||
querySchemaCache
|
||||
(toList configDbSchemas)
|
||||
configDbExtraSearchPath
|
||||
configDbPreparedStatements
|
||||
case result of
|
||||
Left e -> do
|
||||
hPutStrLn stderr $ "An error ocurred when loading the schema cache:\n" <> show e
|
||||
exitFailure
|
||||
Right sCache -> return $ JSON.encode sCache
|
||||
|
||||
-- | Command line interface options
|
||||
data CLI = CLI
|
||||
{ cliCommand :: Command
|
||||
, cliPath :: Maybe FilePath
|
||||
}
|
||||
|
||||
data Command
|
||||
= CmdRun
|
||||
| CmdDumpConfig
|
||||
| CmdDumpSchema
|
||||
|
||||
-- | Read command line interface options. Also prints help.
|
||||
readCLIShowHelp :: IO CLI
|
||||
readCLIShowHelp =
|
||||
O.customExecParser prefs opts
|
||||
where
|
||||
prefs = O.prefs $ O.showHelpOnError <> O.showHelpOnEmpty
|
||||
opts = O.info parser $ O.fullDesc <> progDesc
|
||||
parser = O.helper <*> exampleParser <*> cliParser
|
||||
|
||||
progDesc =
|
||||
O.progDesc $
|
||||
"PostgREST "
|
||||
<> BS.unpack prettyVersion
|
||||
<> " / create a REST API to an existing Postgres database"
|
||||
|
||||
exampleParser =
|
||||
O.infoOption exampleConfigFile $
|
||||
O.long "example"
|
||||
<> O.short 'e'
|
||||
<> O.help "Show an example configuration file"
|
||||
|
||||
cliParser :: O.Parser CLI
|
||||
cliParser =
|
||||
CLI
|
||||
<$> (dumpConfigFlag <|> dumpSchemaFlag)
|
||||
<*> O.optional configFileOption
|
||||
|
||||
configFileOption =
|
||||
O.strArgument $
|
||||
O.metavar "FILENAME"
|
||||
<> O.help "Path to configuration file"
|
||||
|
||||
dumpConfigFlag =
|
||||
O.flag CmdRun CmdDumpConfig $
|
||||
O.long "dump-config"
|
||||
<> O.help "Dump loaded configuration and exit"
|
||||
|
||||
dumpSchemaFlag =
|
||||
O.flag CmdRun CmdDumpSchema $
|
||||
O.long "dump-schema"
|
||||
<> O.help "Dump loaded schema as JSON and exit (for debugging, output structure is unstable)"
|
||||
|
||||
exampleConfigFile :: [Char]
|
||||
exampleConfigFile =
|
||||
[str|## Admin server used for checks. It's disabled by default unless a port is specified.
|
||||
|# admin-server-port = 3001
|
||||
|
|
||||
|## The database role to use when no client authentication is provided
|
||||
|# db-anon-role = "anon"
|
||||
|
|
||||
|## Notification channel for reloading the schema cache
|
||||
|db-channel = "pgrst"
|
||||
|
|
||||
|## Enable or disable the notification channel
|
||||
|db-channel-enabled = true
|
||||
|
|
||||
|## Enable in-database configuration
|
||||
|db-config = true
|
||||
|
|
||||
|## Extra schemas to add to the search_path of every request
|
||||
|db-extra-search-path = "public"
|
||||
|
|
||||
|## Limit rows in response
|
||||
|# db-max-rows = 1000
|
||||
|
|
||||
|## Allow getting the EXPLAIN plan through the `Accept: application/vnd.pgrst.plan` header
|
||||
|# db-plan-enabled = false
|
||||
|
|
||||
|## Number of open connections in the pool
|
||||
|db-pool = 10
|
||||
|
|
||||
|## Time in seconds to wait to acquire a slot from the connection pool
|
||||
|# db-pool-acquisition-timeout = 10
|
||||
|
|
||||
|## Stored proc to exec immediately after auth
|
||||
|# db-pre-request = "stored_proc_name"
|
||||
|
|
||||
|## Enable or disable prepared statements. disabling is only necessary when behind a connection pooler.
|
||||
|## When disabled, statements will be parametrized but won't be prepared.
|
||||
|db-prepared-statements = true
|
||||
|
|
||||
|## The name of which database schema to expose to REST clients
|
||||
|db-schemas = "public"
|
||||
|
|
||||
|## How to terminate database transactions
|
||||
|## Possible values are:
|
||||
|## commit (default)
|
||||
|## Transaction is always committed, this can not be overriden
|
||||
|## commit-allow-override
|
||||
|## Transaction is committed, but can be overriden with Prefer tx=rollback header
|
||||
|## rollback
|
||||
|## Transaction is always rolled back, this can not be overriden
|
||||
|## rollback-allow-override
|
||||
|## Transaction is rolled back, but can be overriden with Prefer tx=commit header
|
||||
|db-tx-end = "commit"
|
||||
|
|
||||
|## The standard connection URI format, documented at
|
||||
|## https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING
|
||||
|db-uri = "postgresql://"
|
||||
|
|
||||
|## Determine if GUC request settings for headers, cookies and jwt claims use the legacy names (string with dashes, invalid starting from PostgreSQL v14) with text values instead of the new names (string without dashes, valid on all PostgreSQL versions) with json values.
|
||||
|## For PostgreSQL v14 and up, this setting will be ignored.
|
||||
|db-use-legacy-gucs = true
|
||||
|
|
||||
|# jwt-aud = "your_audience_claim"
|
||||
|
|
||||
|## Jspath to the role claim key
|
||||
|jwt-role-claim-key = ".role"
|
||||
|
|
||||
|## Choose a secret, JSON Web Key (or set) to enable JWT auth
|
||||
|## (use "@filename" to load from separate file)
|
||||
|# jwt-secret = "secret_with_at_least_32_characters"
|
||||
|jwt-secret-is-base64 = false
|
||||
|
|
||||
|## Logging level, the admitted values are: crit, error, warn and info.
|
||||
|log-level = "error"
|
||||
|
|
||||
|## Determine if the OpenAPI output should follow or ignore role privileges or be disabled entirely.
|
||||
|## Admitted values: follow-privileges, ignore-privileges, disabled
|
||||
|openapi-mode = "follow-privileges"
|
||||
|
|
||||
|## Base url for the OpenAPI output
|
||||
|openapi-server-proxy-uri = ""
|
||||
|
|
||||
|## Content types to produce raw output
|
||||
|# raw-media-types="image/png, image/jpg"
|
||||
|
|
||||
|server-host = "!4"
|
||||
|server-port = 3000
|
||||
|
|
||||
|## Unix socket location
|
||||
|## if specified it takes precedence over server-port
|
||||
|# server-unix-socket = "/tmp/pgrst.sock"
|
||||
|
|
||||
|## Unix socket file mode
|
||||
|## When none is provided, 660 is applied by default
|
||||
|# server-unix-socket-mode = "660"
|
||||
|]
|
||||
+380
-242
@@ -1,217 +1,363 @@
|
||||
{-|
|
||||
Module : PostgREST.Config
|
||||
Description : Manages PostgREST configuration options.
|
||||
Description : Manages PostgREST configuration type and parser.
|
||||
|
||||
This module provides a helper function to read the command line
|
||||
arguments using the optparse-applicative and the AppConfig type to store
|
||||
them. It also can be used to define other middleware configuration that
|
||||
may be delegated to some sort of external configuration.
|
||||
|
||||
It currently includes a hardcoded CORS policy but this could easly be
|
||||
turned in configurable behaviour if needed.
|
||||
|
||||
Other hardcoded options such as the minimum version number also belong here.
|
||||
-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-# OPTIONS_GHC -fno-warn-type-defaults #-}
|
||||
|
||||
module PostgREST.Config ( prettyVersion
|
||||
, docsVersion
|
||||
, readOptions
|
||||
, corsPolicy
|
||||
, AppConfig (..)
|
||||
, configPoolTimeout'
|
||||
)
|
||||
where
|
||||
module PostgREST.Config
|
||||
( AppConfig (..)
|
||||
, Environment
|
||||
, JSPath
|
||||
, JSPathExp(..)
|
||||
, LogLevel(..)
|
||||
, OpenAPIMode(..)
|
||||
, Proxy(..)
|
||||
, toText
|
||||
, isMalformedProxyUri
|
||||
, readAppConfig
|
||||
, readPGRSTEnvironment
|
||||
, toURI
|
||||
, parseSecret
|
||||
) where
|
||||
|
||||
import qualified Data.ByteString as B
|
||||
import qualified Data.ByteString.Char8 as BS
|
||||
import qualified Data.CaseInsensitive as CI
|
||||
import qualified Data.Configurator as C
|
||||
import qualified Text.PrettyPrint.ANSI.Leijen as L
|
||||
import qualified Crypto.JOSE.Types as JOSE
|
||||
import qualified Crypto.JWT as JWT
|
||||
import qualified Data.Aeson as JSON
|
||||
import qualified Data.ByteString as BS
|
||||
import qualified Data.ByteString.Base64 as B64
|
||||
import qualified Data.ByteString.Lazy as LBS
|
||||
import qualified Data.Configurator as C
|
||||
import qualified Data.Map.Strict as M
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.Text.Encoding as T
|
||||
|
||||
import Control.Lens (preview)
|
||||
import Control.Monad (fail)
|
||||
import Crypto.JWT (StringOrURI, stringOrUri)
|
||||
import Data.List (lookup)
|
||||
import Data.List.NonEmpty (fromList)
|
||||
import Data.Scientific (floatingOrInteger)
|
||||
import Data.Text (dropEnd, dropWhileEnd,
|
||||
intercalate, splitOn, strip, take,
|
||||
unpack)
|
||||
import Data.Text.IO (hPutStrLn)
|
||||
import Data.Version (versionBranch)
|
||||
import Development.GitRev (gitHash)
|
||||
import Network.Wai.Middleware.Cors (CorsResourcePolicy (..))
|
||||
import Numeric (readOct)
|
||||
import Paths_postgrest (version)
|
||||
import System.IO.Error (IOError)
|
||||
import System.Posix.Types (FileMode)
|
||||
import Control.Lens (preview)
|
||||
import Control.Monad (fail)
|
||||
import Crypto.JWT (JWK, JWKSet, StringOrURI, stringOrUri)
|
||||
import Data.Aeson (toJSON)
|
||||
import Data.Either.Combinators (mapLeft)
|
||||
import Data.List (lookup)
|
||||
import Data.List.NonEmpty (fromList, toList)
|
||||
import Data.Maybe (fromJust)
|
||||
import Data.Scientific (floatingOrInteger)
|
||||
import Numeric (readOct, showOct)
|
||||
import System.Environment (getEnvironment)
|
||||
import System.Posix.Types (FileMode)
|
||||
|
||||
import Control.Applicative
|
||||
import Data.Monoid
|
||||
import Network.Wai
|
||||
import Options.Applicative hiding (str)
|
||||
import Text.Heredoc
|
||||
import Text.PrettyPrint.ANSI.Leijen hiding ((<$>), (<>))
|
||||
import PostgREST.Config.JSPath (JSPath, JSPathExp (..),
|
||||
dumpJSPath, pRoleClaimKey)
|
||||
import PostgREST.Config.Proxy (Proxy (..),
|
||||
isMalformedProxyUri, toURI)
|
||||
import PostgREST.MediaType (MediaType (..), toMime)
|
||||
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier, dumpQi,
|
||||
toQi)
|
||||
|
||||
import PostgREST.Error (ApiRequestError (..))
|
||||
import PostgREST.Parsers (pRoleClaimKey)
|
||||
import PostgREST.Types (JSPath, JSPathExp (..))
|
||||
import Protolude hiding (concat, hPutStrLn, intercalate, null,
|
||||
take, toS, (<>))
|
||||
import Protolude.Conv (toS)
|
||||
import Protolude hiding (Proxy, toList)
|
||||
|
||||
|
||||
-- | Config file settings for the server
|
||||
data AppConfig = AppConfig {
|
||||
configDatabase :: Text
|
||||
, configAnonRole :: Text
|
||||
, configOpenAPIProxyUri :: Maybe Text
|
||||
, configSchemas :: NonEmpty Text
|
||||
, configHost :: Text
|
||||
, configPort :: Int
|
||||
, configSocket :: Maybe FilePath
|
||||
, configSocketMode :: Either Text FileMode
|
||||
|
||||
, configJwtSecret :: Maybe B.ByteString
|
||||
, configJwtSecretIsBase64 :: Bool
|
||||
, configJwtAudience :: Maybe StringOrURI
|
||||
|
||||
, configPool :: Int
|
||||
, configPoolTimeout :: Int
|
||||
, configMaxRows :: Maybe Integer
|
||||
, configReqCheck :: Maybe Text
|
||||
, configQuiet :: Bool
|
||||
, configSettings :: [(Text, Text)]
|
||||
, configRoleClaimKey :: Either ApiRequestError JSPath
|
||||
, configExtraSearchPath :: [Text]
|
||||
|
||||
, configRootSpec :: Maybe Text
|
||||
, configRawMediaTypes :: [B.ByteString]
|
||||
data AppConfig = AppConfig
|
||||
{ configAppSettings :: [(Text, Text)]
|
||||
, configDbAnonRole :: Maybe Text
|
||||
, configDbChannel :: Text
|
||||
, configDbChannelEnabled :: Bool
|
||||
, configDbExtraSearchPath :: [Text]
|
||||
, configDbMaxRows :: Maybe Integer
|
||||
, configDbPlanEnabled :: Bool
|
||||
, configDbPoolSize :: Int
|
||||
, configDbPoolAcquisitionTimeout :: Maybe Int
|
||||
, configDbPreRequest :: Maybe QualifiedIdentifier
|
||||
, configDbPreparedStatements :: Bool
|
||||
, configDbRootSpec :: Maybe QualifiedIdentifier
|
||||
, configDbSchemas :: NonEmpty Text
|
||||
, configDbConfig :: Bool
|
||||
, configDbTxAllowOverride :: Bool
|
||||
, configDbTxRollbackAll :: Bool
|
||||
, configDbUri :: Text
|
||||
, configDbUseLegacyGucs :: Bool
|
||||
, configFilePath :: Maybe FilePath
|
||||
, configJWKS :: Maybe JWKSet
|
||||
, configJwtAudience :: Maybe StringOrURI
|
||||
, configJwtRoleClaimKey :: JSPath
|
||||
, configJwtSecret :: Maybe BS.ByteString
|
||||
, configJwtSecretIsBase64 :: Bool
|
||||
, configLogLevel :: LogLevel
|
||||
, configOpenApiMode :: OpenAPIMode
|
||||
, configOpenApiSecurityActive :: Bool
|
||||
, configOpenApiServerProxyUri :: Maybe Text
|
||||
, configRawMediaTypes :: [MediaType]
|
||||
, configServerHost :: Text
|
||||
, configServerPort :: Int
|
||||
, configServerUnixSocket :: Maybe FilePath
|
||||
, configServerUnixSocketMode :: FileMode
|
||||
, configAdminServerPort :: Maybe Int
|
||||
}
|
||||
|
||||
configPoolTimeout' :: (Fractional a) => AppConfig -> a
|
||||
configPoolTimeout' =
|
||||
fromRational . toRational . configPoolTimeout
|
||||
data LogLevel = LogCrit | LogError | LogWarn | LogInfo
|
||||
|
||||
dumpLogLevel :: LogLevel -> Text
|
||||
dumpLogLevel = \case
|
||||
LogCrit -> "crit"
|
||||
LogError -> "error"
|
||||
LogWarn -> "warn"
|
||||
LogInfo -> "info"
|
||||
|
||||
defaultCorsPolicy :: CorsResourcePolicy
|
||||
defaultCorsPolicy = CorsResourcePolicy Nothing
|
||||
["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"] ["Authorization"] Nothing
|
||||
(Just $ 60*60*24) False False True
|
||||
data OpenAPIMode = OAFollowPriv | OAIgnorePriv | OADisabled
|
||||
deriving Eq
|
||||
|
||||
-- | CORS policy to be used in by Wai Cors middleware
|
||||
corsPolicy :: Request -> Maybe CorsResourcePolicy
|
||||
corsPolicy req = case lookup "origin" headers of
|
||||
Just origin -> Just defaultCorsPolicy {
|
||||
corsOrigins = Just ([origin], True)
|
||||
, corsRequestHeaders = "Authentication":accHeaders
|
||||
, corsExposedHeaders = Just [
|
||||
"Content-Encoding", "Content-Location", "Content-Range", "Content-Type"
|
||||
, "Date", "Location", "Server", "Transfer-Encoding", "Range-Unit"
|
||||
dumpOpenApiMode :: OpenAPIMode -> Text
|
||||
dumpOpenApiMode = \case
|
||||
OAFollowPriv -> "follow-privileges"
|
||||
OAIgnorePriv -> "ignore-privileges"
|
||||
OADisabled -> "disabled"
|
||||
|
||||
-- | Dump the config
|
||||
toText :: AppConfig -> Text
|
||||
toText conf =
|
||||
unlines $ (\(k, v) -> k <> " = " <> v) <$> pgrstSettings ++ appSettings
|
||||
where
|
||||
-- apply conf to all pgrst settings
|
||||
pgrstSettings = (\(k, v) -> (k, v conf)) <$>
|
||||
[("db-anon-role", q . fromMaybe "" . configDbAnonRole)
|
||||
,("db-channel", q . configDbChannel)
|
||||
,("db-channel-enabled", T.toLower . show . configDbChannelEnabled)
|
||||
,("db-extra-search-path", q . T.intercalate "," . configDbExtraSearchPath)
|
||||
,("db-max-rows", maybe "\"\"" show . configDbMaxRows)
|
||||
,("db-plan-enabled", T.toLower . show . configDbPlanEnabled)
|
||||
,("db-pool", show . configDbPoolSize)
|
||||
,("db-pool-acquisition-timeout", maybe "\"\"" show . configDbPoolAcquisitionTimeout)
|
||||
,("db-pre-request", q . maybe mempty dumpQi . configDbPreRequest)
|
||||
,("db-prepared-statements", T.toLower . show . configDbPreparedStatements)
|
||||
,("db-root-spec", q . maybe mempty dumpQi . configDbRootSpec)
|
||||
,("db-schemas", q . T.intercalate "," . toList . configDbSchemas)
|
||||
,("db-config", T.toLower . show . configDbConfig)
|
||||
,("db-tx-end", q . showTxEnd)
|
||||
,("db-uri", q . configDbUri)
|
||||
,("db-use-legacy-gucs", T.toLower . show . configDbUseLegacyGucs)
|
||||
,("jwt-aud", T.decodeUtf8 . LBS.toStrict . JSON.encode . maybe "" toJSON . configJwtAudience)
|
||||
,("jwt-role-claim-key", q . T.intercalate mempty . fmap dumpJSPath . configJwtRoleClaimKey)
|
||||
,("jwt-secret", q . T.decodeUtf8 . showJwtSecret)
|
||||
,("jwt-secret-is-base64", T.toLower . show . configJwtSecretIsBase64)
|
||||
,("log-level", q . dumpLogLevel . configLogLevel)
|
||||
,("openapi-mode", q . dumpOpenApiMode . configOpenApiMode)
|
||||
,("openapi-security-active", T.toLower . show . configOpenApiSecurityActive)
|
||||
,("openapi-server-proxy-uri", q . fromMaybe mempty . configOpenApiServerProxyUri)
|
||||
,("raw-media-types", q . T.decodeUtf8 . BS.intercalate "," . fmap toMime . configRawMediaTypes)
|
||||
,("server-host", q . configServerHost)
|
||||
,("server-port", show . configServerPort)
|
||||
,("server-unix-socket", q . maybe mempty T.pack . configServerUnixSocket)
|
||||
,("server-unix-socket-mode", q . T.pack . showSocketMode)
|
||||
,("admin-server-port", maybe "\"\"" show . configAdminServerPort)
|
||||
]
|
||||
}
|
||||
Nothing -> Nothing
|
||||
where
|
||||
headers = requestHeaders req
|
||||
accHeaders = case lookup "access-control-request-headers" headers of
|
||||
Just hdrs -> map (CI.mk . toS . strip . toS) $ BS.split ',' hdrs
|
||||
Nothing -> []
|
||||
|
||||
-- | User friendly version number
|
||||
prettyVersion :: Text
|
||||
prettyVersion =
|
||||
intercalate "." (map show $ versionBranch version)
|
||||
<> " (" <> take 7 $(gitHash) <> ")"
|
||||
-- quote all app.settings
|
||||
appSettings = second q <$> configAppSettings conf
|
||||
|
||||
-- | Version number used in docs
|
||||
docsVersion :: Text
|
||||
docsVersion = "v" <> dropEnd 1 (dropWhileEnd (/= '.') prettyVersion)
|
||||
-- quote strings and replace " with \"
|
||||
q s = "\"" <> T.replace "\"" "\\\"" s <> "\""
|
||||
|
||||
-- | Function to read and parse options from the command line
|
||||
readOptions :: IO AppConfig
|
||||
readOptions = do
|
||||
-- First read the config file path from command line
|
||||
cfgPath <- customExecParser parserPrefs opts
|
||||
-- Now read the actual config file
|
||||
conf <- catches (C.load cfgPath)
|
||||
[ Handler (\(ex :: IOError) -> exitErr $ "Cannot open config file:\n\t" <> show ex)
|
||||
, Handler (\(C.ParseError err) -> exitErr $ "Error parsing config file:\n" <> err)
|
||||
]
|
||||
showTxEnd c = case (configDbTxRollbackAll c, configDbTxAllowOverride c) of
|
||||
( False, False ) -> "commit"
|
||||
( False, True ) -> "commit-allow-override"
|
||||
( True , False ) -> "rollback"
|
||||
( True , True ) -> "rollback-allow-override"
|
||||
showJwtSecret c
|
||||
| configJwtSecretIsBase64 c = B64.encode secret
|
||||
| otherwise = secret
|
||||
where
|
||||
secret = fromMaybe mempty $ configJwtSecret c
|
||||
showSocketMode c = showOct (configServerUnixSocketMode c) mempty
|
||||
|
||||
case C.runParser parseConfig conf of
|
||||
-- This class is needed for the polymorphism of overrideFromDbOrEnvironment
|
||||
-- because C.required and C.optional have different signatures
|
||||
class JustIfMaybe a b where
|
||||
justIfMaybe :: a -> b
|
||||
|
||||
instance JustIfMaybe a a where
|
||||
justIfMaybe = identity
|
||||
|
||||
instance JustIfMaybe a (Maybe a) where
|
||||
justIfMaybe = Just
|
||||
|
||||
-- | Reads and parses the config and overrides its parameters from env vars,
|
||||
-- files or db settings.
|
||||
readAppConfig :: [(Text, Text)] -> Maybe FilePath -> Maybe Text -> IO (Either Text AppConfig)
|
||||
readAppConfig dbSettings optPath prevDbUri = do
|
||||
env <- readPGRSTEnvironment
|
||||
-- if no filename provided, start with an empty map to read config from environment
|
||||
conf <- maybe (return $ Right M.empty) loadConfig optPath
|
||||
|
||||
case C.runParser (parser optPath env dbSettings) =<< mapLeft show conf of
|
||||
Left err ->
|
||||
exitErr $ "Error parsing config file:\n\t" <> err
|
||||
Right appConf ->
|
||||
return appConf
|
||||
|
||||
return . Left $ "Error in config " <> err
|
||||
Right parsedConfig ->
|
||||
Right <$> decodeLoadFiles parsedConfig
|
||||
where
|
||||
parseConfig =
|
||||
AppConfig
|
||||
<$> reqString "db-uri"
|
||||
<*> reqString "db-anon-role"
|
||||
<*> optString "openapi-server-proxy-uri"
|
||||
<*> (fromList . splitOnCommas <$> reqValue "db-schema")
|
||||
<*> (fromMaybe "!4" <$> optString "server-host")
|
||||
<*> (fromMaybe 3000 <$> optInt "server-port")
|
||||
<*> (fmap unpack <$> optString "server-unix-socket")
|
||||
<*> parseSocketFileMode "server-unix-socket-mode"
|
||||
<*> (fmap encodeUtf8 <$> optString "jwt-secret")
|
||||
<*> (fromMaybe False <$> optBool "secret-is-base64")
|
||||
<*> parseJwtAudience "jwt-aud"
|
||||
<*> (fromMaybe 10 <$> optInt "db-pool")
|
||||
<*> (fromMaybe 10 <$> optInt "db-pool-timeout")
|
||||
<*> optInt "max-rows"
|
||||
<*> optString "pre-request"
|
||||
<*> pure False
|
||||
<*> (fmap (fmap coerceText) <$> C.subassocs "app.settings" C.value)
|
||||
<*> (maybe (Right [JSPKey "role"]) parseRoleClaimKey <$> optValue "role-claim-key")
|
||||
<*> (maybe ["public"] splitOnCommas <$> optValue "db-extra-search-path")
|
||||
<*> optString "root-spec"
|
||||
<*> (maybe [] (fmap encodeUtf8 . splitOnCommas) <$> optValue "raw-media-types")
|
||||
-- Both C.ParseError and IOError are shown here
|
||||
loadConfig :: FilePath -> IO (Either SomeException C.Config)
|
||||
loadConfig = try . C.load
|
||||
|
||||
parseSocketFileMode :: C.Key -> C.Parser C.Config (Either Text FileMode)
|
||||
decodeLoadFiles :: AppConfig -> IO AppConfig
|
||||
decodeLoadFiles parsedConfig =
|
||||
decodeJWKS <$>
|
||||
(decodeSecret =<< readSecretFile =<< readDbUriFile prevDbUri parsedConfig)
|
||||
|
||||
parser :: Maybe FilePath -> Environment -> [(Text, Text)] -> C.Parser C.Config AppConfig
|
||||
parser optPath env dbSettings =
|
||||
AppConfig
|
||||
<$> parseAppSettings "app.settings"
|
||||
<*> optString "db-anon-role"
|
||||
<*> (fromMaybe "pgrst" <$> optString "db-channel")
|
||||
<*> (fromMaybe True <$> optBool "db-channel-enabled")
|
||||
<*> (maybe ["public"] splitOnCommas <$> optValue "db-extra-search-path")
|
||||
<*> optWithAlias (optInt "db-max-rows")
|
||||
(optInt "max-rows")
|
||||
<*> (fromMaybe False <$> optBool "db-plan-enabled")
|
||||
<*> (fromMaybe 10 <$> optInt "db-pool")
|
||||
<*> optInt "db-pool-acquisition-timeout"
|
||||
<*> (fmap toQi <$> optWithAlias (optString "db-pre-request")
|
||||
(optString "pre-request"))
|
||||
<*> (fromMaybe True <$> optBool "db-prepared-statements")
|
||||
<*> (fmap toQi <$> optWithAlias (optString "db-root-spec")
|
||||
(optString "root-spec"))
|
||||
<*> (fromList . maybe ["public"] splitOnCommas <$> optWithAlias (optValue "db-schemas")
|
||||
(optValue "db-schema"))
|
||||
<*> (fromMaybe True <$> optBool "db-config")
|
||||
<*> parseTxEnd "db-tx-end" snd
|
||||
<*> parseTxEnd "db-tx-end" fst
|
||||
<*> (fromMaybe "postgresql://" <$> optString "db-uri")
|
||||
<*> (fromMaybe True <$> optBool "db-use-legacy-gucs")
|
||||
<*> pure optPath
|
||||
<*> pure Nothing
|
||||
<*> parseJwtAudience "jwt-aud"
|
||||
<*> parseRoleClaimKey "jwt-role-claim-key" "role-claim-key"
|
||||
<*> (fmap encodeUtf8 <$> optString "jwt-secret")
|
||||
<*> (fromMaybe False <$> optWithAlias
|
||||
(optBool "jwt-secret-is-base64")
|
||||
(optBool "secret-is-base64"))
|
||||
<*> parseLogLevel "log-level"
|
||||
<*> parseOpenAPIMode "openapi-mode"
|
||||
<*> (fromMaybe False <$> optBool "openapi-security-active")
|
||||
<*> parseOpenAPIServerProxyURI "openapi-server-proxy-uri"
|
||||
<*> (maybe [] (fmap (MTOther . encodeUtf8) . splitOnCommas) <$> optValue "raw-media-types")
|
||||
<*> (fromMaybe "!4" <$> optString "server-host")
|
||||
<*> (fromMaybe 3000 <$> optInt "server-port")
|
||||
<*> (fmap T.unpack <$> optString "server-unix-socket")
|
||||
<*> parseSocketFileMode "server-unix-socket-mode"
|
||||
<*> optInt "admin-server-port"
|
||||
where
|
||||
parseAppSettings :: C.Key -> C.Parser C.Config [(Text, Text)]
|
||||
parseAppSettings key = addFromEnv . fmap (fmap coerceText) <$> C.subassocs key C.value
|
||||
where
|
||||
addFromEnv f = M.toList $ M.union fromEnv $ M.fromList f
|
||||
fromEnv = M.mapKeys fromJust $ M.filterWithKey (\k _ -> isJust k) $ M.mapKeys normalize env
|
||||
normalize k = ("app.settings." <>) <$> T.stripPrefix "PGRST_APP_SETTINGS_" (toS k)
|
||||
|
||||
parseSocketFileMode :: C.Key -> C.Parser C.Config FileMode
|
||||
parseSocketFileMode k =
|
||||
C.optional k C.string >>= \case
|
||||
Nothing -> pure $ Right 432 -- return default 660 mode if no value was provided
|
||||
optString k >>= \case
|
||||
Nothing -> pure 432 -- return default 660 mode if no value was provided
|
||||
Just fileModeText ->
|
||||
case (readOct . unpack) fileModeText of
|
||||
case readOct $ T.unpack fileModeText of
|
||||
[] ->
|
||||
pure $ Left "Invalid server-unix-socket-mode: not an octal"
|
||||
fail "Invalid server-unix-socket-mode: not an octal"
|
||||
(fileMode, _):_ ->
|
||||
if fileMode < 384 || fileMode > 511
|
||||
then pure $ Left "Invalid server-unix-socket-mode: needs to be between 600 and 777"
|
||||
else pure $ Right fileMode
|
||||
then fail "Invalid server-unix-socket-mode: needs to be between 600 and 777"
|
||||
else pure fileMode
|
||||
|
||||
parseOpenAPIMode :: C.Key -> C.Parser C.Config OpenAPIMode
|
||||
parseOpenAPIMode k =
|
||||
optString k >>= \case
|
||||
Nothing -> pure OAFollowPriv
|
||||
Just "follow-privileges" -> pure OAFollowPriv
|
||||
Just "ignore-privileges" -> pure OAIgnorePriv
|
||||
Just "disabled" -> pure OADisabled
|
||||
Just _ -> fail "Invalid openapi-mode. Check your configuration."
|
||||
|
||||
parseOpenAPIServerProxyURI :: C.Key -> C.Parser C.Config (Maybe Text)
|
||||
parseOpenAPIServerProxyURI k =
|
||||
optString k >>= \case
|
||||
Nothing -> pure Nothing
|
||||
Just val | isMalformedProxyUri val -> fail "Malformed proxy uri, a correct example: https://example.com:8443/basePath"
|
||||
| otherwise -> pure $ Just val
|
||||
|
||||
parseJwtAudience :: C.Key -> C.Parser C.Config (Maybe StringOrURI)
|
||||
parseJwtAudience k =
|
||||
C.optional k C.string >>= \case
|
||||
optString k >>= \case
|
||||
Nothing -> pure Nothing -- no audience in config file
|
||||
Just aud -> case preview stringOrUri (unpack aud) of
|
||||
Just aud -> case preview stringOrUri (T.unpack aud) of
|
||||
Nothing -> fail "Invalid Jwt audience. Check your configuration."
|
||||
(Just "") -> pure Nothing
|
||||
aud' -> pure aud'
|
||||
|
||||
reqString :: C.Key -> C.Parser C.Config Text
|
||||
reqString k = C.required k C.string
|
||||
parseLogLevel :: C.Key -> C.Parser C.Config LogLevel
|
||||
parseLogLevel k =
|
||||
optString k >>= \case
|
||||
Nothing -> pure LogError
|
||||
Just "crit" -> pure LogCrit
|
||||
Just "error" -> pure LogError
|
||||
Just "warn" -> pure LogWarn
|
||||
Just "info" -> pure LogInfo
|
||||
Just _ -> fail "Invalid logging level. Check your configuration."
|
||||
|
||||
reqValue :: C.Key -> C.Parser C.Config C.Value
|
||||
reqValue k = C.required k C.value
|
||||
parseTxEnd :: C.Key -> ((Bool, Bool) -> Bool) -> C.Parser C.Config Bool
|
||||
parseTxEnd k f =
|
||||
optString k >>= \case
|
||||
-- RollbackAll AllowOverride
|
||||
Nothing -> pure $ f (False, False)
|
||||
Just "commit" -> pure $ f (False, False)
|
||||
Just "commit-allow-override" -> pure $ f (False, True)
|
||||
Just "rollback" -> pure $ f (True, False)
|
||||
Just "rollback-allow-override" -> pure $ f (True, True)
|
||||
Just _ -> fail "Invalid transaction termination. Check your configuration."
|
||||
|
||||
parseRoleClaimKey :: C.Key -> C.Key -> C.Parser C.Config JSPath
|
||||
parseRoleClaimKey k al =
|
||||
optWithAlias (optString k) (optString al) >>= \case
|
||||
Nothing -> pure [JSPKey "role"]
|
||||
Just rck -> either (fail . show) pure $ pRoleClaimKey rck
|
||||
|
||||
optWithAlias :: C.Parser C.Config (Maybe a) -> C.Parser C.Config (Maybe a) -> C.Parser C.Config (Maybe a)
|
||||
optWithAlias orig alias =
|
||||
orig >>= \case
|
||||
Just v -> pure $ Just v
|
||||
Nothing -> alias
|
||||
|
||||
optString :: C.Key -> C.Parser C.Config (Maybe Text)
|
||||
optString k = mfilter (/= "") <$> C.optional k C.string
|
||||
optString k = mfilter (/= "") <$> overrideFromDbOrEnvironment C.optional k coerceText
|
||||
|
||||
optValue :: C.Key -> C.Parser C.Config (Maybe C.Value)
|
||||
optValue k = C.optional k C.value
|
||||
optValue k = overrideFromDbOrEnvironment C.optional k identity
|
||||
|
||||
optInt :: (Read i, Integral i) => C.Key -> C.Parser C.Config (Maybe i)
|
||||
optInt k = join <$> C.optional k (coerceInt <$> C.value)
|
||||
optInt k = join <$> overrideFromDbOrEnvironment C.optional k coerceInt
|
||||
|
||||
optBool :: C.Key -> C.Parser C.Config (Maybe Bool)
|
||||
optBool k = join <$> C.optional k (coerceBool <$> C.value)
|
||||
optBool k = join <$> overrideFromDbOrEnvironment C.optional k coerceBool
|
||||
|
||||
overrideFromDbOrEnvironment :: JustIfMaybe a b =>
|
||||
(C.Key -> C.Parser C.Value a -> C.Parser C.Config b) ->
|
||||
C.Key -> (C.Value -> a) -> C.Parser C.Config b
|
||||
overrideFromDbOrEnvironment necessity key coercion =
|
||||
case reloadableDbSetting <|> M.lookup envVarName env of
|
||||
Just dbOrEnvVal -> pure $ justIfMaybe $ coercion $ C.String dbOrEnvVal
|
||||
Nothing -> necessity key (coercion <$> C.value)
|
||||
where
|
||||
dashToUnderscore '-' = '_'
|
||||
dashToUnderscore c = c
|
||||
envVarName = "PGRST_" <> (toUpper . dashToUnderscore <$> toS key)
|
||||
reloadableDbSetting =
|
||||
let dbSettingName = T.pack $ dashToUnderscore <$> toS key in
|
||||
if dbSettingName `notElem` [
|
||||
"server_host", "server_port", "server_unix_socket", "server_unix_socket_mode", "admin_server_port", "log_level",
|
||||
"db_uri", "db_channel_enabled", "db_channel", "db_pool", "db_pool_acquisition_timeout", "db_config"]
|
||||
then lookup dbSettingName dbSettings
|
||||
else Nothing
|
||||
|
||||
coerceText :: C.Value -> Text
|
||||
coerceText (C.String s) = s
|
||||
@@ -219,90 +365,82 @@ readOptions = do
|
||||
|
||||
coerceInt :: (Read i, Integral i) => C.Value -> Maybe i
|
||||
coerceInt (C.Number x) = rightToMaybe $ floatingOrInteger x
|
||||
coerceInt (C.String x) = readMaybe $ toS x
|
||||
coerceInt (C.String x) = readMaybe x
|
||||
coerceInt _ = Nothing
|
||||
|
||||
coerceBool :: C.Value -> Maybe Bool
|
||||
coerceBool (C.Bool b) = Just b
|
||||
coerceBool (C.String b) = readMaybe $ toS b
|
||||
coerceBool (C.String s) =
|
||||
-- parse all kinds of text: True, true, TRUE, "true", ...
|
||||
case readMaybe $ T.toTitle $ T.filter isAlpha $ toS s of
|
||||
Just b -> Just b
|
||||
-- numeric instead?
|
||||
Nothing -> (> 0) <$> (readMaybe s :: Maybe Integer)
|
||||
coerceBool _ = Nothing
|
||||
|
||||
parseRoleClaimKey :: C.Value -> Either ApiRequestError JSPath
|
||||
parseRoleClaimKey (C.String s) = pRoleClaimKey s
|
||||
parseRoleClaimKey v = pRoleClaimKey $ show v
|
||||
|
||||
splitOnCommas :: C.Value -> [Text]
|
||||
splitOnCommas (C.String s) = strip <$> splitOn "," s
|
||||
splitOnCommas (C.String s) = T.strip <$> T.splitOn "," s
|
||||
splitOnCommas _ = []
|
||||
|
||||
opts = info (helper <*> pathParser) $
|
||||
fullDesc
|
||||
<> progDesc (
|
||||
"PostgREST "
|
||||
<> toS prettyVersion
|
||||
<> " / create a REST API to an existing Postgres database"
|
||||
)
|
||||
<> footerDoc (Just $
|
||||
text "Example Config File:"
|
||||
L.<> nest 2 (hardline L.<> exampleCfg)
|
||||
)
|
||||
-- | Read the JWT secret from a file if configJwtSecret is actually a
|
||||
-- filepath(has @ as its prefix). To check if the JWT secret is provided is
|
||||
-- in fact a file path, it must be decoded as 'Text' to be processed.
|
||||
readSecretFile :: AppConfig -> IO AppConfig
|
||||
readSecretFile conf =
|
||||
maybe (return conf) readSecret maybeFilename
|
||||
where
|
||||
maybeFilename = T.stripPrefix "@" . decodeUtf8 =<< configJwtSecret conf
|
||||
readSecret filename = do
|
||||
jwtSecret <- chomp <$> BS.readFile (toS filename)
|
||||
return $ conf { configJwtSecret = Just jwtSecret }
|
||||
chomp bs = fromMaybe bs (BS.stripSuffix "\n" bs)
|
||||
|
||||
parserPrefs = prefs showHelpOnError
|
||||
decodeSecret :: AppConfig -> IO AppConfig
|
||||
decodeSecret conf@AppConfig{..} =
|
||||
case (configJwtSecretIsBase64, configJwtSecret) of
|
||||
(True, Just secret) ->
|
||||
either fail (return . updateSecret) $ decodeB64 secret
|
||||
_ -> return conf
|
||||
where
|
||||
updateSecret bs = conf { configJwtSecret = Just bs }
|
||||
decodeB64 = B64.decode . encodeUtf8 . T.strip . replaceUrlChars . decodeUtf8
|
||||
replaceUrlChars = T.replace "_" "/" . T.replace "-" "+" . T.replace "." "="
|
||||
|
||||
exitErr :: Text -> IO a
|
||||
exitErr err = do
|
||||
hPutStrLn stderr err
|
||||
exitFailure
|
||||
-- | 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.
|
||||
decodeJWKS :: AppConfig -> AppConfig
|
||||
decodeJWKS conf =
|
||||
conf { configJWKS = parseSecret <$> configJwtSecret conf }
|
||||
|
||||
exampleCfg :: Doc
|
||||
exampleCfg = vsep . map (text . toS) . lines $
|
||||
[str|db-uri = "postgres://user:pass@localhost:5432/dbname"
|
||||
|db-schema = "public" # this schema gets added to the search_path of every request
|
||||
|db-anon-role = "postgres"
|
||||
|db-pool = 10
|
||||
|db-pool-timeout = 10
|
||||
|
|
||||
|server-host = "!4"
|
||||
|server-port = 3000
|
||||
|
|
||||
|## unix socket location
|
||||
|## if specified it takes precedence over server-port
|
||||
|# server-unix-socket = "/tmp/pgrst.sock"
|
||||
|## unix socket file mode
|
||||
|## when none is provided, 660 is applied by default
|
||||
|# server-unix-socket-mode = "660"
|
||||
|
|
||||
|## base url for swagger output
|
||||
|# openapi-server-proxy-uri = ""
|
||||
|
|
||||
|## choose a secret, JSON Web Key (or set) to enable JWT auth
|
||||
|## (use "@filename" to load from separate file)
|
||||
|# jwt-secret = "secret_with_at_least_32_characters"
|
||||
|# secret-is-base64 = false
|
||||
|# jwt-aud = "your_audience_claim"
|
||||
|
|
||||
|## limit rows in response
|
||||
|# max-rows = 1000
|
||||
|
|
||||
|## stored proc to exec immediately after auth
|
||||
|# pre-request = "stored_proc_name"
|
||||
|
|
||||
|## jspath to the role claim key
|
||||
|# role-claim-key = ".role"
|
||||
|
|
||||
|## extra schemas to add to the search_path of every request
|
||||
|# db-extra-search-path = "extensions, util"
|
||||
|
|
||||
|## stored proc that overrides the root "/" spec
|
||||
|## it must be inside the db-schema
|
||||
|# root-spec = "stored_proc_name"
|
||||
|
|
||||
|## content types to produce raw output
|
||||
|# raw-media-types="image/png, image/jpg"
|
||||
|]
|
||||
parseSecret :: ByteString -> JWKSet
|
||||
parseSecret bytes =
|
||||
fromMaybe (maybe secret (\jwk' -> JWT.JWKSet [jwk']) maybeJWK)
|
||||
maybeJWKSet
|
||||
where
|
||||
maybeJWKSet = JSON.decodeStrict bytes :: Maybe JWKSet
|
||||
maybeJWK = JSON.decodeStrict bytes :: Maybe JWK
|
||||
secret = JWT.JWKSet [JWT.fromKeyMaterial keyMaterial]
|
||||
keyMaterial = JWT.OctKeyMaterial . JWT.OctKeyParameters $ JOSE.Base64Octets bytes
|
||||
|
||||
pathParser :: Parser FilePath
|
||||
pathParser =
|
||||
strArgument $
|
||||
metavar "FILENAME" <>
|
||||
help "Path to configuration file"
|
||||
-- | Read database uri from a separate file if `db-uri` is a filepath.
|
||||
readDbUriFile :: Maybe Text -> AppConfig -> IO AppConfig
|
||||
readDbUriFile maybeDbUri conf =
|
||||
case maybeDbUri of
|
||||
Just prevDbUri ->
|
||||
pure $ conf { configDbUri = prevDbUri }
|
||||
Nothing ->
|
||||
case T.stripPrefix "@" $ configDbUri conf of
|
||||
Nothing -> return conf
|
||||
Just filename -> do
|
||||
dbUri <- T.strip <$> readFile (toS filename)
|
||||
return $ conf { configDbUri = dbUri }
|
||||
|
||||
type Environment = M.Map [Char] Text
|
||||
|
||||
-- | Read environment variables that start with PGRST_
|
||||
readPGRSTEnvironment :: IO Environment
|
||||
readPGRSTEnvironment =
|
||||
M.map T.pack . M.fromList . filter (isPrefixOf "PGRST_" . fst) <$> getEnvironment
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
|
||||
module PostgREST.Config.Database
|
||||
( pgVersionStatement
|
||||
, queryDbSettings
|
||||
, queryPgVersion
|
||||
) where
|
||||
|
||||
import PostgREST.Config.PgVersion (PgVersion (..))
|
||||
|
||||
import qualified Hasql.Decoders as HD
|
||||
import qualified Hasql.Encoders as HE
|
||||
import Hasql.Session (Session, statement)
|
||||
import qualified Hasql.Statement as SQL
|
||||
import qualified Hasql.Transaction as SQL
|
||||
import qualified Hasql.Transaction.Sessions as SQL
|
||||
|
||||
import Text.InterpolatedString.Perl6 (q)
|
||||
|
||||
import Protolude
|
||||
|
||||
queryPgVersion :: Session PgVersion
|
||||
queryPgVersion = statement mempty pgVersionStatement
|
||||
|
||||
pgVersionStatement :: SQL.Statement () PgVersion
|
||||
pgVersionStatement = SQL.Statement sql HE.noParams versionRow False
|
||||
where
|
||||
sql = "SELECT current_setting('server_version_num')::integer, current_setting('server_version')"
|
||||
versionRow = HD.singleRow $ PgVersion <$> column HD.int4 <*> column HD.text
|
||||
|
||||
queryDbSettings :: Bool -> Session [(Text, Text)]
|
||||
queryDbSettings prepared =
|
||||
let transaction = if prepared then SQL.transaction else SQL.unpreparedTransaction in
|
||||
transaction SQL.ReadCommitted SQL.Read $ SQL.statement mempty dbSettingsStatement
|
||||
|
||||
-- | Get db settings from the connection role. Global settings will be overridden by database specific settings.
|
||||
dbSettingsStatement :: SQL.Statement () [(Text, Text)]
|
||||
dbSettingsStatement = SQL.Statement sql HE.noParams decodeSettings False
|
||||
where
|
||||
sql = [q|
|
||||
WITH
|
||||
role_setting (database, setting) AS (
|
||||
SELECT setdatabase,
|
||||
unnest(setconfig)
|
||||
FROM pg_catalog.pg_db_role_setting
|
||||
WHERE setrole = CURRENT_USER::regrole::oid
|
||||
AND setdatabase IN (0, (SELECT oid FROM pg_catalog.pg_database WHERE datname = CURRENT_CATALOG))
|
||||
),
|
||||
kv_settings (database, k, v) AS (
|
||||
SELECT database,
|
||||
substr(setting, 1, strpos(setting, '=') - 1),
|
||||
substr(setting, strpos(setting, '=') + 1)
|
||||
FROM role_setting
|
||||
WHERE setting LIKE 'pgrst.%'
|
||||
)
|
||||
SELECT DISTINCT ON (key)
|
||||
replace(k, 'pgrst.', '') AS key,
|
||||
v AS value
|
||||
FROM kv_settings
|
||||
ORDER BY key, database DESC;
|
||||
|]
|
||||
decodeSettings = HD.rowList $ (,) <$> column HD.text <*> column HD.text
|
||||
|
||||
column :: HD.Value a -> HD.Row a
|
||||
column = HD.column . HD.nonNullable
|
||||
@@ -0,0 +1,51 @@
|
||||
module PostgREST.Config.JSPath
|
||||
( JSPath
|
||||
, JSPathExp(..)
|
||||
, dumpJSPath
|
||||
, pRoleClaimKey
|
||||
) where
|
||||
|
||||
import qualified Text.ParserCombinators.Parsec as P
|
||||
|
||||
import Data.Either.Combinators (mapLeft)
|
||||
import Text.ParserCombinators.Parsec ((<?>))
|
||||
import Text.Read (read)
|
||||
|
||||
import Protolude
|
||||
|
||||
|
||||
-- | full jspath, e.g. .property[0].attr.detail
|
||||
type JSPath = [JSPathExp]
|
||||
|
||||
-- | jspath expression, e.g. .property, .property[0] or ."property-dash"
|
||||
data JSPathExp
|
||||
= JSPKey Text
|
||||
| JSPIdx Int
|
||||
|
||||
dumpJSPath :: JSPathExp -> Text
|
||||
-- TODO: this needs to be quoted properly for special chars
|
||||
dumpJSPath (JSPKey k) = "." <> show k
|
||||
dumpJSPath (JSPIdx i) = "[" <> show i <> "]"
|
||||
|
||||
-- Used for the config value "role-claim-key"
|
||||
pRoleClaimKey :: Text -> Either Text JSPath
|
||||
pRoleClaimKey selStr =
|
||||
mapLeft show $ P.parse pJSPath ("failed to parse role-claim-key value (" <> toS selStr <> ")") (toS selStr)
|
||||
|
||||
pJSPath :: P.Parser JSPath
|
||||
pJSPath = toJSPath <$> (period *> pPath `P.sepBy` period <* P.eof)
|
||||
where
|
||||
toJSPath :: [(Text, Maybe Int)] -> JSPath
|
||||
toJSPath = concatMap (\(key, idx) -> JSPKey key : maybeToList (JSPIdx <$> idx))
|
||||
period = P.char '.' <?> "period (.)"
|
||||
pPath :: P.Parser (Text, Maybe Int)
|
||||
pPath = (,) <$> pJSPKey <*> P.optionMaybe pJSPIdx
|
||||
|
||||
pJSPKey :: P.Parser Text
|
||||
pJSPKey = toS <$> P.many1 (P.alphaNum <|> P.oneOf "_$@") <|> pQuotedValue <?> "attribute name [a..z0..9_$@])"
|
||||
|
||||
pJSPIdx :: P.Parser Int
|
||||
pJSPIdx = P.char '[' *> (read <$> P.many1 P.digit) <* P.char ']' <?> "array index [0..n]"
|
||||
|
||||
pQuotedValue :: P.Parser Text
|
||||
pQuotedValue = toS <$> (P.char '"' *> P.many (P.noneOf "\"") <* P.char '"')
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user